Merge OPR.99.0.6.3 send verify outcomes

This commit is contained in:
mvschwarz committed 2026-06-11 04:01:04 -07:00
commit 5f5bb4f363
5 files changed
+210 -3

No files matched your search

+10
View File
@@ -139,8 +139,18 @@ via single-hop ssh. SSH success is NOT verify success: the remote rig's
console.log(`Sent to ${session}`);
if (opts.verify) {
// Legacy line preserved verbatim (existing scripts grep `Verified:`);
// the Delivery line below carries the honest three-outcome vocabulary
// (OPR.99.0.6.3): `Verified: no` alone collapsed a landed-but-redraw-
// raced send into the same line as a miss.
const verified = res.data["verified"] as boolean | undefined;
console.log(`Verified: ${verified ? "yes" : "no"}`);
const outcome = res.data["outcome"] as string | undefined;
if (outcome === "delivered") {
console.log("Delivery: delivered (message landed; render confirmed)");
} else if (outcome === "rendered-unconfirmed") {
console.log(`Delivery: rendered-unconfirmed (landed; pane re-render not confirmed - confirm with: rig capture ${session})`);
}
}
});
+55
View File
@@ -69,6 +69,15 @@ describe("Send CLI", () => {
if (parsed.session === "dev-impl@my-rig") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, sessionName: "dev-impl@my-rig" }));
} else if (parsed.session === "verified-session") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, sessionName: "verified-session", verified: true, outcome: "delivered" }));
} else if (parsed.session === "racy-session") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, sessionName: "racy-session", verified: false, outcome: "rendered-unconfirmed" }));
} else if (parsed.session === "dead-session") {
res.writeHead(502, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, sessionName: "dead-session", reason: "submit_failed", outcome: "failed", error: "Text is visible in 'dead-session' but was not submitted (Enter failed)." }));
} else if (parsed.session === "busy-session") {
res.writeHead(409, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, sessionName: "busy-session", reason: "mid_work", error: "Target pane appears mid-task. Use force: true to send anyway." }));
@@ -113,6 +122,52 @@ describe("Send CLI", () => {
expect(exitCode).toBe(1);
});
// OPR.99.0.6.3 — honest delivery-outcome vocabulary; legacy Verified: line preserved.
it("verify confirmed prints Delivery: delivered AND the legacy Verified: yes", async () => {
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "send", "verified-session", "hello", "--verify"]);
});
const output = logs.join("\n");
expect(output).toContain("Sent to verified-session");
expect(output).toContain("Verified: yes");
expect(output).toContain("Delivery: delivered");
});
it("verify redraw-race prints Delivery: rendered-unconfirmed (landed, with capture guidance) AND legacy Verified: no", async () => {
const { logs, exitCode } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "send", "racy-session", "hello", "--verify"]);
});
const output = logs.join("\n");
expect(output).toContain("Sent to racy-session");
expect(output).toContain("Verified: no");
expect(output).toContain("Delivery: rendered-unconfirmed");
expect(output).toContain("landed");
expect(output).toContain("rig capture racy-session");
// The middle is NOT dressed as failure: exit stays clean.
expect(exitCode).toBeUndefined();
});
it("verify genuine transport failure stays an error path, distinct from the middle (discriminator)", async () => {
const { logs, exitCode } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "send", "dead-session", "hello", "--verify"]);
});
const output = logs.join("\n");
// HTTP 502 -> error branch; no success lines, exit non-zero.
expect(output).not.toContain("Sent to dead-session");
expect(output).not.toContain("Delivery: rendered-unconfirmed");
expect(output).toContain("not submitted");
expect(exitCode).toBe(2);
});
it("verify --json passes the additive outcome field through", async () => {
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "send", "racy-session", "hello", "--verify", "--json"]);
});
const parsed = JSON.parse(logs.join("\n"));
expect(parsed.verified).toBe(false);
expect(parsed.outcome).toBe("rendered-unconfirmed");
});
it("send --json prints raw JSON", async () => {
const { logs } = await captureLogs(async () => {
await makeCmd().parseAsync(["node", "rig", "send", "dev-impl@my-rig", "hello", "--json"]);
@@ -311,6 +311,21 @@ export interface SendResult {
ok: boolean;
sessionName: string;
verified?: boolean;
/**
* OPR.99.0.6.3 — honest delivery-outcome vocabulary (additive; `verified`
* keeps its exact semantics for existing parsers). Three distinguishable
* states, mirroring the restore honest-outcome style:
* - `delivered`: text + Enter landed AND the post-send capture re-confirmed
* the snippet (the strong positive; was `Verified: yes`).
* - `rendered-unconfirmed`: text + Enter BOTH succeeded (the message landed)
* but the post-send capture raced a TUI redraw and could not re-confirm
* the snippet. Landed-but-unconfirmable, NOT a failure — confirm with
* `rig capture` if it matters. (Was collapsed into `Verified: no`.)
* - `failed`: the transport itself failed (paste or Enter did not land) —
* set on the send_failed / submit_failed returns for vocabulary symmetry;
* their `ok:false` + HTTP mapping is unchanged.
*/
outcome?: "delivered" | "rendered-unconfirmed" | "failed";
warning?: string;
error?: string;
reason?: string;
@@ -706,6 +721,7 @@ export class SessionTransport {
ok: false,
sessionName,
reason: "send_failed",
outcome: "failed",
error: `Failed to send text to '${sessionName}': ${textResult.message}`,
...(waitMode ? { sent: false, ...waitEvidence } : {}),
};
@@ -721,12 +737,16 @@ export class SessionTransport {
ok: false,
sessionName,
reason: "submit_failed",
outcome: "failed",
error: `Text is visible in '${sessionName}' but was not submitted (Enter failed). The agent may need manual attention.`,
...(waitMode ? { sent: true, ...waitEvidence } : {}),
};
}
// 6. Verify if requested
// 6. Verify if requested. At this point text + Enter BOTH succeeded, so the
// message LANDED; the capture only re-confirms the render. Not re-confirming
// (a TUI redraw race, or the capture throwing) is therefore the honest
// middle outcome `rendered-unconfirmed` — never a failure (OPR.99.0.6.3).
if (opts?.verify) {
await this.sleep(500);
try {
@@ -735,9 +755,9 @@ export class SessionTransport {
const preCount = countOccurrences(preVerifyContent ?? "", snippet);
const postCount = countOccurrences(content ?? "", snippet);
const verified = postCount > preCount;
return { ok: true, sessionName, verified, ...(waitMode ? { sent: true, ...waitEvidence } : {}) };
return { ok: true, sessionName, verified, outcome: verified ? "delivered" : "rendered-unconfirmed", ...(waitMode ? { sent: true, ...waitEvidence } : {}) };
} catch {
return { ok: true, sessionName, verified: false, ...(waitMode ? { sent: true, ...waitEvidence } : {}) };
return { ok: true, sessionName, verified: false, outcome: "rendered-unconfirmed", ...(waitMode ? { sent: true, ...waitEvidence } : {}) };
}
}
@@ -334,6 +334,8 @@ describe("SessionTransport", () => {
const result = await transport.send("dev-impl@my-rig", "hello", { verify: true });
expect(result.ok).toBe(true);
expect(result.verified).toBe(true);
// OPR.99.0.6.3: a confirmed render is the strong positive outcome.
expect(result.outcome).toBe("delivered");
});
it("send with verify does not false-positive on pre-existing pane content", async () => {
@@ -347,6 +349,79 @@ describe("SessionTransport", () => {
expect(result.ok).toBe(true);
expect(result.verified).toBe(false);
// OPR.99.0.6.3: text + Enter both succeeded, only the render re-confirm
// missed — the honest middle, NOT a failure.
expect(result.outcome).toBe("rendered-unconfirmed");
});
// OPR.99.0.6.3 — honest delivery-outcome vocabulary
it("verify capture throwing after a successful send is the middle outcome, not a failure", async () => {
seedCanonicalRig();
let captureCount = 0;
const tmux = mockTmux({
capturePaneContent: async () => {
captureCount++;
// Pre-verify + mid-work captures succeed; the post-send verify capture throws
// (e.g. pane busy mid-redraw).
if (captureCount >= 3) throw new Error("pane busy");
return "some output\n❯ ";
},
});
const transport = createTransport(tmux);
const result = await transport.send("dev-impl@my-rig", "hello", { verify: true });
expect(result.ok).toBe(true);
expect(result.verified).toBe(false);
expect(result.outcome).toBe("rendered-unconfirmed");
});
it("DISCRIMINATOR: a redraw-race send and a genuine transport failure surface differently", async () => {
seedCanonicalRig();
// Redraw-race: send + submit succeed, post-capture cannot re-confirm.
const racyTmux = mockTmux({
capturePaneContent: async () => "prior output\nhello\n❯ ",
});
const middle = await createTransport(racyTmux).send("dev-impl@my-rig", "hello", { verify: true });
// Genuine transport failure: Enter does not land.
const brokenTmux = mockTmux({
sendKeys: async () => ({ ok: false, code: "session_not_found", message: "session died" }),
});
const failure = await createTransport(brokenTmux).send("dev-impl@my-rig", "hello", { verify: true });
// The acceptance criterion: the two states are NOT equal in surfaced outcome.
expect(middle.ok).toBe(true);
expect(middle.outcome).toBe("rendered-unconfirmed");
expect(failure.ok).toBe(false);
expect(failure.outcome).toBe("failed");
expect(middle.outcome).not.toBe(failure.outcome);
});
it("send_failed and submit_failed carry outcome 'failed' (vocabulary symmetry, ok:false unchanged)", async () => {
seedCanonicalRig();
const noPaste = mockTmux({
sendText: async () => ({ ok: false, code: "session_not_found", message: "gone" }),
});
const sendFailed = await createTransport(noPaste).send("dev-impl@my-rig", "hello");
expect(sendFailed.ok).toBe(false);
expect(sendFailed.reason).toBe("send_failed");
expect(sendFailed.outcome).toBe("failed");
const noEnter = mockTmux({
sendKeys: async () => ({ ok: false, code: "session_not_found", message: "gone" }),
});
const submitFailed = await createTransport(noEnter).send("dev-impl@my-rig", "hello");
expect(submitFailed.ok).toBe(false);
expect(submitFailed.reason).toBe("submit_failed");
expect(submitFailed.outcome).toBe("failed");
});
it("send without verify carries no outcome field (additive, verify-scoped)", async () => {
seedCanonicalRig();
const transport = createTransport(mockTmux());
const result = await transport.send("dev-impl@my-rig", "hello");
expect(result.ok).toBe(true);
expect(result.outcome).toBeUndefined();
});
// Test 7: send with mid-work detected → refusal
@@ -116,6 +116,53 @@ describe("transport routes", () => {
expect(body.sessionName).toBe("dev-impl@my-rig");
});
// OPR.99.0.6.3 — the additive outcome field auto-surfaces through the
// c.json(result) passthrough; failure HTTP mapping unchanged.
it("POST /send with verify surfaces the outcome field in the JSON response (passthrough)", async () => {
seedRig();
// Pre-existing pane content means the post-capture cannot re-confirm:
// the redraw-race middle outcome.
const tmux = {
...mockTmux(),
capturePaneContent: async () => "idle\nhello\n❯ ",
} as unknown as TmuxAdapter;
const transport = new SessionTransport({ db, rigRepo, sessionRegistry, tmuxAdapter: tmux });
const app = createApp({ sessionTransport: transport });
const res = await app.request("/api/transport/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session: "dev-impl@my-rig", text: "hello", verify: true }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.ok).toBe(true);
expect(body.verified).toBe(false);
expect(body.outcome).toBe("rendered-unconfirmed");
});
it("POST /send verify failure stays HTTP-error-distinct from the middle outcome (discriminator)", async () => {
seedRig();
const tmux = {
...mockTmux(),
sendKeys: async () => ({ ok: false as const, code: "session_not_found", message: "session died" }),
} as unknown as TmuxAdapter;
const transport = new SessionTransport({ db, rigRepo, sessionRegistry, tmuxAdapter: tmux });
const app = createApp({ sessionTransport: transport });
const res = await app.request("/api/transport/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ session: "dev-impl@my-rig", text: "hello", verify: true }),
});
// submit_failed maps to 502 (unchanged); the middle outcome above is 200.
expect(res.status).toBe(502);
const body = await res.json();
expect(body.ok).toBe(false);
expect(body.reason).toBe("submit_failed");
expect(body.outcome).toBe("failed");
});
it("POST /send with mid-work refusal returns 409", async () => {
seedRig();
const tmux = {