fix: route document asks to comment-first askClaude (plan T6 Step 4); machine-author guard + offer cleanup
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,4 +44,47 @@ suite("comment → reply → offer → proposal (PUC-8, D10/D19)", () => {
|
||||
const id = await api.threadController.createThreadOnSelection("hello");
|
||||
assert.strictEqual(id, undefined); // gate refuses thread creation entirely
|
||||
});
|
||||
|
||||
// Finding 1 (code review, session native-surfaces-exec): `cowriting.edit` with NO
|
||||
// selection used to route through `cowriting.editDocument` → the removed
|
||||
// `askEditInstruction` webview prompt — a rejecting stub `void`'d before any
|
||||
// try/catch, i.e. a completely silent dead end (no toast, no thread). It must now
|
||||
// reach ThreadController.askClaude()'s top-anchored whole-document path (D19) —
|
||||
// the same comments-first ask the selection route already used. There is no API to
|
||||
// drive VS Code's native comment-widget submission from a host E2E test (the actual
|
||||
// comment→reply→offer→proposal round trip is exercised above via the
|
||||
// createThreadOnSelection seam), so this asserts the routing itself: the gesture
|
||||
// reaches ThreadController.askClaude (never the old broken stub) and does not throw.
|
||||
test("cowriting.edit with no selection on a coedited doc reaches the top-anchored ask path, not a silent dead end", async () => {
|
||||
const api = await activateApi();
|
||||
const doc = await vscode.workspace.openTextDocument({
|
||||
language: "markdown",
|
||||
content: "# T\n\nA document-level ask with no selection.\n",
|
||||
});
|
||||
const ed = await vscode.window.showTextDocument(doc);
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
// Defensive stubs, mirroring the suite's other test — no real turn should fire
|
||||
// from this gesture alone (no comment text is ever submitted), but guard against
|
||||
// a real @cline/sdk call if the routing regresses.
|
||||
api.threadController.setTurnRunnerForTest(async () => ({ replacement: "stub", model: "stub", sessionId: "s1" }));
|
||||
api.editFlow.setEditTurnForTest(async () => ({ replacement: "stub", model: "stub", sessionId: "s1" }));
|
||||
ed.selection = new vscode.Selection(0, 0, 0, 0); // no selection
|
||||
|
||||
let askClaudeCalls = 0;
|
||||
const origAskClaude = api.threadController.askClaude.bind(api.threadController);
|
||||
api.threadController.askClaude = async () => {
|
||||
askClaudeCalls++;
|
||||
return origAskClaude();
|
||||
};
|
||||
try {
|
||||
await vscode.commands.executeCommand("cowriting.edit");
|
||||
} finally {
|
||||
api.threadController.askClaude = origAskClaude;
|
||||
}
|
||||
assert.strictEqual(
|
||||
askClaudeCalls,
|
||||
1,
|
||||
"cowriting.edit (no selection) reached ThreadController.askClaude — not the removed editDocument prompt stub",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,9 +72,17 @@ suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
|
||||
|
||||
// PUC-3 behavior: editDocument invoked with a tab URI targets THAT document,
|
||||
// not whatever editor happens to be active (mirrors #41's clicked-doc resolution).
|
||||
// Finding 1 fix (native-surfaces code review): editDocument no longer prompts via
|
||||
// the removed askEditInstruction webview stub (that path threw a silent, `void`'d,
|
||||
// unhandled rejection — no toast, no thread, no proposal). It now resolves/focuses
|
||||
// its target document and hands off to ThreadController.askClaude() (D19), same as
|
||||
// editSelection. The turn→proposal cut itself (EditFlow.runEditAndPropose against a
|
||||
// {kind:"document"} target) is exercised directly, without the command, in
|
||||
// f11Toolbar.test.ts / f12Accept.test.ts / f12Review.test.ts — this test's job is
|
||||
// the URI-targeting/focus behavior, plus confirming the routing actually reaches
|
||||
// askClaude (proving Finding 1's dead end is gone).
|
||||
test("editDocument(uri) targets the clicked tab's document, not the active editor", async () => {
|
||||
const api = await getApi();
|
||||
const editFlow = api.editFlow;
|
||||
|
||||
// Doc A is the active editor; Doc B is the "clicked tab" we pass by URI.
|
||||
const a = await freshDoc("docs/f12-active.md", "# Active\n\nThe active editor paragraph.\n");
|
||||
@@ -82,55 +90,68 @@ suite("F12 SLICE-1 — Ask-Claude reach (#42, INV-38)", () => {
|
||||
await vscode.window.showTextDocument(a.doc);
|
||||
await settle();
|
||||
|
||||
// Stub the document instruction prompt (the webview can't run in CI) + the LLM turn.
|
||||
const origPrompt = editFlow.askEditInstruction;
|
||||
editFlow.askEditInstruction = async () => "rewrite it";
|
||||
editFlow.setEditTurnForTest(async () => ({
|
||||
replacement: "# Tab\n\nThe REWRITTEN tab paragraph.\n",
|
||||
model: "sonnet",
|
||||
sessionId: "e2e-f12-tab",
|
||||
}));
|
||||
let askClaudeCalls = 0;
|
||||
// Capture the active-editor doc INSIDE the spy, before delegating to the real
|
||||
// askClaude — askClaude's own `workbench.action.addComment` side effect moves
|
||||
// focus to the ephemeral comment-input widget (a `comment://` URI), which would
|
||||
// make a post-hoc `activeTextEditor` check meaningless. What we're proving here
|
||||
// is editDocument's OWN resolve-and-focus step ran against the right document
|
||||
// before handing off.
|
||||
let focusedDocAtHandoff: string | undefined;
|
||||
const origAskClaude = api.threadController.askClaude.bind(api.threadController);
|
||||
api.threadController.askClaude = async () => {
|
||||
askClaudeCalls++;
|
||||
focusedDocAtHandoff = vscode.window.activeTextEditor?.document.uri.toString();
|
||||
return origAskClaude();
|
||||
};
|
||||
try {
|
||||
await vscode.commands.executeCommand("cowriting.editDocument", b.doc.uri);
|
||||
await settle();
|
||||
} finally {
|
||||
editFlow.askEditInstruction = origPrompt;
|
||||
api.threadController.askClaude = origAskClaude;
|
||||
}
|
||||
|
||||
// The proposal(s) landed on the TAB doc (B), and the ACTIVE doc (A) has none.
|
||||
assert.ok(api.proposalController.listProposals(b.doc).length >= 1, "tab doc B received the document-edit proposal(s)");
|
||||
assert.strictEqual(
|
||||
api.proposalController.listProposals(a.doc).length,
|
||||
0,
|
||||
"active doc A was NOT edited — editDocument honored the tab URI",
|
||||
askClaudeCalls,
|
||||
1,
|
||||
"editDocument(uri) reached ThreadController.askClaude — not the removed prompt stub",
|
||||
);
|
||||
assert.strictEqual(
|
||||
focusedDocAtHandoff,
|
||||
b.doc.uri.toString(),
|
||||
"editDocument(uri) focused the TAB doc B, not the previously-active doc A, before handing off to askClaude",
|
||||
);
|
||||
// F12 (INV-48): EditorProposalController optimistically applies proposals into the
|
||||
// buffer, so the proposed text is now in the tab doc B. Confirm one of the proposal
|
||||
// texts is present (the tab doc was edited, not A).
|
||||
assert.ok(b.doc.getText().includes("REWRITTEN"), "F12 optimistic-apply: proposed text in tab doc B");
|
||||
});
|
||||
|
||||
// No URI arg (palette / keybinding) → fall back to the active editor.
|
||||
test("editDocument() with no arg targets the active editor", async () => {
|
||||
const api = await getApi();
|
||||
const editFlow = api.editFlow;
|
||||
const a = await freshDoc("docs/f12-noarg.md", "# No arg\n\nThe active doc paragraph here.\n");
|
||||
await vscode.window.showTextDocument(a.doc);
|
||||
await settle();
|
||||
|
||||
const origPrompt = editFlow.askEditInstruction;
|
||||
editFlow.askEditInstruction = async () => "rewrite it";
|
||||
editFlow.setEditTurnForTest(async () => ({
|
||||
replacement: "# No arg\n\nThe REWRITTEN active doc paragraph.\n",
|
||||
model: "sonnet",
|
||||
sessionId: "e2e-f12-noarg",
|
||||
}));
|
||||
let askClaudeCalls = 0;
|
||||
// See the sibling test above for why this is captured inside the spy rather
|
||||
// than after askClaude() returns.
|
||||
let focusedDocAtHandoff: string | undefined;
|
||||
const origAskClaude = api.threadController.askClaude.bind(api.threadController);
|
||||
api.threadController.askClaude = async () => {
|
||||
askClaudeCalls++;
|
||||
focusedDocAtHandoff = vscode.window.activeTextEditor?.document.uri.toString();
|
||||
return origAskClaude();
|
||||
};
|
||||
try {
|
||||
await vscode.commands.executeCommand("cowriting.editDocument");
|
||||
await settle();
|
||||
} finally {
|
||||
editFlow.askEditInstruction = origPrompt;
|
||||
api.threadController.askClaude = origAskClaude;
|
||||
}
|
||||
assert.ok(api.proposalController.listProposals(a.doc).length >= 1, "active doc received the proposal(s) on no-arg");
|
||||
|
||||
assert.strictEqual(askClaudeCalls, 1, "editDocument() with no arg reached ThreadController.askClaude");
|
||||
assert.strictEqual(
|
||||
focusedDocAtHandoff,
|
||||
a.doc.uri.toString(),
|
||||
"editDocument() with no arg kept the active editor's doc active before handing off to askClaude",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user