mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
test: preserve drafts at managed compaction input boundaries
This commit is contained in:
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();
|
||||
|
||||
Reference in new issue
Block a user