mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-11 17:32:22 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b3917cea0 | ||
|
|
64920dadb3 | ||
|
|
6994c61e35 | ||
|
|
5119324bbd |
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
COMPOSER_DRAFT_PATTERN,
|
||||
COMPOSER_EMPTY_CODEX_PLACEHOLDER_PATTERN as CODEX_EMPTY_COMPOSER_PATTERN,
|
||||
COMPOSER_EMPTY_PATTERN,
|
||||
COMPOSER_PROMPT_CLASS,
|
||||
COMPOSER_SELECTION_PATTERN,
|
||||
COMPOSER_SELECTION_SCAN_PATTERN,
|
||||
findComposerInputLineIndex,
|
||||
@@ -682,7 +683,9 @@ function verificationSnippet(text: string): { snippet: string; envelope: boolean
|
||||
/** Count whole visible envelope blocks, not prefixes of longer messages.
|
||||
* A new envelope, prompt or same-column output ends an indented user block.
|
||||
* Whitespace is compared only after retaining boundaries and quote indentation. */
|
||||
function countEnvelopeOccurrences(transcript: string, expected: string): number {
|
||||
function countEnvelopeOccurrences(transcript: string, expected: string, promptClass: string): number {
|
||||
const promptPrefix = new RegExp(`^[${promptClass}]\\s+`);
|
||||
const promptBoundary = new RegExp(`^[${promptClass}](?:\\s|$)`);
|
||||
const lines = transcript.split("\n");
|
||||
let count = 0;
|
||||
let block: string[] | null = null;
|
||||
@@ -696,8 +699,8 @@ function countEnvelopeOccurrences(transcript: string, expected: string): number
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]!;
|
||||
const indent = /^[ \t]*/.exec(line)![0].length;
|
||||
const prompted = /^[❯›]\s+/.test(line.slice(indent));
|
||||
const header = line.slice(indent).replace(/^[❯›]\s+/, "");
|
||||
const prompted = promptPrefix.test(line.slice(indent));
|
||||
const header = line.slice(indent).replace(promptPrefix, "");
|
||||
const atBoundary = (!block && !inAgentOutput) || indent <= blockIndent;
|
||||
const candidate = normalizeVerificationText([header, ...lines.slice(i + 1, i + 5)].join("\n"));
|
||||
const envelopeHeader = /^From: [^\n]+\nTo: [^\n]+\n(?:Sent: [^\n]+\n)?---\n/.test(candidate);
|
||||
@@ -714,7 +717,7 @@ function countEnvelopeOccurrences(transcript: string, expected: string): number
|
||||
// quotes stay in the body, and blank separators do not change its text.
|
||||
finish();
|
||||
inAgentOutput = true;
|
||||
} else if (atBoundary && /^[❯›](?:\s|$)/.test(line.slice(indent))) {
|
||||
} else if (atBoundary && promptBoundary.test(line.slice(indent))) {
|
||||
finish();
|
||||
} else if (block) {
|
||||
block.push(line);
|
||||
@@ -727,15 +730,16 @@ function countEnvelopeOccurrences(transcript: string, expected: string): number
|
||||
/** The last prompt marker owns the current input region. Never count it or its
|
||||
* continuation lines as submitted transcript, in either capture. If a capture
|
||||
* ends at a historical prompt with no later input marker, leave it unconfirmed. */
|
||||
function verificationTranscript(content: string | null): string {
|
||||
function verificationTranscript(content: string | null, promptClass: string): string {
|
||||
const indentedPrompt = new RegExp(`^([ \\t]*)[${promptClass}]`);
|
||||
const lines = (content ?? "").replace(/\r\n/g, "\n").split("\n");
|
||||
let inputAt = lastPromptLine(lines);
|
||||
let inputAt = findComposerInputLineIndex(lines, promptClass);
|
||||
if (inputAt >= 0) {
|
||||
let indent = /^[ \t]*/.exec(lines[inputAt]!)![0].length;
|
||||
// Claude indents draft continuation lines. A quoted prompt on one of them
|
||||
// belongs to the enclosing input, not a new transcript/composer boundary.
|
||||
for (let i = inputAt - 1; i >= 0; i--) {
|
||||
const prompt = /^([ \t]*)[❯›]/.exec(lines[i]!);
|
||||
const prompt = indentedPrompt.exec(lines[i]!);
|
||||
if (prompt && prompt[1]!.length < indent) {
|
||||
inputAt = i;
|
||||
indent = prompt[1]!.length;
|
||||
@@ -2097,9 +2101,12 @@ export class SessionTransport {
|
||||
// its original prefix-count verification even while a command is running.
|
||||
const terminal = runtime === "terminal";
|
||||
const { snippet, envelope } = terminal ? { snippet: text.slice(0, 40), envelope: false } : verificationSnippet(text);
|
||||
// Keep Claude's existing verification boundaries; Codex also draws ».
|
||||
const promptClass = runtime === "claude-code" ? "❯›" : COMPOSER_PROMPT_CLASS;
|
||||
const transcript = terminal ? (pane: string | null) => pane ?? ""
|
||||
: envelope ? verificationTranscript : (pane: string | null) => normalizeVerificationText(verificationTranscript(pane));
|
||||
const count = envelope ? countEnvelopeOccurrences : countOccurrences;
|
||||
: (pane: string | null) => envelope ? verificationTranscript(pane, promptClass)
|
||||
: normalizeVerificationText(verificationTranscript(pane, promptClass));
|
||||
const count = envelope ? (pane: string, expected: string) => countEnvelopeOccurrences(pane, expected, promptClass) : countOccurrences;
|
||||
const preCount = count(transcript(preVerifyContent), snippet);
|
||||
const postCount = count(transcript(content), snippet);
|
||||
const verified = postCount > preCount;
|
||||
|
||||
@@ -543,10 +543,10 @@ describe("SessionTransport", () => {
|
||||
});
|
||||
}
|
||||
|
||||
function seedCanonicalRig() {
|
||||
function seedCanonicalRig(runtime = "claude-code") {
|
||||
const rig = rigRepo.createRig("my-rig");
|
||||
const node = rigRepo.addNode(rig.id, "dev.impl", {
|
||||
role: "worker", runtime: "claude-code",
|
||||
role: "worker", runtime,
|
||||
});
|
||||
const session = sessionRegistry.registerSession(node.id, "dev-impl@my-rig");
|
||||
sessionRegistry.updateStatus(session.id, "running");
|
||||
@@ -894,6 +894,82 @@ describe("SessionTransport", () => {
|
||||
expect(result).toMatchObject({ ok: true, verified: false, outcome: "rendered-unconfirmed" });
|
||||
});
|
||||
|
||||
it.each(["❯", "›", "»"].flatMap(glyph =>
|
||||
["raw staged", "raw submitted", "envelope staged", "envelope submitted", "quoted draft", "quoted prompt in draft", "after reply", "flat transcript boundary"]
|
||||
.map(mode => [glyph, mode] as const),
|
||||
))("send verification recognises composer %s: %s", async (glyph, mode) => {
|
||||
seedCanonicalRig(glyph === "❯" ? "claude-code" : "codex");
|
||||
const message = mode.startsWith("raw") ? "Please check this task."
|
||||
: "From: sender-agent@demo\nTo: dev-impl@my-rig\n---\nPlease check this task.";
|
||||
const render = (text: string) => glyph + " " + text.split("\n").join("\n ");
|
||||
const verified = mode === "raw submitted" || mode === "envelope submitted" || mode === "after reply" || mode === "flat transcript boundary";
|
||||
let submitted = false;
|
||||
const tmux = mockTmux({
|
||||
sendKeys: async () => { submitted = true; return { ok: true }; },
|
||||
capturePaneContent: async () => {
|
||||
if (!submitted) return "Prior output\n" + glyph + " ";
|
||||
if (mode === "quoted draft") return glyph + " Check this envelope:\n " + message.split("\n").join("\n ");
|
||||
if (mode === "quoted prompt in draft") return render(message) + "\n " + glyph + " quoted prompt";
|
||||
if (mode === "flat transcript boundary") return message + "\n" + glyph + " previous input\nAgent reply\n" + glyph + " ";
|
||||
const previous = mode === "after reply"
|
||||
? render(message.replace("Please check this task.", "A different task.")) + "\nAgent reply\n" : "";
|
||||
return previous + render(message) + (verified ? "\n" + glyph + " " : "");
|
||||
},
|
||||
});
|
||||
const result = await createTransport(tmux, { sleep: async () => undefined })
|
||||
.send("dev-impl@my-rig", message, { verify: true });
|
||||
expect(result).toMatchObject({ ok: true, verified,
|
||||
outcome: verified ? "delivered" : "rendered-unconfirmed" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["raw", "same-column below draft"], ["raw", "below framed draft"],
|
||||
["envelope", "same-column below draft"], ["envelope", "below framed draft"],
|
||||
["envelope", "assistant quoted envelope"],
|
||||
["envelope", "flat body »"], ["envelope", "flat body ›"], ["envelope", "flat body ❯"],
|
||||
])("Claude verification keeps existing boundaries: %s %s", async (kind, shape) => {
|
||||
const rig = rigRepo.createRig("verification");
|
||||
const node = rigRepo.addNode(rig.id, "worker", { role: "worker", runtime: "claude-code" });
|
||||
const address = "worker@verification";
|
||||
const session = sessionRegistry.registerSession(node.id, address);
|
||||
sessionRegistry.updateStatus(session.id, "running");
|
||||
sessionRegistry.updateBinding(node.id, { tmuxSession: address });
|
||||
let message = kind === "raw" ? "Check the blue ledger, then report its total."
|
||||
: `From: sender@fixture\nTo: ${address}\nSent: 10-13 10:00Z\n---\nCheck the blue ledger.\nReport its total.\n---\n↩ Reply: rig send sender@fixture "..."`;
|
||||
const render = (glyph: string, text: string) => glyph + " " + text.split("\n").join("\n ");
|
||||
let post = render("❯", message) + "\n» reply quotation below live input";
|
||||
if (shape === "below framed draft") post = "────────────────────\n" + render("❯", message) + "\n────────────────────\n? for shortcuts\n» later quote";
|
||||
if (shape === "assistant quoted envelope") post = "❯ Different request\nAssistant reply with quotation:\n" + render("»", message) + "\n❯ ";
|
||||
if (shape.startsWith("flat body")) {
|
||||
message = message.replace("Check the blue ledger.", shape.slice(-1) + " Check the blue ledger.");
|
||||
post = message + "\n❯ ";
|
||||
}
|
||||
let submitted = false;
|
||||
const tmux = mockTmux({
|
||||
sendKeys: async () => { submitted = true; return { ok: true }; },
|
||||
capturePaneContent: async () => submitted ? post : "❯ ",
|
||||
});
|
||||
const result = await createTransport(tmux, { sleep: async () => undefined }).send(address, message, { verify: true });
|
||||
const verified = shape === "flat body »";
|
||||
expect(result).toMatchObject({ ok: true, verified,
|
||||
outcome: verified ? "delivered" : "rendered-unconfirmed" });
|
||||
});
|
||||
|
||||
it("unknown-runtime verification keeps existing boundaries", async () => {
|
||||
const { node } = seedCanonicalRig();
|
||||
db.prepare("UPDATE nodes SET runtime = NULL WHERE id = ?").run(node.id);
|
||||
expect(db.prepare("SELECT runtime FROM nodes WHERE id = ?").get(node.id)).toEqual({ runtime: null });
|
||||
const message = "Check the blue ledger, then report its total.";
|
||||
let submitted = false;
|
||||
const tmux = mockTmux({
|
||||
sendKeys: async () => { submitted = true; return { ok: true }; },
|
||||
capturePaneContent: async () => submitted ? "❯ " + message + "\n» reply quotation below live input" : "❯ ",
|
||||
});
|
||||
const result = await createTransport(tmux, { sleep: async () => undefined })
|
||||
.send("dev-impl@my-rig", message, { verify: true });
|
||||
expect(result).toMatchObject({ ok: true, verified: false, outcome: "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();
|
||||
|
||||
Reference in new issue
Block a user