proteum 2.3.0 → 2.4.1
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.md +8 -3
- package/README.md +20 -15
- package/agents/project/AGENTS.md +15 -10
- package/agents/project/DOCUMENTATION.md +1326 -0
- package/agents/project/app-root/AGENTS.md +2 -2
- package/agents/project/diagnostics.md +9 -8
- package/agents/project/root/AGENTS.md +14 -8
- package/agents/project/tests/AGENTS.md +1 -0
- package/cli/commands/dev.ts +148 -25
- package/cli/commands/diagnose.ts +2 -0
- package/cli/commands/explain.ts +38 -9
- package/cli/commands/mcp.ts +126 -9
- package/cli/commands/orient.ts +44 -17
- package/cli/commands/runtime.ts +100 -17
- package/cli/mcp/router.ts +1010 -0
- package/cli/presentation/commands.ts +34 -24
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +129 -21
- package/cli/runtime/devSessions.ts +328 -2
- package/cli/runtime/mcpDaemon.ts +288 -0
- package/cli/runtime/ports.ts +151 -0
- package/cli/utils/agents.ts +93 -17
- package/cli/utils/appRoots.ts +232 -0
- package/common/dev/diagnostics.ts +1 -1
- package/common/dev/inspection.ts +8 -1
- package/common/dev/mcpPayloads.ts +431 -17
- package/common/dev/mcpServer.ts +33 -0
- package/docs/agent-routing.md +32 -21
- package/docs/dev-commands.md +1 -1
- package/docs/dev-sessions.md +3 -1
- package/docs/diagnostics.md +21 -20
- package/docs/mcp.md +109 -52
- package/docs/migrate-from-2.1.3.md +3 -5
- package/docs/request-tracing.md +3 -3
- package/package.json +10 -3
- package/server/app/devMcp.ts +45 -0
- package/server/services/router/request/ip.test.cjs +0 -1
- package/tests/agents-utils.test.cjs +58 -3
- package/tests/cli-mcp-command.test.cjs +262 -0
- package/tests/codex-mcp-usage.test.cjs +307 -0
- package/tests/dev-sessions.test.cjs +113 -0
- package/tests/dev-transpile-watch.test.cjs +0 -1
- package/tests/eslint-rules.test.cjs +0 -1
- package/tests/inspection.test.cjs +0 -1
- package/tests/mcp.test.cjs +748 -2
- package/tests/router-cache-config.test.cjs +0 -1
- package/vitest.config.mjs +9 -0
- package/cli/mcp/provider.ts +0 -365
- package/cli/mcp/stdio.ts +0 -16
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawn } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const TEST_PROMPT = `Follow this project's agent instructions exactly.
|
|
8
|
+
|
|
9
|
+
Do a read-only runtime orientation and health pass for this app.
|
|
10
|
+
|
|
11
|
+
1. Confirm the intended app/worktree and whether exactly one usable dev server is running for it. If no usable dev server exists, start one using the project's prescribed dev-session workflow. Do not start a second live server for the same worktree.
|
|
12
|
+
2. Report compact runtime status: app root, dev URL, package manager, route count, controller count, connected apps, and manifest freshness.
|
|
13
|
+
3. Resolve selected project instruction files and briefly say why each one was selected.
|
|
14
|
+
4. Pick one real browser route from the compact project map, prefer a public route; explain which page/controller owns it, then reproduce one request to that route.
|
|
15
|
+
5. Diagnose that request and summarize only the important result: status, owner, hot calls, SQL count, error events, and likely next action.
|
|
16
|
+
6. Show the latest trace/perf summary for that same request or route, capped to the smallest useful detail.
|
|
17
|
+
7. Include recent dev log lines only if they explain a warning or failure.
|
|
18
|
+
8. Do not edit files. Do not run broad tests unless the runtime evidence points to a concrete failure.
|
|
19
|
+
|
|
20
|
+
Return a compact report with:
|
|
21
|
+
- Runtime
|
|
22
|
+
- Selected Instructions
|
|
23
|
+
- Route Owner
|
|
24
|
+
- Diagnosis
|
|
25
|
+
- Trace/Perf
|
|
26
|
+
- Any Follow-up Needed
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const enabled = process.env.PROTEUM_RUN_CODEX_MCP_USAGE_TEST === '1';
|
|
30
|
+
const codexUsageTest = enabled ? test : test.skip;
|
|
31
|
+
|
|
32
|
+
const parsePositiveInteger = (value, fallback) => {
|
|
33
|
+
const parsed = Number.parseInt(String(value || ''), 10);
|
|
34
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const createOutputDir = () => {
|
|
38
|
+
const configured = process.env.PROTEUM_CODEX_MCP_USAGE_OUTPUT_DIR;
|
|
39
|
+
if (configured) {
|
|
40
|
+
fs.mkdirSync(configured, { recursive: true });
|
|
41
|
+
return configured;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-codex-mcp-usage-'));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const runCodex = async ({ appRoot, codexCli, outputDir, timeoutMs }) =>
|
|
48
|
+
await new Promise((resolve, reject) => {
|
|
49
|
+
const lastMessageFile = path.join(outputDir, 'last-message.md');
|
|
50
|
+
const child = spawn(
|
|
51
|
+
codexCli,
|
|
52
|
+
[
|
|
53
|
+
'exec',
|
|
54
|
+
'--json',
|
|
55
|
+
'--color',
|
|
56
|
+
'never',
|
|
57
|
+
'--cd',
|
|
58
|
+
appRoot,
|
|
59
|
+
'--skip-git-repo-check',
|
|
60
|
+
'--sandbox',
|
|
61
|
+
'workspace-write',
|
|
62
|
+
'--ask-for-approval',
|
|
63
|
+
'never',
|
|
64
|
+
'--output-last-message',
|
|
65
|
+
lastMessageFile,
|
|
66
|
+
'-',
|
|
67
|
+
],
|
|
68
|
+
{
|
|
69
|
+
cwd: appRoot,
|
|
70
|
+
env: {
|
|
71
|
+
...process.env,
|
|
72
|
+
NO_COLOR: '1',
|
|
73
|
+
},
|
|
74
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
let stdout = '';
|
|
78
|
+
let stderr = '';
|
|
79
|
+
let timedOut = false;
|
|
80
|
+
const timeout = setTimeout(() => {
|
|
81
|
+
timedOut = true;
|
|
82
|
+
child.kill('SIGTERM');
|
|
83
|
+
setTimeout(() => child.kill('SIGKILL'), 5000).unref();
|
|
84
|
+
}, timeoutMs);
|
|
85
|
+
|
|
86
|
+
child.stdout.on('data', (chunk) => {
|
|
87
|
+
stdout += chunk.toString();
|
|
88
|
+
});
|
|
89
|
+
child.stderr.on('data', (chunk) => {
|
|
90
|
+
stderr += chunk.toString();
|
|
91
|
+
});
|
|
92
|
+
child.stdin.end(TEST_PROMPT);
|
|
93
|
+
child.once('error', reject);
|
|
94
|
+
child.once('close', (status) => {
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
resolve({
|
|
97
|
+
lastMessage: fs.existsSync(lastMessageFile) ? fs.readFileSync(lastMessageFile, 'utf8') : '',
|
|
98
|
+
lastMessageFile,
|
|
99
|
+
status,
|
|
100
|
+
stderr,
|
|
101
|
+
stdout,
|
|
102
|
+
timedOut,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const parseJsonl = (content) =>
|
|
108
|
+
content
|
|
109
|
+
.split(/\r?\n/)
|
|
110
|
+
.map((line) => line.trim())
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.map((line) => {
|
|
113
|
+
try {
|
|
114
|
+
return { json: JSON.parse(line), line };
|
|
115
|
+
} catch {
|
|
116
|
+
return { json: undefined, line };
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const walk = (value, visitor, seen = new Set()) => {
|
|
121
|
+
if (value === null || value === undefined) return;
|
|
122
|
+
if (typeof value !== 'object') {
|
|
123
|
+
visitor(value, undefined);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (seen.has(value)) return;
|
|
127
|
+
seen.add(value);
|
|
128
|
+
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
for (const entry of value) walk(entry, visitor, seen);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
135
|
+
visitor(entry, key);
|
|
136
|
+
walk(entry, visitor, seen);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const tokenKeys = {
|
|
141
|
+
cached: ['cached_input_tokens', 'cachedInputTokens', 'cached_tokens'],
|
|
142
|
+
input: ['input_tokens', 'inputTokens', 'prompt_tokens', 'promptTokens'],
|
|
143
|
+
output: ['output_tokens', 'outputTokens', 'completion_tokens', 'completionTokens'],
|
|
144
|
+
reasoning: ['reasoning_tokens', 'reasoningTokens', 'reasoning_output_tokens', 'reasoningOutputTokens'],
|
|
145
|
+
total: ['total_tokens', 'totalTokens'],
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const getNumericTokenField = (object, keys) => {
|
|
149
|
+
for (const key of keys) {
|
|
150
|
+
if (typeof object[key] === 'number' && Number.isFinite(object[key])) return object[key];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return 0;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const collectTokenUsage = (events) => {
|
|
157
|
+
const samples = [];
|
|
158
|
+
|
|
159
|
+
for (const event of events) {
|
|
160
|
+
walk(event.json, (value) => {
|
|
161
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return;
|
|
162
|
+
const sample = {
|
|
163
|
+
cached: getNumericTokenField(value, tokenKeys.cached),
|
|
164
|
+
input: getNumericTokenField(value, tokenKeys.input),
|
|
165
|
+
output: getNumericTokenField(value, tokenKeys.output),
|
|
166
|
+
reasoning: getNumericTokenField(value, tokenKeys.reasoning),
|
|
167
|
+
total: getNumericTokenField(value, tokenKeys.total),
|
|
168
|
+
};
|
|
169
|
+
if (!sample.total) sample.total = sample.input + sample.output;
|
|
170
|
+
if (sample.total || sample.input || sample.output || sample.cached || sample.reasoning) samples.push(sample);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return samples.reduce(
|
|
175
|
+
(summary, sample) => ({
|
|
176
|
+
cached: Math.max(summary.cached, sample.cached),
|
|
177
|
+
input: Math.max(summary.input, sample.input),
|
|
178
|
+
output: Math.max(summary.output, sample.output),
|
|
179
|
+
reasoning: Math.max(summary.reasoning, sample.reasoning),
|
|
180
|
+
samples: summary.samples + 1,
|
|
181
|
+
total: Math.max(summary.total, sample.total),
|
|
182
|
+
}),
|
|
183
|
+
{ cached: 0, input: 0, output: 0, reasoning: 0, samples: 0, total: 0 },
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const countByName = (names) =>
|
|
188
|
+
names.reduce((counts, name) => {
|
|
189
|
+
counts[name] = (counts[name] || 0) + 1;
|
|
190
|
+
return counts;
|
|
191
|
+
}, {});
|
|
192
|
+
|
|
193
|
+
const collectProteumMcpCalls = (events) => {
|
|
194
|
+
const calls = [];
|
|
195
|
+
|
|
196
|
+
for (const [index, event] of events.entries()) {
|
|
197
|
+
const names = new Set();
|
|
198
|
+
walk(event.json, (value, key) => {
|
|
199
|
+
if (typeof value !== 'string') return;
|
|
200
|
+
if (!['name', 'toolName', 'tool_name', 'recipient', 'recipient_name'].includes(String(key))) return;
|
|
201
|
+
|
|
202
|
+
const match = value.match(/(?:mcp__proteum__|proteum[.:/])([A-Za-z0-9_]+)/);
|
|
203
|
+
if (match) names.add(match[1]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (names.size === 0 && event.line.includes('mcp__proteum__')) {
|
|
207
|
+
for (const match of event.line.matchAll(/mcp__proteum__([A-Za-z0-9_]+)/g)) names.add(match[1]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const name of names) calls.push({ event: index, name });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const names = calls.map((call) => call.name);
|
|
214
|
+
return { byName: countByName(names), calls, total: calls.length };
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const collectProteumCliCalls = (events) => {
|
|
218
|
+
const commands = [];
|
|
219
|
+
const commandPattern = /(?:^|\s)(?:npx\s+)?proteum\s+[A-Za-z0-9:_-]+|node\s+\S*cli\/bin\.js\s+[A-Za-z0-9:_-]+/;
|
|
220
|
+
|
|
221
|
+
for (const [index, event] of events.entries()) {
|
|
222
|
+
const eventCommands = new Set();
|
|
223
|
+
walk(event.json, (value, key) => {
|
|
224
|
+
if (typeof value !== 'string') return;
|
|
225
|
+
if (!['cmd', 'command', 'shell', 'args', 'arguments'].includes(String(key))) return;
|
|
226
|
+
const match = value.match(commandPattern);
|
|
227
|
+
if (match) eventCommands.add(match[0].trim());
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
for (const command of eventCommands) commands.push({ command, event: index });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
byCommand: countByName(commands.map((entry) => entry.command)),
|
|
235
|
+
commands,
|
|
236
|
+
total: commands.length,
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const analyzeCodexOutput = ({ stdout, stderr, lastMessage }) => {
|
|
241
|
+
const events = parseJsonl(stdout);
|
|
242
|
+
const parseErrors = events.filter((event) => !event.json).length;
|
|
243
|
+
const mcpCalls = collectProteumMcpCalls(events);
|
|
244
|
+
const cliCalls = collectProteumCliCalls(events);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
cliCalls,
|
|
248
|
+
eventCount: events.length,
|
|
249
|
+
lastMessageCharacters: lastMessage.length,
|
|
250
|
+
mcpCalls,
|
|
251
|
+
parseErrors,
|
|
252
|
+
stderrCharacters: stderr.length,
|
|
253
|
+
tokenUsage: collectTokenUsage(events),
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
codexUsageTest('Codex runtime health prompt uses Proteum MCP before CLI fallbacks', async () => {
|
|
258
|
+
const appRoot = process.env.PROTEUM_CODEX_MCP_USAGE_CWD;
|
|
259
|
+
assert.ok(appRoot, 'Set PROTEUM_CODEX_MCP_USAGE_CWD to the Proteum app root to test.');
|
|
260
|
+
assert.equal(fs.existsSync(appRoot), true, `Proteum app root does not exist: ${appRoot}`);
|
|
261
|
+
|
|
262
|
+
const outputDir = createOutputDir();
|
|
263
|
+
const codexCli = process.env.CODEX_CLI || 'codex';
|
|
264
|
+
const timeoutMs = parsePositiveInteger(process.env.PROTEUM_CODEX_MCP_USAGE_TIMEOUT_MS, 20 * 60 * 1000);
|
|
265
|
+
const minMcpCalls = parsePositiveInteger(process.env.PROTEUM_CODEX_MCP_MIN_MCP_CALLS, 4);
|
|
266
|
+
const maxCliCalls = parsePositiveInteger(process.env.PROTEUM_CODEX_MCP_MAX_CLI_CALLS, 4);
|
|
267
|
+
|
|
268
|
+
const result = await runCodex({ appRoot, codexCli, outputDir, timeoutMs });
|
|
269
|
+
const transcriptFile = path.join(outputDir, 'codex-events.jsonl');
|
|
270
|
+
const stderrFile = path.join(outputDir, 'stderr.txt');
|
|
271
|
+
const summaryFile = path.join(outputDir, 'summary.json');
|
|
272
|
+
fs.writeFileSync(transcriptFile, result.stdout);
|
|
273
|
+
fs.writeFileSync(stderrFile, result.stderr);
|
|
274
|
+
|
|
275
|
+
const summary = analyzeCodexOutput(result);
|
|
276
|
+
fs.writeFileSync(
|
|
277
|
+
summaryFile,
|
|
278
|
+
JSON.stringify(
|
|
279
|
+
{
|
|
280
|
+
...summary,
|
|
281
|
+
appRoot,
|
|
282
|
+
codexCli,
|
|
283
|
+
status: result.status,
|
|
284
|
+
timedOut: result.timedOut,
|
|
285
|
+
transcriptFile,
|
|
286
|
+
stderrFile,
|
|
287
|
+
lastMessageFile: result.lastMessageFile,
|
|
288
|
+
},
|
|
289
|
+
null,
|
|
290
|
+
2,
|
|
291
|
+
),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
assert.equal(result.timedOut, false, `Codex CLI timed out. Summary: ${summaryFile}`);
|
|
295
|
+
assert.equal(result.status, 0, `Codex CLI exited with ${result.status}. Summary: ${summaryFile}`);
|
|
296
|
+
assert.equal(summary.parseErrors, 0, `Codex JSONL output contained parse errors. Summary: ${summaryFile}`);
|
|
297
|
+
assert.ok(summary.tokenUsage.samples > 0, `Codex JSONL output did not include token usage. Summary: ${summaryFile}`);
|
|
298
|
+
assert.ok(summary.tokenUsage.total > 0, `Codex token usage was not quantified. Summary: ${summaryFile}`);
|
|
299
|
+
assert.ok(summary.mcpCalls.total >= minMcpCalls, `Expected at least ${minMcpCalls} Proteum MCP calls. Summary: ${summaryFile}`);
|
|
300
|
+
assert.ok(
|
|
301
|
+
(summary.mcpCalls.byName.workflow_start || 0) >= 1,
|
|
302
|
+
`Expected at least one Proteum MCP workflow_start call. Summary: ${summaryFile}`,
|
|
303
|
+
);
|
|
304
|
+
assert.ok(summary.cliCalls.total <= maxCliCalls, `Expected at most ${maxCliCalls} Proteum CLI calls. Summary: ${summaryFile}`);
|
|
305
|
+
|
|
306
|
+
console.log(`Codex MCP usage summary: ${summaryFile}`);
|
|
307
|
+
}, 25 * 60 * 1000);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const { spawn } = require('node:child_process');
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const coreRoot = path.resolve(__dirname, '..');
|
|
8
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
9
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
10
|
+
require('ts-node/register/transpile-only');
|
|
11
|
+
require('../cli/context.ts');
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
createDevSessionRecord,
|
|
15
|
+
prepareDevSessionStart,
|
|
16
|
+
resolveDevSessionFilePath,
|
|
17
|
+
writeDevSessionRecord,
|
|
18
|
+
} = require('../cli/runtime/devSessions.ts');
|
|
19
|
+
|
|
20
|
+
const createTempAppRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-dev-session-app-'));
|
|
21
|
+
|
|
22
|
+
const createSessionRecord = ({ appRoot, pid = process.pid, port = 3101, sessionFilePath }) => ({
|
|
23
|
+
...createDevSessionRecord({ appRoot, port, sessionFilePath }),
|
|
24
|
+
pid,
|
|
25
|
+
publicUrl: `http://localhost:${port}`,
|
|
26
|
+
state: 'ready',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('prepareDevSessionStart cleans stale and invalid same-worktree sessions', async () => {
|
|
30
|
+
const appRoot = createTempAppRoot();
|
|
31
|
+
const staleSessionFilePath = resolveDevSessionFilePath({ appRoot, port: 3101 });
|
|
32
|
+
const invalidSessionFilePath = resolveDevSessionFilePath({ appRoot, port: 3102 });
|
|
33
|
+
const requestedSessionFilePath = resolveDevSessionFilePath({ appRoot, port: 3103 });
|
|
34
|
+
|
|
35
|
+
await writeDevSessionRecord(
|
|
36
|
+
createSessionRecord({
|
|
37
|
+
appRoot,
|
|
38
|
+
pid: 999999,
|
|
39
|
+
port: 3101,
|
|
40
|
+
sessionFilePath: staleSessionFilePath,
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
fs.mkdirSync(path.dirname(invalidSessionFilePath), { recursive: true });
|
|
44
|
+
fs.writeFileSync(invalidSessionFilePath, '{ invalid json');
|
|
45
|
+
|
|
46
|
+
const result = await prepareDevSessionStart({
|
|
47
|
+
appRoot,
|
|
48
|
+
replaceExisting: false,
|
|
49
|
+
sessionFilePath: requestedSessionFilePath,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
assert.equal(result.blocking.length, 0);
|
|
53
|
+
assert.equal(result.cleaned.length, 2);
|
|
54
|
+
assert.equal(fs.existsSync(staleSessionFilePath), false);
|
|
55
|
+
assert.equal(fs.existsSync(invalidSessionFilePath), false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('prepareDevSessionStart blocks another live same-worktree session', async () => {
|
|
59
|
+
const appRoot = createTempAppRoot();
|
|
60
|
+
const blockingSessionFilePath = resolveDevSessionFilePath({ appRoot, port: 3101 });
|
|
61
|
+
const requestedSessionFilePath = resolveDevSessionFilePath({ appRoot, port: 3102 });
|
|
62
|
+
|
|
63
|
+
await writeDevSessionRecord(
|
|
64
|
+
createSessionRecord({
|
|
65
|
+
appRoot,
|
|
66
|
+
pid: process.pid,
|
|
67
|
+
port: 3101,
|
|
68
|
+
sessionFilePath: blockingSessionFilePath,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const result = await prepareDevSessionStart({
|
|
73
|
+
appRoot,
|
|
74
|
+
currentPid: process.pid + 1,
|
|
75
|
+
replaceExisting: false,
|
|
76
|
+
sessionFilePath: requestedSessionFilePath,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
assert.equal(result.blocking.length, 1);
|
|
80
|
+
assert.equal(result.blocking[0].sessionFilePath, blockingSessionFilePath);
|
|
81
|
+
assert.equal(fs.existsSync(blockingSessionFilePath), true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('prepareDevSessionStart replaces the exact requested session file only with replaceExisting', async () => {
|
|
85
|
+
const appRoot = createTempAppRoot();
|
|
86
|
+
const requestedSessionFilePath = resolveDevSessionFilePath({ appRoot, port: 3101 });
|
|
87
|
+
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
|
|
88
|
+
stdio: 'ignore',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await writeDevSessionRecord(
|
|
93
|
+
createSessionRecord({
|
|
94
|
+
appRoot,
|
|
95
|
+
pid: child.pid,
|
|
96
|
+
port: 3101,
|
|
97
|
+
sessionFilePath: requestedSessionFilePath,
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const result = await prepareDevSessionStart({
|
|
102
|
+
appRoot,
|
|
103
|
+
replaceExisting: true,
|
|
104
|
+
sessionFilePath: requestedSessionFilePath,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
assert.equal(result.blocking.length, 0);
|
|
108
|
+
assert.equal(result.replaced?.stopped, true);
|
|
109
|
+
assert.equal(fs.existsSync(requestedSessionFilePath), false);
|
|
110
|
+
} finally {
|
|
111
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
@@ -5,7 +5,6 @@ const net = require('node:net');
|
|
|
5
5
|
const os = require('node:os');
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const { spawn } = require('node:child_process');
|
|
8
|
-
const test = require('node:test');
|
|
9
8
|
|
|
10
9
|
const coreRoot = path.resolve(__dirname, '..');
|
|
11
10
|
const cliBin = path.join(coreRoot, 'cli', 'bin.js');
|