feat(scripts): let the mock provider serve a scripted turn sequence

This commit is contained in:
John Smith
2026-09-24 02:25:28 -04:00
parent c98176ea8e
commit 14e26b4099
5 changed files with 134 additions and 7 deletions
+7 -1
View File
@@ -7,7 +7,7 @@
"asset-curator",
"qa-playtester"
],
"last_cycle": 205,
"last_cycle": 206,
"completed_targets": [
{
"cycle": 75,
@@ -1204,6 +1204,12 @@
"target": "Full-suite regression + run-log refresh (no change)",
"metric_delta": "No code change. Ran the FULL newIDE/app Jest suite at cycle 204's state: 185 suites, 2358 passed, 1 skipped, 114 snapshots, 0 failures. Refreshed runs/RUNS.md through cycle 204 and rewrote the backlog to only the genuinely open items (the earlier list had accumulated a design idea now explicitly assessed as architecturally invasive).",
"commit": "chore(autonomous): full-suite regression and run-log refresh"
},
{
"cycle": 206,
"target": "Make the mock provider scriptable for deterministic offline harness runs (Tier 4)",
"metric_delta": "The committed mock always answered with one canned reply, so the studio's multi-step flow (plan -> spawn -> report) could not be exercised offline. Added MOCK_SCRIPT support: a JSON array of turns (content and/or toolCalls) consumed one per request, emitted both non-streamed AND as SSE tool-call deltas. Added scripts/dev/mock-script.example.json (plan -> spawn_agent -> report) and documented it in scripts/README.md. Verified: node --check, the example is valid JSON, and a live 3-request smoke returned the exact scripted sequence (create_or_update_plan, spawn_agent, plain report). This lets @feedback-loop assert the studio loop without a real model or credentials. Divergence +1 forkOnly (the example).",
"commit": "feat(scripts): let the mock provider serve a scripted turn sequence"
}
],
"backlog": [
+1
View File
@@ -192,6 +192,7 @@
"runs/RUNS.md",
"scripts/check-fork-divergence.js",
"scripts/dev/mock-ai-provider.js",
"scripts/dev/mock-script.example.json",
"scripts/security/check-lockfiles.js",
"scripts/sonarcloud-drift.js"
],
+5 -1
View File
@@ -37,7 +37,11 @@ See [MAINTENANCE.md](../MAINTENANCE.md) for how these fit together.
`Access-Control-Expose-Headers`, because a browser client cannot read
custom response headers from a cross-origin endpoint without the latter —
the same requirement applies to any real telemetry proxy. Point the editor's
custom AI base URL at `http://localhost:11435/v1` to use it.
custom AI base URL at `http://localhost:11435/v1` to use it. Set
`MOCK_SCRIPT=<file>` to serve a fixed sequence of turns (content and/or
`toolCalls`) from a JSON array, one per request — see
`dev/mock-script.example.json` (plan -> spawn_agent -> report) — to drive a
deterministic multi-step studio run offline.
## Documentation and translations
+81 -5
View File
@@ -39,6 +39,37 @@ const CORS = {
'x-omniroute-model, x-omniroute-latency-ms, x-omniroute-tokens-in, x-omniroute-tokens-out, x-omniroute-cache',
};
/**
* Optional deterministic script: a JSON array of turns, consumed one per
* request (the last turn repeats). Each turn is either
* { "content": "..." } a plain answer, or
* { "content": "...", "toolCalls": [ {"id","name","arguments"} ] }
* with `arguments` a JSON string (or object) as the OpenAI API expects. Set
* `MOCK_SCRIPT=/path/to/script.json` to drive a fixed multi-step run (e.g.
* plan -> spawn_agent -> report) through the harness without a real model.
*/
const loadScript = () => {
const path = process.env.MOCK_SCRIPT;
if (!path) return null;
try {
const parsed = JSON.parse(require('fs').readFileSync(path, 'utf8'));
return Array.isArray(parsed) && parsed.length > 0 ? parsed : null;
} catch (error) {
console.error('MOCK_SCRIPT could not be read:', error.message);
return null;
}
};
const script = loadScript();
let requestCount = 0;
const nextTurn = () => {
if (!script) return null;
const turn = script[Math.min(requestCount, script.length - 1)];
requestCount += 1;
return turn;
};
const json = (res, status, body, extraHeaders) => {
res.writeHead(
status,
@@ -93,7 +124,24 @@ const server = http.createServer((req, res) => {
// A malformed body still gets a deterministic reply.
}
const messages = Array.isArray(payload.messages) ? payload.messages : [];
const reply = buildReply(messages, rawBody);
const turn = nextTurn();
const reply =
turn && typeof turn.content === 'string'
? turn.content
: buildReply(messages, rawBody);
const scriptedToolCalls =
turn && Array.isArray(turn.toolCalls) ? turn.toolCalls : [];
const toolCalls = scriptedToolCalls.map((call, index) => ({
id: call.id || `call_${index}`,
type: 'function',
function: {
name: call.name,
arguments:
typeof call.arguments === 'string'
? call.arguments
: JSON.stringify(call.arguments || {}),
},
}));
const headers = telemetry(rawBody, reply);
if (payload.stream === true) {
@@ -101,7 +149,7 @@ const server = http.createServer((req, res) => {
200,
Object.assign({ 'Content-Type': 'text/event-stream' }, CORS, headers)
);
const chunks = reply.match(/.{1,24}/g) || [reply];
const chunks = reply.match(/.{1,24}/g) || (reply ? [reply] : []);
for (const chunk of chunks) {
res.write(
'data: ' +
@@ -109,15 +157,43 @@ const server = http.createServer((req, res) => {
'\n\n'
);
}
if (toolCalls.length > 0) {
res.write(
'data: ' +
JSON.stringify({
choices: [
{
delta: {
tool_calls: toolCalls.map((call, index) => ({
index,
id: call.id,
function: call.function,
})),
},
},
],
}) +
'\n\n'
);
}
res.write(
'data: ' +
JSON.stringify({ choices: [{ delta: {}, finish_reason: 'stop' }] }) +
JSON.stringify({
choices: [
{
delta: {},
finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
},
],
}) +
'\n\n'
);
res.write('data: [DONE]\n\n');
return res.end();
}
const message = { role: 'assistant', content: reply };
if (toolCalls.length > 0) message.tool_calls = toolCalls;
return json(
res,
200,
@@ -128,8 +204,8 @@ const server = http.createServer((req, res) => {
choices: [
{
index: 0,
message: { role: 'assistant', content: reply },
finish_reason: 'stop',
message,
finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop',
},
],
},
+40
View File
@@ -0,0 +1,40 @@
[
{
"content": "Planning first.",
"toolCalls": [
{
"id": "call_plan_1",
"name": "create_or_update_plan",
"arguments": {
"tasks": [
{
"id": "design",
"title": "Design the town",
"description": "Write a one-sentence design.",
"status": "pending",
"dependsOn": []
}
]
}
}
]
},
{
"content": "Delegating the design task.",
"toolCalls": [
{
"id": "call_spawn_1",
"name": "spawn_agent",
"arguments": {
"role": "designer",
"short_title": "Design the town",
"task": "Write GDD_Overview in one sentence.",
"related_task_id": "design"
}
}
]
},
{
"content": "The designer reported back; the design task is done."
}
]