Skip to content

Commit 57840d4

Browse files
Proactive Runtime Botcursoragent
andcommitted
fix(relayflows): keep 1603-raw-spawn-readiness, add new case alongside it
The RelayFlow PR-proof dispatcher requires a PR to touch exactly its one declared case (scripts/pr-proof/prepare.mjs / contract.mjs). Deleting 1603-raw-spawn-readiness (pre-existing on main since #1708) alongside adding 1603-hosted-exit-durable-delivery made the diff touch two case directories and failed dispatch validation. Restore 1603-raw-spawn-readiness unmodified and keep the new case as this PR's sole declared case. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c7daa17 commit 57840d4

3 files changed

Lines changed: 225 additions & 8 deletions

File tree

‎tests/relayflows/cases/1603-hosted-exit-durable-delivery/run.mjs‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
// This case replaces the retired `1603-raw-spawn-readiness` case (see PR
2-
// #1750). That case proved a *different*, already-fixed bug — raw CLI spawn
3-
// readiness — via the MCP `spawn` tool against a mocked HTTP actions API. On
4-
// current `main` the readiness-wait behavior it asserted against `base` is
5-
// already present, so its `base` arm no longer reproduces anything, and it
6-
// never touched a real broker/Relaycast boundary at all, so it could not
7-
// stand in as proof of issue #1603's actual fix here: durable, restart-safe
8-
// delivery of the hosted `agent_exited` event.
1+
// This case supersedes `1603-raw-spawn-readiness` as issue #1603's declared
2+
// RelayFlow proof (see PR #1750). That case proved a *different*,
3+
// already-fixed bug — raw CLI spawn readiness — via the MCP `spawn` tool
4+
// against a mocked HTTP actions API. On current `main` the readiness-wait
5+
// behavior it asserted against `base` is already present, so its `base` arm
6+
// no longer reproduces anything, and it never touched a real
7+
// broker/Relaycast boundary at all, so it could not stand in as proof of
8+
// issue #1603's actual fix here: durable, restart-safe delivery of the
9+
// hosted `agent_exited` event.
10+
//
11+
// `1603-raw-spawn-readiness` itself is left in place, unmodified, rather
12+
// than deleted: it predates this branch (merged to `main` in #1708, well
13+
// before this PR's base), the RelayFlow PR-proof dispatcher requires a PR to
14+
// touch exactly its one declared case
15+
// (`scripts/pr-proof/prepare.mjs`/`contract.mjs`), and it is still an
16+
// accurate (if now redundant) regression case for the readiness-wait
17+
// behavior it covers. Retiring it — if still wanted once it no longer
18+
// carries the #1603 proof — is a separate, standalone change.
919
//
1020
// This case is an honest, binary-boundary proof of exactly that restart
1121
// durability, scoped to what it can truthfully drive without a real
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"version": 1,
3+
"id": "1603-raw-spawn-readiness",
4+
"kind": "bugfix",
5+
"title": "Wait for raw CLI spawn readiness",
6+
"runner": {
7+
"command": ["node", "tests/relayflows/cases/1603-raw-spawn-readiness/run.mjs"]
8+
},
9+
"timeoutSeconds": 900,
10+
"expected": {
11+
"base": {
12+
"outcome": "bug",
13+
"signature": "raw_cli_spawn_returns_dispatch_ack_without_readiness"
14+
},
15+
"head": {
16+
"outcome": "fixed",
17+
"signature": "raw_cli_spawn_waits_for_verified_readiness"
18+
}
19+
}
20+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import assert from 'node:assert/strict';
2+
import { execFileSync, spawn } from 'node:child_process';
3+
import { writeFile } from 'node:fs/promises';
4+
import http from 'node:http';
5+
import path from 'node:path';
6+
import { pathToFileURL } from 'node:url';
7+
8+
const CASE_ID = '1603-raw-spawn-readiness';
9+
const arm = process.env.RELAY_PR_PROOF_ARM;
10+
const targetDir = process.env.RELAY_PR_PROOF_TARGET_DIR;
11+
const resultPath = process.env.RELAY_PR_PROOF_RESULT_PATH;
12+
const childEnv = { ...process.env };
13+
delete childEnv.CODEX_MANAGED_BY_NPM;
14+
15+
assert.ok(arm === 'base' || arm === 'head', 'RELAY_PR_PROOF_ARM must be base or head');
16+
assert.ok(targetDir, 'RELAY_PR_PROOF_TARGET_DIR is required');
17+
assert.ok(resultPath, 'RELAY_PR_PROOF_RESULT_PATH is required');
18+
19+
const expectedSha =
20+
arm === 'base' ? process.env.RELAY_PR_PROOF_BASE_SHA : process.env.RELAY_PR_PROOF_HEAD_SHA;
21+
assert.ok(expectedSha, `missing expected ${arm} SHA`);
22+
const targetSha = execFileSync('git', ['-C', targetDir, 'rev-parse', 'HEAD'], {
23+
encoding: 'utf8',
24+
}).trim();
25+
assert.equal(targetSha, expectedSha, `target checkout does not match exact ${arm} SHA`);
26+
27+
async function run(command, args, env = childEnv) {
28+
await new Promise((resolve, reject) => {
29+
const child = spawn(command, args, {
30+
cwd: targetDir,
31+
env,
32+
stdio: ['ignore', 'pipe', 'pipe'],
33+
});
34+
let output = '';
35+
const append = (chunk) => {
36+
output = `${output}${chunk}`.slice(-16_000);
37+
};
38+
child.stdout.on('data', append);
39+
child.stderr.on('data', append);
40+
child.once('error', reject);
41+
child.once('close', (code, signal) => {
42+
if (code === 0) resolve();
43+
else reject(new Error(`${command} ${args.join(' ')} failed (${code ?? signal}):\n${output}`));
44+
});
45+
});
46+
}
47+
48+
await run('npm', ['ci', '--ignore-scripts', '--no-audit', '--no-fund', '--include=optional'], {
49+
...childEnv,
50+
NODE_OPTIONS: '--max-old-space-size=256',
51+
});
52+
await run('npm', ['run', 'build:core']);
53+
54+
const requests = [];
55+
let invocationReads = 0;
56+
const server = http.createServer(async (request, response) => {
57+
const bodyChunks = [];
58+
for await (const chunk of request) bodyChunks.push(chunk);
59+
const bodyText = Buffer.concat(bodyChunks).toString('utf8');
60+
const body = bodyText ? JSON.parse(bodyText) : undefined;
61+
requests.push({ method: request.method, url: request.url, body });
62+
63+
let data;
64+
if (request.method === 'POST' && request.url === '/v1/actions/spawn/invoke') {
65+
data = {
66+
invocation_id: 'inv_raw_readiness_proof',
67+
action_name: 'spawn',
68+
status: 'dispatched',
69+
input: body?.input,
70+
};
71+
} else if (
72+
request.method === 'GET' &&
73+
request.url === '/v1/actions/spawn/invocations/inv_raw_readiness_proof'
74+
) {
75+
invocationReads += 1;
76+
data =
77+
invocationReads === 1
78+
? {
79+
invocation_id: 'inv_raw_readiness_proof',
80+
action_name: 'spawn',
81+
status: 'running',
82+
}
83+
: {
84+
invocation_id: 'inv_raw_readiness_proof',
85+
action_name: 'spawn',
86+
status: 'completed',
87+
output: { spawned: true, ready: true },
88+
};
89+
} else {
90+
response.writeHead(404, { 'content-type': 'application/json' });
91+
response.end(JSON.stringify({ ok: false, error: { code: 'not_found', message: request.url } }));
92+
return;
93+
}
94+
95+
response.writeHead(200, { 'content-type': 'application/json' });
96+
response.end(JSON.stringify({ ok: true, data }));
97+
});
98+
99+
await new Promise((resolve, reject) => {
100+
server.once('error', reject);
101+
server.listen(0, '127.0.0.1', resolve);
102+
});
103+
104+
let client;
105+
try {
106+
const address = server.address();
107+
assert.ok(address && typeof address === 'object');
108+
const clientModule = await import(
109+
pathToFileURL(path.join(targetDir, 'node_modules/@modelcontextprotocol/sdk/dist/esm/client/index.js'))
110+
.href
111+
);
112+
const transportModule = await import(
113+
pathToFileURL(path.join(targetDir, 'node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.js'))
114+
.href
115+
);
116+
client = new clientModule.Client({ name: 'relayflow-raw-spawn-readiness', version: '1.0.0' });
117+
const transport = new transportModule.StdioClientTransport({
118+
command: process.execPath,
119+
args: [path.join(targetDir, 'packages/cli/dist/cli/index.js'), 'mcp'],
120+
cwd: targetDir,
121+
stderr: 'inherit',
122+
env: {
123+
...childEnv,
124+
RELAY_BASE_URL: `http://127.0.0.1:${address.port}`,
125+
RELAY_WORKSPACE_KEY: 'rk_live_relayflow_proof',
126+
RELAY_AGENT_TOKEN: 'at_live_relayflow_proof',
127+
RELAY_AGENT_NAME: 'relayflow-orchestrator',
128+
RELAY_SKIP_BOOTSTRAP: '1',
129+
AGENT_RELAY_TELEMETRY_DISABLED: '1',
130+
},
131+
});
132+
await client.connect(transport);
133+
const result = await client.callTool({
134+
name: 'spawn',
135+
arguments: { name: 'RelayflowRawWorker', cli: 'codex', target_node: 'proof-node' },
136+
});
137+
assert.equal(result.isError, undefined, `spawn returned an MCP error: ${JSON.stringify(result)}`);
138+
139+
const invokeRequests = requests.filter(
140+
(entry) => entry.method === 'POST' && entry.url === '/v1/actions/spawn/invoke'
141+
);
142+
const readRequests = requests.filter(
143+
(entry) => entry.method === 'GET' && entry.url === '/v1/actions/spawn/invocations/inv_raw_readiness_proof'
144+
);
145+
assert.equal(invokeRequests.length, 1, 'spawn must invoke the production actions HTTP path exactly once');
146+
const actionInput = invokeRequests[0].body?.input;
147+
assert.equal(actionInput?.cli, 'codex');
148+
assert.equal(actionInput?.name, 'RelayflowRawWorker');
149+
150+
if (arm === 'base') {
151+
assert.equal(actionInput.verify_ready, undefined, 'base unexpectedly requested verified readiness');
152+
assert.equal(readRequests.length, 0, 'base unexpectedly read the action invocation');
153+
assert.equal(result.structuredContent?.invocation?.status, 'dispatched');
154+
await writeFile(
155+
resultPath,
156+
`${JSON.stringify({
157+
version: 1,
158+
caseId: CASE_ID,
159+
arm,
160+
outcome: 'bug',
161+
signature: 'raw_cli_spawn_returns_dispatch_ack_without_readiness',
162+
details:
163+
'The public MCP spawn tool returned the dispatch acknowledgement and never read the action invocation for readiness.',
164+
})}\n`
165+
);
166+
} else {
167+
assert.equal(actionInput.verify_ready, true, 'head did not request the broker readiness contract');
168+
assert.equal(readRequests.length, 2, 'head must poll through running to completed readiness');
169+
assert.equal(result.structuredContent?.invocation?.status, 'completed');
170+
assert.deepEqual(result.structuredContent?.invocation?.output, { spawned: true, ready: true });
171+
await writeFile(
172+
resultPath,
173+
`${JSON.stringify({
174+
version: 1,
175+
caseId: CASE_ID,
176+
arm,
177+
outcome: 'fixed',
178+
signature: 'raw_cli_spawn_waits_for_verified_readiness',
179+
details:
180+
'The public MCP spawn tool sent verify_ready, polled the production action invocation path, and returned only the spawned-and-ready completion.',
181+
})}\n`
182+
);
183+
}
184+
} finally {
185+
await client?.close().catch(() => undefined);
186+
await new Promise((resolve) => server.close(resolve));
187+
}

0 commit comments

Comments
 (0)