@tarunspandit/codexflow 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.example.md +18 -0
- package/CHANGELOG.md +311 -0
- package/CHATGPT_PROMPT.md +12 -0
- package/CODEX_PROMPT.md +12 -0
- package/CONTRIBUTING.md +51 -0
- package/DOMAIN_SETUP.md +241 -0
- package/FAQ.md +327 -0
- package/FAQ_ZH.md +279 -0
- package/LICENSE +21 -0
- package/NOTICE +9 -0
- package/PUBLIC_LAUNCH_CHECKLIST.md +111 -0
- package/README.md +287 -0
- package/README_ZH.md +461 -0
- package/SECURITY.md +145 -0
- package/config.example.env +31 -0
- package/dist/analysis/cache.js +27 -0
- package/dist/analysis/cache.js.map +1 -0
- package/dist/analysis/classify.js +102 -0
- package/dist/analysis/classify.js.map +1 -0
- package/dist/analysis/extract.js +139 -0
- package/dist/analysis/extract.js.map +1 -0
- package/dist/analysis/graph.js +22 -0
- package/dist/analysis/graph.js.map +1 -0
- package/dist/analysis/impact.js +167 -0
- package/dist/analysis/impact.js.map +1 -0
- package/dist/analysis/index.js +215 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/analysis/inventory.js +51 -0
- package/dist/analysis/inventory.js.map +1 -0
- package/dist/analysis/providers.js +24 -0
- package/dist/analysis/providers.js.map +1 -0
- package/dist/analysis/rank.js +27 -0
- package/dist/analysis/rank.js.map +1 -0
- package/dist/analysis/types.js +8 -0
- package/dist/analysis/types.js.map +1 -0
- package/dist/bashOps.js +233 -0
- package/dist/bashOps.js.map +1 -0
- package/dist/capabilitiesOps.js +365 -0
- package/dist/capabilitiesOps.js.map +1 -0
- package/dist/codexSessions.js +379 -0
- package/dist/codexSessions.js.map +1 -0
- package/dist/config.js +289 -0
- package/dist/config.js.map +1 -0
- package/dist/fsOps.js +286 -0
- package/dist/fsOps.js.map +1 -0
- package/dist/gitOps.js +79 -0
- package/dist/gitOps.js.map +1 -0
- package/dist/guard.js +198 -0
- package/dist/guard.js.map +1 -0
- package/dist/http.js +1671 -0
- package/dist/http.js.map +1 -0
- package/dist/proContext.js +274 -0
- package/dist/proContext.js.map +1 -0
- package/dist/profileStore.js +89 -0
- package/dist/profileStore.js.map +1 -0
- package/dist/projectCatalog.js +134 -0
- package/dist/projectCatalog.js.map +1 -0
- package/dist/redact.js +73 -0
- package/dist/redact.js.map +1 -0
- package/dist/searchOps.js +186 -0
- package/dist/searchOps.js.map +1 -0
- package/dist/server.js +2502 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio.js +36 -0
- package/dist/stdio.js.map +1 -0
- package/dist/toolCardWidget.js +1155 -0
- package/dist/toolCardWidget.js.map +1 -0
- package/dist/workspaceOps.js +229 -0
- package/dist/workspaceOps.js.map +1 -0
- package/docs/.nojekyll +1 -0
- package/docs/favicon.svg +5 -0
- package/docs/index.html +638 -0
- package/docs/og.png +0 -0
- package/docs/script.js +80 -0
- package/docs/star.svg +11 -0
- package/docs/styles.css +1229 -0
- package/docs/zh.html +436 -0
- package/package.json +94 -0
- package/scripts/analysis-cli-smoke.mjs +81 -0
- package/scripts/analysis-smoke.mjs +179 -0
- package/scripts/clean.mjs +6 -0
- package/scripts/cli-smoke.mjs +168 -0
- package/scripts/codexflow.mjs +4375 -0
- package/scripts/doctor-smoke.mjs +90 -0
- package/scripts/execute-handoff-smoke.mjs +1110 -0
- package/scripts/http-smoke.mjs +812 -0
- package/scripts/pro-apply.mjs +141 -0
- package/scripts/pro-bundle.mjs +121 -0
- package/scripts/pro-smoke.mjs +95 -0
- package/scripts/settings-smoke.mjs +756 -0
- package/scripts/smoke.mjs +1194 -0
- package/scripts/stress.mjs +835 -0
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
function encode(message) {
|
|
7
|
+
return `${JSON.stringify(message)}\n`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class McpStdioClient {
|
|
11
|
+
constructor(command, args, options) {
|
|
12
|
+
this.child = spawn(command, args, options);
|
|
13
|
+
this.buffer = '';
|
|
14
|
+
this.nextId = 1;
|
|
15
|
+
this.pending = new Map();
|
|
16
|
+
this.child.stdout.on('data', (chunk) => this.onData(String(chunk)));
|
|
17
|
+
this.child.stderr.on('data', (chunk) => process.stderr.write(chunk));
|
|
18
|
+
this.child.on('exit', (code) => {
|
|
19
|
+
for (const { reject } of this.pending.values()) reject(new Error(`server exited ${code}`));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
onData(chunk) {
|
|
24
|
+
this.buffer += chunk;
|
|
25
|
+
while (true) {
|
|
26
|
+
const index = this.buffer.indexOf('\n');
|
|
27
|
+
if (index < 0) return;
|
|
28
|
+
const line = this.buffer.slice(0, index).replace(/\r$/, '');
|
|
29
|
+
this.buffer = this.buffer.slice(index + 1);
|
|
30
|
+
if (!line.trim()) continue;
|
|
31
|
+
const msg = JSON.parse(line);
|
|
32
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
33
|
+
const { resolve, reject, timer } = this.pending.get(msg.id);
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
this.pending.delete(msg.id);
|
|
36
|
+
if (msg.error) reject(new Error(msg.error.message));
|
|
37
|
+
else resolve(msg.result);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
request(method, params) {
|
|
43
|
+
const id = this.nextId++;
|
|
44
|
+
const msg = { jsonrpc: '2.0', id, method, params };
|
|
45
|
+
this.child.stdin.write(encode(msg));
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const timer = setTimeout(() => reject(new Error(`timeout waiting for ${method}`)), 15000);
|
|
48
|
+
timer.unref();
|
|
49
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
notify(method, params = {}) {
|
|
54
|
+
this.child.stdin.write(encode({ jsonrpc: '2.0', method, params }));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
close() {
|
|
58
|
+
this.child.kill('SIGTERM');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pkg = JSON.parse(await fs.readFile('package.json', 'utf8'));
|
|
63
|
+
|
|
64
|
+
function assertCommand(args, expected) {
|
|
65
|
+
const result = spawnSync(process.execPath, args, { cwd: path.resolve('.'), encoding: 'utf8' });
|
|
66
|
+
if (result.status !== 0) {
|
|
67
|
+
throw new Error(`${args.join(' ')} failed: ${result.stderr || result.stdout}`);
|
|
68
|
+
}
|
|
69
|
+
if (!result.stdout.includes(expected)) {
|
|
70
|
+
throw new Error(`${args.join(' ')} did not print ${expected}: ${result.stdout}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
assertCommand(['dist/stdio.js', '--version'], pkg.version);
|
|
75
|
+
assertCommand(['dist/stdio.js', '--help'], 'CodexFlow MCP stdio server');
|
|
76
|
+
assertCommand(['dist/http.js', '--version'], pkg.version);
|
|
77
|
+
assertCommand(['dist/http.js', '--help'], 'CodexFlow MCP HTTP server');
|
|
78
|
+
|
|
79
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-smoke-'));
|
|
80
|
+
await fs.writeFile(path.join(tmp, 'demo.txt'), 'alpha\nread\nread\nomega\n', 'utf8');
|
|
81
|
+
await fs.writeFile(path.join(tmp, 'other.txt'), 'keep\n', 'utf8');
|
|
82
|
+
await fs.writeFile(path.join(tmp, 'config.txt'), 'OPENAI_API_KEY=sk-realSecretValue123\n', 'utf8');
|
|
83
|
+
await fs.writeFile(path.join(tmp, 'AGENTS.md'), '# Smoke Agents\n\n- Preserve demo.txt.\n', 'utf8');
|
|
84
|
+
const secondProject = path.join(tmp, 'second-project');
|
|
85
|
+
await fs.mkdir(secondProject, { recursive: true });
|
|
86
|
+
await fs.writeFile(path.join(secondProject, 'package.json'), '{"name":"second-project"}\n', 'utf8');
|
|
87
|
+
await fs.writeFile(path.join(secondProject, 'project-only.txt'), 'routed-to-second-project\n', 'utf8');
|
|
88
|
+
const codexHistoryDir = path.join(tmp, 'codex-history');
|
|
89
|
+
const codexSessionDir = path.join(codexHistoryDir, 'sessions', '2026', '06', '20');
|
|
90
|
+
await fs.mkdir(codexSessionDir, { recursive: true });
|
|
91
|
+
const codexSessionPath = path.join(codexSessionDir, 'rollout-2026-06-20T01-02-03-019cc369-bd7c-7891-b371-7b20b4fe0b18.jsonl');
|
|
92
|
+
await fs.writeFile(codexSessionPath, [
|
|
93
|
+
JSON.stringify({ timestamp: '2026-06-20T01:02:03Z', type: 'session_meta', payload: { id: '019cc369-bd7c-7891-b371-7b20b4fe0b18', cwd: tmp } }),
|
|
94
|
+
JSON.stringify({ timestamp: '2026-06-20T01:02:04Z', type: 'response_item', payload: { type: 'message', role: 'user', content: 'Fix the smoke session browser' } }),
|
|
95
|
+
JSON.stringify({ timestamp: '2026-06-20T01:02:05Z', type: 'response_item', payload: { type: 'message', role: 'assistant', content: 'Session browser plan.' } }),
|
|
96
|
+
JSON.stringify({ timestamp: '2026-06-20T01:02:06Z', type: 'response_item', payload: { type: 'function_call', name: 'bash' } }),
|
|
97
|
+
JSON.stringify({ timestamp: '2026-06-20T01:02:07Z', type: 'response_item', payload: { type: 'function_call_output', output: 'ok' } })
|
|
98
|
+
].join('\n') + '\n', 'utf8');
|
|
99
|
+
const olderCodexSessionId = '019cc368-1111-7222-8333-123456789abc';
|
|
100
|
+
await fs.writeFile(path.join(codexSessionDir, `rollout-2026-06-19T01-02-03-${olderCodexSessionId}.jsonl`), [
|
|
101
|
+
JSON.stringify({ timestamp: '2026-06-19T01:02:03Z', type: 'session_meta', payload: { id: olderCodexSessionId, cwd: tmp } }),
|
|
102
|
+
JSON.stringify({ timestamp: '2026-06-19T01:02:04Z', type: 'response_item', payload: { type: 'message', role: 'user', content: 'Older session still readable by id' } })
|
|
103
|
+
].join('\n') + '\n', 'utf8');
|
|
104
|
+
const largeCodexSessionId = '019cc367-aaaa-7333-8444-123456789def';
|
|
105
|
+
await fs.writeFile(path.join(codexSessionDir, `rollout-2026-06-18T01-02-03-${largeCodexSessionId}.jsonl`), [
|
|
106
|
+
JSON.stringify({ timestamp: '2026-06-18T01:02:03Z', type: 'session_meta', payload: { id: largeCodexSessionId, cwd: tmp } }),
|
|
107
|
+
JSON.stringify({ timestamp: '2026-06-18T01:02:04Z', type: 'response_item', payload: { type: 'message', role: 'user', content: 'Large metadata session' } }),
|
|
108
|
+
JSON.stringify({ timestamp: '2026-06-18T01:02:05Z', type: 'response_item', payload: { type: 'function_call_output', output: 'x'.repeat(140000) } }),
|
|
109
|
+
JSON.stringify({ timestamp: '2026-06-18T01:02:06Z', type: 'response_item', payload: { type: 'message', role: 'assistant', content: 'Large tail summary' } })
|
|
110
|
+
].join('\n') + '\n', 'utf8');
|
|
111
|
+
const unreadableCodexSessionPath = path.join(codexSessionDir, 'rollout-2026-06-17T01-02-03-019cc366-bbbb-7444-8555-123456789aaa.jsonl');
|
|
112
|
+
await fs.writeFile(unreadableCodexSessionPath, [
|
|
113
|
+
JSON.stringify({ timestamp: '2026-06-17T01:02:03Z', type: 'session_meta', payload: { id: '019cc366-bbbb-7444-8555-123456789aaa', cwd: tmp } })
|
|
114
|
+
].join('\n') + '\n', 'utf8');
|
|
115
|
+
await fs.chmod(unreadableCodexSessionPath, 0o000);
|
|
116
|
+
await fs.mkdir(path.join(tmp, '.codex', 'skills', 'smoke-skill'), { recursive: true });
|
|
117
|
+
await fs.writeFile(path.join(tmp, '.codex', 'skills', 'smoke-skill', 'SKILL.md'), [
|
|
118
|
+
'---',
|
|
119
|
+
'name: smoke-skill',
|
|
120
|
+
'description: Smoke test skill discovery.',
|
|
121
|
+
'---',
|
|
122
|
+
'',
|
|
123
|
+
'# Smoke Skill',
|
|
124
|
+
''
|
|
125
|
+
].join('\n'), 'utf8');
|
|
126
|
+
await fs.mkdir(path.join(tmp, '.agents', 'skills', 'smoke-skill'), { recursive: true });
|
|
127
|
+
await fs.writeFile(path.join(tmp, '.agents', 'skills', 'smoke-skill', 'SKILL.md'), [
|
|
128
|
+
'---',
|
|
129
|
+
'name: smoke-skill',
|
|
130
|
+
'description: Duplicate smoke test skill discovery.',
|
|
131
|
+
'---',
|
|
132
|
+
'',
|
|
133
|
+
'# Duplicate Smoke Skill',
|
|
134
|
+
''
|
|
135
|
+
].join('\n'), 'utf8');
|
|
136
|
+
const outsideSkillRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-outside-skills-'));
|
|
137
|
+
await fs.mkdir(path.join(outsideSkillRoot, 'outside-skill'), { recursive: true });
|
|
138
|
+
await fs.writeFile(path.join(outsideSkillRoot, 'outside-skill', 'SKILL.md'), [
|
|
139
|
+
'---',
|
|
140
|
+
'name: outside-skill',
|
|
141
|
+
'description: Outside workspace skill.',
|
|
142
|
+
'---',
|
|
143
|
+
'',
|
|
144
|
+
'# Outside Skill',
|
|
145
|
+
''
|
|
146
|
+
].join('\n'), 'utf8');
|
|
147
|
+
try {
|
|
148
|
+
await fs.symlink(outsideSkillRoot, path.join(tmp, 'skills'), 'dir');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (process.platform !== 'win32' || error?.code !== 'EPERM') throw error;
|
|
151
|
+
}
|
|
152
|
+
await fs.writeFile(path.join(tmp, 'package.json'), JSON.stringify({
|
|
153
|
+
scripts: {
|
|
154
|
+
'test': "node --test",
|
|
155
|
+
'build:clients': "node -e \"console.log('clients ok')\""
|
|
156
|
+
}
|
|
157
|
+
}, null, 2), 'utf8');
|
|
158
|
+
await fs.mkdir(path.join(tmp, 'src'), { recursive: true });
|
|
159
|
+
await fs.writeFile(path.join(tmp, 'src', 'auth.ts'), 'export function authenticate(user) { return Boolean(user); }\n', 'utf8');
|
|
160
|
+
await fs.mkdir(path.join(tmp, 'test'), { recursive: true });
|
|
161
|
+
await fs.writeFile(path.join(tmp, 'test', 'auth.test.ts'), "import { authenticate } from '../src/auth.js';\nvoid authenticate('test');\n", 'utf8');
|
|
162
|
+
await fs.writeFile(path.join(tmp, 'é.ts'), 'export const accent = 1;\n', 'utf8');
|
|
163
|
+
await fs.writeFile(path.join(tmp, '旧名.ts'), 'export const renamed = true;\n', 'utf8');
|
|
164
|
+
const outside = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-outside-'));
|
|
165
|
+
await fs.writeFile(path.join(outside, 'secret.txt'), 'do-not-read', 'utf8');
|
|
166
|
+
const danglingSymlinks = [];
|
|
167
|
+
for (const [linkPath, targetPath] of [
|
|
168
|
+
['dangling-outside.txt', path.join(outside, 'created-outside.txt')],
|
|
169
|
+
['dangling-env.txt', path.join(tmp, '.env')]
|
|
170
|
+
]) {
|
|
171
|
+
try {
|
|
172
|
+
await fs.symlink(targetPath, path.join(tmp, linkPath));
|
|
173
|
+
danglingSymlinks.push(linkPath);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (process.platform !== 'win32' || error?.code !== 'EPERM') throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
let symlinkEscapePath = 'secret-link.txt';
|
|
179
|
+
try {
|
|
180
|
+
await fs.symlink(path.join(outside, 'secret.txt'), path.join(tmp, symlinkEscapePath));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (process.platform !== 'win32' || error?.code !== 'EPERM') throw error;
|
|
183
|
+
symlinkEscapePath = 'secret-link-dir/secret.txt';
|
|
184
|
+
await fs.symlink(outside, path.join(tmp, 'secret-link-dir'), 'junction');
|
|
185
|
+
}
|
|
186
|
+
for (const args of [['init'], ['config', 'core.quotePath', 'true'], ['add', 'demo.txt', 'other.txt', 'AGENTS.md', 'package.json', 'src/auth.ts', 'test/auth.test.ts', 'é.ts', '旧名.ts']]) {
|
|
187
|
+
const result = spawnSync('git', args, { cwd: tmp, encoding: 'utf8' });
|
|
188
|
+
if (result.status !== 0) {
|
|
189
|
+
throw new Error(`git ${args.join(' ')} failed: ${result.stderr || result.stdout}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const commitResult = spawnSync('git', ['-c', 'user.email=smoke@example.com', '-c', 'user.name=Smoke Test', 'commit', '-m', 'initial smoke fixture'], { cwd: tmp, encoding: 'utf8' });
|
|
193
|
+
if (commitResult.status !== 0) {
|
|
194
|
+
throw new Error(`git commit failed: ${commitResult.stderr || commitResult.stdout}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const client = new McpStdioClient('node', ['dist/stdio.js', '--root', tmp, '--allow-root', tmp, '--bash', 'safe', '--tool-mode', 'full'], {
|
|
198
|
+
cwd: path.resolve('.'),
|
|
199
|
+
env: { ...process.env, CODEXFLOW_ROOT: tmp, CODEXFLOW_ALLOWED_ROOTS: tmp, CODEXFLOW_WIDGET_DOMAIN: 'https://widgets.codexflow.test', CODEXFLOW_TOOL_CARDS: '0' }
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await client.request('initialize', {
|
|
203
|
+
protocolVersion: '2024-11-05',
|
|
204
|
+
capabilities: {},
|
|
205
|
+
clientInfo: { name: 'codexflow-smoke', version: '0.1.0' }
|
|
206
|
+
});
|
|
207
|
+
client.notify('notifications/initialized');
|
|
208
|
+
const tools = await client.request('tools/list', {});
|
|
209
|
+
const toolNames = tools.tools.map((tool) => tool.name);
|
|
210
|
+
for (const expected of ['server_config', 'codexflow_self_test', 'codexflow_inventory', 'list_projects', 'select_project', 'list_workspaces', 'open_current_workspace', 'open_workspace', 'workspace_snapshot', 'inspect_workspace', 'tree', 'search', 'load_skill', 'read', 'write', 'edit', 'apply_patch', 'bash', 'git_status', 'git_diff', 'show_changes', 'read_handoff', 'wait_for_handoff', 'codex_context', 'handoff_to_agent', 'handoff_to_codex', 'export_pro_context']) {
|
|
211
|
+
if (!toolNames.includes(expected)) throw new Error(`missing tool: ${expected}`);
|
|
212
|
+
}
|
|
213
|
+
const toolCardUri = 'ui://widget/codexflow-tool-card-v10.html';
|
|
214
|
+
const toolsByName = new Map(tools.tools.map((tool) => [tool.name, tool]));
|
|
215
|
+
function hasWidgetMeta(name) {
|
|
216
|
+
const meta = toolsByName.get(name)?._meta ?? {};
|
|
217
|
+
return meta.ui?.resourceUri === toolCardUri && meta['openai/outputTemplate'] === toolCardUri;
|
|
218
|
+
}
|
|
219
|
+
function hasToolCardStatusMeta(name) {
|
|
220
|
+
const meta = toolsByName.get(name)?._meta ?? {};
|
|
221
|
+
return Boolean(meta['openai/toolInvocation/invoking'] || meta['openai/toolInvocation/invoked']);
|
|
222
|
+
}
|
|
223
|
+
async function expectToolError(name, args, pattern, targetClient = client) {
|
|
224
|
+
const result = await targetClient.request('tools/call', { name, arguments: args });
|
|
225
|
+
if (!result.isError) {
|
|
226
|
+
throw new Error(`${name} unexpectedly succeeded`);
|
|
227
|
+
}
|
|
228
|
+
const text = result.content?.find?.((part) => part.type === 'text')?.text ?? JSON.stringify(result.structuredContent);
|
|
229
|
+
if (pattern && !pattern.test(text)) {
|
|
230
|
+
throw new Error(`${name} error did not match ${pattern}: ${text}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (const visualTool of toolNames) {
|
|
234
|
+
if (visualTool === 'list_projects' || visualTool === 'select_project') {
|
|
235
|
+
if (!hasWidgetMeta(visualTool)) throw new Error(`${visualTool} did not expose its required project picker widget`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (hasWidgetMeta(visualTool) || hasToolCardStatusMeta(visualTool)) throw new Error(`${visualTool} exposed widget metadata while CODEXFLOW_TOOL_CARDS is off`);
|
|
239
|
+
}
|
|
240
|
+
const projectList = await client.request('tools/call', { name: 'list_projects', arguments: { refresh: true } });
|
|
241
|
+
const secondProjectReal = await fs.realpath(secondProject);
|
|
242
|
+
const secondChoice = projectList.structuredContent.projects.find((project) => project.root === secondProjectReal);
|
|
243
|
+
if (!secondChoice) throw new Error(`project picker did not discover second project: ${JSON.stringify(projectList.structuredContent)}`);
|
|
244
|
+
const selectedProject = await client.request('tools/call', { name: 'select_project', arguments: { project_id: secondChoice.project_id, include_tree: false } });
|
|
245
|
+
if (selectedProject.structuredContent.root !== secondProjectReal || !selectedProject.structuredContent.skills) {
|
|
246
|
+
throw new Error(`project selection did not advertise routed capabilities: ${JSON.stringify(selectedProject.structuredContent)}`);
|
|
247
|
+
}
|
|
248
|
+
const routedRead = await client.request('tools/call', { name: 'read', arguments: { path: 'project-only.txt' } });
|
|
249
|
+
if (!routedRead.structuredContent.text?.includes('routed-to-second-project')) {
|
|
250
|
+
throw new Error(`workspace-less read was not routed to the selected project: ${JSON.stringify(routedRead.structuredContent)}`);
|
|
251
|
+
}
|
|
252
|
+
await client.request('tools/call', { name: 'open_workspace', arguments: { root: tmp, include_tree: false } });
|
|
253
|
+
const cardClient = new McpStdioClient('node', ['dist/stdio.js', '--root', tmp, '--allow-root', tmp, '--bash', 'safe', '--tool-mode', 'full'], {
|
|
254
|
+
cwd: path.resolve('.'),
|
|
255
|
+
env: { ...process.env, CODEXFLOW_ROOT: tmp, CODEXFLOW_ALLOWED_ROOTS: tmp, CODEXFLOW_TOOL_CARDS: '1' }
|
|
256
|
+
});
|
|
257
|
+
await cardClient.request('initialize', {
|
|
258
|
+
protocolVersion: '2024-11-05',
|
|
259
|
+
capabilities: {},
|
|
260
|
+
clientInfo: { name: 'codexflow-smoke-card-opt-in', version: '0.1.0' }
|
|
261
|
+
});
|
|
262
|
+
cardClient.notify('notifications/initialized');
|
|
263
|
+
const cardTools = await cardClient.request('tools/list', {});
|
|
264
|
+
const cardSearchMeta = cardTools.tools.find((tool) => tool.name === 'search')?._meta ?? {};
|
|
265
|
+
if (cardSearchMeta.ui?.resourceUri !== toolCardUri || cardSearchMeta['openai/outputTemplate'] !== toolCardUri) {
|
|
266
|
+
throw new Error('CODEXFLOW_TOOL_CARDS=1 did not opt search into widget metadata');
|
|
267
|
+
}
|
|
268
|
+
const cardOpened = await cardClient.request('tools/call', { name: 'open_current_workspace', arguments: { include_tree: false } });
|
|
269
|
+
const cardSearch = await cardClient.request('tools/call', {
|
|
270
|
+
name: 'search',
|
|
271
|
+
arguments: { workspace_id: cardOpened.structuredContent.workspace_id, query: 'read', path: 'demo.txt', max_results: 5 }
|
|
272
|
+
});
|
|
273
|
+
if (!cardSearch.structuredContent.text?.includes('read')) {
|
|
274
|
+
throw new Error(`CODEXFLOW_TOOL_CARDS=1 search did not include structured text: ${JSON.stringify(cardSearch.structuredContent)}`);
|
|
275
|
+
}
|
|
276
|
+
const cardInspect = await cardClient.request('tools/call', { name: 'inspect_workspace', arguments: { workspace_id: cardOpened.structuredContent.workspace_id } });
|
|
277
|
+
if (cardInspect.structuredContent.codexflow_tool !== 'inspect_workspace' || !cardInspect.structuredContent.coverage) {
|
|
278
|
+
throw new Error(`inspect workspace card payload missing analysis: ${JSON.stringify(cardInspect.structuredContent)}`);
|
|
279
|
+
}
|
|
280
|
+
const cardStructuredSearch = await cardClient.request('tools/call', {
|
|
281
|
+
name: 'search',
|
|
282
|
+
arguments: { workspace_id: cardOpened.structuredContent.workspace_id, query: 'authenticate', path: 'src', intent: 'symbol', include_tests: true }
|
|
283
|
+
});
|
|
284
|
+
if (cardStructuredSearch.structuredContent.codexflow_tool !== 'search' || !cardStructuredSearch.structuredContent.analysis?.groups?.definitions?.length) {
|
|
285
|
+
throw new Error(`structured search card payload missing grouped analysis: ${JSON.stringify(cardStructuredSearch.structuredContent)}`);
|
|
286
|
+
}
|
|
287
|
+
if (spawnSync(process.platform === 'win32' ? 'where' : 'sh', process.platform === 'win32' ? ['rg'] : ['-lc', 'command -v rg >/dev/null 2>&1']).status === 0) {
|
|
288
|
+
const cardRegexSearch = await cardClient.request('tools/call', {
|
|
289
|
+
name: 'search',
|
|
290
|
+
arguments: { workspace_id: cardOpened.structuredContent.workspace_id, query: '(?i)READ', path: 'demo.txt', regex: true, max_results: 5 }
|
|
291
|
+
});
|
|
292
|
+
if (!cardRegexSearch.structuredContent.matches?.length || cardRegexSearch.structuredContent.used !== 'ripgrep') {
|
|
293
|
+
throw new Error(`ripgrep regex search did not accept rg syntax: ${JSON.stringify(cardRegexSearch.structuredContent)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
await cardClient.close();
|
|
297
|
+
const resources = await client.request('resources/list', {});
|
|
298
|
+
const toolCard = resources.resources.find((resource) => resource.uri === toolCardUri);
|
|
299
|
+
if (!toolCard) throw new Error(`missing tool-card resource: ${toolCardUri}`);
|
|
300
|
+
if (toolCard.mimeType !== 'text/html;profile=mcp-app') throw new Error(`unexpected tool-card mime type: ${toolCard.mimeType}`);
|
|
301
|
+
const legacyToolCardUri = 'ui://widget/codexflow-tool-card-v8.html';
|
|
302
|
+
const legacyToolCard = resources.resources.find((resource) => resource.uri === legacyToolCardUri);
|
|
303
|
+
if (!legacyToolCard) throw new Error(`missing legacy tool-card resource: ${legacyToolCardUri}`);
|
|
304
|
+
const widget = await client.request('resources/read', { uri: toolCardUri });
|
|
305
|
+
const widgetText = widget.contents?.[0]?.text ?? '';
|
|
306
|
+
const widgetMeta = widget.contents?.[0]?._meta ?? {};
|
|
307
|
+
if (!widgetText.includes('<meta charset="utf-8">') || !widgetText.includes('Waiting for tool result') || !widgetText.includes('renderWorkspace') || !widgetText.includes('renderSelfTest') || !widgetText.includes('renderWorkspaceAnalysis') || !widgetText.includes('renderStructuredSearch') || !widgetText.includes('renderChangeAnalysis') || !widgetText.includes('details class="fold"') || !widgetText.includes('ui/notifications/tool-result')) {
|
|
308
|
+
throw new Error('tool-card widget resource did not include expected Apps bridge code');
|
|
309
|
+
}
|
|
310
|
+
if (!widgetMeta.ui?.csp || !widgetMeta['openai/widgetCSP']) {
|
|
311
|
+
throw new Error('tool-card widget resource did not expose standard and ChatGPT CSP metadata');
|
|
312
|
+
}
|
|
313
|
+
if (widgetMeta.ui?.domain !== 'https://widgets.codexflow.test' || widgetMeta['openai/widgetDomain'] !== 'https://widgets.codexflow.test') {
|
|
314
|
+
throw new Error('tool-card widget resource did not expose standard and ChatGPT widget domain metadata');
|
|
315
|
+
}
|
|
316
|
+
const legacyWidget = await client.request('resources/read', { uri: legacyToolCardUri });
|
|
317
|
+
if (legacyWidget.contents?.[0]?.uri !== legacyToolCardUri) {
|
|
318
|
+
throw new Error('legacy tool-card widget resource did not preserve requested URI');
|
|
319
|
+
}
|
|
320
|
+
if (!(legacyWidget.contents?.[0]?.text ?? '').includes('Waiting for tool result')) {
|
|
321
|
+
throw new Error('legacy tool-card widget resource did not serve widget HTML');
|
|
322
|
+
}
|
|
323
|
+
const current = await client.request('tools/call', { name: 'open_current_workspace', arguments: { include_tree: false } });
|
|
324
|
+
const realTmp = await fs.realpath(tmp);
|
|
325
|
+
if (current.structuredContent.root !== realTmp) throw new Error(`open_current_workspace opened ${current.structuredContent.root}, expected ${realTmp}`);
|
|
326
|
+
if (current.structuredContent.codexflow_tool !== 'open_current_workspace') throw new Error('tool result was not tagged for widget rendering');
|
|
327
|
+
if (current.structuredContent.tool_mode !== 'full') throw new Error(`open_current_workspace did not expose tool_mode: ${current.structuredContent.tool_mode}`);
|
|
328
|
+
if (current.structuredContent.skill_inventory?.length) {
|
|
329
|
+
throw new Error('open_current_workspace discovered skills by default');
|
|
330
|
+
}
|
|
331
|
+
const currentWithSkills = await client.request('tools/call', { name: 'open_current_workspace', arguments: { include_tree: false, include_skills: true } });
|
|
332
|
+
if (!currentWithSkills.structuredContent.skill_inventory?.some?.((skill) => skill.name === 'smoke-skill')) {
|
|
333
|
+
throw new Error('open_current_workspace did not discover workspace skill inventory when requested');
|
|
334
|
+
}
|
|
335
|
+
if (currentWithSkills.structuredContent.skill_inventory?.some?.((skill) => skill.name === 'outside-skill')) {
|
|
336
|
+
throw new Error('open_current_workspace followed a symlinked workspace skill root outside the workspace');
|
|
337
|
+
}
|
|
338
|
+
const selfTest = await client.request('tools/call', {
|
|
339
|
+
name: 'codexflow_self_test',
|
|
340
|
+
arguments: {
|
|
341
|
+
workspace_id: current.structuredContent.workspace_id,
|
|
342
|
+
max_skills: 12
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
if (selfTest.structuredContent.status === 'fail' || !selfTest.structuredContent.expected_tools?.includes?.('codexflow_self_test')) {
|
|
346
|
+
throw new Error(`codexflow_self_test failed: ${JSON.stringify(selfTest.structuredContent)}`);
|
|
347
|
+
}
|
|
348
|
+
if (JSON.stringify([...(selfTest.structuredContent.expected_tools ?? [])].sort()) !== JSON.stringify([...(selfTest.structuredContent.registered_tools ?? [])].sort())) {
|
|
349
|
+
throw new Error(`codexflow_self_test expected/registered tools mismatch: ${JSON.stringify(selfTest.structuredContent)}`);
|
|
350
|
+
}
|
|
351
|
+
if (!selfTest.structuredContent.files_touched?.includes?.('.ai-bridge/codexflow-self-test.md')) {
|
|
352
|
+
throw new Error('codexflow_self_test did not run the .ai-bridge write/edit probe');
|
|
353
|
+
}
|
|
354
|
+
const snapshotAlias = await client.request('tools/call', {
|
|
355
|
+
name: 'workspace_snapshot',
|
|
356
|
+
arguments: {
|
|
357
|
+
workspace_id: current.structuredContent.workspace_id,
|
|
358
|
+
max_depth: 1,
|
|
359
|
+
max_files: 20,
|
|
360
|
+
include_skills: false
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
if (!snapshotAlias.structuredContent.tree) {
|
|
364
|
+
throw new Error('workspace_snapshot did not accept max_files alias or return a tree');
|
|
365
|
+
}
|
|
366
|
+
await expectToolError('load_skill', { name: 'smoke-skill', source: 'workspace' }, /Multiple skills named smoke-skill/);
|
|
367
|
+
const loadedSkill = await client.request('tools/call', {
|
|
368
|
+
name: 'load_skill',
|
|
369
|
+
arguments: {
|
|
370
|
+
name: 'smoke-skill',
|
|
371
|
+
source: 'workspace',
|
|
372
|
+
path: '$WORKSPACE/.codex/skills/smoke-skill/SKILL.md'
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
if (loadedSkill.structuredContent.skill?.name !== 'smoke-skill' || !loadedSkill.structuredContent.text?.includes('# Smoke Skill')) {
|
|
376
|
+
throw new Error('load_skill did not return bounded SKILL.md content for smoke-skill');
|
|
377
|
+
}
|
|
378
|
+
await expectToolError('load_skill', { name: 'missing-skill' }, /Skill not found/);
|
|
379
|
+
await expectToolError('load_skill', { name: 'outside-skill', source: 'workspace', include_global_skills: false }, /Skill not found/);
|
|
380
|
+
const inventory = await client.request('tools/call', { name: 'codexflow_inventory', arguments: { include_global_skills: false, include_mcp_servers: false } });
|
|
381
|
+
if (inventory.structuredContent.codexflow_tool !== 'codexflow_inventory') throw new Error('inventory result was not tagged for widget rendering');
|
|
382
|
+
const opened = await client.request('tools/call', { name: 'open_workspace', arguments: { root: tmp, include_tree: true } });
|
|
383
|
+
const ws = opened.structuredContent.workspace_id;
|
|
384
|
+
const workspaceAnalysis = await client.request('tools/call', { name: 'inspect_workspace', arguments: { workspace_id: ws } });
|
|
385
|
+
if (!workspaceAnalysis.structuredContent.languages?.includes('typescript') || !workspaceAnalysis.structuredContent.coverage) {
|
|
386
|
+
throw new Error(`inspect_workspace omitted analysis: ${JSON.stringify(workspaceAnalysis.structuredContent)}`);
|
|
387
|
+
}
|
|
388
|
+
const legacySearch = await client.request('tools/call', { name: 'search', arguments: { workspace_id: ws, query: 'authenticate', path: 'src' } });
|
|
389
|
+
for (const key of ['matches', 'truncated', 'used']) {
|
|
390
|
+
if (!(key in legacySearch.structuredContent)) throw new Error(`legacy search lost ${key}`);
|
|
391
|
+
}
|
|
392
|
+
if ('analysis' in legacySearch.structuredContent) throw new Error('legacy search unexpectedly paid the structured-analysis cost');
|
|
393
|
+
const structuredSearch = await client.request('tools/call', {
|
|
394
|
+
name: 'search',
|
|
395
|
+
arguments: { workspace_id: ws, query: 'authenticate', path: 'src', intent: 'symbol', include_tests: true }
|
|
396
|
+
});
|
|
397
|
+
if (!structuredSearch.structuredContent.analysis?.groups?.definitions?.length || !structuredSearch.structuredContent.analysis.groups.tests?.length) {
|
|
398
|
+
throw new Error(`structured search omitted grouped analysis: ${JSON.stringify(structuredSearch.structuredContent)}`);
|
|
399
|
+
}
|
|
400
|
+
const openedByPath = await client.request('tools/call', { name: 'open_workspace', arguments: { path: tmp, include_tree: false } });
|
|
401
|
+
if (openedByPath.structuredContent.workspace_id !== ws) {
|
|
402
|
+
throw new Error(`open_workspace path alias returned ${openedByPath.structuredContent.workspace_id}, expected ${ws}`);
|
|
403
|
+
}
|
|
404
|
+
await client.request('tools/call', { name: 'read', arguments: { workspace_id: ws, path: 'demo.txt' } });
|
|
405
|
+
await fs.writeFile(path.join(tmp, 'tokens.txt'), [
|
|
406
|
+
'Authorization: Bearer ghp_abcdefghijklmnopqrstuvwxyz123456',
|
|
407
|
+
'https://example.test/mcp?codexflow_token=verysecretcodexflowtoken123&x=1',
|
|
408
|
+
'codexflow_token=secretsecret12345',
|
|
409
|
+
'"codexflow_token": "shortcodextoken"',
|
|
410
|
+
'ANTHROPIC_API_KEY=sk-ant-abcdefghijklmnopqrstuvwxyz123456',
|
|
411
|
+
'"api_key": "jsonsecretvalueabcdefghijklmnop"',
|
|
412
|
+
'service_token: yamlsecretvalueabcdefghijklmnop',
|
|
413
|
+
'ngrok config add-authtoken 2abcDEFghiJKLmnopQRSTuvWXyz_1234567890',
|
|
414
|
+
'cloudflared tunnel run --token eyJhbGciOiJIUzI1NiJ9.eyJ0dW5uZWwiOiJjb2RleHBybyJ9.signature1234567890',
|
|
415
|
+
'cloudflared tunnel run --token-file /Users/rebel/.codexflow/cloudflare-tunnel-token'
|
|
416
|
+
].join('\n'), 'utf8');
|
|
417
|
+
const secretRead = await client.request('tools/call', { name: 'read', arguments: { workspace_id: ws, path: 'config.txt' } });
|
|
418
|
+
const secretPayload = JSON.stringify(secretRead);
|
|
419
|
+
if (secretPayload.includes('sk-realSecretValue123') || !secretPayload.includes('[REDACTED_SECRET]')) {
|
|
420
|
+
throw new Error('read did not redact secret-looking content');
|
|
421
|
+
}
|
|
422
|
+
const tokenRead = await client.request('tools/call', { name: 'read', arguments: { workspace_id: ws, path: 'tokens.txt' } });
|
|
423
|
+
const tokenPayload = JSON.stringify(tokenRead);
|
|
424
|
+
for (const leaked of ['ghp_abcdefghijklmnopqrstuvwxyz123456', 'verysecretcodexflowtoken123', 'secretsecret12345', 'shortcodextoken', 'sk-ant-abcdefghijklmnopqrstuvwxyz123456', 'jsonsecretvalueabcdefghijklmnop', 'yamlsecretvalueabcdefghijklmnop', '2abcDEFghiJKLmnopQRSTuvWXyz_1234567890', 'eyJhbGciOiJIUzI1NiJ9.eyJ0dW5uZWwiOiJjb2RleHBybyJ9.signature1234567890']) {
|
|
425
|
+
if (tokenPayload.includes(leaked)) throw new Error(`read leaked token-like content: ${leaked}`);
|
|
426
|
+
}
|
|
427
|
+
if (!tokenPayload.includes('/Users/rebel/.codexflow/cloudflare-tunnel-token')) {
|
|
428
|
+
throw new Error('redaction hid a non-secret Cloudflare token-file path');
|
|
429
|
+
}
|
|
430
|
+
await expectToolError('write', { workspace_id: ws, path: 'notes.md', content: 'OPENAI_API_KEY=sk-realSecretValue123\n' }, /Secret-looking content is blocked/);
|
|
431
|
+
await expectToolError('write', { workspace_id: ws, path: 'token.txt', content: 'codexflow_token=shorttok\n' }, /Secret-looking content is blocked/);
|
|
432
|
+
await expectToolError('write', { workspace_id: ws, path: 'notes.yaml', content: 'api_key: yamlsecretvalueabcdefghijklmnop\n' }, /Secret-looking content is blocked/);
|
|
433
|
+
await client.request('tools/call', {
|
|
434
|
+
name: 'write',
|
|
435
|
+
arguments: {
|
|
436
|
+
workspace_id: ws,
|
|
437
|
+
path: 'env-ref.js',
|
|
438
|
+
content: 'const TOKEN = process.env.TOKEN;\nconst OPENAI_API_KEY = process.env.OPENAI_API_KEY;\nconst apiToken = getToken();\n'
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
const inspectAfterWrite = await client.request('tools/call', { name: 'inspect_workspace', arguments: { workspace_id: ws } });
|
|
442
|
+
if (inspectAfterWrite.structuredContent.cache?.hit !== false || !inspectAfterWrite.structuredContent.files?.some((file) => file.path === 'env-ref.js')) {
|
|
443
|
+
throw new Error(`write did not invalidate workspace analysis: ${JSON.stringify(inspectAfterWrite.structuredContent.cache)}`);
|
|
444
|
+
}
|
|
445
|
+
const envRefRead = await client.request('tools/call', { name: 'read', arguments: { workspace_id: ws, path: 'env-ref.js' } });
|
|
446
|
+
const envRefPayload = JSON.stringify(envRefRead);
|
|
447
|
+
if (envRefPayload.includes('[REDACTED_SECRET]')) {
|
|
448
|
+
throw new Error('env-var token references were incorrectly redacted as literal secrets');
|
|
449
|
+
}
|
|
450
|
+
const symlinkRead = await client.request('tools/call', { name: 'read', arguments: { workspace_id: ws, path: symlinkEscapePath } });
|
|
451
|
+
if (!symlinkRead.isError) throw new Error('symlink escape read was not blocked');
|
|
452
|
+
for (const linkPath of danglingSymlinks) {
|
|
453
|
+
await expectToolError('write', { workspace_id: ws, path: linkPath, content: 'escaped write\n' }, /symlink/i);
|
|
454
|
+
}
|
|
455
|
+
await client.request('tools/call', { name: 'edit', arguments: { workspace_id: ws, path: 'demo.txt', old_text: 'read\nread', new_text: 'read\nwrite' } });
|
|
456
|
+
await client.request('tools/call', { name: 'edit', arguments: { workspace_id: ws, path: 'src/auth.ts', old_text: 'return Boolean(user);', new_text: 'return Boolean(user?.trim());' } });
|
|
457
|
+
const inspectAfterEdit = await client.request('tools/call', { name: 'inspect_workspace', arguments: { workspace_id: ws } });
|
|
458
|
+
if (inspectAfterEdit.structuredContent.cache?.hit !== false) {
|
|
459
|
+
throw new Error(`edit did not invalidate workspace analysis: ${JSON.stringify(inspectAfterEdit.structuredContent.cache)}`);
|
|
460
|
+
}
|
|
461
|
+
const changes = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws } });
|
|
462
|
+
if (!changes.structuredContent.changed || !changes.structuredContent.diff.includes('demo.txt')) {
|
|
463
|
+
throw new Error('show_changes did not report the edited demo.txt diff');
|
|
464
|
+
}
|
|
465
|
+
if (!changes.structuredContent.analysis?.risk_signals?.some((risk) => risk.id === 'authentication')) {
|
|
466
|
+
throw new Error(`show_changes omitted authentication risk analysis: ${JSON.stringify(changes.structuredContent.analysis)}`);
|
|
467
|
+
}
|
|
468
|
+
if (!changes.structuredContent.analysis?.related_tests?.some((file) => file.path === 'test/auth.test.ts')) {
|
|
469
|
+
throw new Error(`show_changes omitted related auth test: ${JSON.stringify(changes.structuredContent.analysis)}`);
|
|
470
|
+
}
|
|
471
|
+
if (!changes.structuredContent.analysis?.recommended_commands?.some((item) => item.command === 'npm test')) {
|
|
472
|
+
throw new Error(`show_changes omitted existing npm test recommendation: ${JSON.stringify(changes.structuredContent.analysis)}`);
|
|
473
|
+
}
|
|
474
|
+
const repeatedChanges = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws } });
|
|
475
|
+
if (repeatedChanges.structuredContent.changed || repeatedChanges.structuredContent.diff || repeatedChanges.structuredContent.review_checkpoint_hit !== true || repeatedChanges.structuredContent.additions !== 0 || repeatedChanges.structuredContent.deletions !== 0) {
|
|
476
|
+
throw new Error(`show_changes repeated the same review instead of using the last-shown checkpoint: ${JSON.stringify(repeatedChanges.structuredContent)}`);
|
|
477
|
+
}
|
|
478
|
+
if ('analysis' in repeatedChanges.structuredContent) {
|
|
479
|
+
throw new Error(`show_changes recomputed analysis for an unchanged checkpoint: ${JSON.stringify(repeatedChanges.structuredContent.analysis)}`);
|
|
480
|
+
}
|
|
481
|
+
await client.request('tools/call', { name: 'edit', arguments: { workspace_id: ws, path: 'other.txt', old_text: 'keep', new_text: 'unrelated dirty file' } });
|
|
482
|
+
const patchResult = await client.request('tools/call', {
|
|
483
|
+
name: 'apply_patch',
|
|
484
|
+
arguments: {
|
|
485
|
+
workspace_id: ws,
|
|
486
|
+
patch: [
|
|
487
|
+
'diff --git a/demo.txt b/demo.txt',
|
|
488
|
+
'index f41f61c..be6d0ff 100644',
|
|
489
|
+
'--- a/demo.txt',
|
|
490
|
+
'+++ b/demo.txt',
|
|
491
|
+
'@@ -1,4 +1,4 @@',
|
|
492
|
+
' alpha',
|
|
493
|
+
' read',
|
|
494
|
+
' write',
|
|
495
|
+
'-omega',
|
|
496
|
+
'+omega patched'
|
|
497
|
+
].join('\n') + '\n'
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
if (!patchResult.structuredContent.changed || !patchResult.structuredContent.paths?.includes?.('demo.txt')) {
|
|
501
|
+
throw new Error(`apply_patch did not report the patched file: ${JSON.stringify(patchResult.structuredContent)}`);
|
|
502
|
+
}
|
|
503
|
+
if (patchResult.structuredContent.diff?.includes?.('other.txt')) {
|
|
504
|
+
throw new Error(`apply_patch leaked unrelated workspace diff: ${patchResult.structuredContent.diff}`);
|
|
505
|
+
}
|
|
506
|
+
const inspectAfterPatch = await client.request('tools/call', { name: 'inspect_workspace', arguments: { workspace_id: ws } });
|
|
507
|
+
if (inspectAfterPatch.structuredContent.cache?.hit !== false) {
|
|
508
|
+
throw new Error(`apply_patch did not invalidate workspace analysis: ${JSON.stringify(inspectAfterPatch.structuredContent.cache)}`);
|
|
509
|
+
}
|
|
510
|
+
const patchedRead = await client.request('tools/call', { name: 'read', arguments: { workspace_id: ws, path: 'demo.txt' } });
|
|
511
|
+
if (!patchedRead.content?.[0]?.text?.includes('omega patched')) {
|
|
512
|
+
throw new Error(`apply_patch did not update demo.txt: ${patchedRead.content?.[0]?.text}`);
|
|
513
|
+
}
|
|
514
|
+
await expectToolError('apply_patch', {
|
|
515
|
+
workspace_id: ws,
|
|
516
|
+
patch: [
|
|
517
|
+
'diff --git a/.env b/.env',
|
|
518
|
+
'new file mode 100644',
|
|
519
|
+
'index 0000000..e69de29',
|
|
520
|
+
'--- /dev/null',
|
|
521
|
+
'+++ b/.env',
|
|
522
|
+
'@@ -0,0 +1 @@',
|
|
523
|
+
'+SAFE_PLACEHOLDER=1'
|
|
524
|
+
].join('\n') + '\n'
|
|
525
|
+
}, /blocked/i);
|
|
526
|
+
await expectToolError('apply_patch', {
|
|
527
|
+
workspace_id: ws,
|
|
528
|
+
patch: [
|
|
529
|
+
'diff --git old/.env new/.env',
|
|
530
|
+
'new file mode 100644',
|
|
531
|
+
'index 0000000..e69de29',
|
|
532
|
+
'--- /dev/null',
|
|
533
|
+
'+++ new/.env',
|
|
534
|
+
'@@ -0,0 +1 @@',
|
|
535
|
+
'+SAFE_PLACEHOLDER=1'
|
|
536
|
+
].join('\n') + '\n'
|
|
537
|
+
}, /blocked/i);
|
|
538
|
+
await expectToolError('apply_patch', {
|
|
539
|
+
workspace_id: ws,
|
|
540
|
+
patch: [
|
|
541
|
+
'diff --git "a/foo\\057.env" "b/foo\\057.env"',
|
|
542
|
+
'new file mode 100644',
|
|
543
|
+
'index 0000000..e69de29',
|
|
544
|
+
'--- /dev/null',
|
|
545
|
+
'+++ "b/foo\\057.env"',
|
|
546
|
+
'@@ -0,0 +1 @@',
|
|
547
|
+
'+SAFE_PLACEHOLDER=1'
|
|
548
|
+
].join('\n') + '\n'
|
|
549
|
+
}, /blocked/i);
|
|
550
|
+
await expectToolError('apply_patch', {
|
|
551
|
+
workspace_id: ws,
|
|
552
|
+
patch: [
|
|
553
|
+
'diff --git a/demo.txt b/demo.txt',
|
|
554
|
+
'index be6d0ff..f4aa735 100644',
|
|
555
|
+
'--- a/demo.txt',
|
|
556
|
+
'+++ b/demo.txt',
|
|
557
|
+
'@@ -1,4 +1,4 @@',
|
|
558
|
+
' alpha',
|
|
559
|
+
' read',
|
|
560
|
+
' write',
|
|
561
|
+
'-omega patched',
|
|
562
|
+
'+omega copied',
|
|
563
|
+
'diff --git a/demo.txt b/.env',
|
|
564
|
+
'similarity index 100%',
|
|
565
|
+
'copy from demo.txt',
|
|
566
|
+
'copy to .env'
|
|
567
|
+
].join('\n') + '\n'
|
|
568
|
+
}, /blocked/i);
|
|
569
|
+
await expectToolError('apply_patch', {
|
|
570
|
+
workspace_id: ws,
|
|
571
|
+
patch: [
|
|
572
|
+
'diff --git a/link-outside b/link-outside',
|
|
573
|
+
'new file mode 120000 ',
|
|
574
|
+
'index 0000000..2e65efe',
|
|
575
|
+
'--- /dev/null',
|
|
576
|
+
'+++ b/link-outside',
|
|
577
|
+
'@@ -0,0 +1 @@',
|
|
578
|
+
'+/tmp/outside-target'
|
|
579
|
+
].join('\n') + '\n'
|
|
580
|
+
}, /symlink/i);
|
|
581
|
+
const postPatchChanges = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws } });
|
|
582
|
+
if (!postPatchChanges.structuredContent.changed || !postPatchChanges.structuredContent.diff.includes('omega patched')) {
|
|
583
|
+
throw new Error(`show_changes did not report new patch changes after checkpoint: ${JSON.stringify(postPatchChanges.structuredContent)}`);
|
|
584
|
+
}
|
|
585
|
+
const statsOnlyDiff = await client.request('tools/call', { name: 'git_diff', arguments: { workspace_id: ws, include_diff: false } });
|
|
586
|
+
if (statsOnlyDiff.structuredContent.include_diff !== false || statsOnlyDiff.structuredContent.diff !== '') {
|
|
587
|
+
throw new Error(`git_diff include_diff=false returned raw diff: ${JSON.stringify(statsOnlyDiff.structuredContent)}`);
|
|
588
|
+
}
|
|
589
|
+
if (!statsOnlyDiff.content?.[0]?.text?.includes('Raw diff omitted by include_diff=false')) {
|
|
590
|
+
throw new Error('git_diff include_diff=false did not report omitted diff in text output');
|
|
591
|
+
}
|
|
592
|
+
const statsOnlyChanges = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, path: 'other.txt', include_diff: false } });
|
|
593
|
+
if (!statsOnlyChanges.structuredContent.changed || statsOnlyChanges.structuredContent.diff !== '') {
|
|
594
|
+
throw new Error(`show_changes include_diff=false should keep stats and omit diff: ${JSON.stringify(statsOnlyChanges.structuredContent)}`);
|
|
595
|
+
}
|
|
596
|
+
const fullChangesAfterStatsOnly = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, path: './other.txt' } });
|
|
597
|
+
if (!fullChangesAfterStatsOnly.structuredContent.changed || fullChangesAfterStatsOnly.structuredContent.review_checkpoint_hit || !fullChangesAfterStatsOnly.structuredContent.diff.includes('other.txt')) {
|
|
598
|
+
throw new Error(`show_changes include_diff=false consumed the next full diff: ${JSON.stringify(fullChangesAfterStatsOnly.structuredContent)}`);
|
|
599
|
+
}
|
|
600
|
+
const statsOnlyAfterCheckpoint = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, path: 'other.txt', include_diff: false } });
|
|
601
|
+
if (!statsOnlyAfterCheckpoint.structuredContent.changed || statsOnlyAfterCheckpoint.structuredContent.diff !== '' || statsOnlyAfterCheckpoint.structuredContent.additions !== 1) {
|
|
602
|
+
throw new Error(`show_changes include_diff=false lost stats after checkpoint: ${JSON.stringify(statsOnlyAfterCheckpoint.structuredContent)}`);
|
|
603
|
+
}
|
|
604
|
+
if (statsOnlyAfterCheckpoint.structuredContent.review_marked) {
|
|
605
|
+
throw new Error(`show_changes include_diff=false claimed it updated the review checkpoint: ${JSON.stringify(statsOnlyAfterCheckpoint.structuredContent)}`);
|
|
606
|
+
}
|
|
607
|
+
const demoChanges = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, path: 'demo.txt' } });
|
|
608
|
+
if (!demoChanges.structuredContent.changed || !demoChanges.structuredContent.changed_files?.some?.((line) => line.includes('demo.txt'))) {
|
|
609
|
+
throw new Error(`path-scoped show_changes did not report demo.txt: ${JSON.stringify(demoChanges.structuredContent.changed_files)}`);
|
|
610
|
+
}
|
|
611
|
+
if (demoChanges.structuredContent.changed_files?.some?.((line) => line.includes('env-ref.js'))) {
|
|
612
|
+
throw new Error(`path-scoped show_changes leaked unrelated env-ref.js status: ${JSON.stringify(demoChanges.structuredContent.changed_files)}`);
|
|
613
|
+
}
|
|
614
|
+
if (JSON.stringify(demoChanges.structuredContent.analysis?.changed_paths) !== JSON.stringify(['demo.txt'])) {
|
|
615
|
+
throw new Error(`path-scoped show_changes leaked unrelated analysis: ${JSON.stringify(demoChanges.structuredContent.analysis)}`);
|
|
616
|
+
}
|
|
617
|
+
await fs.writeFile(path.join(tmp, 'é.ts'), 'export const accent = 2;\n', 'utf8');
|
|
618
|
+
const utf8Changes = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, path: 'é.ts', since: 'workspace' } });
|
|
619
|
+
if (JSON.stringify(utf8Changes.structuredContent.analysis?.changed_paths) !== JSON.stringify(['é.ts'])) {
|
|
620
|
+
throw new Error(`show_changes did not decode a Git-quoted UTF-8 path: ${JSON.stringify(utf8Changes.structuredContent.analysis)}`);
|
|
621
|
+
}
|
|
622
|
+
const renameResult = spawnSync('git', ['mv', '旧名.ts', '新名.ts'], { cwd: tmp, encoding: 'utf8' });
|
|
623
|
+
if (renameResult.status !== 0) throw new Error(`git mv UTF-8 path failed: ${renameResult.stderr || renameResult.stdout}`);
|
|
624
|
+
const utf8RenameChanges = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, staged: true, since: 'workspace' } });
|
|
625
|
+
if (!utf8RenameChanges.structuredContent.analysis?.changed_paths?.includes?.('新名.ts')) {
|
|
626
|
+
throw new Error(`show_changes did not decode a Git-quoted UTF-8 rename: ${JSON.stringify(utf8RenameChanges.structuredContent.analysis)}`);
|
|
627
|
+
}
|
|
628
|
+
const restoreRenameResult = spawnSync('git', ['mv', '新名.ts', '旧名.ts'], { cwd: tmp, encoding: 'utf8' });
|
|
629
|
+
if (restoreRenameResult.status !== 0) throw new Error(`git mv UTF-8 fixture restore failed: ${restoreRenameResult.stderr || restoreRenameResult.stdout}`);
|
|
630
|
+
const cleanPathChanges = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, path: 'package.json' } });
|
|
631
|
+
if (cleanPathChanges.structuredContent.changed || cleanPathChanges.structuredContent.changed_files?.length || cleanPathChanges.structuredContent.diff.includes('demo.txt')) {
|
|
632
|
+
throw new Error(`path-scoped show_changes leaked unrelated changes: ${JSON.stringify(cleanPathChanges.structuredContent)}`);
|
|
633
|
+
}
|
|
634
|
+
await fs.writeFile(path.join(tmp, 'staged-only.txt'), 'ready\n', 'utf8');
|
|
635
|
+
const stageOnlyResult = spawnSync('git', ['add', 'staged-only.txt'], { cwd: tmp, encoding: 'utf8' });
|
|
636
|
+
if (stageOnlyResult.status !== 0) throw new Error(`git add staged-only.txt failed: ${stageOnlyResult.stderr || stageOnlyResult.stdout}`);
|
|
637
|
+
await fs.writeFile(path.join(tmp, 'unstaged-only.txt'), 'dirty\n', 'utf8');
|
|
638
|
+
const defaultStagedPathChanges = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, path: 'staged-only.txt' } });
|
|
639
|
+
if (defaultStagedPathChanges.structuredContent.changed || defaultStagedPathChanges.structuredContent.diff || defaultStagedPathChanges.structuredContent.additions !== 0) {
|
|
640
|
+
throw new Error(`default show_changes reported staged-only changes as unstaged: ${JSON.stringify(defaultStagedPathChanges.structuredContent)}`);
|
|
641
|
+
}
|
|
642
|
+
const stagedChanges = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, staged: true } });
|
|
643
|
+
if (!stagedChanges.structuredContent.changed || !stagedChanges.structuredContent.diff.includes('staged-only.txt') || stagedChanges.structuredContent.diff.includes('unstaged-only.txt')) {
|
|
644
|
+
throw new Error(`staged show_changes mixed staged and unstaged files: ${JSON.stringify(stagedChanges.structuredContent)}`);
|
|
645
|
+
}
|
|
646
|
+
if (JSON.stringify(stagedChanges.structuredContent.analysis?.changed_paths) !== JSON.stringify(['staged-only.txt'])) {
|
|
647
|
+
throw new Error(`staged show_changes mixed analysis paths: ${JSON.stringify(stagedChanges.structuredContent.analysis)}`);
|
|
648
|
+
}
|
|
649
|
+
await client.request('tools/call', { name: 'write', arguments: { workspace_id: ws, path: 'new-review.txt', content: 'new file\n' } });
|
|
650
|
+
const untrackedChanges = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, path: 'new-review.txt' } });
|
|
651
|
+
if (!untrackedChanges.structuredContent.changed || !untrackedChanges.structuredContent.changed_files?.some?.((line) => line.includes('new-review.txt'))) {
|
|
652
|
+
throw new Error(`show_changes did not report untracked new file: ${JSON.stringify(untrackedChanges.structuredContent)}`);
|
|
653
|
+
}
|
|
654
|
+
await fs.writeFile(path.join(tmp, 'new-review.txt'), 'new file changed\n', 'utf8');
|
|
655
|
+
const changedUntrackedChanges = await client.request('tools/call', { name: 'show_changes', arguments: { workspace_id: ws, path: 'new-review.txt' } });
|
|
656
|
+
if (!changedUntrackedChanges.structuredContent.changed || changedUntrackedChanges.structuredContent.review_checkpoint_hit) {
|
|
657
|
+
throw new Error(`show_changes checkpoint hid changed untracked file content: ${JSON.stringify(changedUntrackedChanges.structuredContent)}`);
|
|
658
|
+
}
|
|
659
|
+
const codexContext = await client.request('tools/call', { name: 'codex_context', arguments: { workspace_id: ws, target_path: 'demo.txt' } });
|
|
660
|
+
if (!codexContext.structuredContent.agents_files.includes('AGENTS.md')) throw new Error('codex_context did not include AGENTS.md');
|
|
661
|
+
if (codexContext.structuredContent.agents_files.length !== 1) throw new Error(`codex_context returned duplicate AGENTS files: ${codexContext.structuredContent.agents_files.join(', ')}`);
|
|
662
|
+
if (!codexContext.content?.[0]?.text?.includes('Smoke Agents')) throw new Error('codex_context did not include AGENTS.md content');
|
|
663
|
+
const pwdBash = await client.request('tools/call', { name: 'bash', arguments: { workspace_id: ws, command: 'pwd' } });
|
|
664
|
+
const pwdBashText = pwdBash.content?.[0]?.text ?? '';
|
|
665
|
+
if (!pwdBashText.includes('Exit: 0') || pwdBashText.includes('## stdout') || pwdBashText.includes('## stderr')) {
|
|
666
|
+
throw new Error(`default bash transcript should be compact: ${pwdBashText}`);
|
|
667
|
+
}
|
|
668
|
+
if (!pwdBash.structuredContent.stdout?.includes(tmp)) {
|
|
669
|
+
throw new Error(`compact bash transcript dropped structured stdout: ${JSON.stringify(pwdBash.structuredContent)}`);
|
|
670
|
+
}
|
|
671
|
+
await expectToolError('bash', { workspace_id: ws, command: 'find /tmp' }, /blocked/i);
|
|
672
|
+
await expectToolError('bash', { workspace_id: ws, command: 'find . -fprint leaked.txt' }, /blocked/i);
|
|
673
|
+
await expectToolError('bash', { workspace_id: ws, command: 'git show HEAD:.env' }, /blocked/i);
|
|
674
|
+
await expectToolError('bash', { workspace_id: ws, command: 'ls $HOME' }, /blocked/i);
|
|
675
|
+
const clientBuild = await client.request('tools/call', { name: 'bash', arguments: { workspace_id: ws, command: 'npm run build:clients', timeout_ms: 60000 } });
|
|
676
|
+
if (!clientBuild.structuredContent.stdout?.includes('clients ok')) {
|
|
677
|
+
throw new Error('safe bash did not run npm run build:clients');
|
|
678
|
+
}
|
|
679
|
+
const exported = await client.request('tools/call', { name: 'export_pro_context', arguments: { workspace_id: ws, selected_paths: ['demo.txt'], max_files: 4, max_total_bytes: 80000 } });
|
|
680
|
+
if (exported.structuredContent.path !== '.ai-bridge/pro-context.md') throw new Error('export_pro_context wrote an unexpected path');
|
|
681
|
+
if (!exported.structuredContent.files_included?.includes('demo.txt')) {
|
|
682
|
+
throw new Error(`export_pro_context dropped an explicit selected path: ${JSON.stringify(exported.structuredContent.files_included)}`);
|
|
683
|
+
}
|
|
684
|
+
await fs.stat(path.join(tmp, '.ai-bridge', 'pro-context.md'));
|
|
685
|
+
const oneFileExport = await client.request('tools/call', {
|
|
686
|
+
name: 'export_pro_context',
|
|
687
|
+
arguments: {
|
|
688
|
+
workspace_id: ws,
|
|
689
|
+
selected_paths: ['demo.txt'],
|
|
690
|
+
max_files: 1,
|
|
691
|
+
max_total_bytes: 80000
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
if (JSON.stringify(oneFileExport.structuredContent.files_included) !== JSON.stringify(['demo.txt'])) {
|
|
695
|
+
throw new Error(`export_pro_context did not prioritize selected path with max_files=1: ${JSON.stringify(oneFileExport.structuredContent.files_included)}`);
|
|
696
|
+
}
|
|
697
|
+
const exactExport = await client.request('tools/call', {
|
|
698
|
+
name: 'export_pro_context',
|
|
699
|
+
arguments: {
|
|
700
|
+
workspace_id: ws,
|
|
701
|
+
selected_paths: ['demo.txt'],
|
|
702
|
+
include_important_files: false,
|
|
703
|
+
include_changed_files: false,
|
|
704
|
+
include_diff: false,
|
|
705
|
+
include_ai_bridge: false,
|
|
706
|
+
max_files: 4,
|
|
707
|
+
max_total_bytes: 80000
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
if (!exactExport.structuredContent.files_included?.includes('demo.txt')) {
|
|
711
|
+
throw new Error(`selected-only export did not include demo.txt: ${JSON.stringify(exactExport.structuredContent.files_included)}`);
|
|
712
|
+
}
|
|
713
|
+
if (exactExport.structuredContent.files_included?.some?.((file) => file !== 'demo.txt')) {
|
|
714
|
+
throw new Error(`selected-only export included unexpected files: ${JSON.stringify(exactExport.structuredContent.files_included)}`);
|
|
715
|
+
}
|
|
716
|
+
const exactProContext = await fs.readFile(path.join(tmp, '.ai-bridge', 'pro-context.md'), 'utf8');
|
|
717
|
+
if (!exactProContext.includes('Auto-include important root files: no') || !exactProContext.includes('Auto-include changed files: no')) {
|
|
718
|
+
throw new Error('selected-only export did not record disabled auto-inclusion settings');
|
|
719
|
+
}
|
|
720
|
+
if (exactProContext.includes('### AGENTS.md') || exactProContext.includes('### package.json') || exactProContext.includes('### env-ref.js')) {
|
|
721
|
+
throw new Error('selected-only export leaked auto-included important or changed files');
|
|
722
|
+
}
|
|
723
|
+
const agentHandoff = await client.request('tools/call', {
|
|
724
|
+
name: 'handoff_to_agent',
|
|
725
|
+
arguments: {
|
|
726
|
+
workspace_id: ws,
|
|
727
|
+
agent: 'opencode',
|
|
728
|
+
model: 'provider/cheap-model',
|
|
729
|
+
title: 'Smoke agent plan',
|
|
730
|
+
plan: '- Verify demo.txt contains write.'
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
if (agentHandoff.structuredContent.agent !== 'opencode') throw new Error('handoff_to_agent did not preserve target agent');
|
|
734
|
+
const escapedHandoff = await client.request('tools/call', {
|
|
735
|
+
name: 'handoff_to_agent',
|
|
736
|
+
arguments: {
|
|
737
|
+
workspace_id: ws,
|
|
738
|
+
agent: 'opencode',
|
|
739
|
+
model: 'foo; touch /tmp/pwned',
|
|
740
|
+
title: 'Escaped model plan',
|
|
741
|
+
plan: '- Verify shell hints quote model names.'
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
const escapedPrompt = escapedHandoff.content?.find?.((part) => part.type === 'text')?.text ?? '';
|
|
745
|
+
if (!escapedPrompt.includes("--model 'foo; touch /tmp/pwned'")) {
|
|
746
|
+
throw new Error(`handoff_to_agent did not shell-quote the model hint: ${escapedPrompt}`);
|
|
747
|
+
}
|
|
748
|
+
if (escapedPrompt.includes('--model foo; touch')) {
|
|
749
|
+
throw new Error(`handoff_to_agent exposed an unquoted model hint: ${escapedPrompt}`);
|
|
750
|
+
}
|
|
751
|
+
for (const bridgeFile of ['agent-status.md', 'implementation-diff.patch', 'execution-log.jsonl']) {
|
|
752
|
+
await fs.stat(path.join(tmp, '.ai-bridge', bridgeFile));
|
|
753
|
+
}
|
|
754
|
+
const handoffContext = await client.request('tools/call', { name: 'read_handoff', arguments: { workspace_id: ws } });
|
|
755
|
+
for (const expectedFile of ['.ai-bridge/agent-status.md', '.ai-bridge/implementation-diff.patch', '.ai-bridge/execution-log.jsonl']) {
|
|
756
|
+
if (!handoffContext.structuredContent.files.includes(expectedFile)) {
|
|
757
|
+
throw new Error(`read_handoff did not include ${expectedFile}`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const runStatePayload = {
|
|
761
|
+
version: 1,
|
|
762
|
+
state: 'completed',
|
|
763
|
+
iteration: 1,
|
|
764
|
+
plan_hash: 'smoke-plan-hash',
|
|
765
|
+
executor: 'opencode',
|
|
766
|
+
model: 'provider/cheap-model',
|
|
767
|
+
exit_code: 0,
|
|
768
|
+
timed_out: false,
|
|
769
|
+
started_at: new Date(Date.now() - 1000).toISOString(),
|
|
770
|
+
finished_at: new Date().toISOString(),
|
|
771
|
+
status_file: '.ai-bridge/agent-status.md',
|
|
772
|
+
diff_file: '.ai-bridge/implementation-diff.patch',
|
|
773
|
+
log_file: '.ai-bridge/execution-log.jsonl'
|
|
774
|
+
};
|
|
775
|
+
await fs.writeFile(path.join(tmp, '.ai-bridge', 'handoff-run-state.json'), `${JSON.stringify(runStatePayload, null, 2)}\n`, 'utf8');
|
|
776
|
+
const waitCompleted = await client.request('tools/call', {
|
|
777
|
+
name: 'wait_for_handoff',
|
|
778
|
+
arguments: { workspace_id: ws, max_wait_seconds: 1, poll_ms: 250, plan_hash: 'smoke-plan-hash' }
|
|
779
|
+
});
|
|
780
|
+
if (waitCompleted.structuredContent.awaited_completed !== true || waitCompleted.structuredContent.state !== 'completed') {
|
|
781
|
+
throw new Error(`wait_for_handoff did not report completion: ${JSON.stringify(waitCompleted.structuredContent)}`);
|
|
782
|
+
}
|
|
783
|
+
if (waitCompleted.structuredContent.awaited_terminal !== true || waitCompleted.structuredContent.succeeded !== true) {
|
|
784
|
+
throw new Error(`wait_for_handoff did not report terminal success fields: ${JSON.stringify(waitCompleted.structuredContent)}`);
|
|
785
|
+
}
|
|
786
|
+
if (waitCompleted.structuredContent.exit_code !== 0 || waitCompleted.structuredContent.status_file !== '.ai-bridge/agent-status.md') {
|
|
787
|
+
throw new Error(`wait_for_handoff missing completion fields: ${JSON.stringify(waitCompleted.structuredContent)}`);
|
|
788
|
+
}
|
|
789
|
+
const waitMismatch = await client.request('tools/call', {
|
|
790
|
+
name: 'wait_for_handoff',
|
|
791
|
+
arguments: { workspace_id: ws, max_wait_seconds: 1, poll_ms: 250, plan_hash: 'a-different-hash' }
|
|
792
|
+
});
|
|
793
|
+
if (waitMismatch.structuredContent.awaited_completed !== false || waitMismatch.structuredContent.state !== 'running' || waitMismatch.structuredContent.plan_hash_mismatch !== true) {
|
|
794
|
+
throw new Error(`wait_for_handoff did not keep waiting on plan-hash mismatch: ${JSON.stringify(waitMismatch.structuredContent)}`);
|
|
795
|
+
}
|
|
796
|
+
await fs.writeFile(path.join(tmp, '.ai-bridge', 'handoff-run-state.json'), `${JSON.stringify({
|
|
797
|
+
...runStatePayload,
|
|
798
|
+
state: 'failed',
|
|
799
|
+
plan_hash: 'failed-plan',
|
|
800
|
+
exit_code: 2,
|
|
801
|
+
status_file: 'demo.txt',
|
|
802
|
+
diff_file: '../demo.txt',
|
|
803
|
+
log_file: '.ai-bridge/execution-log.jsonl'
|
|
804
|
+
}, null, 2)}\n`, 'utf8');
|
|
805
|
+
const waitFailed = await client.request('tools/call', {
|
|
806
|
+
name: 'wait_for_handoff',
|
|
807
|
+
arguments: { workspace_id: ws, max_wait_seconds: 1, poll_ms: 250, plan_hash: 'failed-plan' }
|
|
808
|
+
});
|
|
809
|
+
if (waitFailed.structuredContent.awaited_terminal !== true || waitFailed.structuredContent.awaited_completed !== false || waitFailed.structuredContent.succeeded !== false || waitFailed.structuredContent.state !== 'failed') {
|
|
810
|
+
throw new Error(`wait_for_handoff did not report failed terminal state: ${JSON.stringify(waitFailed.structuredContent)}`);
|
|
811
|
+
}
|
|
812
|
+
if (waitFailed.structuredContent.status_file !== '.ai-bridge/agent-status.md' || waitFailed.structuredContent.diff_file !== '.ai-bridge/implementation-diff.patch') {
|
|
813
|
+
throw new Error(`wait_for_handoff trusted forged artifact paths: ${JSON.stringify(waitFailed.structuredContent)}`);
|
|
814
|
+
}
|
|
815
|
+
await fs.writeFile(path.join(tmp, '.ai-bridge', 'handoff-run-state.json'), `${JSON.stringify({
|
|
816
|
+
...runStatePayload,
|
|
817
|
+
state: 'timed_out',
|
|
818
|
+
plan_hash: 'timed-out-plan',
|
|
819
|
+
exit_code: null,
|
|
820
|
+
timed_out: true
|
|
821
|
+
}, null, 2)}\n`, 'utf8');
|
|
822
|
+
const waitTimedOut = await client.request('tools/call', {
|
|
823
|
+
name: 'wait_for_handoff',
|
|
824
|
+
arguments: { workspace_id: ws, max_wait_seconds: 1, poll_ms: 250, plan_hash: 'timed-out-plan' }
|
|
825
|
+
});
|
|
826
|
+
if (waitTimedOut.structuredContent.awaited_terminal !== true || waitTimedOut.structuredContent.awaited_completed !== false || waitTimedOut.structuredContent.succeeded !== false || waitTimedOut.structuredContent.state !== 'timed_out') {
|
|
827
|
+
throw new Error(`wait_for_handoff did not report timed-out terminal state: ${JSON.stringify(waitTimedOut.structuredContent)}`);
|
|
828
|
+
}
|
|
829
|
+
await fs.rm(path.join(tmp, '.ai-bridge', 'handoff-run-state.json'), { force: true });
|
|
830
|
+
await client.request('tools/call', { name: 'handoff_to_codex', arguments: { workspace_id: ws, title: 'Smoke Codex plan', plan: '- Verify demo.txt contains write.', append: true } });
|
|
831
|
+
await fs.writeFile(path.join(tmp, '.ai-bridge', 'current-plan.md'), 'x'.repeat(190000), 'utf8');
|
|
832
|
+
await expectToolError('handoff_to_agent', {
|
|
833
|
+
workspace_id: ws,
|
|
834
|
+
agent: 'opencode',
|
|
835
|
+
title: 'Oversized append plan',
|
|
836
|
+
plan: '- This append should fail before loading the existing plan.',
|
|
837
|
+
append: true
|
|
838
|
+
}, /File is too large/);
|
|
839
|
+
client.close();
|
|
840
|
+
async function assertToolMode(mode, expected, hidden, extraEnv = {}) {
|
|
841
|
+
const args = ['dist/stdio.js', '--root', tmp, '--allow-root', tmp, '--bash', 'safe'];
|
|
842
|
+
if (mode) args.push('--tool-mode', mode);
|
|
843
|
+
const modeClient = new McpStdioClient('node', args, {
|
|
844
|
+
cwd: path.resolve('.'),
|
|
845
|
+
env: { ...process.env, CODEXFLOW_ROOT: tmp, CODEXFLOW_ALLOWED_ROOTS: tmp, CODEXFLOW_TOOL_MODE: '', ...extraEnv }
|
|
846
|
+
});
|
|
847
|
+
await modeClient.request('initialize', {
|
|
848
|
+
protocolVersion: '2024-11-05',
|
|
849
|
+
capabilities: {},
|
|
850
|
+
clientInfo: { name: `codexflow-${mode || 'default'}-smoke`, version: '0.1.0' }
|
|
851
|
+
});
|
|
852
|
+
modeClient.notify('notifications/initialized');
|
|
853
|
+
const modeTools = await modeClient.request('tools/list', {});
|
|
854
|
+
const names = modeTools.tools.map((tool) => tool.name);
|
|
855
|
+
for (const expectedName of expected) {
|
|
856
|
+
if (!names.includes(expectedName)) throw new Error(`${mode || 'default'} mode missing ${expectedName}; got ${names.join(', ')}`);
|
|
857
|
+
}
|
|
858
|
+
for (const hiddenName of hidden) {
|
|
859
|
+
if (names.includes(hiddenName)) throw new Error(`${mode || 'default'} mode should hide ${hiddenName}; got ${names.join(', ')}`);
|
|
860
|
+
}
|
|
861
|
+
const superActions = await modeClient.request('tools/call', { name: 'codexflow', arguments: { action: 'list_actions' } });
|
|
862
|
+
const expectedActions = names.filter((name) => name !== 'codexflow').sort();
|
|
863
|
+
const actualActions = [...superActions.structuredContent.actions].sort();
|
|
864
|
+
if (JSON.stringify(actualActions) !== JSON.stringify(expectedActions)) {
|
|
865
|
+
throw new Error(`${mode || 'default'} supertool actions did not match registered tools: expected ${expectedActions.join(', ')} got ${actualActions.join(', ')}`);
|
|
866
|
+
}
|
|
867
|
+
modeClient.close();
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
await assertToolMode('', ['codexflow', 'server_config', 'codexflow_self_test', 'list_projects', 'select_project', 'open_current_workspace', 'open_workspace', 'inspect_workspace', 'tree', 'search', 'load_skill', 'read', 'write', 'edit', 'apply_patch', 'bash', 'show_changes', 'read_handoff', 'wait_for_handoff', 'export_pro_context', 'handoff_to_agent'], ['codexflow_inventory', 'workspace_snapshot', 'git_status', 'git_diff', 'codex_context', 'handoff_to_codex']);
|
|
871
|
+
await assertToolMode('minimal', ['codexflow', 'server_config', 'codexflow_self_test', 'list_projects', 'select_project', 'open_current_workspace', 'open_workspace', 'load_skill', 'read', 'write', 'edit', 'apply_patch', 'bash', 'show_changes'], ['inspect_workspace', 'tree', 'search', 'read_handoff', 'wait_for_handoff', 'export_pro_context', 'handoff_to_agent', 'codex_context']);
|
|
872
|
+
await assertToolMode('', ['codexflow', 'server_config', 'show_changes', 'search'], ['inspect_workspace'], { CODEXFLOW_ANALYSIS: '0' });
|
|
873
|
+
|
|
874
|
+
const handoffWriteClient = new McpStdioClient('node', ['dist/stdio.js', '--root', tmp, '--allow-root', tmp, '--write', 'handoff'], {
|
|
875
|
+
cwd: path.resolve('.'),
|
|
876
|
+
env: { ...process.env, CODEXFLOW_ROOT: tmp, CODEXFLOW_ALLOWED_ROOTS: tmp, CODEXFLOW_TOOL_MODE: '' }
|
|
877
|
+
});
|
|
878
|
+
await handoffWriteClient.request('initialize', {
|
|
879
|
+
protocolVersion: '2024-11-05',
|
|
880
|
+
capabilities: {},
|
|
881
|
+
clientInfo: { name: 'codexflow-write-handoff-smoke', version: '0.1.0' }
|
|
882
|
+
});
|
|
883
|
+
handoffWriteClient.notify('notifications/initialized');
|
|
884
|
+
const handoffWriteTools = await handoffWriteClient.request('tools/list', {});
|
|
885
|
+
const handoffWriteToolNames = handoffWriteTools.tools.map((tool) => tool.name);
|
|
886
|
+
for (const hiddenWriteTool of ['write', 'edit', 'apply_patch']) {
|
|
887
|
+
if (handoffWriteToolNames.includes(hiddenWriteTool)) {
|
|
888
|
+
throw new Error(`--write handoff should not advertise ${hiddenWriteTool} tool; got ${handoffWriteToolNames.join(', ')}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
const handoffWriteConfig = await handoffWriteClient.request('tools/call', { name: 'server_config', arguments: {} });
|
|
892
|
+
if (handoffWriteConfig.structuredContent.writeMode !== 'handoff' || handoffWriteConfig.structuredContent.registeredTools?.includes?.('write') || handoffWriteConfig.structuredContent.registeredTools?.includes?.('edit') || handoffWriteConfig.structuredContent.registeredTools?.includes?.('apply_patch')) {
|
|
893
|
+
throw new Error(`server_config did not report write handoff with hidden edit tools: ${JSON.stringify(handoffWriteConfig.structuredContent)}`);
|
|
894
|
+
}
|
|
895
|
+
const handoffSelfTest = await handoffWriteClient.request('tools/call', { name: 'codexflow_self_test', arguments: { write_probe: false, bash_probe: false, pro_context_probe: false } });
|
|
896
|
+
if (handoffSelfTest.structuredContent.status === 'fail') {
|
|
897
|
+
throw new Error(`codexflow_self_test failed under --write handoff: ${JSON.stringify(handoffSelfTest.structuredContent)}`);
|
|
898
|
+
}
|
|
899
|
+
for (const hiddenWriteTool of ['write', 'edit', 'apply_patch']) {
|
|
900
|
+
if (handoffSelfTest.structuredContent.expected_tools?.includes?.(hiddenWriteTool) || handoffSelfTest.structuredContent.registered_tools?.includes?.(hiddenWriteTool)) {
|
|
901
|
+
throw new Error(`codexflow_self_test exposed ${hiddenWriteTool} under --write handoff: ${JSON.stringify(handoffSelfTest.structuredContent)}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
handoffWriteClient.close();
|
|
905
|
+
|
|
906
|
+
const noBashClient = new McpStdioClient('node', ['dist/stdio.js', '--root', tmp, '--allow-root', tmp, '--bash', 'off'], {
|
|
907
|
+
cwd: path.resolve('.'),
|
|
908
|
+
env: { ...process.env, CODEXFLOW_ROOT: tmp, CODEXFLOW_ALLOWED_ROOTS: tmp, CODEXFLOW_TOOL_MODE: '' }
|
|
909
|
+
});
|
|
910
|
+
await noBashClient.request('initialize', {
|
|
911
|
+
protocolVersion: '2024-11-05',
|
|
912
|
+
capabilities: {},
|
|
913
|
+
clientInfo: { name: 'codexflow-no-bash-smoke', version: '0.1.0' }
|
|
914
|
+
});
|
|
915
|
+
noBashClient.notify('notifications/initialized');
|
|
916
|
+
const noBashTools = await noBashClient.request('tools/list', {});
|
|
917
|
+
const noBashToolNames = noBashTools.tools.map((tool) => tool.name);
|
|
918
|
+
if (noBashToolNames.includes('bash')) {
|
|
919
|
+
throw new Error(`--bash off should not advertise bash tool; got ${noBashToolNames.join(', ')}`);
|
|
920
|
+
}
|
|
921
|
+
const noBashConfig = await noBashClient.request('tools/call', { name: 'server_config', arguments: {} });
|
|
922
|
+
if (noBashConfig.structuredContent.bashMode !== 'off') {
|
|
923
|
+
throw new Error(`server_config did not report bash off: ${JSON.stringify(noBashConfig.structuredContent)}`);
|
|
924
|
+
}
|
|
925
|
+
noBashClient.close();
|
|
926
|
+
|
|
927
|
+
const disabledWriteClient = new McpStdioClient('node', ['dist/stdio.js', '--root', tmp, '--allow-root', tmp, '--write', 'off'], {
|
|
928
|
+
cwd: path.resolve('.'),
|
|
929
|
+
env: { ...process.env, CODEXFLOW_ROOT: tmp, CODEXFLOW_ALLOWED_ROOTS: tmp, CODEXFLOW_TOOL_MODE: '' }
|
|
930
|
+
});
|
|
931
|
+
await disabledWriteClient.request('initialize', {
|
|
932
|
+
protocolVersion: '2024-11-05',
|
|
933
|
+
capabilities: {},
|
|
934
|
+
clientInfo: { name: 'codexflow-write-off-smoke', version: '0.1.0' }
|
|
935
|
+
});
|
|
936
|
+
disabledWriteClient.notify('notifications/initialized');
|
|
937
|
+
const disabledWriteTools = await disabledWriteClient.request('tools/list', {});
|
|
938
|
+
const disabledWriteToolNames = disabledWriteTools.tools.map((tool) => tool.name);
|
|
939
|
+
for (const hiddenWriteTool of ['write', 'edit', 'apply_patch']) {
|
|
940
|
+
if (disabledWriteToolNames.includes(hiddenWriteTool)) {
|
|
941
|
+
throw new Error(`--write off should not advertise ${hiddenWriteTool} tool; got ${disabledWriteToolNames.join(', ')}`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const disabledWriteConfig = await disabledWriteClient.request('tools/call', { name: 'server_config', arguments: {} });
|
|
945
|
+
if (disabledWriteConfig.structuredContent.writeMode !== 'off') {
|
|
946
|
+
throw new Error(`server_config did not report write off: ${JSON.stringify(disabledWriteConfig.structuredContent)}`);
|
|
947
|
+
}
|
|
948
|
+
const disabledSelfTest = await disabledWriteClient.request('tools/call', { name: 'codexflow_self_test', arguments: { write_probe: false, bash_probe: false, pro_context_probe: false } });
|
|
949
|
+
if (disabledSelfTest.structuredContent.status === 'fail') {
|
|
950
|
+
throw new Error(`codexflow_self_test failed under --write off: ${JSON.stringify(disabledSelfTest.structuredContent)}`);
|
|
951
|
+
}
|
|
952
|
+
for (const hiddenWriteTool of ['write', 'edit', 'apply_patch']) {
|
|
953
|
+
if (disabledSelfTest.structuredContent.expected_tools?.includes?.(hiddenWriteTool) || disabledSelfTest.structuredContent.registered_tools?.includes?.(hiddenWriteTool)) {
|
|
954
|
+
throw new Error(`codexflow_self_test exposed ${hiddenWriteTool} under --write off: ${JSON.stringify(disabledSelfTest.structuredContent)}`);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
disabledWriteClient.close();
|
|
958
|
+
|
|
959
|
+
const standardCodexSessionsClient = new McpStdioClient('node', ['dist/stdio.js', '--root', tmp, '--allow-root', tmp], {
|
|
960
|
+
cwd: path.resolve('.'),
|
|
961
|
+
env: {
|
|
962
|
+
...process.env,
|
|
963
|
+
CODEXFLOW_ROOT: tmp,
|
|
964
|
+
CODEXFLOW_ALLOWED_ROOTS: tmp,
|
|
965
|
+
CODEXFLOW_CODEX_SESSIONS: 'metadata',
|
|
966
|
+
CODEXFLOW_CODEX_DIR: codexHistoryDir
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
await standardCodexSessionsClient.request('initialize', {
|
|
970
|
+
protocolVersion: '2024-11-05',
|
|
971
|
+
capabilities: {},
|
|
972
|
+
clientInfo: { name: 'codexflow-standard-codex-sessions-smoke', version: '0.1.0' }
|
|
973
|
+
});
|
|
974
|
+
standardCodexSessionsClient.notify('notifications/initialized');
|
|
975
|
+
const standardCodexSessionTools = await standardCodexSessionsClient.request('tools/list', {});
|
|
976
|
+
const standardCodexSessionToolNames = standardCodexSessionTools.tools.map((tool) => tool.name);
|
|
977
|
+
if (!standardCodexSessionToolNames.includes('codex_sessions')) {
|
|
978
|
+
throw new Error(`standard mode with Codex sessions enabled missed codex_sessions: ${standardCodexSessionToolNames.join(', ')}`);
|
|
979
|
+
}
|
|
980
|
+
if (standardCodexSessionToolNames.includes('read_codex_session')) {
|
|
981
|
+
throw new Error(`metadata mode should not expose read_codex_session: ${standardCodexSessionToolNames.join(', ')}`);
|
|
982
|
+
}
|
|
983
|
+
const metadataSessions = await standardCodexSessionsClient.request('tools/call', { name: 'codex_sessions', arguments: { query: 'Large tail summary', max_sessions: 5 } });
|
|
984
|
+
if (metadataSessions.structuredContent.total_found !== 0 || JSON.stringify(metadataSessions.structuredContent).includes('Large tail summary')) {
|
|
985
|
+
throw new Error(`metadata mode exposed transcript tail content: ${JSON.stringify(metadataSessions.structuredContent)}`);
|
|
986
|
+
}
|
|
987
|
+
standardCodexSessionsClient.close();
|
|
988
|
+
|
|
989
|
+
const fullTranscriptClient = new McpStdioClient('node', ['dist/stdio.js', '--root', tmp, '--allow-root', tmp, '--bash', 'safe'], {
|
|
990
|
+
cwd: path.resolve('.'),
|
|
991
|
+
env: { ...process.env, CODEXFLOW_ROOT: tmp, CODEXFLOW_ALLOWED_ROOTS: tmp, CODEXFLOW_BASH_TRANSCRIPT: 'full' }
|
|
992
|
+
});
|
|
993
|
+
await fullTranscriptClient.request('initialize', {
|
|
994
|
+
protocolVersion: '2024-11-05',
|
|
995
|
+
capabilities: {},
|
|
996
|
+
clientInfo: { name: 'codexflow-full-bash-transcript-smoke', version: '0.1.0' }
|
|
997
|
+
});
|
|
998
|
+
fullTranscriptClient.notify('notifications/initialized');
|
|
999
|
+
const fullTranscriptBash = await fullTranscriptClient.request('tools/call', { name: 'bash', arguments: { command: 'pwd' } });
|
|
1000
|
+
const fullTranscriptText = fullTranscriptBash.content?.[0]?.text ?? '';
|
|
1001
|
+
if (!fullTranscriptText.includes('## stdout') || !fullTranscriptText.includes(tmp)) {
|
|
1002
|
+
throw new Error(`full bash transcript mode did not preserve raw stdout in chat text: ${fullTranscriptText}`);
|
|
1003
|
+
}
|
|
1004
|
+
fullTranscriptClient.close();
|
|
1005
|
+
|
|
1006
|
+
const emptyCodexDirClient = new McpStdioClient('node', ['dist/stdio.js', '--root', tmp, '--allow-root', tmp], {
|
|
1007
|
+
cwd: path.resolve('.'),
|
|
1008
|
+
env: {
|
|
1009
|
+
...process.env,
|
|
1010
|
+
CODEXFLOW_ROOT: tmp,
|
|
1011
|
+
CODEXFLOW_ALLOWED_ROOTS: tmp,
|
|
1012
|
+
CODEXFLOW_CODEX_DIR: ''
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
await emptyCodexDirClient.request('initialize', {
|
|
1016
|
+
protocolVersion: '2024-11-05',
|
|
1017
|
+
capabilities: {},
|
|
1018
|
+
clientInfo: { name: 'codexflow-empty-codex-dir-smoke', version: '0.1.0' }
|
|
1019
|
+
});
|
|
1020
|
+
emptyCodexDirClient.notify('notifications/initialized');
|
|
1021
|
+
const emptyCodexDirConfig = await emptyCodexDirClient.request('tools/call', { name: 'server_config', arguments: {} });
|
|
1022
|
+
const expectedDefaultCodexDir = path.join(os.homedir(), '.codex');
|
|
1023
|
+
if (emptyCodexDirConfig.structuredContent.codexDir !== expectedDefaultCodexDir) {
|
|
1024
|
+
throw new Error(`empty CODEXFLOW_CODEX_DIR resolved to ${emptyCodexDirConfig.structuredContent.codexDir}, expected ${expectedDefaultCodexDir}`);
|
|
1025
|
+
}
|
|
1026
|
+
emptyCodexDirClient.close();
|
|
1027
|
+
|
|
1028
|
+
const invalidContextDir = spawnSync('node', ['dist/stdio.js', '--root', tmp, '--allow-root', tmp], {
|
|
1029
|
+
cwd: path.resolve('.'),
|
|
1030
|
+
env: { ...process.env, CODEXFLOW_ROOT: tmp, CODEXFLOW_ALLOWED_ROOTS: tmp, CODEXFLOW_CONTEXT_DIR: 'src' },
|
|
1031
|
+
encoding: 'utf8',
|
|
1032
|
+
timeout: 5000
|
|
1033
|
+
});
|
|
1034
|
+
if (invalidContextDir.status === 0 || !String(invalidContextDir.stderr || invalidContextDir.stdout).includes('CODEXFLOW_CONTEXT_DIR')) {
|
|
1035
|
+
throw new Error(`invalid CODEXFLOW_CONTEXT_DIR=src was not rejected: status=${invalidContextDir.status} stdout=${invalidContextDir.stdout} stderr=${invalidContextDir.stderr}`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const codexSessionsClient = new McpStdioClient('node', ['dist/stdio.js', '--root', tmp, '--allow-root', tmp, '--tool-mode', 'full'], {
|
|
1039
|
+
cwd: path.resolve('.'),
|
|
1040
|
+
env: {
|
|
1041
|
+
...process.env,
|
|
1042
|
+
CODEXFLOW_ROOT: tmp,
|
|
1043
|
+
CODEXFLOW_ALLOWED_ROOTS: tmp,
|
|
1044
|
+
CODEXFLOW_CODEX_SESSIONS: 'read',
|
|
1045
|
+
CODEXFLOW_CODEX_DIR: codexHistoryDir
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
await codexSessionsClient.request('initialize', {
|
|
1049
|
+
protocolVersion: '2024-11-05',
|
|
1050
|
+
capabilities: {},
|
|
1051
|
+
clientInfo: { name: 'codexflow-codex-sessions-smoke', version: '0.1.0' }
|
|
1052
|
+
});
|
|
1053
|
+
codexSessionsClient.notify('notifications/initialized');
|
|
1054
|
+
const codexSessionTools = await codexSessionsClient.request('tools/list', {});
|
|
1055
|
+
const codexSessionToolNames = codexSessionTools.tools.map((tool) => tool.name);
|
|
1056
|
+
for (const expectedName of ['codex_sessions', 'read_codex_session']) {
|
|
1057
|
+
if (!codexSessionToolNames.includes(expectedName)) {
|
|
1058
|
+
throw new Error(`codex session opt-in mode missing ${expectedName}: ${codexSessionToolNames.join(', ')}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const codexSessions = await codexSessionsClient.request('tools/call', { name: 'codex_sessions', arguments: { max_sessions: 5 } });
|
|
1062
|
+
const session = codexSessions.structuredContent.sessions?.[0];
|
|
1063
|
+
if (!session || session.session_id !== '019cc369-bd7c-7891-b371-7b20b4fe0b18' || session.title !== 'Fix the smoke session browser' || session.project_dir !== tmp) {
|
|
1064
|
+
throw new Error(`codex_sessions did not return parsed Codex metadata: ${JSON.stringify(codexSessions.structuredContent)}`);
|
|
1065
|
+
}
|
|
1066
|
+
if (session.resume_command !== 'codex resume 019cc369-bd7c-7891-b371-7b20b4fe0b18') {
|
|
1067
|
+
throw new Error(`codex_sessions returned wrong resume command: ${JSON.stringify(session)}`);
|
|
1068
|
+
}
|
|
1069
|
+
const codexTranscript = await codexSessionsClient.request('tools/call', {
|
|
1070
|
+
name: 'read_codex_session',
|
|
1071
|
+
arguments: { session_id: '019cc369-bd7c-7891-b371-7b20b4fe0b18', max_messages: 10 }
|
|
1072
|
+
});
|
|
1073
|
+
if (!codexTranscript.content?.[0]?.text?.includes('Fix the smoke session browser') || !codexTranscript.content?.[0]?.text?.includes('[Tool: bash]')) {
|
|
1074
|
+
throw new Error(`read_codex_session did not return bounded transcript text: ${codexTranscript.content?.[0]?.text}`);
|
|
1075
|
+
}
|
|
1076
|
+
const topOneSessions = await codexSessionsClient.request('tools/call', { name: 'codex_sessions', arguments: { max_sessions: 1 } });
|
|
1077
|
+
if (topOneSessions.structuredContent.sessions?.some?.((item) => item.session_id === olderCodexSessionId)) {
|
|
1078
|
+
throw new Error(`codex_sessions max_sessions did not limit visible results: ${JSON.stringify(topOneSessions.structuredContent)}`);
|
|
1079
|
+
}
|
|
1080
|
+
const olderCodexTranscript = await codexSessionsClient.request('tools/call', {
|
|
1081
|
+
name: 'read_codex_session',
|
|
1082
|
+
arguments: { session_id: olderCodexSessionId, max_messages: 10 }
|
|
1083
|
+
});
|
|
1084
|
+
if (!olderCodexTranscript.content?.[0]?.text?.includes('Older session still readable by id')) {
|
|
1085
|
+
throw new Error(`read_codex_session only searched visible list window: ${olderCodexTranscript.content?.[0]?.text}`);
|
|
1086
|
+
}
|
|
1087
|
+
const sourcePathTranscript = await codexSessionsClient.request('tools/call', {
|
|
1088
|
+
name: 'read_codex_session',
|
|
1089
|
+
arguments: { source_path: session.source_path, max_messages: 10 }
|
|
1090
|
+
});
|
|
1091
|
+
if (!sourcePathTranscript.content?.[0]?.text?.includes('Fix the smoke session browser')) {
|
|
1092
|
+
throw new Error(`read_codex_session rejected source_path returned by codex_sessions: ${sourcePathTranscript.content?.[0]?.text}`);
|
|
1093
|
+
}
|
|
1094
|
+
const largeTailSessions = await codexSessionsClient.request('tools/call', { name: 'codex_sessions', arguments: { query: 'Large tail summary', max_sessions: 5 } });
|
|
1095
|
+
if (largeTailSessions.structuredContent.total_found !== 0 || JSON.stringify(largeTailSessions.structuredContent).includes('Large tail summary')) {
|
|
1096
|
+
throw new Error(`read mode codex_sessions exposed transcript tail summary: ${JSON.stringify(largeTailSessions.structuredContent)}`);
|
|
1097
|
+
}
|
|
1098
|
+
codexSessionsClient.close();
|
|
1099
|
+
|
|
1100
|
+
const sessionGuardClient = new McpStdioClient('node', [
|
|
1101
|
+
'dist/stdio.js',
|
|
1102
|
+
'--root',
|
|
1103
|
+
tmp,
|
|
1104
|
+
'--allow-root',
|
|
1105
|
+
tmp,
|
|
1106
|
+
'--bash',
|
|
1107
|
+
'safe',
|
|
1108
|
+
'--bash-session',
|
|
1109
|
+
'codex-main',
|
|
1110
|
+
'--require-bash-session'
|
|
1111
|
+
], {
|
|
1112
|
+
cwd: path.resolve('.'),
|
|
1113
|
+
env: {
|
|
1114
|
+
...process.env,
|
|
1115
|
+
CODEXFLOW_ROOT: tmp,
|
|
1116
|
+
CODEXFLOW_ALLOWED_ROOTS: tmp,
|
|
1117
|
+
CODEXFLOW_BASH_SESSION_ID: '',
|
|
1118
|
+
CODEXFLOW_REQUIRE_BASH_SESSION: ''
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
await sessionGuardClient.request('initialize', {
|
|
1122
|
+
protocolVersion: '2024-11-05',
|
|
1123
|
+
capabilities: {},
|
|
1124
|
+
clientInfo: { name: 'codexflow-bash-session-smoke', version: '0.1.0' }
|
|
1125
|
+
});
|
|
1126
|
+
sessionGuardClient.notify('notifications/initialized');
|
|
1127
|
+
const guardedConfig = await sessionGuardClient.request('tools/call', { name: 'server_config', arguments: {} });
|
|
1128
|
+
if (guardedConfig.structuredContent.bashSessionId !== 'codex-main' || guardedConfig.structuredContent.requireBashSession !== true) {
|
|
1129
|
+
throw new Error(`server_config did not expose bash session guard: ${JSON.stringify(guardedConfig.structuredContent)}`);
|
|
1130
|
+
}
|
|
1131
|
+
await expectToolError('bash', { command: 'pwd' }, /bash session/i, sessionGuardClient);
|
|
1132
|
+
await expectToolError('bash', { command: 'pwd', session_id: 'other-session' }, /codex-main/i, sessionGuardClient);
|
|
1133
|
+
const guardedBash = await sessionGuardClient.request('tools/call', { name: 'bash', arguments: { command: 'pwd', session_id: 'codex-main' } });
|
|
1134
|
+
if (guardedBash.structuredContent.bash_session_id !== 'codex-main' || !guardedBash.content?.[0]?.text?.includes('Exit: 0')) {
|
|
1135
|
+
throw new Error(`bash session guard did not allow matching session id: ${JSON.stringify(guardedBash.structuredContent)}`);
|
|
1136
|
+
}
|
|
1137
|
+
const guardedSelfTest = await sessionGuardClient.request('tools/call', {
|
|
1138
|
+
name: 'codexflow_self_test',
|
|
1139
|
+
arguments: { write_probe: false, pro_context_probe: false }
|
|
1140
|
+
});
|
|
1141
|
+
if (guardedSelfTest.structuredContent.status === 'fail') {
|
|
1142
|
+
throw new Error(`codexflow_self_test failed under bash session guard: ${JSON.stringify(guardedSelfTest.structuredContent.checks)}`);
|
|
1143
|
+
}
|
|
1144
|
+
sessionGuardClient.close();
|
|
1145
|
+
|
|
1146
|
+
const nonGitRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-non-git-'));
|
|
1147
|
+
await fs.writeFile(path.join(nonGitRoot, 'README.md'), '# Non-git fixture\n', 'utf8');
|
|
1148
|
+
const nonGitClient = new McpStdioClient('node', ['dist/stdio.js', '--root', nonGitRoot, '--allow-root', nonGitRoot, '--tool-mode', 'full'], {
|
|
1149
|
+
cwd: path.resolve('.'),
|
|
1150
|
+
env: { ...process.env, CODEXFLOW_ROOT: nonGitRoot, CODEXFLOW_ALLOWED_ROOTS: nonGitRoot }
|
|
1151
|
+
});
|
|
1152
|
+
await nonGitClient.request('initialize', {
|
|
1153
|
+
protocolVersion: '2024-11-05',
|
|
1154
|
+
capabilities: {},
|
|
1155
|
+
clientInfo: { name: 'codexflow-non-git-smoke', version: '0.1.0' }
|
|
1156
|
+
});
|
|
1157
|
+
nonGitClient.notify('notifications/initialized');
|
|
1158
|
+
const nonGitDiff = await nonGitClient.request('tools/call', { name: 'git_diff', arguments: { include_diff: false } });
|
|
1159
|
+
const nonGitPayload = JSON.stringify(nonGitDiff);
|
|
1160
|
+
if (!nonGitDiff.structuredContent.diff_error || !nonGitDiff.structuredContent.diff || nonGitDiff.structuredContent.changed) {
|
|
1161
|
+
throw new Error(`git_diff include_diff=false hid non-git diagnostics: ${nonGitPayload}`);
|
|
1162
|
+
}
|
|
1163
|
+
if (!/not a git repository|git unavailable|fatal:/i.test(nonGitPayload)) {
|
|
1164
|
+
throw new Error(`git_diff include_diff=false did not preserve the git diagnostic text: ${nonGitPayload}`);
|
|
1165
|
+
}
|
|
1166
|
+
nonGitClient.close();
|
|
1167
|
+
|
|
1168
|
+
const lowerAgentsRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-lower-agents-'));
|
|
1169
|
+
await fs.writeFile(path.join(lowerAgentsRoot, 'agents.md'), '# Lowercase agents\n\n- Lowercase instruction file loaded.\n', 'utf8');
|
|
1170
|
+
await fs.mkdir(path.join(lowerAgentsRoot, 'src'));
|
|
1171
|
+
await fs.writeFile(path.join(lowerAgentsRoot, 'src', 'demo.ts'), 'export const demo = true;\n', 'utf8');
|
|
1172
|
+
const lowerClient = new McpStdioClient('node', ['dist/stdio.js', '--root', lowerAgentsRoot, '--allow-root', lowerAgentsRoot, '--tool-mode', 'full'], {
|
|
1173
|
+
cwd: path.resolve('.'),
|
|
1174
|
+
env: { ...process.env, CODEXFLOW_ROOT: lowerAgentsRoot, CODEXFLOW_ALLOWED_ROOTS: lowerAgentsRoot }
|
|
1175
|
+
});
|
|
1176
|
+
await lowerClient.request('initialize', {
|
|
1177
|
+
protocolVersion: '2024-11-05',
|
|
1178
|
+
capabilities: {},
|
|
1179
|
+
clientInfo: { name: 'codexflow-lower-agents-smoke', version: '0.1.0' }
|
|
1180
|
+
});
|
|
1181
|
+
lowerClient.notify('notifications/initialized');
|
|
1182
|
+
const lowerOpened = await lowerClient.request('tools/call', { name: 'open_current_workspace', arguments: { include_tree: false } });
|
|
1183
|
+
if (lowerOpened.structuredContent.agents_path !== 'agents.md') {
|
|
1184
|
+
throw new Error(`lowercase agents.md was reported as ${lowerOpened.structuredContent.agents_path}`);
|
|
1185
|
+
}
|
|
1186
|
+
const lowerContext = await lowerClient.request('tools/call', { name: 'codex_context', arguments: { target_path: 'src/demo.ts', include_ai_bridge: false, include_git: false } });
|
|
1187
|
+
if (!lowerContext.structuredContent.agents_files.includes('agents.md')) {
|
|
1188
|
+
throw new Error(`codex_context did not preserve lowercase agents.md: ${lowerContext.structuredContent.agents_files.join(', ')}`);
|
|
1189
|
+
}
|
|
1190
|
+
if (!lowerContext.content?.[0]?.text?.includes('Lowercase instruction file loaded.')) {
|
|
1191
|
+
throw new Error('codex_context did not include lowercase agents.md content');
|
|
1192
|
+
}
|
|
1193
|
+
lowerClient.close();
|
|
1194
|
+
console.log('✓ smoke test passed');
|