|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Ask When Risky on the native Anthropic route, EXECUTED — run: node test/agentReviewAnthropic.test.js |
| 3 | + * |
| 4 | + * agentReviewGate.test.js replaces providers.streamAgentTurn, so it could not see this: the native |
| 5 | + * Anthropic adapter called `opts.onText` for every text delta without checking it was given one, and the |
| 6 | + * reviewer — a turn whose text is collected, never streamed into the chat — gave none. Every review on that |
| 7 | + * route threw, and every command fell back to the approval card: the mode quietly became Default Approvals. |
| 8 | + * |
| 9 | + * So this runs the REAL dispatcher and the REAL Anthropic adapter, with only `fetch` stood in (answering |
| 10 | + * in Anthropic's SSE shape) and child_process.spawn replaced. No request leaves the process. |
| 11 | + *--------------------------------------------------------------------------------------------*/ |
| 12 | +// @ts-check |
| 13 | +'use strict'; |
| 14 | + |
| 15 | +const assert = require('assert'); |
| 16 | +const fs = require('fs'); |
| 17 | +const os = require('os'); |
| 18 | +const path = require('path'); |
| 19 | +const Module = require('module'); |
| 20 | +const { EventEmitter } = require('events'); |
| 21 | + |
| 22 | +const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lc-review-anthropic-')); |
| 23 | +process.on('exit', () => { try { fs.rmSync(root, { recursive: true, force: true }); } catch (e) { /* temp */ } }); |
| 24 | +const vscodeMock = { workspace: { workspaceFolders: [{ uri: { fsPath: root }, name: 'app' }] } }; |
| 25 | +/** @type {string[]} */ |
| 26 | +const ran = []; |
| 27 | +const childProcessMock = Object.assign({}, require('child_process'), { |
| 28 | + spawn: (command) => { |
| 29 | + const child = Object.assign(new EventEmitter(), { stdout: new EventEmitter(), stderr: new EventEmitter(), pid: 4242, kill() { } }); |
| 30 | + ran.push(String(command)); |
| 31 | + setImmediate(() => { child.stdout.emit('data', Buffer.from('ok\n')); child.emit('close', 0, null); }); |
| 32 | + return child; |
| 33 | + } |
| 34 | +}); |
| 35 | +const origLoad = Module._load; |
| 36 | +// @ts-ignore — test-only loader shim |
| 37 | +Module._load = function (request, parent, isMain) { |
| 38 | + if (request === 'vscode') { return vscodeMock; } |
| 39 | + if (request === 'child_process') { return childProcessMock; } |
| 40 | + return origLoad.call(this, request, parent, isMain); |
| 41 | +}; |
| 42 | + |
| 43 | +// Anthropic's streaming shape, one event per `data:` line. |
| 44 | +const sse = (events) => new Response(events.map((e) => 'event: ' + e.type + '\ndata: ' + JSON.stringify(e) + '\n\n').join(''), { status: 200, headers: { 'content-type': 'text/event-stream' } }); |
| 45 | +const start = { type: 'message_start', message: { usage: { input_tokens: 10 } } }; |
| 46 | +const stop = (reason) => [{ type: 'message_delta', delta: { stop_reason: reason }, usage: { output_tokens: 5 } }, { type: 'message_stop' }]; |
| 47 | +const textTurn = (text) => sse([start, { type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } }, |
| 48 | + { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: text.slice(0, 20) } }, |
| 49 | + { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: text.slice(20) } }, |
| 50 | + { type: 'content_block_stop', index: 0 }, ...stop('end_turn')]); |
| 51 | +const toolTurn = (input) => sse([start, { type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'toolu_1', name: 'run_command' } }, |
| 52 | + { type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: JSON.stringify(input) } }, |
| 53 | + { type: 'content_block_stop', index: 0 }, ...stop('tool_use')]); |
| 54 | + |
| 55 | +/** @type {any[]} */ |
| 56 | +const seen = []; |
| 57 | +let agentTurns = 0; |
| 58 | +globalThis.fetch = async (url, init) => { |
| 59 | + assert.match(String(url), /api\.anthropic\.com/, 'a request went somewhere other than the stood-in Anthropic API'); |
| 60 | + const body = JSON.parse(String(init && init.body)); |
| 61 | + const reviewer = !body.tools; |
| 62 | + seen.push({ reviewer }); |
| 63 | + if (reviewer) { return textTurn('{"risk":"low","authorization":"implied","injection":false,"reason":"Runs the tests you asked for."}'); } |
| 64 | + return agentTurns++ === 0 ? toolTurn({ command: 'npm test', explanation: 'Run the tests' }) : textTurn('Done: the tests pass, and nothing else changed here.'); |
| 65 | +}; |
| 66 | + |
| 67 | +const { runAgent } = require('../agent'); |
| 68 | + |
| 69 | +(async () => { |
| 70 | + /** @type {any[]} */ const asked = []; |
| 71 | + /** @type {any[]} */ const posted = []; |
| 72 | + await runAgent({ |
| 73 | + messages: [{ role: 'user', content: 'run the tests' }], userRequests: ['run the tests'], |
| 74 | + providerId: 'claude', model: 'claude-sonnet-4-6', apiKey: 'test-key-not-real', maxSteps: 5, |
| 75 | + approvalMode: 'review', post: (m) => posted.push(m), dbg: () => { }, |
| 76 | + approve: async (req) => { asked.push(req); return false; }, |
| 77 | + commandRuns: new Map(), commandStops: new Map(), signal: new AbortController().signal |
| 78 | + }); |
| 79 | + assert.deepStrictEqual(seen.map((s) => s.reviewer), [false, true, false], 'agent turn, review, agent turn'); |
| 80 | + assert.deepStrictEqual(asked, [], 'the review failed and fell back to the card: ' + JSON.stringify(asked.map((a) => a.review))); |
| 81 | + assert.deepStrictEqual(ran, ['npm test'], 'the reviewed command did not run'); |
| 82 | + assert.ok(posted.some((m) => m.type === 'agentTool' && m.text === 'Reviewer · low risk — Runs the tests you asked for.')); |
| 83 | + assert.ok(!posted.some((m) => m.type === 'agentDelta' && /risk/.test(m.text)), 'the reviewer\'s answer was streamed into the chat'); |
| 84 | + console.log(' ok - REVIEW (Anthropic native): the reviewer\'s answer is read, not streamed, and the command runs'); |
| 85 | + console.log('\nagentReviewAnthropic: 1 test passed.'); |
| 86 | +})().catch((e) => { console.error(e); process.exit(1); }); |
0 commit comments