|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * The prompt a run sends is the HOST's prompt, with or without a skills menu — run: node test/agentPromptHost.test.js |
| 3 | + * |
| 4 | + * runAgent built its system prompt as (ctx.skills ? buildSystem(menu) : SYSTEM_BASE). buildSystem answers a run |
| 5 | + * with no menu with the host's base prompt, but the fallback bypassed it and sent SYSTEM_BASE: in the browser |
| 6 | + * build, with levelcode.ai.skills.enabled off, the model was told about run_command, background:true and |
| 7 | + * "EVERY run_command" while run_command itself was withheld. |
| 8 | + * |
| 9 | + * Real runAgent with the browser flag, `vscode` and the provider replaced (the harness of agentRunCommand.test.js). |
| 10 | + *--------------------------------------------------------------------------------------------*/ |
| 11 | +// @ts-check |
| 12 | +'use strict'; |
| 13 | + |
| 14 | +const assert = require('assert'); |
| 15 | +const fs = require('fs'); |
| 16 | +const os = require('os'); |
| 17 | +const path = require('path'); |
| 18 | +const Module = require('module'); |
| 19 | + |
| 20 | +const root = fs.mkdtempSync(path.join(os.tmpdir(), 'lc-prompt-')); |
| 21 | +const vscodeMock = { |
| 22 | + workspace: { workspaceFolders: [{ name: 'app', uri: { fsPath: root, path: root, toString: () => 'file://' + root } }], findFiles: async () => [], asRelativePath: (u) => String(u.fsPath || u) }, |
| 23 | + env: { appRoot: '' }, |
| 24 | + Uri: { file: (p) => ({ fsPath: p, path: p }) }, |
| 25 | + extensions: { getExtension: () => undefined }, |
| 26 | +}; |
| 27 | +const origLoad = Module._load; |
| 28 | +// @ts-ignore — test-only loader shim |
| 29 | +Module._load = function (request, parent, isMain) { return request === 'vscode' ? vscodeMock : origLoad.call(this, request, parent, isMain); }; |
| 30 | +const AI = path.join(__dirname, '..'); |
| 31 | +const providers = require('../providers/index'); |
| 32 | +const turns = []; |
| 33 | +providers.streamAgentTurn = async (o) => { turns.push({ system: o.system, tools: o.tools }); return { stop_reason: 'end_turn', content: [{ type: 'text', text: 'Done: x' }] }; }; |
| 34 | + |
| 35 | +function load(browser) { |
| 36 | + for (const k of Object.keys(require.cache)) { if (k.startsWith(AI + path.sep) && !k.includes(path.sep + 'test' + path.sep) && !k.includes(path.sep + 'providers' + path.sep)) { delete require.cache[k]; } } |
| 37 | + if (browser) { globalThis.__LEVELCODE_BROWSER_HOST__ = true; } else { delete globalThis.__LEVELCODE_BROWSER_HOST__; } |
| 38 | + return require('../agent'); |
| 39 | +} |
| 40 | +const ctxFor = (over) => Object.assign({ messages: [{ role: 'user', content: 'go' }], maxSteps: 3, post: () => {}, dbg: () => {}, approve: async () => true, commandRuns: new Map(), commandStops: new Map(), signal: new AbortController().signal }, over); |
| 41 | +const MENU = [{ name: 'code-review', description: 'review a diff' }]; |
| 42 | + |
| 43 | +let n = 0; |
| 44 | +async function test(name, fn) { turns.length = 0; await fn(); n++; console.log(' ok - ' + name); } |
| 45 | + |
| 46 | +(async () => { |
| 47 | + try { |
| 48 | + await test('browser, skills off: no run_command promises (this was the leak)', async () => { |
| 49 | + const agent = load(true); |
| 50 | + await agent.runAgent(ctxFor({ skills: null })); |
| 51 | + const { system, tools } = turns[0]; |
| 52 | + assert.ok(!tools.some((t) => t.name === 'run_command'), 'run_command is withheld'); |
| 53 | + // What the desktop prompt promises: a background flag, a rule about EVERY run_command, approval for commands. |
| 54 | + assert.ok(!/background:true|EVERY run_command|Only run_command still needs approval|run_command accepts an optional/.test(system), 'the desktop prompt leaked'); |
| 55 | + assert.match(system, /There is no terminal/); |
| 56 | + }); |
| 57 | + await test('browser, skills on: the same host prompt plus the menu', async () => { |
| 58 | + const agent = load(true); |
| 59 | + await agent.runAgent(ctxFor({ skills: { menu: () => MENU, load: async () => '' } })); |
| 60 | + const { system } = turns[0]; |
| 61 | + assert.match(system, /There is no terminal/); |
| 62 | + assert.match(system, /Available skills \(call use_skill with the name\):\n- code-review: review a diff/); |
| 63 | + }); |
| 64 | + await test('desktop, skills off: the desktop prompt, byte for byte what it was', async () => { |
| 65 | + const agent = load(false); |
| 66 | + await agent.runAgent(ctxFor({ skills: null })); |
| 67 | + assert.match(turns[0].system, /Commands that do NOT exit on their own \(dev servers, file watchers, tail -f\) MUST be run with run_command background:true/); |
| 68 | + assert.ok(!/There is no terminal/.test(turns[0].system)); |
| 69 | + }); |
| 70 | + console.log(`\n${n} tests passed`); |
| 71 | + } finally { fs.rmSync(root, { recursive: true, force: true }); } |
| 72 | +})().catch((e) => { console.error(e); process.exit(1); }); |
0 commit comments