import { test } from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { execFileSync } from 'node:child_process' import { createCheckpoint, restoreCheckpoint, listCheckpoints, isGitRepo, dirtySubmodules } from '../src/checkpoint.mjs' function git(project, args) { return execFileSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', ...args], { cwd: project, encoding: 'utf8' }) } function tempRepo() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-cp-')) git(dir, ['init', '-q']) fs.writeFileSync(path.join(dir, 'tracked.txt'), 'one\n') git(dir, ['add', '.']) git(dir, ['commit', '-qm', 'init']) return dir } test('createCheckpoint captures a patch and untracked list', () => { const project = tempRepo() const runDir = path.join(project, 'run') fs.appendFileSync(path.join(project, 'tracked.txt'), 'two\n') fs.writeFileSync(path.join(project, 'new.txt'), 'untracked\n') const cp = createCheckpoint(project, runDir, 'before iteration 1', { id: 'iter-01' }) assert.equal(cp.id, 'iter-01') assert.ok(cp.patchBytes > 0) assert.ok(cp.untracked.includes('new.txt')) assert.ok(fs.existsSync(cp.patchFile)) assert.equal(listCheckpoints(runDir).length, 1) }) test('restoreCheckpoint reverts tracked edits and removes new untracked files', () => { const project = tempRepo() const runDir = path.join(project, 'run') const cp = createCheckpoint(project, runDir, 'before iteration 1', { id: 'iter-01' }) fs.writeFileSync(path.join(project, 'tracked.txt'), 'changed\n') fs.writeFileSync(path.join(project, 'created-later.txt'), 'junk\n') const res = restoreCheckpoint(project, cp) assert.equal(res.ok, true) assert.equal(fs.readFileSync(path.join(project, 'tracked.txt'), 'utf8'), 'one\n') assert.equal(fs.existsSync(path.join(project, 'created-later.txt')), false) }) test('restoreCheckpoint keeps untracked files that existed at checkpoint time', () => { const project = tempRepo() const runDir = path.join(project, 'run') fs.writeFileSync(path.join(project, 'preexisting.txt'), 'keep me\n') const cp = createCheckpoint(project, runDir, 'before iteration 1', { id: 'iter-01' }) fs.writeFileSync(path.join(project, 'later.txt'), 'junk\n') restoreCheckpoint(project, cp) assert.equal(fs.existsSync(path.join(project, 'preexisting.txt')), true) assert.equal(fs.existsSync(path.join(project, 'later.txt')), false) }) test('restoreCheckpoint reproduces pre-existing changes captured at checkpoint time', () => { const project = tempRepo() const runDir = path.join(project, 'run') fs.writeFileSync(path.join(project, 'tracked.txt'), 'one\npreexisting\n') const cp = createCheckpoint(project, runDir, 'before iteration 1', { id: 'iter-01' }) fs.writeFileSync(path.join(project, 'tracked.txt'), 'totally different\n') const res = restoreCheckpoint(project, cp) assert.equal(res.ok, true) assert.equal(fs.readFileSync(path.join(project, 'tracked.txt'), 'utf8'), 'one\npreexisting\n') }) test('checkpoint helpers are no-ops outside a git repository', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-nogit-')) assert.equal(isGitRepo(dir), false) assert.equal(createCheckpoint(dir, path.join(dir, 'run'), 'x'), null) }) test('restoreCheckpoint refuses to run when a submodule is dirty', () => { const project = tempRepo() const sub = path.join(project, 'sub') fs.mkdirSync(sub) git(sub, ['init', '-q']) fs.writeFileSync(path.join(sub, 'x.txt'), 'a\n') git(sub, ['add', '.']) git(sub, ['commit', '-qm', 'sub init']) fs.writeFileSync( path.join(project, '.gitmodules'), `[submodule "sub"]\n\tpath = sub\n\turl = ./sub\n`, ) git(project, ['add', '.gitmodules']) git(project, ['commit', '-qm', 'add submodule']) // Dirty the submodule pointer by committing inside it and staging the pointer. fs.appendFileSync(path.join(sub, 'x.txt'), 'b\n') git(sub, ['commit', '-qam', 'sub change']) git(project, ['add', 'sub']) assert.deepEqual(dirtySubmodules(project), ['sub']) const cp = createCheckpoint(project, path.join(project, 'run'), 'before iteration 1', { id: 'iter-01' }) const res = restoreCheckpoint(project, cp) assert.equal(res.ok, false) assert.match(res.error, /submodule/i) })