test: preserve drafts at managed compaction input boundaries

This commit is contained in:
dev-guard committed 2026-11-01 13:45:30 +00:00
1 parent b25dd19a5a
commit 4220416ae5
1 file changed
+75 -7
@@ -27,7 +27,7 @@ function frame(body: string): ComposerSnapshot {
cursor: { x: 2 + stringWidth(body), y: 2, width: 100000, height: 60 }, inMode: false };
}
async function fixture(opts: { pane: string | null } = { pane: "%1" }) {
async function fixture(opts: { pane: string | null; mode?: "off" | "draft-aware"; footer?: boolean } = { pane: "%1" }) {
const db = new Database(":memory:");
migrate(db, ALL_MIGRATIONS);
db.exec(`INSERT INTO rigs(id,name) VALUES ('rig','test');
@@ -38,25 +38,32 @@ async function fixture(opts: { pane: string | null } = { pane: "%1" }) {
const guard = new SeatDeliveryGuard(db, name => resolveGuardTarget(db, name));
guard.recoverActivation();
let body = "", staged = "";
let onStage: (() => void) | undefined;
const snapshot = () => {
const s = frame(body);
if (opts.footer) s.screen += "\n✔ Update installed · /restart to apply";
return s;
};
const writes: string[] = [];
const tmux = new TmuxAdapter(async command => {
if (command.includes("paste-buffer")) { writes.push(`paste:${staged}`); body = staged; }
if (command.includes("paste-buffer")) { writes.push(`paste:${staged}`); body += staged; }
if (command.includes("send-keys") && command.includes("Enter")) { writes.push("enter"); body = ""; }
return "";
}, { writeFile: async (_path, text) => { staged = text; }, unlink: async () => {}, tmpName: () => "/fixture/compact-input", bufferName: () => "compact-input" });
}, { writeFile: async (_path, text) => { staged = text; onStage?.(); }, unlink: async () => {}, tmpName: () => "/fixture/compact-input", bufferName: () => "compact-input" });
tmux.deliveryGuard = guard;
vi.spyOn(tmux, "probeSession").mockResolvedValue({ state: "present" });
vi.spyOn(tmux, "listPanes").mockImplementation(async () => [{ id: "%1" } as never]);
vi.spyOn(tmux, "getPaneCommand").mockResolvedValue("node");
vi.spyOn(tmux, "captureComposerSnapshot").mockImplementation(async () => frame(body));
vi.spyOn(tmux, "capturePaneContent").mockImplementation(async () => frame(body).screen);
vi.spyOn(tmux, "captureComposerSnapshot").mockImplementation(async () => snapshot());
vi.spyOn(tmux, "capturePaneContent").mockImplementation(async () => snapshot().screen);
vi.spyOn(tmux, "getPaneCursorPosition").mockImplementation(async () => snapshot().cursor);
const transport = new SessionTransport({ db, tmuxAdapter: tmux, rigRepo: new RigRepository(db), sessionRegistry: new SessionRegistry(db),
sleep: (ms) => new Promise((r) => setTimeout(r, Math.min(ms, 5))), waitForIdlePollMs: 5 });
// Readiness is the variable under test: the pane says idle or working.
let activity = "idle";
(transport as any).classifySendReadiness = async () => ({ state: activity, reason: "fixture", evidenceSource: "pane_heuristic" });
(transport as any).claudeDeliveryObservation = async () => ({ state: "unknown", detail: "fixture" });
await guard.set("a", { mode: "draft-aware" }, "human@test", "draft-aware seat");
await guard.set("a", { mode: opts.mode ?? "draft-aware" }, "human@test", "fixture input policy");
const home = mkdtempSync(join(tmpdir(), "compaction-draft-aware-"));
let clock = 10_000;
const policy = { enabled: true, thresholdPercent: 80, preCompactInstruction: "Write a restore map.", compactInstruction: "", messageInline: "", messageFilePath: "", postRestoreAuditInstruction: "" };
@@ -74,9 +81,70 @@ async function fixture(opts: { pane: string | null } = { pane: "%1" }) {
const effects = () => ({ compactPastes: writes.filter((w) => w.startsWith("paste:/compact")).length, prepPastes: writes.filter((w) => w.startsWith("paste:") && !w.startsWith("paste:/compact")).length, enters: writes.filter((w) => w === "enter").length });
const compactRows = (state?: string) => (db.prepare("SELECT outbox_id AS id, delivery_state AS state FROM outbox_entries WHERE body LIKE '/compact%'").all() as Array<{ id: string; state: string }>)
.filter((r) => !state || r.state === state);
return { db, e, transport, writes, compactRows, rows, effects, stageMap, activity: (s: string) => { activity = s; }, draft: (text: string) => { body = text; } };
return { db, e, transport, tmux, writes, compactRows, rows, effects, stageMap, body: () => body,
onStage: (fn: () => void) => { onStage = fn; }, activity: (s: string) => { activity = s; }, draft: (text: string) => { body = text; } };
}
it.each([
{ footer: false, skipMap: false }, { footer: false, skipMap: true },
{ footer: true, skipMap: false }, { footer: true, skipMap: true },
])("managed preparation preserves a draft with typing guard off (footer=$footer, skipMap=$skipMap)", async ({ footer, skipMap }) => {
const f = await fixture({ pane: "%1", mode: "off", footer });
f.draft("an unsent message");
const outcome = await f.e.triggerManualCompact(input, { operatorInitiated: true, skipMap });
f.e.cancelPreparation(seat);
expect(outcome).toMatchObject({ triggered: false, reason: "draft_input_busy", preparation: { delivery: "not_sent", compact: "not_sent" } });
expect(f.body()).toBe("an unsent message");
expect(f.effects()).toEqual({ compactPastes: 0, prepPastes: 0, enters: 0 });
});
it("checks the composer after buffer setup, immediately before the preparation paste", async () => {
const f = await fixture({ pane: "%1", mode: "off", footer: true });
f.onStage(() => f.draft("typed while the buffer was prepared"));
const outcome = await f.e.triggerManualCompact(input, { operatorInitiated: true, skipMap: true });
expect(outcome).toMatchObject({ reason: "draft_input_busy", preparation: { delivery: "not_sent" } });
expect(f.body()).toBe("typed while the buffer was prepared");
expect(f.writes).toEqual([]);
});
it("an unreadable composer holds managed preparation without input", async () => {
const f = await fixture({ pane: "%1", mode: "off" });
vi.spyOn(f.tmux, "getPaneCursorPosition").mockResolvedValue(null);
const outcome = await f.e.triggerManualCompact(input, { operatorInitiated: true, skipMap: true });
expect(outcome).toMatchObject({ reason: "draft_input_unknown", preparation: { delivery: "not_sent" } });
expect(f.writes).toEqual([]);
});
it("a draft arriving after preparation prevents the managed command paste", async () => {
const f = await fixture({ pane: "%1", mode: "off" });
const send = f.transport.send.bind(f.transport);
vi.spyOn(f.transport, "send").mockImplementation(async (s, text, opts) => {
if (text.startsWith("/compact")) f.draft("draft before command");
return send(s, text, opts);
});
const outcome = await f.e.triggerManualCompact(input, { operatorInitiated: true, skipMap: true });
expect(outcome).toMatchObject({ reason: "draft_input_busy", preparation: { compact: "not_sent" } });
expect(f.body()).toBe("draft before command");
expect(f.effects()).toEqual({ compactPastes: 0, prepPastes: 1, enters: 1 });
});
it("an owed restore stage holds its place while the composer has a draft", async () => {
const f = await fixture({ pane: "%1", mode: "off" });
expect(await f.e.triggerManualCompact(input, { operatorInitiated: true, skipMap: true })).toMatchObject({ triggered: true });
f.draft("draft before restore");
const before = f.writes.length;
await f.e.maybeAutoCompact(input);
expect(f.body()).toBe("draft before restore");
expect(f.writes.length).toBe(before);
expect(f.e.getOwedRestore(seat)?.stage).toBe("turn_boundary");
});
it.each([false, true])("empty composer still permits managed preparation and command (footer=%s)", async footer => {
const f = await fixture({ pane: "%1", mode: "off", footer });
expect(await f.e.triggerManualCompact(input, { operatorInitiated: true, skipMap: true })).toMatchObject({ triggered: true });
expect(f.effects()).toEqual({ compactPastes: 1, prepPastes: 1, enters: 2 });
});
it("automatic, recorded pane (existing-behaviour control, green before this PR's fixes): busy then idle, the lifecycle lease refuses before any send, so no /compact and no held row", async () => {
const f = await fixture();
expect(f.transport.guardedDelivery).toBeDefined();