cabloy 5.1.201 → 5.1.203

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.
Files changed (55) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/CONTRIBUTING.md +18 -17
  4. package/README.md +2 -2
  5. package/package.json +2 -3
  6. package/repo-agent-governance/managed-assets.json +33 -33
  7. package/repo-agent-governance/policies/repository.md +1 -0
  8. package/repo-agent-governance/skills/cabloy-spec-execution/SKILL.md +3 -2
  9. package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +1 -1
  10. package/repo-agent-governance/skills/cabloy-spec-execution/references/status-and-evidence.md +11 -10
  11. package/repo-agent-governance/skills/cabloy-spec-generation/SKILL.md +1 -1
  12. package/repo-agent-governance/skills/cabloy-spec-generation/references/canonical-spec-input.md +4 -2
  13. package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-specs-document-set.md +16 -13
  14. package/repo-agent-governance/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +18 -11
  15. package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -2
  16. package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +1 -1
  17. package/repo-agent-governance/skills/cabloy-worktree-environment/SKILL.md +5 -5
  18. package/repo-agent-governance/tests/spec-audit.test.mjs +323 -0
  19. package/repo-agent-governance/tools/spec-audit/audit.mjs +129 -3
  20. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +26 -7
  21. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.test.mjs +82 -0
  22. package/repo-agent-governance/tools/spec-charts/spec-parser.mjs +40 -1
  23. package/repo-docs/ai/ai-spec-driven-development.md +1 -0
  24. package/repo-docs/ai/playbook-spec-execution.md +3 -2
  25. package/repo-docs/ai/playbook-spec-generation.md +1 -1
  26. package/repo-docs/assets/img/cabloy-admin-ssr-cover-en.png +0 -0
  27. package/repo-docs/assets/img/cabloy-admin-tabs-cover-en.png +0 -0
  28. package/repo-docs/assets/img/cabloy-ai-crud-cover-en.png +0 -0
  29. package/repo-docs/demo.md +14 -0
  30. package/repo-docs/frontend/scripts.md +10 -18
  31. package/repo-docs/fullstack/parallel-worktree-environment.md +2 -2
  32. package/repo-docs/index.md +2 -2
  33. package/repo-docs/reference/repo-scripts.md +15 -31
  34. package/repo-e2e/config/playwright.config.ts +14 -19
  35. package/repo-e2e/scripts/e2e.test.ts +17 -0
  36. package/repo-e2e/scripts/e2e.ts +5 -1
  37. package/repo-e2e/scripts/runE2e.ts +3 -15
  38. package/repo-e2e/scripts/runE2eArgs.test.ts +21 -17
  39. package/repo-e2e/scripts/runE2eArgs.ts +16 -29
  40. package/repo-e2e/scripts/startE2eVona.ts +20 -9
  41. package/repo-e2e/specs/a-commerce.spec.ts +232 -31
  42. package/repo-e2e/specs/cabloy-basic.spec.ts +39 -12
  43. package/repo-e2e/specs/home-user-account.spec.ts +9 -2
  44. package/scripts/upgrade.test.ts +147 -0
  45. package/scripts/upgrade.ts +49 -27
  46. package/vona/packages-cli/cli/package.json +1 -1
  47. package/vona/packages-cli/cli-set-api/package.json +1 -1
  48. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +51 -5
  49. package/vona/packages-cli/cli-set-api/src/lib/command/bin.dev.ts +6 -1
  50. package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +52 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +41 -0
  52. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/e2eFixture.ts +52 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/e2eFixture.ts +224 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/e2eFixture.ts +694 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-trade/test/e2eFixture.test.ts +434 -0
@@ -0,0 +1,147 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, resolve } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import { mergeFrameworkE2eAssets, reconcileFrameworkE2ePackageJson } from './upgrade.ts';
8
+
9
+ const frameworkFiles = [
10
+ 'repo-e2e/config/playwright.config.ts',
11
+ 'repo-e2e/scripts/runE2e.ts',
12
+ 'repo-e2e/specs/cabloy-basic.spec.ts',
13
+ 'repo-e2e/specs/home-user-account.spec.ts',
14
+ 'repo-e2e/specs/a-commerce.spec.ts',
15
+ ];
16
+
17
+ function writeFile(root: string, path: string, content: string): void {
18
+ const file = resolve(root, path);
19
+ mkdirSync(dirname(file), { recursive: true });
20
+ writeFileSync(file, content);
21
+ }
22
+
23
+ function readPackage(root: string): {
24
+ scripts: Record<string, string>;
25
+ dependencies?: Record<string, string>;
26
+ devDependencies?: Record<string, string>;
27
+ } {
28
+ return JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf-8'));
29
+ }
30
+
31
+ function fixture(): { root: string; project: string; source: string; dispose(): void } {
32
+ const root = mkdtempSync(resolve(tmpdir(), 'cabloy-upgrade-e2e-'));
33
+ const project = resolve(root, 'project');
34
+ const source = resolve(root, 'source');
35
+ writeFile(project, '__CABLOY_BASIC__', '');
36
+ writeFile(
37
+ project,
38
+ 'package.json',
39
+ `${JSON.stringify(
40
+ {
41
+ scripts: {
42
+ 'test:e2e': 'node repo-e2e/scripts/runE2e.ts --clean',
43
+ 'test:e2e:fast': 'node repo-e2e/scripts/runE2e.ts --fast',
44
+ 'test:project': 'node project-tests.ts',
45
+ },
46
+ dependencies: { '@playwright/test': '^1.0.0', 'project-owned': '^2.0.0' },
47
+ devDependencies: { 'another-tool': '^3.0.0' },
48
+ },
49
+ null,
50
+ 2,
51
+ )}\n`,
52
+ );
53
+ writeFile(
54
+ source,
55
+ 'package.json',
56
+ `${JSON.stringify({ scripts: { 'test:e2e': 'node repo-e2e/scripts/runE2e.ts' }, devDependencies: { '@playwright/test': '^1.62.1' } })}\n`,
57
+ );
58
+ for (const path of frameworkFiles) {
59
+ writeFile(project, path, `old ${path}`);
60
+ writeFile(source, path, `new ${path}`);
61
+ }
62
+ writeFile(project, 'repo-e2e/specs/project-owned.spec.ts', 'project browser test');
63
+ writeFile(project, 'repo-e2e/config/playwright.basic.config.ts', 'legacy config');
64
+ return { root, project, source, dispose: () => rmSync(root, { recursive: true, force: true }) };
65
+ }
66
+
67
+ test('reconciles one managed E2E script and removes only the known retired fast script', () => {
68
+ const { project, source, dispose } = fixture();
69
+ try {
70
+ reconcileFrameworkE2ePackageJson(false, project, source);
71
+ const first = readFileSync(resolve(project, 'package.json'), 'utf-8');
72
+ const pkg = readPackage(project);
73
+ assert.equal(pkg.scripts['test:e2e'], 'node repo-e2e/scripts/runE2e.ts');
74
+ assert.equal(Object.hasOwn(pkg.scripts, 'test:e2e:fast'), false);
75
+ assert.equal(pkg.scripts['test:project'], 'node project-tests.ts');
76
+ assert.deepEqual(pkg.dependencies, { 'project-owned': '^2.0.0' });
77
+ assert.deepEqual(pkg.devDependencies, {
78
+ 'another-tool': '^3.0.0',
79
+ '@playwright/test': '^1.62.1',
80
+ });
81
+ reconcileFrameworkE2ePackageJson(false, project, source);
82
+ assert.equal(readFileSync(resolve(project, 'package.json'), 'utf-8'), first);
83
+ } finally {
84
+ dispose();
85
+ }
86
+ });
87
+
88
+ test('preserves a customized fast alias and project-owned browser specs', () => {
89
+ const { project, source, dispose } = fixture();
90
+ try {
91
+ const pkg = readPackage(project);
92
+ pkg.scripts['test:e2e:fast'] = 'node project-tests.ts --quick';
93
+ writeFile(project, 'package.json', `${JSON.stringify(pkg, null, 2)}\n`);
94
+ reconcileFrameworkE2ePackageJson(false, project, source);
95
+ mergeFrameworkE2eAssets(false, project, source);
96
+ assert.equal(readPackage(project).scripts['test:e2e:fast'], 'node project-tests.ts --quick');
97
+ for (const path of frameworkFiles) {
98
+ assert.equal(readFileSync(resolve(project, path), 'utf-8'), `new ${path}`);
99
+ }
100
+ assert.equal(
101
+ readFileSync(resolve(project, 'repo-e2e/specs/project-owned.spec.ts'), 'utf-8'),
102
+ 'project browser test',
103
+ );
104
+ assert.equal(existsSync(resolve(project, 'repo-e2e/config/playwright.basic.config.ts')), false);
105
+ } finally {
106
+ dispose();
107
+ }
108
+ });
109
+
110
+ test('isolated dry-run reports changes without writing files or contacting npm', () => {
111
+ const { project, source, dispose } = fixture();
112
+ const before = readFileSync(resolve(project, 'package.json'), 'utf-8');
113
+ try {
114
+ reconcileFrameworkE2ePackageJson(true, project, source);
115
+ mergeFrameworkE2eAssets(true, project, source);
116
+ assert.equal(readFileSync(resolve(project, 'package.json'), 'utf-8'), before);
117
+ assert.equal(
118
+ readFileSync(resolve(project, 'repo-e2e/scripts/runE2e.ts'), 'utf-8'),
119
+ 'old repo-e2e/scripts/runE2e.ts',
120
+ );
121
+ assert.equal(existsSync(resolve(project, 'repo-e2e/config/playwright.basic.config.ts')), true);
122
+ assert.equal(
123
+ readFileSync(resolve(project, 'repo-e2e/specs/project-owned.spec.ts'), 'utf-8'),
124
+ 'project browser test',
125
+ );
126
+ } finally {
127
+ dispose();
128
+ }
129
+ });
130
+
131
+ test('Start edition is not reconciled by the Basic framework E2E upgrader', () => {
132
+ const { project, source, dispose } = fixture();
133
+ try {
134
+ rmSync(resolve(project, '__CABLOY_BASIC__'));
135
+ writeFile(project, '__CABLOY_START__', '');
136
+ const before = readFileSync(resolve(project, 'package.json'), 'utf-8');
137
+ reconcileFrameworkE2ePackageJson(false, project, source);
138
+ mergeFrameworkE2eAssets(false, project, source);
139
+ assert.equal(readFileSync(resolve(project, 'package.json'), 'utf-8'), before);
140
+ assert.equal(
141
+ readFileSync(resolve(project, 'repo-e2e/scripts/runE2e.ts'), 'utf-8'),
142
+ 'old repo-e2e/scripts/runE2e.ts',
143
+ );
144
+ } finally {
145
+ dispose();
146
+ }
147
+ });
@@ -68,7 +68,8 @@ const FRAMEWORK_E2E_FILES_CABLOY_BASIC: string[] = [
68
68
  'repo-e2e/specs/a-commerce.spec.ts',
69
69
  ];
70
70
 
71
- const FRAMEWORK_E2E_SCRIPT_NAMES_CABLOY_BASIC: string[] = ['test:e2e', 'test:e2e:fast'];
71
+ const FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC = 'test:e2e';
72
+ const LEGACY_FRAMEWORK_E2E_FAST_SCRIPT_CABLOY_BASIC = 'node repo-e2e/scripts/runE2e.ts --fast';
72
73
 
73
74
  const FRAMEWORK_E2E_DEV_DEPENDENCY_CABLOY_BASIC = '@playwright/test';
74
75
 
@@ -147,9 +148,9 @@ function copyDirectory(src: string, dest: string): void {
147
148
  cpSync(src, dest, { recursive: true, filter: shouldCopyPath });
148
149
  }
149
150
 
150
- function resolveEdition(): 'basic' | 'start' {
151
- const hasBasic = existsSync(resolve(ROOT_DIR, '__CABLOY_BASIC__'));
152
- const hasStart = existsSync(resolve(ROOT_DIR, '__CABLOY_START__'));
151
+ function resolveEdition(rootDir = ROOT_DIR): 'basic' | 'start' {
152
+ const hasBasic = existsSync(resolve(rootDir, '__CABLOY_BASIC__'));
153
+ const hasStart = existsSync(resolve(rootDir, '__CABLOY_START__'));
153
154
  if (hasBasic === hasStart) {
154
155
  throw new Error(
155
156
  hasBasic
@@ -226,12 +227,16 @@ async function extractTarball(tarballPath: string, targetDir: string): Promise<v
226
227
  }
227
228
  }
228
229
 
229
- function mergeFrameworkE2eAssets(dryRun?: boolean): void {
230
- if (!isCabloyBasic()) return;
230
+ export function mergeFrameworkE2eAssets(
231
+ dryRun?: boolean,
232
+ rootDir = ROOT_DIR,
233
+ tempDir = TEMP_DIR,
234
+ ): void {
235
+ if (resolveEdition(rootDir) !== 'basic') return;
231
236
 
232
237
  for (const dir of FRAMEWORK_E2E_DIRS_CABLOY_BASIC) {
233
- const src = resolve(TEMP_DIR, dir);
234
- const dest = resolve(ROOT_DIR, dir);
238
+ const src = resolve(tempDir, dir);
239
+ const dest = resolve(rootDir, dir);
235
240
  if (!existsSync(src)) {
236
241
  throw new Error(`Expected framework E2E directory in package: ${dir}`);
237
242
  }
@@ -243,8 +248,8 @@ function mergeFrameworkE2eAssets(dryRun?: boolean): void {
243
248
  }
244
249
 
245
250
  for (const file of FRAMEWORK_E2E_FILES_CABLOY_BASIC) {
246
- const src = resolve(TEMP_DIR, file);
247
- const dest = resolve(ROOT_DIR, file);
251
+ const src = resolve(tempDir, file);
252
+ const dest = resolve(rootDir, file);
248
253
  if (!existsSync(src)) {
249
254
  throw new Error(`Expected framework E2E file in package: ${file}`);
250
255
  }
@@ -265,7 +270,7 @@ function mergeFrameworkE2eAssets(dryRun?: boolean): void {
265
270
  'repo-e2e/specs/a-commerce/commerce.spec.ts',
266
271
  ];
267
272
  for (const file of legacyFrameworkFiles) {
268
- const dest = resolve(ROOT_DIR, file);
273
+ const dest = resolve(rootDir, file);
269
274
  if (!existsSync(dest)) continue;
270
275
  if (dryRun) {
271
276
  log(` [dry-run] Remove legacy framework E2E file: ${file}`);
@@ -334,11 +339,15 @@ function reconcileGovernanceAssets(dryRun?: boolean): void {
334
339
  }
335
340
  }
336
341
 
337
- function reconcileFrameworkE2ePackageJson(dryRun?: boolean): void {
338
- if (!isCabloyBasic()) return;
342
+ export function reconcileFrameworkE2ePackageJson(
343
+ dryRun?: boolean,
344
+ rootDir = ROOT_DIR,
345
+ tempDir = TEMP_DIR,
346
+ ): void {
347
+ if (resolveEdition(rootDir) !== 'basic') return;
339
348
 
340
- const projectPackagePath = resolve(ROOT_DIR, 'package.json');
341
- const sourcePackagePath = resolve(TEMP_DIR, 'package.json');
349
+ const projectPackagePath = resolve(rootDir, 'package.json');
350
+ const sourcePackagePath = resolve(tempDir, 'package.json');
342
351
  const projectPackage = readPackageJson(projectPackagePath);
343
352
  const sourcePackage = readPackageJson(sourcePackagePath);
344
353
  const sourcePlaywrightVersion =
@@ -349,19 +358,30 @@ function reconcileFrameworkE2ePackageJson(dryRun?: boolean): void {
349
358
  );
350
359
  }
351
360
 
361
+ const sourceScript = sourcePackage.scripts?.[FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC];
362
+ if (!sourceScript) {
363
+ throw new Error(
364
+ `Expected framework E2E script in package.json: ${FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC}`,
365
+ );
366
+ }
367
+
352
368
  let changed = false;
353
- for (const name of FRAMEWORK_E2E_SCRIPT_NAMES_CABLOY_BASIC) {
354
- const sourceValue = sourcePackage.scripts?.[name];
355
- if (!sourceValue) {
356
- throw new Error(`Expected framework E2E script in package.json: ${name}`);
357
- }
358
- if (projectPackage.scripts?.[name] === sourceValue) continue;
369
+ if (projectPackage.scripts?.[FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC] !== sourceScript) {
359
370
  changed = true;
360
371
  if (dryRun) {
361
- log(` [dry-run] Set package.json scripts.${name}`);
372
+ log(` [dry-run] Set package.json scripts.${FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC}`);
362
373
  } else {
363
374
  projectPackage.scripts ??= {};
364
- projectPackage.scripts[name] = sourceValue;
375
+ projectPackage.scripts[FRAMEWORK_E2E_SCRIPT_CABLOY_BASIC] = sourceScript;
376
+ }
377
+ }
378
+
379
+ if (projectPackage.scripts?.['test:e2e:fast'] === LEGACY_FRAMEWORK_E2E_FAST_SCRIPT_CABLOY_BASIC) {
380
+ changed = true;
381
+ if (dryRun) {
382
+ log(' [dry-run] Remove package.json scripts.test:e2e:fast (retired framework value)');
383
+ } else {
384
+ delete projectPackage.scripts['test:e2e:fast'];
365
385
  }
366
386
  }
367
387
 
@@ -635,7 +655,9 @@ async function main(): Promise<void> {
635
655
  }
636
656
  }
637
657
 
638
- main().catch(err => {
639
- console.error(`\nUpgrade failed: ${err.message}`);
640
- process.exit(1);
641
- });
658
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
659
+ main().catch(err => {
660
+ console.error(`\nUpgrade failed: ${err.message}`);
661
+ process.exit(1);
662
+ });
663
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.157",
3
+ "version": "1.1.158",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.155",
3
+ "version": "1.1.156",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -2,6 +2,7 @@ import type { glob } from '@cabloy/module-glob';
2
2
  import type { VonaConfigMeta, VonaMetaFlavor, VonaMetaMode } from '@cabloy/module-info';
3
3
 
4
4
  import { BeanCliBase } from '@cabloy/cli';
5
+ import { spawn } from 'node:child_process';
5
6
  import path from 'node:path';
6
7
  import nodemon from 'nodemon';
7
8
  import { rimraf } from 'rimraf';
@@ -11,10 +12,23 @@ import type { VonaBinConfigOptions } from './toolsBin/types.ts';
11
12
  import { getImportEsm } from '../utils.ts';
12
13
  import { generateVonaMeta } from './toolsBin/generateVonaMeta.ts';
13
14
 
15
+ export function resolveDevMode(mode: unknown): 'dev' | 'test' {
16
+ if (mode === undefined || mode === 'dev') return 'dev';
17
+ if (mode === 'test') return 'test';
18
+ throw new Error('Invalid --mode: expected dev or test');
19
+ }
20
+
21
+ export function resolveTestWorkers(mode: 'dev' | 'test', workers: unknown): number | undefined {
22
+ if (mode !== 'test') return workers as number | undefined;
23
+ if (workers === undefined || workers === 1) return 1;
24
+ throw new Error('--mode=test requires --workers=1');
25
+ }
26
+
14
27
  declare module '@cabloy/cli' {
15
28
  interface ICommandArgv {
16
29
  workers?: number;
17
30
  flavor?: VonaMetaFlavor;
31
+ mode?: VonaMetaMode;
18
32
  }
19
33
  }
20
34
 
@@ -30,17 +44,49 @@ export class CliBinDev extends BeanCliBase {
30
44
 
31
45
  async _dev(projectPath: string) {
32
46
  const { argv } = this.context;
33
- const mode: VonaMetaMode = 'dev';
47
+ const mode = resolveDevMode(argv.mode);
48
+ const workers = resolveTestWorkers(mode, argv.workers);
34
49
  const flavor: VonaMetaFlavor = argv.flavor || 'normal';
35
50
  const configMeta: VonaConfigMeta = { flavor, mode };
36
51
  const configOptions: VonaBinConfigOptions = {
37
52
  appDir: projectPath,
38
53
  runtimeDir: '.vona',
39
- workers: argv.workers,
54
+ workers,
40
55
  };
41
- const { modulesMeta } = await generateVonaMeta(configMeta, configOptions);
42
- await this._run(projectPath, modulesMeta);
43
- await rimraf(path.join(projectPath, '.vona'));
56
+ try {
57
+ const { modulesMeta } = await generateVonaMeta(configMeta, configOptions);
58
+ if (mode === 'test') {
59
+ await this._runTest(projectPath);
60
+ } else {
61
+ await this._run(projectPath, modulesMeta);
62
+ }
63
+ } finally {
64
+ await rimraf(path.join(projectPath, '.vona'));
65
+ }
66
+ }
67
+
68
+ async _runTest(projectPath: string) {
69
+ const child = spawn(process.execPath, [getImportEsm(), '.vona/bootstrap.ts'], {
70
+ cwd: projectPath,
71
+ stdio: 'inherit',
72
+ });
73
+ const forwardSignal = (signal: NodeJS.Signals) => child.kill(signal);
74
+ const onInterrupt = () => forwardSignal('SIGINT');
75
+ const onTerminate = () => forwardSignal('SIGTERM');
76
+ process.on('SIGINT', onInterrupt);
77
+ process.on('SIGTERM', onTerminate);
78
+ try {
79
+ await new Promise<void>((resolve, reject) => {
80
+ child.once('error', reject);
81
+ child.once('exit', (code, signal) => {
82
+ if (code === 0) resolve();
83
+ else reject(new Error(`Test-mode dev server exited: ${signal ?? code}`));
84
+ });
85
+ });
86
+ } finally {
87
+ process.off('SIGINT', onInterrupt);
88
+ process.off('SIGTERM', onTerminate);
89
+ }
44
90
  }
45
91
 
46
92
  async _run(projectPath: string, _modulesMeta: Awaited<ReturnType<typeof glob>>) {
@@ -3,7 +3,7 @@ export default {
3
3
  info: {
4
4
  version: '5.0.0',
5
5
  title: 'Cli: Bin: Dev',
6
- usage: 'npm run vona :bin:dev -- [--workers=] [--flavor=]',
6
+ usage: 'npm run vona :bin:dev -- [--workers=] [--flavor=] [--mode=dev|test]',
7
7
  },
8
8
  options: {
9
9
  workers: {
@@ -14,5 +14,10 @@ export default {
14
14
  description: 'flavor',
15
15
  type: 'string',
16
16
  },
17
+ mode: {
18
+ description: 'dev or test',
19
+ type: 'string',
20
+ choices: ['dev', 'test'],
21
+ },
17
22
  },
18
23
  };
@@ -0,0 +1,52 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { describe, it } from 'node:test';
6
+
7
+ import { CliBinDev, resolveDevMode, resolveTestWorkers } from '../src/lib/bean/cli.bin.dev.ts';
8
+ import command from '../src/lib/command/bin.dev.ts';
9
+
10
+ describe('bin.dev mode', () => {
11
+ it('defaults to dev and accepts test explicitly', () => {
12
+ assert.equal(resolveDevMode(undefined), 'dev');
13
+ assert.equal(resolveDevMode('dev'), 'dev');
14
+ assert.equal(resolveDevMode('test'), 'test');
15
+ assert.deepEqual(command.options.mode.choices, ['dev', 'test']);
16
+ });
17
+
18
+ it('rejects invalid modes before runtime generation', () => {
19
+ for (const value of ['', 'prod', 'unknown', true, ['dev', 'test'], null]) {
20
+ assert.throws(() => resolveDevMode(value), /Invalid --mode: expected dev or test/);
21
+ }
22
+ });
23
+
24
+ it('forces one worker in test mode without changing dev workers', () => {
25
+ assert.equal(resolveTestWorkers('test', undefined), 1);
26
+ assert.equal(resolveTestWorkers('test', 1), 1);
27
+ assert.equal(resolveTestWorkers('dev', undefined), undefined);
28
+ assert.equal(resolveTestWorkers('dev', 3), 3);
29
+ for (const workers of [0, 2, '1', null, [1]]) {
30
+ assert.throws(() => resolveTestWorkers('test', workers), /requires --workers=1/);
31
+ }
32
+ });
33
+
34
+ it('supervises test-mode bootstrap exit without starting a server', async () => {
35
+ const projectPath = mkdtempSync(join(tmpdir(), 'vona-bin-dev-mode-'));
36
+ try {
37
+ mkdirSync(join(projectPath, '.vona'));
38
+ writeFileSync(join(projectPath, '.vona/register.js'), '');
39
+ const cli = Object.create(CliBinDev.prototype) as CliBinDev;
40
+ for (const code of [0, 7]) {
41
+ writeFileSync(join(projectPath, '.vona/bootstrap.ts'), `process.exit(${code});`);
42
+ if (code === 0) {
43
+ await cli._runTest(projectPath);
44
+ } else {
45
+ await assert.rejects(cli._runTest(projectPath), /Test-mode dev server exited: 7/);
46
+ }
47
+ }
48
+ } finally {
49
+ rmSync(projectPath, { recursive: true, force: true });
50
+ }
51
+ });
52
+ });
@@ -1063,11 +1063,13 @@ declare module 'vona-module-commerce-trade' {
1063
1063
  /** controller: begin */
1064
1064
  export * from '../controller/cart.ts';
1065
1065
  export * from '../controller/checkout.ts';
1066
+ export * from '../controller/e2eFixture.ts';
1066
1067
  export * from '../controller/order.ts';
1067
1068
  export * from '../controller/stockAudit.ts';
1068
1069
  export * from '../controller/stockBalance.ts';
1069
1070
  import type { IControllerOptionsCart } from '../controller/cart.ts';
1070
1071
  import type { IControllerOptionsCheckout } from '../controller/checkout.ts';
1072
+ import type { IControllerOptionsE2eFixture } from '../controller/e2eFixture.ts';
1071
1073
  import type { IControllerOptionsOrder } from '../controller/order.ts';
1072
1074
  import type { IControllerOptionsStockAudit } from '../controller/stockAudit.ts';
1073
1075
  import type { IControllerOptionsStockBalance } from '../controller/stockBalance.ts';
@@ -1077,6 +1079,7 @@ declare module 'vona-module-a-web' {
1077
1079
  export interface IControllerRecord {
1078
1080
  'commerce-trade:cart': IControllerOptionsCart;
1079
1081
  'commerce-trade:checkout': IControllerOptionsCheckout;
1082
+ 'commerce-trade:e2eFixture': IControllerOptionsE2eFixture;
1080
1083
  'commerce-trade:order': IControllerOptionsOrder;
1081
1084
  'commerce-trade:stockAudit': IControllerOptionsStockAudit;
1082
1085
  'commerce-trade:stockBalance': IControllerOptionsStockBalance;
@@ -1108,6 +1111,17 @@ declare module 'vona-module-commerce-trade' {
1108
1111
  get $onionOptions(): IControllerOptionsCheckout;
1109
1112
  }
1110
1113
 
1114
+ export interface ControllerE2eFixture {
1115
+ /** @internal */
1116
+ get scope(): ScopeModuleCommerceTrade;
1117
+ }
1118
+
1119
+ export interface ControllerE2eFixture {
1120
+ get $beanFullName(): 'commerce-trade.controller.e2eFixture';
1121
+ get $onionName(): 'commerce-trade:e2eFixture';
1122
+ get $onionOptions(): IControllerOptionsE2eFixture;
1123
+ }
1124
+
1111
1125
  export interface ControllerOrder {
1112
1126
  /** @internal */
1113
1127
  get scope(): ScopeModuleCommerceTrade;
@@ -1148,6 +1162,8 @@ import type { ControllerCart } from '../controller/cart.ts';
1148
1162
  // @ts-ignore ignore
1149
1163
  import type { ControllerCheckout } from '../controller/checkout.ts';
1150
1164
  // @ts-ignore ignore
1165
+ import type { ControllerE2eFixture } from '../controller/e2eFixture.ts';
1166
+ // @ts-ignore ignore
1151
1167
  import type { ControllerOrder } from '../controller/order.ts';
1152
1168
  // @ts-ignore ignore
1153
1169
  import type { ControllerStockAudit } from '../controller/stockAudit.ts';
@@ -1163,6 +1179,10 @@ declare module 'vona-module-commerce-trade' {
1163
1179
  actions?: TypeControllerOptionsActions<ControllerCheckout>;
1164
1180
  }
1165
1181
 
1182
+ export interface IControllerOptionsE2eFixture {
1183
+ actions?: TypeControllerOptionsActions<ControllerE2eFixture>;
1184
+ }
1185
+
1166
1186
  export interface IControllerOptionsOrder {
1167
1187
  actions?: TypeControllerOptionsActions<ControllerOrder>;
1168
1188
  }
@@ -1192,6 +1212,9 @@ declare module 'vona-module-a-web' {
1192
1212
  export interface IApiPathPostRecord{
1193
1213
  '/commerce/trade/cart/items': undefined;
1194
1214
  '/commerce/trade/checkout': undefined;
1215
+ '/commerce/trade/e2eFixture/catalogue': undefined;
1216
+ '/commerce/trade/e2eFixture/order/:orderId/payment/:attemptId/dispatch': undefined;
1217
+ '/commerce/trade/e2eFixture/order/:orderId/refund/:attemptId/dispatch': undefined;
1195
1218
  '/commerce/trade/order/:id/requestRefund': undefined;
1196
1219
  '/commerce/trade/order/:id/approveRefund': undefined;
1197
1220
  '/commerce/trade/order/:id/rejectRefund': undefined;
@@ -1207,6 +1230,9 @@ export interface IApiPathPatchRecord{
1207
1230
  export interface IApiPathDeleteRecord{
1208
1231
  '/commerce/trade/cart/items/:id': undefined;
1209
1232
  '/commerce/trade/cart/items': undefined;
1233
+ '/commerce/trade/e2eFixture/order/:orderId': undefined;
1234
+ '/commerce/trade/e2eFixture/customer': undefined;
1235
+ '/commerce/trade/e2eFixture/catalogue/:categoryId': undefined;
1210
1236
  }
1211
1237
 
1212
1238
  }
@@ -1247,6 +1273,7 @@ declare module 'vona' {
1247
1273
  /** bean: end */
1248
1274
  /** service: begin */
1249
1275
  export * from '../service/cart.ts';
1276
+ export * from '../service/e2eFixture.ts';
1250
1277
  export * from '../service/order.ts';
1251
1278
  export * from '../service/stockAudit.ts';
1252
1279
  export * from '../service/stockBalance.ts';
@@ -1256,6 +1283,7 @@ declare module 'vona-module-a-bean' {
1256
1283
 
1257
1284
  export interface IServiceRecord {
1258
1285
  'commerce-trade:cart': never;
1286
+ 'commerce-trade:e2eFixture': never;
1259
1287
  'commerce-trade:order': never;
1260
1288
  'commerce-trade:stockAudit': never;
1261
1289
  'commerce-trade:stockBalance': never;
@@ -1275,6 +1303,16 @@ declare module 'vona-module-commerce-trade' {
1275
1303
  get $onionName(): 'commerce-trade:cart';
1276
1304
  }
1277
1305
 
1306
+ export interface ServiceE2eFixture {
1307
+ /** @internal */
1308
+ get scope(): ScopeModuleCommerceTrade;
1309
+ }
1310
+
1311
+ export interface ServiceE2eFixture {
1312
+ get $beanFullName(): 'commerce-trade.service.e2eFixture';
1313
+ get $onionName(): 'commerce-trade:e2eFixture';
1314
+ }
1315
+
1278
1316
  export interface ServiceOrder {
1279
1317
  /** @internal */
1280
1318
  get scope(): ScopeModuleCommerceTrade;
@@ -1308,11 +1346,13 @@ declare module 'vona-module-commerce-trade' {
1308
1346
  /** service: end */
1309
1347
  /** service: begin */
1310
1348
  import type { ServiceCart } from '../service/cart.ts';
1349
+ import type { ServiceE2eFixture } from '../service/e2eFixture.ts';
1311
1350
  import type { ServiceOrder } from '../service/order.ts';
1312
1351
  import type { ServiceStockAudit } from '../service/stockAudit.ts';
1313
1352
  import type { ServiceStockBalance } from '../service/stockBalance.ts';
1314
1353
  export interface IModuleService {
1315
1354
  'cart': ServiceCart;
1355
+ 'e2eFixture': ServiceE2eFixture;
1316
1356
  'order': ServiceOrder;
1317
1357
  'stockAudit': ServiceStockAudit;
1318
1358
  'stockBalance': ServiceStockBalance;
@@ -1324,6 +1364,7 @@ import 'vona';
1324
1364
  declare module 'vona' {
1325
1365
  export interface IBeanRecordGeneral {
1326
1366
  'commerce-trade.service.cart': ServiceCart;
1367
+ 'commerce-trade.service.e2eFixture': ServiceE2eFixture;
1327
1368
  'commerce-trade.service.order': ServiceOrder;
1328
1369
  'commerce-trade.service.stockAudit': ServiceStockAudit;
1329
1370
  'commerce-trade.service.stockBalance': ServiceStockBalance;
@@ -0,0 +1,52 @@
1
+ import type { TableIdentity } from 'table-identity';
2
+ import type { IDecoratorControllerOptions } from 'vona-module-a-web';
3
+
4
+ import { BeanBase } from 'vona';
5
+ import { Api, v } from 'vona-module-a-openapiutils';
6
+ import { Arg, Controller, Web } from 'vona-module-a-web';
7
+
8
+ export interface IControllerOptionsE2eFixture extends IDecoratorControllerOptions {}
9
+
10
+ // Not part of the application contract: this exists solely for owned, local E2E data.
11
+ @Controller<IControllerOptionsE2eFixture>({
12
+ path: 'e2eFixture',
13
+ meta: { mode: ['dev', 'test'] },
14
+ })
15
+ @Api.exclude()
16
+ export class ControllerE2eFixture extends BeanBase {
17
+ @Web.post('catalogue')
18
+ async createCatalogue() {
19
+ return await this.scope.service.e2eFixture.createCatalogue();
20
+ }
21
+
22
+ @Web.post('order/:orderId/payment/:attemptId/dispatch')
23
+ async dispatchPayment(
24
+ @Arg.param('orderId', v.tableIdentity()) orderId: TableIdentity,
25
+ @Arg.param('attemptId', v.tableIdentity()) attemptId: TableIdentity,
26
+ ) {
27
+ return await this.scope.service.e2eFixture.dispatchPayment(orderId, attemptId);
28
+ }
29
+
30
+ @Web.post('order/:orderId/refund/:attemptId/dispatch')
31
+ async dispatchRefund(
32
+ @Arg.param('orderId', v.tableIdentity()) orderId: TableIdentity,
33
+ @Arg.param('attemptId', v.tableIdentity()) attemptId: TableIdentity,
34
+ ) {
35
+ return await this.scope.service.e2eFixture.dispatchRefund(orderId, attemptId);
36
+ }
37
+
38
+ @Web.delete('order/:orderId')
39
+ async removeOrder(@Arg.param('orderId', v.tableIdentity()) orderId: TableIdentity) {
40
+ await this.scope.service.e2eFixture.removeOrder(orderId);
41
+ }
42
+
43
+ @Web.delete('customer')
44
+ async removeCustomer() {
45
+ await this.scope.service.e2eFixture.removeCustomer();
46
+ }
47
+
48
+ @Web.delete('catalogue/:categoryId')
49
+ async removeCatalogue(@Arg.param('categoryId', v.tableIdentity()) categoryId: TableIdentity) {
50
+ await this.scope.service.e2eFixture.removeCatalogue(categoryId);
51
+ }
52
+ }