Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152import { describe, expect, it } from "vitest";import { closeLock, describeLockReport, openLock, type LockIo } from "../../runner/lockRun";import { lockPathFor, serializeLock, LockStore } from "../../runner/lockfile";import type { ElementDescriptor } from "../../shared/descriptor";
const d = (text: string): ElementDescriptor => ({ tag: "button", role: "button", name: text, text, attrs: { "aria-label": null, "data-testid": null, id: null, name: null, type: null, placeholder: null }, classes: [], css: "button", xpath: "/html/body/button", near: { heading: null, previousText: null, nextText: null }, rect: { x: 0, y: 0, w: 1, h: 1 },});
function io(files: Record<string, string> = {}, over: Partial<LockIo> = {}) { const writes: Array<{ path: string; text: string }> = []; const impl: LockIo = { fileExists: (p) => p in files, readFile: (p) => { if (!(p in files)) throw new Error("ENOENT"); return files[p]!; }, writeFile: (p, data) => { writes.push({ path: p, text: data.toString("utf8") }); files[p] = data.toString("utf8"); }, now: () => Date.UTC(2026, 9, 11, 0, 0, 0, 123), ...over, }; return { io: impl, writes, files };}
describe("lockPathFor", () => { it("is the script's path with .lock.json after it", () => { expect(lockPathFor("scripts/send/send.aite")).toBe("scripts/send/send.aite.lock.json"); });});
describe("openLock / closeLock (P3-1.a to P3-1.c)", () => { it("a script with no lock file runs, and the first capture creates the file in the documented shape", () => { const { io: x, writes } = io(); const opened = openLock("a.aite", x); expect(opened.store).toBeDefined(); expect(opened.problem).toBeUndefined(); opened.store!.capture("step", 'text:"Go"', d("Go")); const report = closeLock(opened, x); expect(report.status).toBe("created"); expect(report).toMatchObject({ path: "a.aite.lock.json", captured: 1, reused: 0 }); expect(writes).toHaveLength(1); const written = JSON.parse(writes[0]!.text) as { version: number; targets: Record<string, { source: string; seen: string; lastRepaired: null }> }; expect(written.version).toBe(1); const entry = Object.values(written.targets)[0]!; expect(entry.source).toBe('text:"Go"'); expect(entry.seen).toBe("2026-10-11T00:00:00Z"); // seconds, no milliseconds expect(entry.lastRepaired).toBeNull(); }); it("a second run reads the file, writes nothing, and the file is byte-identical", () => { const first = io(); const o1 = openLock("a.aite", first.io); o1.store!.capture("step", 'text:"Go"', d("Go")); closeLock(o1, first.io); const before = first.files["a.aite.lock.json"]; const later = io(first.files, { now: () => Date.UTC(2030, 0, 1) }); const o2 = openLock("a.aite", later.io); expect(o2.store!.find("step", 'text:"Go"')?.text).toBe("Go"); o2.store!.capture("step", 'text:"Go"', d("Go")); const r2 = closeLock(o2, later.io); expect(r2.status).toBe("unchanged"); expect(later.writes).toHaveLength(0); expect(first.files["a.aite.lock.json"]).toBe(before); }); it("a new target in a later run is added and the file is updated, old entries untouched", () => { const one = io(); const o1 = openLock("a.aite", one.io); o1.store!.capture("s", "t1", d("One")); closeLock(o1, one.io); const o2 = openLock("a.aite", one.io); o2.store!.capture("s", "t2", d("Two")); const r = closeLock(o2, one.io); expect(r.status).toBe("updated"); expect(r.captured).toBe(1); const again = openLock("a.aite", one.io); expect(again.store!.find("s", "t1")?.text).toBe("One"); expect(again.store!.find("s", "t2")?.text).toBe("Two"); }); it("no file is created when nothing was captured", () => { const { io: x, writes } = io(); const o = openLock("a.aite", x); const r = closeLock(o, x); expect(r.status).toBe("none"); expect(writes).toHaveLength(0); }); it("an unreadable lock file is left alone: the run goes on without it, and nothing is written over it", () => { for (const bad of ["not json", JSON.stringify({ version: 9, targets: {} })]) { const { io: x, writes, files } = io({ "a.aite.lock.json": bad }); const o = openLock("a.aite", x); expect(o.store).toBeUndefined(); expect(o.problem).toMatch(/lock file/); const r = closeLock(o, x); expect(r.status).toBe("unreadable"); expect(r.message).toBeDefined(); expect(writes).toHaveLength(0); expect(files["a.aite.lock.json"]).toBe(bad); } }); it("a file that exists but cannot be read is also left alone", () => { const { io: x } = io({ "a.aite.lock.json": "{}" }, { readFile: () => { throw new Error("EACCES: permission denied"); } }); const o = openLock("a.aite", x); expect(o.store).toBeUndefined(); expect(o.problem).toMatch(/EACCES/); }); it("a failed write is reported, not thrown, and the run's own result is unaffected", () => { const { io: x } = io({}, { writeFile: () => { throw new Error("EROFS: read-only file system"); } }); const o = openLock("a.aite", x); o.store!.capture("s", "t", d("Go")); const r = closeLock(o, x); expect(r.status).toBe("write-failed"); expect(r.message).toMatch(/EROFS/); }); it("an existing file whose entry has a different source is ignored and never overwritten", () => { const store = new LockStore(undefined, () => "2026-01-01T00:00:00Z"); store.capture("s", "other", d("Other")); const lock = store.toFile(); const key = Object.keys(lock.targets)[0]!; // the same key now stands for a different target in the script const tampered = { ...lock, targets: { [key]: { ...lock.targets[key]!, source: "something else" } } }; const { io: x, writes } = io({ "a.aite.lock.json": serializeLock(tampered) }); const o = openLock("a.aite", x); o.store!.capture("s", "other", d("Other")); const r = closeLock(o, x); expect(r.ignored).toBe(1); expect(r.status).toBe("unchanged"); expect(writes).toHaveLength(0); });});
describe("describeLockReport", () => { const base = { path: "a.aite.lock.json", captured: 0, reused: 0, ignored: 0, repaired: 0 }; it("says nothing when nothing changed", () => { expect(describeLockReport({ ...base, status: "unchanged" })).toBeUndefined(); expect(describeLockReport({ ...base, status: "none" })).toBeUndefined(); }); it("tells a person a lock file was created or changed, since it belongs in the commit", () => { expect(describeLockReport({ ...base, status: "created", captured: 2 })).toBe("lock file created: a.aite.lock.json (2 targets captured; commit it)"); expect(describeLockReport({ ...base, status: "updated", captured: 1 })).toBe("lock file updated: a.aite.lock.json (1 target captured; commit it)"); expect(describeLockReport({ ...base, status: "updated", repaired: 2 })).toBe("lock file updated: a.aite.lock.json (2 repaired; commit it)"); expect(describeLockReport({ ...base, status: "updated", captured: 1, repaired: 1 })).toBe("lock file updated: a.aite.lock.json (1 target captured, 1 repaired; commit it)"); }); it("warns about an unreadable file, a failed write and ignored entries", () => { expect(describeLockReport({ ...base, status: "unreadable", message: "not valid JSON" })).toBe("warning: lock file a.aite.lock.json not used: not valid JSON"); expect(describeLockReport({ ...base, status: "write-failed", captured: 1, message: "EROFS" })).toBe("warning: could not write lock file a.aite.lock.json: EROFS"); expect(describeLockReport({ ...base, status: "unchanged", ignored: 2 })).toBe("warning: 2 entries in a.aite.lock.json do not match their targets and were ignored"); });});