proteum 2.4.4 → 2.5.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/README.md +81 -52
- package/agents/project/AGENTS.md +112 -31
- package/agents/project/CODING_STYLE.md +2 -2
- package/agents/project/app-root/AGENTS.md +1 -3
- package/agents/project/client/AGENTS.md +5 -1
- package/agents/project/client/pages/AGENTS.md +21 -9
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/optimizations.md +1 -1
- package/agents/project/root/AGENTS.md +105 -22
- package/agents/project/server/routes/AGENTS.md +30 -1
- package/agents/project/server/services/AGENTS.md +4 -0
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/doctor.ts +54 -3
- package/cli/commands/runtime.ts +6 -0
- package/cli/commands/worktree.ts +116 -0
- package/cli/compiler/artifacts/controllers.ts +16 -15
- package/cli/compiler/artifacts/discovery.ts +129 -17
- package/cli/compiler/artifacts/routing.ts +0 -5
- package/cli/compiler/artifacts/services.ts +253 -76
- package/cli/compiler/common/controllers.ts +159 -57
- package/cli/compiler/common/generatedRouteModules.ts +457 -363
- package/cli/mcp/router.ts +47 -3
- package/cli/presentation/commands.ts +25 -15
- package/cli/runtime/commands.ts +39 -12
- package/cli/runtime/worktreeBootstrap.ts +608 -0
- package/cli/scaffold/index.ts +28 -18
- package/cli/scaffold/templates.ts +44 -33
- package/cli/utils/agents.ts +14 -1
- package/client/app/index.ts +22 -5
- package/client/services/router/index.tsx +23 -3
- package/client/services/router/request/api.ts +16 -6
- package/common/dev/contractsDoctor.ts +1 -1
- package/common/dev/mcpPayloads.ts +8 -1
- package/common/env/proteumEnv.ts +14 -2
- package/common/router/contracts.ts +1 -1
- package/common/router/definitions.ts +177 -0
- package/common/router/index.ts +23 -12
- package/common/router/pageData.ts +5 -5
- package/common/router/register.ts +2 -2
- package/common/router/request/api.ts +12 -2
- package/docs/agent-routing.md +5 -2
- package/docs/diagnostics.md +2 -0
- package/docs/mcp.md +6 -3
- package/docs/migration-2.5.md +226 -0
- package/eslint.js +89 -42
- package/package.json +1 -1
- package/server/app/commands.ts +5 -1
- package/server/app/container/console/index.ts +1 -1
- package/server/app/controller/index.ts +98 -40
- package/server/app/index.ts +120 -3
- package/server/app/service/index.ts +5 -1
- package/server/index.ts +6 -2
- package/server/services/router/index.ts +50 -41
- package/server/services/router/response/index.ts +2 -2
- package/tests/agents-utils.test.cjs +14 -1
- package/tests/cli-mcp-command.test.cjs +84 -0
- package/tests/client-app-error-handling.test.cjs +100 -0
- package/tests/definition-contracts.test.cjs +453 -0
- package/tests/dev-transpile-watch.test.cjs +37 -31
- package/tests/eslint-rules.test.cjs +185 -8
- package/tests/mcp.test.cjs +90 -0
- package/tests/scaffold-templates.test.cjs +18 -0
- package/tests/server-app-report-error.test.cjs +135 -0
- package/tests/worktree-bootstrap.test.cjs +206 -0
- package/types/aliases.d.ts +0 -5
- package/types/controller-input.test.ts +23 -17
- package/types/controller-request-context.test.ts +10 -11
- package/cli/commands/migrate.ts +0 -51
- package/cli/migrate/pageContract.ts +0 -516
- package/docs/migrate-from-2.1.3.md +0 -396
- package/scripts/cleanup-generated-controllers.ts +0 -62
- package/scripts/fix-reference-app-typing.ts +0 -490
- package/scripts/format-router-registrations.ts +0 -119
- package/scripts/migrate-explicit-controllers-and-request.ts +0 -423
- package/scripts/refactor-client-app-imports.ts +0 -244
- package/scripts/refactor-client-pages.ts +0 -587
- package/scripts/refactor-server-controllers.ts +0 -471
- package/scripts/refactor-server-runtime-aliases.ts +0 -360
- package/scripts/restore-client-app-import-files.ts +0 -41
|
@@ -370,24 +370,26 @@ ${cacheConfigSource}
|
|
|
370
370
|
);
|
|
371
371
|
writeFile(
|
|
372
372
|
path.join(appRoot, 'server', 'index.ts'),
|
|
373
|
-
`import {
|
|
373
|
+
`import { defineApplication } from '@server/app';
|
|
374
374
|
import Router from '@server/services/router';
|
|
375
375
|
import SchemaRouter from '@server/services/schema/router';
|
|
376
376
|
|
|
377
377
|
import * as appConfig from '@/server/config/app';
|
|
378
378
|
|
|
379
|
-
export default
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
379
|
+
export default defineApplication({
|
|
380
|
+
services: () => ({}),
|
|
381
|
+
router: (app) =>
|
|
382
|
+
new Router(
|
|
383
|
+
app,
|
|
384
|
+
{
|
|
385
|
+
...appConfig.routerBaseConfig,
|
|
386
|
+
plugins: {
|
|
387
|
+
schema: new SchemaRouter({}, app),
|
|
388
|
+
},
|
|
386
389
|
},
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
390
|
+
app,
|
|
391
|
+
),
|
|
392
|
+
});
|
|
391
393
|
`,
|
|
392
394
|
);
|
|
393
395
|
writeFile(
|
|
@@ -403,56 +405,53 @@ export default class TranspileWatchClient extends ClientApplication {
|
|
|
403
405
|
|
|
404
406
|
public boot() {}
|
|
405
407
|
public handleUpdate() {}
|
|
406
|
-
public handleError(error: Error) {
|
|
407
|
-
throw error;
|
|
408
|
-
}
|
|
409
408
|
}
|
|
410
409
|
`,
|
|
411
410
|
);
|
|
412
411
|
writeFile(
|
|
413
412
|
path.join(appRoot, 'client', 'pages', 'index.tsx'),
|
|
414
|
-
`import
|
|
413
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
415
414
|
import { SharedMarker } from '@test/shared';
|
|
416
415
|
|
|
417
|
-
|
|
418
|
-
'/',
|
|
419
|
-
{
|
|
416
|
+
export default definePageRoute({
|
|
417
|
+
path: '/',
|
|
418
|
+
options: {
|
|
420
419
|
auth: false,
|
|
421
420
|
layout: false,
|
|
422
421
|
},
|
|
423
|
-
null,
|
|
424
|
-
() => {
|
|
422
|
+
data: null,
|
|
423
|
+
render: () => {
|
|
425
424
|
return (
|
|
426
425
|
<main>
|
|
427
426
|
<SharedMarker />
|
|
428
427
|
</main>
|
|
429
428
|
);
|
|
430
429
|
},
|
|
431
|
-
);
|
|
430
|
+
});
|
|
432
431
|
`,
|
|
433
432
|
);
|
|
434
433
|
if (options.staticPage) {
|
|
435
434
|
writeFile(
|
|
436
435
|
path.join(appRoot, 'client', 'pages', 'static-cache.tsx'),
|
|
437
|
-
`import
|
|
436
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
438
437
|
import { SharedMarker } from '@test/shared';
|
|
439
438
|
|
|
440
|
-
|
|
441
|
-
'/static-cache',
|
|
442
|
-
{
|
|
439
|
+
export default definePageRoute({
|
|
440
|
+
path: '/static-cache',
|
|
441
|
+
options: {
|
|
443
442
|
auth: false,
|
|
444
443
|
layout: false,
|
|
445
444
|
static: { urls: ['/static-cache'] },
|
|
446
445
|
},
|
|
447
|
-
null,
|
|
448
|
-
() => {
|
|
446
|
+
data: null,
|
|
447
|
+
render: () => {
|
|
449
448
|
return (
|
|
450
449
|
<main>
|
|
451
450
|
<SharedMarker />
|
|
452
451
|
</main>
|
|
453
452
|
);
|
|
454
453
|
},
|
|
455
|
-
);
|
|
454
|
+
});
|
|
456
455
|
`,
|
|
457
456
|
);
|
|
458
457
|
}
|
|
@@ -539,8 +538,13 @@ test('proteum dev invalidates client assets and reloads for transpiled package s
|
|
|
539
538
|
|
|
540
539
|
try {
|
|
541
540
|
await waitForSessionReady(sessionFile, child, getOutput);
|
|
541
|
+
await request(port, '/', { Accept: 'text/html' });
|
|
542
542
|
|
|
543
|
-
const initialScriptAsset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL')
|
|
543
|
+
const initialScriptAsset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL').catch(
|
|
544
|
+
(error) => {
|
|
545
|
+
throw new Error(`${error.message}\n${getOutput()}`);
|
|
546
|
+
},
|
|
547
|
+
);
|
|
544
548
|
const initialScriptContent = fs.readFileSync(initialScriptAsset, 'utf8');
|
|
545
549
|
const scriptReloadStream = await connectToReloadStream(port + 1);
|
|
546
550
|
|
|
@@ -608,7 +612,9 @@ test('proteum dev applies router HTTP cache config to HTML and public assets', {
|
|
|
608
612
|
const staticResponse = await waitForHeader(port, '/static-cache', 'cache-control', staticHtmlCacheControl);
|
|
609
613
|
assert.equal(staticResponse.headers.get('surrogate-control'), 'static-surrogate');
|
|
610
614
|
|
|
611
|
-
const asset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL')
|
|
615
|
+
const asset = await waitForAssetContaining(appRoot, '.js', 'SCRIPT_MARKER_INITIAL').catch((error) => {
|
|
616
|
+
throw new Error(`${error.message}\n${getOutput()}`);
|
|
617
|
+
});
|
|
612
618
|
const { response: assetResponse } = await request(port, toPublicAssetUrl(appRoot, asset));
|
|
613
619
|
|
|
614
620
|
assert.equal(assetResponse.headers.get('cache-control'), publicAssetCacheControl);
|
|
@@ -3,14 +3,25 @@ const { Linter } = require('eslint');
|
|
|
3
3
|
|
|
4
4
|
const { createProteumEslintConfig } = require('../eslint.js');
|
|
5
5
|
|
|
6
|
-
const lint = (code) => {
|
|
6
|
+
const lint = (code, filename = 'client/example.tsx') => {
|
|
7
7
|
const linter = new Linter({ configType: 'flat' });
|
|
8
8
|
return linter.verify(code, createProteumEslintConfig(), {
|
|
9
|
-
filename
|
|
9
|
+
filename,
|
|
10
10
|
});
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
const swallowedErrorRuleId = 'proteum/no-swallowed-caught-error';
|
|
14
|
+
const noAppImportRuleId = 'proteum/no-app-import';
|
|
15
|
+
|
|
16
|
+
test('proteum lint rejects contextual @app imports', () => {
|
|
17
|
+
const messages = lint(`
|
|
18
|
+
import { Router } from '@app';
|
|
19
|
+
|
|
20
|
+
export const route = Router;
|
|
21
|
+
`);
|
|
22
|
+
|
|
23
|
+
assert.equal(messages.some((message) => message.ruleId === noAppImportRuleId), true);
|
|
24
|
+
});
|
|
14
25
|
|
|
15
26
|
test('proteum lint rejects empty catch blocks', () => {
|
|
16
27
|
const messages = lint(`
|
|
@@ -50,6 +61,32 @@ test('proteum lint rejects generic catch feedback that drops original error deta
|
|
|
50
61
|
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
51
62
|
});
|
|
52
63
|
|
|
64
|
+
test('proteum lint rejects console calls as caught error preservation', () => {
|
|
65
|
+
for (const method of ['error', 'warn']) {
|
|
66
|
+
const messages = lint(`
|
|
67
|
+
export const run = async () => {
|
|
68
|
+
try {
|
|
69
|
+
await Investor.api.getDashboard();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.${method}(error);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('proteum lint rejects direct console promise catch handlers', () => {
|
|
81
|
+
const messages = lint(`
|
|
82
|
+
export const run = () => {
|
|
83
|
+
Investor.api.ensureApiKey().catch(console.log);
|
|
84
|
+
};
|
|
85
|
+
`);
|
|
86
|
+
|
|
87
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
88
|
+
});
|
|
89
|
+
|
|
53
90
|
test('proteum lint allows rethrowing the caught error', () => {
|
|
54
91
|
const messages = lint(`
|
|
55
92
|
export const run = async () => {
|
|
@@ -65,7 +102,7 @@ test('proteum lint allows rethrowing the caught error', () => {
|
|
|
65
102
|
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
66
103
|
});
|
|
67
104
|
|
|
68
|
-
test('proteum lint
|
|
105
|
+
test('proteum lint rejects user feedback that does not route the caught error', () => {
|
|
69
106
|
const messages = lint(`
|
|
70
107
|
export const run = async () => {
|
|
71
108
|
try {
|
|
@@ -78,10 +115,10 @@ test('proteum lint allows surfacing original error details', () => {
|
|
|
78
115
|
};
|
|
79
116
|
`);
|
|
80
117
|
|
|
81
|
-
assert.equal(messages.
|
|
118
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
82
119
|
});
|
|
83
120
|
|
|
84
|
-
test('proteum lint
|
|
121
|
+
test('proteum lint rejects derived message state that does not route the caught error', () => {
|
|
85
122
|
const messages = lint(`
|
|
86
123
|
export const run = async () => {
|
|
87
124
|
try {
|
|
@@ -93,17 +130,157 @@ test('proteum lint allows surfacing a message derived from the caught error', ()
|
|
|
93
130
|
};
|
|
94
131
|
`);
|
|
95
132
|
|
|
96
|
-
assert.equal(messages.
|
|
133
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
97
134
|
});
|
|
98
135
|
|
|
99
|
-
test('proteum lint allows
|
|
136
|
+
test('proteum lint allows client catches routed to context app error handling', () => {
|
|
100
137
|
const messages = lint(`
|
|
101
138
|
export const run = () => {
|
|
139
|
+
const context = useContext();
|
|
102
140
|
Investor.api.ensureApiKey().catch((error) => {
|
|
103
|
-
|
|
141
|
+
context.app.handleError(error);
|
|
104
142
|
});
|
|
105
143
|
};
|
|
106
144
|
`);
|
|
107
145
|
|
|
108
146
|
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
109
147
|
});
|
|
148
|
+
|
|
149
|
+
test('proteum lint allows client catches using app error messages for UI feedback', () => {
|
|
150
|
+
const messages = lint(`
|
|
151
|
+
export const run = async () => {
|
|
152
|
+
const context = useContext();
|
|
153
|
+
try {
|
|
154
|
+
await Investor.api.ensureApiKey();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
setError(context.app.handleError(error, 'Unable to finish this action.'));
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
`);
|
|
160
|
+
|
|
161
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('proteum lint allows client catches routed to local app error handling', () => {
|
|
165
|
+
const messages = lint(`
|
|
166
|
+
export const run = async () => {
|
|
167
|
+
const app = useContext();
|
|
168
|
+
try {
|
|
169
|
+
await Investor.api.ensureApiKey();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
app.handleError(error);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
`);
|
|
175
|
+
|
|
176
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('proteum lint allows client catches routed to useContext app error handling', () => {
|
|
180
|
+
const messages = lint(`
|
|
181
|
+
export const run = async () => {
|
|
182
|
+
try {
|
|
183
|
+
await Investor.api.ensureApiKey();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
useContext().app.handleError(error);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
`);
|
|
189
|
+
|
|
190
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('proteum lint rejects bare client error handlers', () => {
|
|
194
|
+
const messages = lint(`
|
|
195
|
+
export const run = async () => {
|
|
196
|
+
try {
|
|
197
|
+
await Investor.api.ensureApiKey();
|
|
198
|
+
} catch (error) {
|
|
199
|
+
handleError(error);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
`);
|
|
203
|
+
|
|
204
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('proteum lint allows server catches routed to app error reporting', () => {
|
|
208
|
+
const messages = lint(
|
|
209
|
+
`
|
|
210
|
+
export const run = async (context) => {
|
|
211
|
+
try {
|
|
212
|
+
await context.services.Worker.run();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
await context.app.reportError(error, context.request);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
`,
|
|
218
|
+
'server/example.ts',
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('proteum lint rejects server catches routed to client app error handling', () => {
|
|
225
|
+
const messages = lint(
|
|
226
|
+
`
|
|
227
|
+
export const run = async (context) => {
|
|
228
|
+
try {
|
|
229
|
+
await context.services.Worker.run();
|
|
230
|
+
} catch (error) {
|
|
231
|
+
context.app.handleError(error);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
`,
|
|
235
|
+
'server/example.ts',
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('proteum lint rejects raw server error hooks as caught error handling', () => {
|
|
242
|
+
const messages = lint(
|
|
243
|
+
`
|
|
244
|
+
export const run = async (context) => {
|
|
245
|
+
try {
|
|
246
|
+
await context.services.Worker.run();
|
|
247
|
+
} catch (error) {
|
|
248
|
+
await context.app.runHook('error', error, context.request);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
`,
|
|
252
|
+
'server/example.ts',
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('proteum lint allows manual promise rejection', () => {
|
|
259
|
+
const messages = lint(
|
|
260
|
+
`
|
|
261
|
+
export const run = (input) =>
|
|
262
|
+
new Promise((resolve, reject) => {
|
|
263
|
+
input.load().catch((error) => {
|
|
264
|
+
reject(error);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
`,
|
|
268
|
+
'common/example.ts',
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('proteum lint allows direct reject promise catch handlers', () => {
|
|
275
|
+
const messages = lint(
|
|
276
|
+
`
|
|
277
|
+
export const run = (input) =>
|
|
278
|
+
new Promise((resolve, reject) => {
|
|
279
|
+
input.load().catch(reject);
|
|
280
|
+
});
|
|
281
|
+
`,
|
|
282
|
+
'common/example.ts',
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
286
|
+
});
|
package/tests/mcp.test.cjs
CHANGED
|
@@ -186,6 +186,14 @@ test('instruction routing promotes triggered full instruction files', () => {
|
|
|
186
186
|
);
|
|
187
187
|
assert.equal(payload.data.fullReadPolicy.default.includes('read-only'), true);
|
|
188
188
|
assert.equal(payload.data.selected.some((entry) => entry.fullRead === 'full-before-action'), true);
|
|
189
|
+
|
|
190
|
+
const bugFixPayload = resolveInstructionRouting({ appRoot, query: 'fix OAuth redirect bug' });
|
|
191
|
+
assert.equal(
|
|
192
|
+
bugFixPayload.data.selected.some(
|
|
193
|
+
(entry) => entry.file === path.join(appRoot, 'DOCUMENTATION.md') && /Bug fix/.test(entry.reason),
|
|
194
|
+
),
|
|
195
|
+
true,
|
|
196
|
+
);
|
|
189
197
|
});
|
|
190
198
|
|
|
191
199
|
test('workflow start payload combines compact runtime, instructions, owner, and duplicate guidance', () => {
|
|
@@ -777,6 +785,88 @@ test('machine MCP router resolves offline monorepo app candidates before dev is
|
|
|
777
785
|
await server.close();
|
|
778
786
|
});
|
|
779
787
|
|
|
788
|
+
test('machine MCP workflow_start blocks offline unbootstrapped Codex worktrees', async (t) => {
|
|
789
|
+
const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
790
|
+
const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-worktree-offline-'));
|
|
791
|
+
process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
|
|
792
|
+
t.onTestFinished(() => {
|
|
793
|
+
if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
794
|
+
else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-worktree-repo-'));
|
|
798
|
+
const appRoot = path.join(repoRoot, '.codex', 'worktrees', 'product');
|
|
799
|
+
writeProteumAppFixture(appRoot, { identifier: 'ProductApp', name: 'Product', routerPort: 3020 });
|
|
800
|
+
|
|
801
|
+
const server = createProteumMachineMcpServer({ version: 'test' });
|
|
802
|
+
const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
|
|
803
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
804
|
+
|
|
805
|
+
await server.connect(serverTransport);
|
|
806
|
+
await client.connect(clientTransport);
|
|
807
|
+
|
|
808
|
+
const workflow = await client.callTool({ name: 'workflow_start', arguments: { cwd: appRoot, task: 'runtime health' } });
|
|
809
|
+
const payload = JSON.parse(workflow.content[0].text);
|
|
810
|
+
|
|
811
|
+
assert.equal(payload.ok, false);
|
|
812
|
+
assert.match(payload.summary, /has not completed Proteum worktree bootstrap/);
|
|
813
|
+
assert.equal(payload.nextActions.length, 1);
|
|
814
|
+
assert.match(payload.nextActions[0].command, /proteum worktree init --source <source-app-root>/);
|
|
815
|
+
|
|
816
|
+
await client.close();
|
|
817
|
+
await server.close();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test('machine MCP workflow_start blocks live unbootstrapped Codex worktrees before forwarding', async (t) => {
|
|
821
|
+
const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
822
|
+
const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-worktree-live-'));
|
|
823
|
+
process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = registryDir;
|
|
824
|
+
t.onTestFinished(() => {
|
|
825
|
+
if (previousRegistryDir === undefined) delete process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
826
|
+
else process.env.PROTEUM_MACHINE_DEV_SESSION_DIR = previousRegistryDir;
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-worktree-live-repo-'));
|
|
830
|
+
const appRoot = path.join(repoRoot, '.codex', 'worktrees', 'product');
|
|
831
|
+
writeProteumAppFixture(appRoot, { identifier: 'ProductApp', name: 'Product', routerPort: 3020 });
|
|
832
|
+
await writeMachineDevSessionRecord({
|
|
833
|
+
...createDevSessionRecord({
|
|
834
|
+
appRoot,
|
|
835
|
+
port: 3020,
|
|
836
|
+
sessionFilePath: path.join(appRoot, 'var/run/proteum/dev/3020.json'),
|
|
837
|
+
}),
|
|
838
|
+
publicUrl: 'http://localhost:3020',
|
|
839
|
+
state: 'ready',
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
let forwarded = false;
|
|
843
|
+
const server = createProteumMachineMcpServer({
|
|
844
|
+
createDevMcpClient: async () => ({
|
|
845
|
+
callTool: async () => {
|
|
846
|
+
forwarded = true;
|
|
847
|
+
throw new Error('should not forward');
|
|
848
|
+
},
|
|
849
|
+
close: async () => {},
|
|
850
|
+
}),
|
|
851
|
+
version: 'test',
|
|
852
|
+
});
|
|
853
|
+
const client = new Client({ name: 'machine-mcp-test', version: '1.0.0' });
|
|
854
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
855
|
+
|
|
856
|
+
await server.connect(serverTransport);
|
|
857
|
+
await client.connect(clientTransport);
|
|
858
|
+
|
|
859
|
+
const workflow = await client.callTool({ name: 'workflow_start', arguments: { cwd: appRoot, task: 'runtime health' } });
|
|
860
|
+
const payload = JSON.parse(workflow.content[0].text);
|
|
861
|
+
|
|
862
|
+
assert.equal(payload.ok, false);
|
|
863
|
+
assert.equal(forwarded, false);
|
|
864
|
+
assert.match(payload.nextActions[0].command, /proteum worktree init --source <source-app-root>/);
|
|
865
|
+
|
|
866
|
+
await client.close();
|
|
867
|
+
await server.close();
|
|
868
|
+
});
|
|
869
|
+
|
|
780
870
|
test('machine MCP offline resolution inspects occupied ports before suggesting dev start', async (t) => {
|
|
781
871
|
const previousRegistryDir = process.env.PROTEUM_MACHINE_DEV_SESSION_DIR;
|
|
782
872
|
const registryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-machine-offline-port-registry-'));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const coreRoot = path.resolve(__dirname, '..');
|
|
5
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
6
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
7
|
+
require('ts-node/register/transpile-only');
|
|
8
|
+
|
|
9
|
+
const { createServerIndexTemplate } = require('../cli/scaffold/templates.ts');
|
|
10
|
+
|
|
11
|
+
test('server index scaffold uses explicit defineApplication router property', () => {
|
|
12
|
+
const content = createServerIndexTemplate({ appIdentifier: 'ExampleApp' });
|
|
13
|
+
|
|
14
|
+
assert.match(content, /export default defineApplication\(\{/);
|
|
15
|
+
assert.match(content, /services: \(\) => \(\{\}\),/);
|
|
16
|
+
assert.match(content, /router: \(app\) =>\s+new Router\(/);
|
|
17
|
+
assert.doesNotMatch(content, /services: \(app\) => \(\{\s+Router: new Router\(/);
|
|
18
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const Module = require('node:module');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const coreRoot = path.join(__dirname, '..');
|
|
6
|
+
require('module-alias').addAliases({
|
|
7
|
+
'@client': path.join(coreRoot, 'client'),
|
|
8
|
+
'@common': path.join(coreRoot, 'common'),
|
|
9
|
+
'@server': path.join(coreRoot, 'server'),
|
|
10
|
+
});
|
|
11
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
12
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
13
|
+
require('ts-node/register/transpile-only');
|
|
14
|
+
|
|
15
|
+
const appContainerStub = {
|
|
16
|
+
Environment: {
|
|
17
|
+
profile: 'test',
|
|
18
|
+
connectedProjects: [],
|
|
19
|
+
},
|
|
20
|
+
Identity: {},
|
|
21
|
+
Setup: {},
|
|
22
|
+
Console: {
|
|
23
|
+
createBugReport: () => {},
|
|
24
|
+
},
|
|
25
|
+
Trace: {
|
|
26
|
+
finishRequest: () => {},
|
|
27
|
+
record: () => {},
|
|
28
|
+
releaseRequest: () => {},
|
|
29
|
+
},
|
|
30
|
+
handleBug: () => {},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const applicationModulePath = path.join(coreRoot, 'server/app/index.ts');
|
|
34
|
+
const originalLoad = Module._load;
|
|
35
|
+
Module._load = function patchedLoad(request, parent, isMain) {
|
|
36
|
+
if (parent?.filename === applicationModulePath && request === './container') {
|
|
37
|
+
return { __esModule: true, default: appContainerStub };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
let Application;
|
|
44
|
+
try {
|
|
45
|
+
Application = require('../server/app/index.ts').Application;
|
|
46
|
+
} finally {
|
|
47
|
+
Module._load = originalLoad;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class TestApplication extends Application {}
|
|
51
|
+
|
|
52
|
+
const createAppWithHooks = (hooks) => {
|
|
53
|
+
const app = new TestApplication();
|
|
54
|
+
app.hooks = hooks;
|
|
55
|
+
|
|
56
|
+
return app;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
test('server application reports plain errors through the default error hook', async () => {
|
|
60
|
+
const events = [];
|
|
61
|
+
const request = { id: 'request-1' };
|
|
62
|
+
const app = createAppWithHooks({
|
|
63
|
+
error: [
|
|
64
|
+
async (error, hookRequest) => {
|
|
65
|
+
events.push({
|
|
66
|
+
hook: 'error',
|
|
67
|
+
message: error.message,
|
|
68
|
+
requestId: hookRequest.id,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await app.reportError(new Error('boom'), request);
|
|
75
|
+
|
|
76
|
+
assert.deepEqual(events, [{ hook: 'error', message: 'boom', requestId: 'request-1' }]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('server application reports HTTP errors through code-specific hooks', async () => {
|
|
80
|
+
const events = [];
|
|
81
|
+
const error = new Error('missing');
|
|
82
|
+
error.http = 404;
|
|
83
|
+
const request = { id: 'request-404' };
|
|
84
|
+
const app = createAppWithHooks({
|
|
85
|
+
'error.404': [
|
|
86
|
+
async (hookError, hookRequest) => {
|
|
87
|
+
events.push({
|
|
88
|
+
hook: 'error.404',
|
|
89
|
+
message: hookError.message,
|
|
90
|
+
requestId: hookRequest.id,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await app.reportError(error, request);
|
|
97
|
+
|
|
98
|
+
assert.deepEqual(events, [{ hook: 'error.404', message: 'missing', requestId: 'request-404' }]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('server application reports status errors through code-specific hooks', async () => {
|
|
102
|
+
const events = [];
|
|
103
|
+
const error = new Error('forbidden');
|
|
104
|
+
error.status = 403;
|
|
105
|
+
const app = createAppWithHooks({
|
|
106
|
+
'error.403': [
|
|
107
|
+
async (hookError) => {
|
|
108
|
+
events.push({
|
|
109
|
+
hook: 'error.403',
|
|
110
|
+
message: hookError.message,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await app.reportError(error);
|
|
117
|
+
|
|
118
|
+
assert.deepEqual(events, [{ hook: 'error.403', message: 'forbidden' }]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('server application normalizes non-error rejections before reporting', async () => {
|
|
122
|
+
const messages = [];
|
|
123
|
+
const app = createAppWithHooks({
|
|
124
|
+
error: [
|
|
125
|
+
async (error) => {
|
|
126
|
+
messages.push(error.message);
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await app.reportError('string failure');
|
|
132
|
+
await app.reportError({ reason: 'unknown failure' });
|
|
133
|
+
|
|
134
|
+
assert.deepEqual(messages, ['string failure', 'Unknown application error']);
|
|
135
|
+
});
|