proteum 2.5.1 → 2.5.3
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 +3 -3
- package/README.md +12 -9
- package/agents/project/AGENTS.md +10 -8
- package/agents/project/CODING_STYLE.md +1 -1
- package/agents/project/diagnostics.md +2 -2
- package/agents/project/root/AGENTS.md +10 -8
- package/agents/project/tests/AGENTS.md +1 -1
- package/cli/commands/configure.ts +5 -5
- package/cli/commands/dev.ts +3 -3
- package/cli/commands/verify.ts +117 -4
- package/cli/compiler/artifacts/controllerHelper.ts +66 -0
- package/cli/compiler/artifacts/controllers.ts +3 -0
- package/cli/compiler/artifacts/services.ts +14 -8
- package/cli/compiler/common/generatedRouteModules.ts +270 -53
- package/cli/presentation/commands.ts +14 -3
- package/cli/presentation/help.ts +1 -1
- package/cli/runtime/commands.ts +6 -0
- package/cli/scaffold/templates.ts +11 -3
- package/cli/utils/agents.ts +271 -2
- package/cli/verification/changed.ts +460 -0
- package/client/app/index.ts +1 -1
- package/client/services/router/index.tsx +1 -1
- package/common/applicationConfig.ts +177 -0
- package/common/applicationConfigLoader.ts +33 -1
- package/common/dev/contractsDoctor.ts +16 -0
- package/config.ts +5 -1
- package/docs/migration-2.5.md +54 -11
- package/eslint.js +42 -8
- package/package.json +1 -1
- package/tests/agents-utils.test.cjs +72 -0
- package/tests/cli-mcp-command.test.cjs +14 -0
- package/tests/contracts-doctor.test.cjs +98 -0
- package/tests/definition-contracts.test.cjs +129 -0
- package/tests/eslint-rules.test.cjs +100 -0
- package/tests/scaffold-templates.test.cjs +27 -2
- package/tests/verify-changed.test.cjs +200 -0
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
indexRouteDefinitions,
|
|
19
19
|
writeGeneratedRouteModule,
|
|
20
20
|
} = require('../cli/compiler/common/generatedRouteModules.ts');
|
|
21
|
+
const { createTypedControllerHelperContent } = require('../cli/compiler/artifacts/controllerHelper.ts');
|
|
21
22
|
const { findClientRouteFiles } = require('../cli/compiler/artifacts/discovery.ts');
|
|
22
23
|
const { indexControllers } = require('../cli/compiler/common/controllers.ts');
|
|
23
24
|
const {
|
|
@@ -107,6 +108,51 @@ export default definePageRoute({
|
|
|
107
108
|
assert.deepEqual(definition.normalizedOptionKeys, ['auth']);
|
|
108
109
|
});
|
|
109
110
|
|
|
111
|
+
test('route indexer accepts imported const-only option metadata', () => {
|
|
112
|
+
const root = createTempDir();
|
|
113
|
+
const filepath = path.join(root, 'client/pages/feed.tsx');
|
|
114
|
+
const catalogFilepath = path.join(root, 'client/catalogs/routes.ts');
|
|
115
|
+
|
|
116
|
+
writeFile(
|
|
117
|
+
catalogFilepath,
|
|
118
|
+
`export const PAGE_AUTH = {
|
|
119
|
+
feed: {
|
|
120
|
+
auth: {
|
|
121
|
+
hasFeature: 'opportunitiesFreq',
|
|
122
|
+
},
|
|
123
|
+
authTracking: {
|
|
124
|
+
source: 'investor_feed',
|
|
125
|
+
feature: 'opportunitiesFreq',
|
|
126
|
+
action: 'open-feed',
|
|
127
|
+
triggerKind: 'open',
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
} as const;
|
|
131
|
+
`,
|
|
132
|
+
);
|
|
133
|
+
writeFile(
|
|
134
|
+
filepath,
|
|
135
|
+
`import { definePageRoute } from '@common/router/definitions';
|
|
136
|
+
import { PAGE_AUTH } from '@/client/catalogs/routes';
|
|
137
|
+
|
|
138
|
+
export default definePageRoute({
|
|
139
|
+
path: '/feed',
|
|
140
|
+
options: {
|
|
141
|
+
auth: PAGE_AUTH.feed.auth,
|
|
142
|
+
authTracking: PAGE_AUTH.feed.authTracking,
|
|
143
|
+
},
|
|
144
|
+
data: null,
|
|
145
|
+
render: () => null,
|
|
146
|
+
});
|
|
147
|
+
`,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const [definition] = indexRouteDefinitions({ side: 'client', sourceFilepath: filepath });
|
|
151
|
+
|
|
152
|
+
assert.equal(definition.path, '/feed');
|
|
153
|
+
assert.deepEqual(definition.normalizedOptionKeys, ['auth', 'authTracking']);
|
|
154
|
+
});
|
|
155
|
+
|
|
110
156
|
test('router port override updates absolute runtime URLs', () => {
|
|
111
157
|
const root = createTempDir();
|
|
112
158
|
|
|
@@ -262,6 +308,37 @@ export default defineServerRoutes(({ Users }) => [
|
|
|
262
308
|
assert.equal(definition.targetResolution, 'static-expression');
|
|
263
309
|
});
|
|
264
310
|
|
|
311
|
+
test('server route factories can push explicit route definitions from a block body', () => {
|
|
312
|
+
const root = createTempDir();
|
|
313
|
+
const filepath = path.join(root, 'server/routes/api.ts');
|
|
314
|
+
|
|
315
|
+
writeFile(
|
|
316
|
+
filepath,
|
|
317
|
+
`import { defineServerRoute, defineServerRoutes } from '@common/router/definitions';
|
|
318
|
+
|
|
319
|
+
export default defineServerRoutes((app) => {
|
|
320
|
+
const { Users } = app;
|
|
321
|
+
const routeOptions = {};
|
|
322
|
+
const routes = [];
|
|
323
|
+
|
|
324
|
+
routes.push(defineServerRoute({
|
|
325
|
+
method: 'GET',
|
|
326
|
+
path: '/api/users',
|
|
327
|
+
options: routeOptions,
|
|
328
|
+
handler: async () => Users.list(),
|
|
329
|
+
}));
|
|
330
|
+
|
|
331
|
+
return routes;
|
|
332
|
+
});
|
|
333
|
+
`,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const [definition] = indexRouteDefinitions({ side: 'server', sourceFilepath: filepath });
|
|
337
|
+
|
|
338
|
+
assert.equal(definition.methodName, 'get');
|
|
339
|
+
assert.equal(definition.path, '/api/users');
|
|
340
|
+
});
|
|
341
|
+
|
|
265
342
|
test('route definitions register through explicit router registrar', () => {
|
|
266
343
|
const calls = [];
|
|
267
344
|
|
|
@@ -386,6 +463,17 @@ export default class Legacy extends Controller {
|
|
|
386
463
|
);
|
|
387
464
|
});
|
|
388
465
|
|
|
466
|
+
test('generated typed controller helper binds action context to app root', () => {
|
|
467
|
+
const content = createTypedControllerHelperContent('TestApp');
|
|
468
|
+
|
|
469
|
+
assert.match(content, /@server\/app\/controller/);
|
|
470
|
+
assert.match(content, /@\/server\/index/);
|
|
471
|
+
assert.match(content, /export type TApplication = import\('@\/server\/index'\)\.TestApp/);
|
|
472
|
+
assert.match(content, /export type TControllerRequestServices = import\('@\/server\/index'\)\.TControllerRequestServices/);
|
|
473
|
+
assert.match(content, /TControllerActionContext<\n TInput,\n TApplication,\n TControllerRequestServices\n>/);
|
|
474
|
+
assert.match(content, /handler: \(context: TTypedControllerActionContext<z\.output<TSchema>>\) => TResult/);
|
|
475
|
+
});
|
|
476
|
+
|
|
389
477
|
test('service artifact parser reads defineApplication router factories', () => {
|
|
390
478
|
const root = createTempDir();
|
|
391
479
|
const filepath = path.join(root, 'server/index.ts');
|
|
@@ -451,3 +539,44 @@ export default App;
|
|
|
451
539
|
assert.deepEqual(bootstrap.rootServices.map((service) => service.registeredName), ['Router']);
|
|
452
540
|
assert.deepEqual(bootstrap.routerPlugins.map((service) => service.registeredName), ['schema']);
|
|
453
541
|
});
|
|
542
|
+
|
|
543
|
+
test('service artifact parser reads named defineApplication service factories', () => {
|
|
544
|
+
const root = createTempDir();
|
|
545
|
+
const filepath = path.join(root, 'server/index.ts');
|
|
546
|
+
const serviceFilepath = path.join(root, 'server/services/Billing.ts');
|
|
547
|
+
const { parseAppBootstrapSource } = loadServiceArtifactsForAppRoot(root);
|
|
548
|
+
|
|
549
|
+
writeFile(serviceFilepath, 'export default class Billing {}');
|
|
550
|
+
writeFile(
|
|
551
|
+
filepath,
|
|
552
|
+
`import { defineApplication } from '@server/app';
|
|
553
|
+
import Router from '@server/services/router';
|
|
554
|
+
import SchemaRouter from '@server/services/schema/router';
|
|
555
|
+
import Billing from '@/server/services/Billing';
|
|
556
|
+
|
|
557
|
+
const createServices = (app) => ({
|
|
558
|
+
Billing: new Billing(app, {}, app),
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const createRouter = (app) => new Router(
|
|
562
|
+
app,
|
|
563
|
+
{
|
|
564
|
+
plugins: {
|
|
565
|
+
schema: new SchemaRouter({}, app),
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
app,
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
export default defineApplication({
|
|
572
|
+
services: createServices,
|
|
573
|
+
router: createRouter,
|
|
574
|
+
});
|
|
575
|
+
`,
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const bootstrap = parseAppBootstrapSource(filepath);
|
|
579
|
+
|
|
580
|
+
assert.deepEqual(bootstrap.rootServices.map((service) => service.registeredName), ['Billing', 'Router']);
|
|
581
|
+
assert.deepEqual(bootstrap.routerPlugins.map((service) => service.registeredName), ['schema']);
|
|
582
|
+
});
|
|
@@ -146,6 +146,28 @@ test('proteum lint allows client catches routed to context app error handling',
|
|
|
146
146
|
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
+
test('proteum lint rejects optional client error handler calls', () => {
|
|
150
|
+
for (const statement of [
|
|
151
|
+
'app?.handleError(error);',
|
|
152
|
+
'context.app?.handleError(error);',
|
|
153
|
+
'window.app?.handleError(error);',
|
|
154
|
+
]) {
|
|
155
|
+
const messages = lint(`
|
|
156
|
+
export const run = async () => {
|
|
157
|
+
const app = useContext();
|
|
158
|
+
const context = useContext();
|
|
159
|
+
try {
|
|
160
|
+
await Investor.api.ensureApiKey();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
${statement}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
`);
|
|
166
|
+
|
|
167
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
149
171
|
test('proteum lint allows client catches using app error messages for UI feedback', () => {
|
|
150
172
|
const messages = lint(`
|
|
151
173
|
export const run = async () => {
|
|
@@ -221,6 +243,67 @@ test('proteum lint allows server catches routed to app error reporting', () => {
|
|
|
221
243
|
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
222
244
|
});
|
|
223
245
|
|
|
246
|
+
test('proteum lint allows server catches routed to instance app error reporting', () => {
|
|
247
|
+
const messages = lint(
|
|
248
|
+
`
|
|
249
|
+
export class WorkerController {
|
|
250
|
+
async run(context) {
|
|
251
|
+
try {
|
|
252
|
+
await context.services.Worker.run();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
await this.app.reportError(error, context.request);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
`,
|
|
259
|
+
'server/example.ts',
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('proteum lint rejects optional server error reporter calls', () => {
|
|
266
|
+
for (const statement of [
|
|
267
|
+
'app.reportError?.(error);',
|
|
268
|
+
'app?.reportError(error);',
|
|
269
|
+
'context.app?.reportError(error);',
|
|
270
|
+
]) {
|
|
271
|
+
const messages = lint(
|
|
272
|
+
`
|
|
273
|
+
export const run = async (context) => {
|
|
274
|
+
const app = context.app;
|
|
275
|
+
try {
|
|
276
|
+
await context.services.Worker.run();
|
|
277
|
+
} catch (error) {
|
|
278
|
+
${statement}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
`,
|
|
282
|
+
'server/example.ts',
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('proteum lint rejects conditional server error reporter calls', () => {
|
|
290
|
+
const messages = lint(
|
|
291
|
+
`
|
|
292
|
+
export const run = async (context) => {
|
|
293
|
+
const app = context.app;
|
|
294
|
+
try {
|
|
295
|
+
await context.services.Worker.run();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (app) app.reportError(error, context.request);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
`,
|
|
301
|
+
'server/example.ts',
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
assert.equal(messages.some((message) => message.ruleId === swallowedErrorRuleId), true);
|
|
305
|
+
});
|
|
306
|
+
|
|
224
307
|
test('proteum lint rejects server catches routed to client app error handling', () => {
|
|
225
308
|
const messages = lint(
|
|
226
309
|
`
|
|
@@ -271,6 +354,23 @@ test('proteum lint allows manual promise rejection', () => {
|
|
|
271
354
|
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
272
355
|
});
|
|
273
356
|
|
|
357
|
+
test('proteum lint allows returning manual promise rejection', () => {
|
|
358
|
+
const messages = lint(
|
|
359
|
+
`
|
|
360
|
+
export const run = async (input) => {
|
|
361
|
+
try {
|
|
362
|
+
await input.load();
|
|
363
|
+
} catch (error) {
|
|
364
|
+
return Promise.reject(error);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
`,
|
|
368
|
+
'common/example.ts',
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
assert.equal(messages.filter((message) => message.ruleId === swallowedErrorRuleId).length, 0);
|
|
372
|
+
});
|
|
373
|
+
|
|
274
374
|
test('proteum lint allows direct reject promise catch handlers', () => {
|
|
275
375
|
const messages = lint(
|
|
276
376
|
`
|
|
@@ -6,13 +6,38 @@ process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
|
6
6
|
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
7
7
|
require('ts-node/register/transpile-only');
|
|
8
8
|
|
|
9
|
-
const { createServerIndexTemplate } = require('../cli/scaffold/templates.ts');
|
|
9
|
+
const { createCommandTemplate, createControllerTemplate, createServerIndexTemplate } = require('../cli/scaffold/templates.ts');
|
|
10
10
|
|
|
11
11
|
test('server index scaffold uses explicit defineApplication router property', () => {
|
|
12
12
|
const content = createServerIndexTemplate({ appIdentifier: 'ExampleApp' });
|
|
13
13
|
|
|
14
|
-
assert.match(content, /
|
|
14
|
+
assert.match(content, /const ExampleAppApplication = defineApplication\(\{/);
|
|
15
|
+
assert.match(content, /export type ExampleApp = InstanceType<typeof ExampleAppApplication>/);
|
|
16
|
+
assert.match(content, /export default ExampleAppApplication/);
|
|
17
|
+
assert.match(content, /export type TControllerRequestServices = \{\}/);
|
|
15
18
|
assert.match(content, /services: \(\) => \(\{\}\),/);
|
|
16
19
|
assert.match(content, /router: \(app\) =>\s+new Router\(/);
|
|
17
20
|
assert.doesNotMatch(content, /services: \(app\) => \(\{\s+Router: new Router\(/);
|
|
18
21
|
});
|
|
22
|
+
|
|
23
|
+
test('controller scaffold imports app-typed generated controller helpers', () => {
|
|
24
|
+
const content = createControllerTemplate({
|
|
25
|
+
appIdentifier: 'ExampleApp',
|
|
26
|
+
className: 'BillingController',
|
|
27
|
+
methodName: 'read',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.match(content, /from '@generated\/server\/controller'/);
|
|
31
|
+
assert.doesNotMatch(content, /from '@server\/app\/controller'/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('command scaffold infers app type from defineApplication default export', () => {
|
|
35
|
+
const content = createCommandTemplate({
|
|
36
|
+
className: 'BillingCommands',
|
|
37
|
+
methodName: 'sync',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.match(content, /import type AppApplication from '@\/server\/index'/);
|
|
41
|
+
assert.match(content, /type App = InstanceType<typeof AppApplication>/);
|
|
42
|
+
assert.doesNotMatch(content, /import type App from '@\/server\/index'/);
|
|
43
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const { spawnSync } = 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
|
+
const cliBin = path.join(coreRoot, 'cli', 'bin.js');
|
|
9
|
+
|
|
10
|
+
process.env.TS_NODE_PROJECT = path.join(coreRoot, 'cli', 'tsconfig.json');
|
|
11
|
+
process.env.TS_NODE_TRANSPILE_ONLY = '1';
|
|
12
|
+
require('ts-node/register/transpile-only');
|
|
13
|
+
|
|
14
|
+
const { buildChangedVerificationPlan, runChangedVerification } = require('../cli/verification/changed.ts');
|
|
15
|
+
|
|
16
|
+
const createRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'proteum-verify-changed-'));
|
|
17
|
+
|
|
18
|
+
const writeFile = (root, filepath, content = '') => {
|
|
19
|
+
const fullPath = path.join(root, filepath);
|
|
20
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
21
|
+
fs.writeFileSync(fullPath, content);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const planCommands = (plan) => plan.selectedChecks.map((check) => check.command);
|
|
25
|
+
const planIds = (plan) => plan.selectedChecks.map((check) => check.id);
|
|
26
|
+
|
|
27
|
+
test('changed verification planner runs changed test files directly', () => {
|
|
28
|
+
const root = createRoot();
|
|
29
|
+
writeFile(root, 'tests/unit/session.test.ts', 'test("session", () => {});\n');
|
|
30
|
+
|
|
31
|
+
const plan = buildChangedVerificationPlan({
|
|
32
|
+
cwd: root,
|
|
33
|
+
changedFiles: ['tests/unit/session.test.ts'],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
assert.deepEqual(planCommands(plan), ["npx vitest run 'tests/unit/session.test.ts'"]);
|
|
37
|
+
assert.deepEqual(plan.selectedChecks[0].matchedFiles, ['tests/unit/session.test.ts']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('changed verification planner runs related tests for source files', () => {
|
|
41
|
+
const root = createRoot();
|
|
42
|
+
writeFile(root, 'packages/auth/src/session.ts', 'export const session = true;\n');
|
|
43
|
+
|
|
44
|
+
const plan = buildChangedVerificationPlan({
|
|
45
|
+
cwd: root,
|
|
46
|
+
changedFiles: ['packages/auth/src/session.ts'],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.deepEqual(planCommands(plan), ["npx vitest related 'packages/auth/src/session.ts'"]);
|
|
50
|
+
assert.deepEqual(plan.selectedChecks[0].matchedFiles, ['packages/auth/src/session.ts']);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('changed verification planner loads project config and applies Klair-style MCP rules', () => {
|
|
54
|
+
const root = createRoot();
|
|
55
|
+
writeFile(
|
|
56
|
+
root,
|
|
57
|
+
'proteum.verify.config.ts',
|
|
58
|
+
`import { defineVerificationConfig } from 'proteum/config';
|
|
59
|
+
|
|
60
|
+
export default defineVerificationConfig({
|
|
61
|
+
suites: {
|
|
62
|
+
mcpFast: 'npm run test:mcp:fast',
|
|
63
|
+
mcpBranches: 'npm run test:mcp:branches',
|
|
64
|
+
},
|
|
65
|
+
rules: [
|
|
66
|
+
{
|
|
67
|
+
id: 'mcp-fast',
|
|
68
|
+
match: ['packages/mcp/src/**', 'packages/2_usecases/src/mcp-v7/**'],
|
|
69
|
+
run: ['mcpFast'],
|
|
70
|
+
reason: 'MCP behavior changed.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'mcp-branches',
|
|
74
|
+
match: [
|
|
75
|
+
'packages/mcp/src/**/catalog*.ts',
|
|
76
|
+
'packages/mcp/src/**/dispatcher*.ts',
|
|
77
|
+
'packages/mcp/src/**/fallback*.ts',
|
|
78
|
+
'packages/mcp/src/**/validation*.ts',
|
|
79
|
+
'packages/2_usecases/src/mcp-v7/**/catalog*.ts',
|
|
80
|
+
'packages/2_usecases/src/mcp-v7/**/dispatcher*.ts',
|
|
81
|
+
'packages/2_usecases/src/mcp-v7/**/fallback*.ts',
|
|
82
|
+
'packages/2_usecases/src/mcp-v7/**/validation*.ts',
|
|
83
|
+
],
|
|
84
|
+
run: ['mcpBranches'],
|
|
85
|
+
reason: 'MCP branch-risk behavior changed.',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
`,
|
|
90
|
+
);
|
|
91
|
+
writeFile(root, 'packages/mcp/src/server.ts', 'export const server = true;\n');
|
|
92
|
+
writeFile(root, 'packages/mcp/src/dispatcher.ts', 'export const dispatcher = true;\n');
|
|
93
|
+
|
|
94
|
+
const normalPlan = buildChangedVerificationPlan({
|
|
95
|
+
cwd: root,
|
|
96
|
+
changedFiles: ['packages/mcp/src/server.ts'],
|
|
97
|
+
});
|
|
98
|
+
assert.ok(planCommands(normalPlan).includes('npm run test:mcp:fast'));
|
|
99
|
+
assert.equal(planCommands(normalPlan).includes('npm run test:mcp:branches'), false);
|
|
100
|
+
|
|
101
|
+
const branchRiskPlan = buildChangedVerificationPlan({
|
|
102
|
+
cwd: root,
|
|
103
|
+
changedFiles: ['packages/mcp/src/dispatcher.ts'],
|
|
104
|
+
});
|
|
105
|
+
assert.ok(planCommands(branchRiskPlan).includes('npm run test:mcp:fast'));
|
|
106
|
+
assert.ok(planCommands(branchRiskPlan).includes('npm run test:mcp:branches'));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('changed verification planner skips tests for docs-only changes', () => {
|
|
110
|
+
const root = createRoot();
|
|
111
|
+
writeFile(root, 'docs/testing.md', '# Testing\n');
|
|
112
|
+
|
|
113
|
+
const plan = buildChangedVerificationPlan({
|
|
114
|
+
cwd: root,
|
|
115
|
+
changedFiles: ['docs/testing.md'],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
assert.deepEqual(plan.selectedChecks, []);
|
|
119
|
+
assert.equal(plan.docsOnly, true);
|
|
120
|
+
assert.deepEqual(plan.skippedChecks.map((check) => check.id), ['builtin:docs-only']);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('changed verification dry-run reports the plan without executing checks', async () => {
|
|
124
|
+
const root = createRoot();
|
|
125
|
+
writeFile(
|
|
126
|
+
root,
|
|
127
|
+
'proteum.verify.config.ts',
|
|
128
|
+
`import { defineVerificationConfig } from 'proteum/config';
|
|
129
|
+
|
|
130
|
+
export default defineVerificationConfig({
|
|
131
|
+
suites: {
|
|
132
|
+
fail: 'node -e "process.exit(7)"',
|
|
133
|
+
},
|
|
134
|
+
always: ['fail'],
|
|
135
|
+
});
|
|
136
|
+
`,
|
|
137
|
+
);
|
|
138
|
+
writeFile(root, 'README.md', '# Docs\n');
|
|
139
|
+
|
|
140
|
+
const result = await runChangedVerification({
|
|
141
|
+
cwd: root,
|
|
142
|
+
changedFiles: ['README.md'],
|
|
143
|
+
dryRun: true,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
assert.equal(result.dryRun, true);
|
|
147
|
+
assert.equal(result.executions.length, 0);
|
|
148
|
+
assert.equal(result.result.ok, true);
|
|
149
|
+
assert.ok(planIds(result).includes('always:fail'));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('changed verification captures command failures and returns a failed result', async () => {
|
|
153
|
+
const root = createRoot();
|
|
154
|
+
writeFile(
|
|
155
|
+
root,
|
|
156
|
+
'proteum.verify.config.ts',
|
|
157
|
+
`import { defineVerificationConfig } from 'proteum/config';
|
|
158
|
+
|
|
159
|
+
export default defineVerificationConfig({
|
|
160
|
+
suites: {
|
|
161
|
+
fail: 'node -e "process.exit(7)"',
|
|
162
|
+
},
|
|
163
|
+
always: ['fail'],
|
|
164
|
+
});
|
|
165
|
+
`,
|
|
166
|
+
);
|
|
167
|
+
writeFile(root, 'README.md', '# Docs\n');
|
|
168
|
+
|
|
169
|
+
const result = await runChangedVerification({
|
|
170
|
+
cwd: root,
|
|
171
|
+
changedFiles: ['README.md'],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
assert.equal(result.result.ok, false);
|
|
175
|
+
assert.equal(result.result.failedChecks, 1);
|
|
176
|
+
assert.deepEqual(result.executions.map((execution) => execution.status), ['failed']);
|
|
177
|
+
assert.equal(result.executions[0].exitCode, 7);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('verify changed CLI JSON output keeps the planner and execution shape stable', () => {
|
|
181
|
+
const root = createRoot();
|
|
182
|
+
spawnSync('git', ['init'], { cwd: root, encoding: 'utf8' });
|
|
183
|
+
writeFile(root, 'docs/testing.md', '# Testing\n');
|
|
184
|
+
|
|
185
|
+
const result = spawnSync(process.execPath, [cliBin, 'verify', 'changed', '--dry-run', '--json'], {
|
|
186
|
+
cwd: root,
|
|
187
|
+
encoding: 'utf8',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
assert.equal(result.status, 0, result.stderr);
|
|
191
|
+
const output = JSON.parse(result.stdout);
|
|
192
|
+
|
|
193
|
+
assert.deepEqual(output.changedFiles, ['docs/testing.md']);
|
|
194
|
+
assert.ok(Array.isArray(output.selectedChecks));
|
|
195
|
+
assert.ok(Array.isArray(output.skippedChecks));
|
|
196
|
+
assert.ok(Array.isArray(output.executions));
|
|
197
|
+
assert.equal(typeof output.result.ok, 'boolean');
|
|
198
|
+
assert.equal(output.result.selectedChecks, 0);
|
|
199
|
+
assert.equal(output.result.failedChecks, 0);
|
|
200
|
+
});
|