|
| 1 | +/*--------------------------------------------------------------------------------------------- |
| 2 | + * Guards that `levelcode.ai.agent.maxSteps` is read LIVE, not snapshotted — run: node test/agentMaxSteps.test.js |
| 3 | + * |
| 4 | + * The bug this locks down: the step cap was captured once, as a plain number, when a goal STARTED |
| 5 | + * (`maxSteps: cfg.get('agent.maxSteps')`). Raising it from 25 to 1000 while the agent was running — |
| 6 | + * exactly what you do when autopilot pauses at "step limit" and you want it to keep going — had no |
| 7 | + * effect, because the loop kept comparing against the frozen 25. |
| 8 | + * |
| 9 | + * The fix mirrors the neighbouring `autopilot` getter: extension.js hands runAgent a live getter that |
| 10 | + * re-reads a FRESH getConfiguration on each access, and agent.js's loop gates on `ctx.maxSteps` |
| 11 | + * directly every iteration. Both halves matter, so both are asserted here (from source — a running |
| 12 | + * runAgent needs a workspace + provider, which this pure-unit suite deliberately doesn't stand up). |
| 13 | + * If either half regresses to a snapshot, a raised limit silently won't take effect mid-run again. |
| 14 | + *--------------------------------------------------------------------------------------------*/ |
| 15 | +// @ts-check |
| 16 | +'use strict'; |
| 17 | + |
| 18 | +const assert = require('assert'); |
| 19 | +const fs = require('fs'); |
| 20 | +const path = require('path'); |
| 21 | +const pkg = require('../package.json'); |
| 22 | + |
| 23 | +let n = 0; |
| 24 | +function test(name, fn) { fn(); n++; console.log(' ok - ' + name); } |
| 25 | + |
| 26 | +const read = (rel) => fs.readFileSync(path.join(__dirname, '..', rel), 'utf8'); |
| 27 | + |
| 28 | +/** The contributed setting node, tolerating configuration being a single object or an array. */ |
| 29 | +function maxStepsSetting() { |
| 30 | + const c = pkg.contributes.configuration; |
| 31 | + const props = Array.isArray(c) ? Object.assign({}, ...c.map((x) => x.properties || {})) : (c && c.properties) || {}; |
| 32 | + return props['levelcode.ai.agent.maxSteps']; |
| 33 | +} |
| 34 | + |
| 35 | +test('the setting exists, defaults to 25, and has no upper bound (1000 is valid)', () => { |
| 36 | + const s = maxStepsSetting(); |
| 37 | + assert.ok(s, 'levelcode.ai.agent.maxSteps must be contributed'); |
| 38 | + assert.equal(s.type, 'number'); |
| 39 | + assert.equal(s.default, 25); |
| 40 | + assert.equal(s.minimum, 1); |
| 41 | + // No `maximum`: a user must be able to raise it well past the default (the 1000 in the bug report). |
| 42 | + assert.ok(!('maximum' in s), 'maxSteps must not cap the user below large values like 1000'); |
| 43 | +}); |
| 44 | + |
| 45 | +test('extension.js hands runAgent maxSteps as a LIVE getter over a fresh aiConfig()', () => { |
| 46 | + const ext = read('extension.js'); |
| 47 | + assert.match( |
| 48 | + ext, |
| 49 | + /get\s+maxSteps\s*\(\s*\)\s*\{[\s\S]*?(?:\baiConfig\(\)\s*\.get|\b(?:const|let|var)\s+\w+\s*=\s*aiConfig\(\)[\s\S]*?\b\w+\s*\.get)\(\s*['"]agent\.maxSteps['"]/, |
| 50 | + 'runAgent ctx must expose `get maxSteps()` re-reading a FRESH aiConfig() each access' |
| 51 | + ); |
| 52 | +}); |
| 53 | + |
| 54 | +test('extension.js does NOT re-snapshot the cap from the goal-start cfg (the original bug)', () => { |
| 55 | + const ext = read('extension.js'); |
| 56 | + // A `maxSteps:` property whose value pulls from the captured `cfg` freezes the limit for the whole |
| 57 | + // run. (The start-of-run dbg log may still read cfg for telemetry; only the runAgent input matters.) |
| 58 | + assert.ok( |
| 59 | + !/\bmaxSteps:\s*Math\.max\([^)]*\bcfg\.get\(/.test(ext), |
| 60 | + 'maxSteps passed to runAgent must not be a static snapshot of the start-of-goal cfg' |
| 61 | + ); |
| 62 | +}); |
| 63 | + |
| 64 | +test('agent.js gates the loop on ctx.maxSteps directly, and never hoists it into a local', () => { |
| 65 | + const ag = read('agent.js'); |
| 66 | + assert.match(ag, /while\s*\([^)]*\bctx\.maxSteps\b[^)]*\)/, 'the step loop must read ctx.maxSteps live'); |
| 67 | + // A copy (`const max = ctx.maxSteps`) or a destructure (`const { maxSteps } = ctx`) taken before the |
| 68 | + // loop would evaluate the getter exactly once — reintroducing the snapshot from the other side. |
| 69 | + assert.ok(!/\b(?:const|let|var)\s+\w+\s*=\s*ctx\.maxSteps\b/.test(ag), 'ctx.maxSteps must not be assigned into a variable'); |
| 70 | + assert.ok( |
| 71 | + !/\b(?:const|let|var)\s*\{[^}]*\bmaxSteps\b[^}]*\}\s*=\s*ctx\b/.test(ag), |
| 72 | + 'maxSteps must not be destructured off ctx' |
| 73 | + ); |
| 74 | +}); |
| 75 | + |
| 76 | +console.log(n + ' passing'); |
0 commit comments