cabloy 5.1.210 → 5.1.211
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/.cabloy-version +1 -1
- package/CHANGELOG.md +7 -0
- package/package.json +2 -1
- package/repo-agent-governance/managed-assets.json +6 -6
- package/repo-agent-governance/skills/cabloy-contract-loop/SKILL.md +14 -8
- package/repo-agent-governance/skills/cabloy-contract-loop/references/verification-checklist.md +4 -1
- package/repo-agent-governance/tests/governance.test.mjs +65 -0
- package/repo-docs/ai/playbook-contract-regeneration.md +2 -0
- package/repo-docs/frontend/openapi-sdk-guide.md +23 -4
- package/repo-docs/fullstack/contract-loop-playbook.md +8 -0
- package/repo-docs/fullstack/openapi-to-sdk.md +12 -6
- package/repo-docs/reference/repo-scripts.md +3 -0
- package/scripts/bootstrapAgentGovernance.mjs +18 -0
- package/scripts/generateOpenapiIsolated.test.ts +207 -0
- package/scripts/generateOpenapiIsolated.ts +91 -0
- package/scripts/runIsolatedTest.test.ts +90 -0
- package/scripts/runIsolatedTest.ts +157 -82
- package/scripts/upgrade.test.ts +113 -0
- package/scripts/upgrade.ts +32 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts +8 -1
- package/vona/packages-vona/vona-core/src/lib/core/testClaim.ts +2 -2
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/openapi.config.test.ts +29 -0
- package/zova/openapi.config.ts +13 -4
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/cli/templates/openapi/config/boilerplate/project/openapi.config.ts +13 -4
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/src/suite/a-training/modules/training-student/src/.metadata/index.ts +7 -7
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/schemas.ts +12 -0
- package/zova/src/suite/a-training/modules/training-student/src/api/openapi/types.ts +173 -72
- package/zova/src/suite/a-training/modules/training-student/src/api/trainingStudent.ts +8 -1
- package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/schemaRefresh.ts +21 -12
- package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefreshLifecycle.test.ts +112 -0
- package/zova/src/suite-vendor/a-zova/package.json +2 -2
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.211
|
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cabloy",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.211",
|
|
4
4
|
"gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
|
|
5
5
|
"description": "A Node.js fullstack framework",
|
|
6
6
|
"keywords": [
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"start:one": "cd vona && npm run start:one",
|
|
48
48
|
"test": "cd vona && npm run test",
|
|
49
49
|
"test:e2e": "node scripts/runIsolatedTest.ts e2e",
|
|
50
|
+
"contract:generate:isolated": "node scripts/generateOpenapiIsolated.ts",
|
|
50
51
|
"test:docs:e2e": "npm run docs:build && playwright test --config repo-e2e/docs/playwright.config.ts",
|
|
51
52
|
"db:reset": "cd vona && npm run db:reset",
|
|
52
53
|
"tsc": "npm run tsc:zova && pnpm --dir vona run tsc",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
{
|
|
54
54
|
"adapter": "codex",
|
|
55
55
|
"category": "skill",
|
|
56
|
-
"sha256": "
|
|
56
|
+
"sha256": "bf8de5cbd1602d00aadaa1c1b91c15f4a2272fb03d88faa9a428d96e4486bbe4",
|
|
57
57
|
"source": "skills/cabloy-contract-loop/references/verification-checklist.md",
|
|
58
58
|
"target": ".agents/skills/cabloy-contract-loop/references/verification-checklist.md"
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
"adapter": "codex",
|
|
62
62
|
"category": "skill",
|
|
63
|
-
"sha256": "
|
|
63
|
+
"sha256": "4aeb0477e3cb316b14bca7c4a3d1e277329be4a8f8e14ef31a3ceb858b6b0698",
|
|
64
64
|
"source": "skills/cabloy-contract-loop/SKILL.md",
|
|
65
65
|
"target": ".agents/skills/cabloy-contract-loop/SKILL.md"
|
|
66
66
|
},
|
|
@@ -389,14 +389,14 @@
|
|
|
389
389
|
{
|
|
390
390
|
"adapter": "claude",
|
|
391
391
|
"category": "skill",
|
|
392
|
-
"sha256": "
|
|
392
|
+
"sha256": "bf8de5cbd1602d00aadaa1c1b91c15f4a2272fb03d88faa9a428d96e4486bbe4",
|
|
393
393
|
"source": "skills/cabloy-contract-loop/references/verification-checklist.md",
|
|
394
394
|
"target": ".claude/skills/cabloy-contract-loop/references/verification-checklist.md"
|
|
395
395
|
},
|
|
396
396
|
{
|
|
397
397
|
"adapter": "claude",
|
|
398
398
|
"category": "skill",
|
|
399
|
-
"sha256": "
|
|
399
|
+
"sha256": "4aeb0477e3cb316b14bca7c4a3d1e277329be4a8f8e14ef31a3ceb858b6b0698",
|
|
400
400
|
"source": "skills/cabloy-contract-loop/SKILL.md",
|
|
401
401
|
"target": ".claude/skills/cabloy-contract-loop/SKILL.md"
|
|
402
402
|
},
|
|
@@ -739,14 +739,14 @@
|
|
|
739
739
|
{
|
|
740
740
|
"adapter": "cursor",
|
|
741
741
|
"category": "skill",
|
|
742
|
-
"sha256": "
|
|
742
|
+
"sha256": "bf8de5cbd1602d00aadaa1c1b91c15f4a2272fb03d88faa9a428d96e4486bbe4",
|
|
743
743
|
"source": "skills/cabloy-contract-loop/references/verification-checklist.md",
|
|
744
744
|
"target": ".cursor/skills/cabloy-contract-loop/references/verification-checklist.md"
|
|
745
745
|
},
|
|
746
746
|
{
|
|
747
747
|
"adapter": "cursor",
|
|
748
748
|
"category": "skill",
|
|
749
|
-
"sha256": "
|
|
749
|
+
"sha256": "4aeb0477e3cb316b14bca7c4a3d1e277329be4a8f8e14ef31a3ceb858b6b0698",
|
|
750
750
|
"source": "skills/cabloy-contract-loop/SKILL.md",
|
|
751
751
|
"target": ".cursor/skills/cabloy-contract-loop/SKILL.md"
|
|
752
752
|
},
|
|
@@ -206,7 +206,7 @@ That may include:
|
|
|
206
206
|
- confirming DTO and validation alignment
|
|
207
207
|
- checking Swagger/OpenAPI output
|
|
208
208
|
- confirming that the changed endpoint or schema now reflects the intended contract
|
|
209
|
-
-
|
|
209
|
+
- for AI-driven local OpenAPI generation, using the managed isolated service by default as described in Path A below; do not make a separately running development service a prerequisite
|
|
210
210
|
|
|
211
211
|
If the backend contract output is wrong, frontend regeneration will only spread the mistake.
|
|
212
212
|
|
|
@@ -218,17 +218,23 @@ Once the backend contract is correct, decide how the frontend should consume it.
|
|
|
218
218
|
|
|
219
219
|
Use this when the frontend consumes generated API contracts.
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
For AI development, **default to managed isolated generation**, even when a development Swagger service is currently reachable. An independently managed development service may stop at any time; owning the service lifecycle keeps the generation task continuous and its contract source consistent.
|
|
222
222
|
|
|
223
|
-
|
|
224
|
-
- `npm run zova :openapi:generate ...`
|
|
223
|
+
1. Detect the edition and verify the isolated entrypoint in the active root `package.json`. Cabloy Basic provides:
|
|
225
224
|
|
|
226
|
-
|
|
225
|
+
```bash
|
|
226
|
+
npm run contract:generate:isolated -- <module-name>
|
|
227
|
+
```
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
- first start the backend service, typically with `npm run dev`, and confirm `http://localhost:7102/swagger/json?version=V31` is reachable
|
|
229
|
+
For Cabloy Start or another version, verify the actual command and configuration support rather than assuming the Basic entrypoint exists.
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
2. Initialize module configuration with `npm run zova :openapi:config <module-name>` when needed. Keep module ownership constrained with `operations.match` unless the module intentionally owns a broad API surface; the isolated command reuses the existing generator and ownership checks.
|
|
232
|
+
3. Confirm the effective source uses the isolated service. The command supplies a child-scoped `OPENAPI_SOURCE_URL` to the project default source resolver; explicit module sources or project `modules` overrides retain their precedence. Inspect the project resolver and align conflicting sources deliberately before claiming isolated generation. Do not edit `.env` files or SDK runtime API targets to inject the temporary port.
|
|
233
|
+
4. Coordinate shared output writes. The service and claim-backed test resources are isolated, but generated SDK/schema/metadata files remain shared workspace output: serialize generation and do not assume automatic rollback. Successful runs clean their owned resources after shutdown; failures retain evidence and must remain visible, not trigger a silent fallback to development Swagger.
|
|
234
|
+
|
|
235
|
+
Use `npm run zova :openapi:generate <module-name>` only as a deliberate exception, such as an explicit user request or a contract source that must come from a specific external or development environment. Record the reason and verify the configured source and its availability; current reachability alone is not a reason to override the AI default. If isolated support is absent, report that limitation and establish an explicit alternative rather than silently relying on a development service.
|
|
236
|
+
|
|
237
|
+
See [OpenAPI SDK Guide](../../../repo-docs/frontend/openapi-sdk-guide.md) for source precedence and configuration compatibility, and [Contract Loop Playbook](../../../repo-docs/fullstack/contract-loop-playbook.md#forward-chain-playbook) for the full forward-chain workflow.
|
|
232
238
|
|
|
233
239
|
### Path B: REST/type generation by flavor
|
|
234
240
|
|
package/repo-agent-governance/skills/cabloy-contract-loop/references/verification-checklist.md
CHANGED
|
@@ -15,7 +15,10 @@ After a contract-loop change, verify the branch that actually applies.
|
|
|
15
15
|
- DTO and validation align
|
|
16
16
|
- OpenAPI output reflects the intended shape
|
|
17
17
|
- module ownership is constrained
|
|
18
|
-
-
|
|
18
|
+
- AI-driven SDK generation used the source-confirmed isolated entrypoint by default, regardless of development-service reachability; any direct-source exception has an explicit reason
|
|
19
|
+
- the effective project/module source resolves to the intended contract source; explicit source overrides did not silently bypass isolation
|
|
20
|
+
- regeneration commands completed successfully; isolated failures remained visible rather than falling back silently to development Swagger
|
|
21
|
+
- isolated resources were cleaned after success or retained as failure evidence, and shared SDK/schema/metadata writes were coordinated and inspected without assuming automatic rollback
|
|
19
22
|
- generated SDK or schema outputs are updated
|
|
20
23
|
- thin model facades and downstream consumers still align with the regenerated contract
|
|
21
24
|
- `npm run tsc`
|
|
@@ -211,6 +211,71 @@ test('initializer preserves a legacy-customized adapter during a staged upgrade'
|
|
|
211
211
|
});
|
|
212
212
|
});
|
|
213
213
|
|
|
214
|
+
for (const scenario of [
|
|
215
|
+
{ name: 'adopts a missing command', incoming: true },
|
|
216
|
+
{
|
|
217
|
+
name: 'leaves a matching command byte-identical',
|
|
218
|
+
incoming: true,
|
|
219
|
+
command: 'node scripts/generateOpenapiIsolated.ts',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'reports and preserves a customized command',
|
|
223
|
+
incoming: true,
|
|
224
|
+
command: 'node project-contract.mjs',
|
|
225
|
+
},
|
|
226
|
+
{ name: 'reports and preserves an empty customized command', incoming: true, command: '' },
|
|
227
|
+
{ name: 'tolerates an older staged package without the command', incoming: false },
|
|
228
|
+
]) {
|
|
229
|
+
test(`staged initializer isolated contract bridge ${scenario.name}`, () => {
|
|
230
|
+
withFixture(root => {
|
|
231
|
+
const name = 'contract:generate:isolated';
|
|
232
|
+
const incomingCommand = 'node scripts/generateOpenapiIsolated.ts';
|
|
233
|
+
const fixtureBootstrap = resolve(root, 'scripts/bootstrapAgentGovernance.mjs');
|
|
234
|
+
mkdirSync(dirname(fixtureBootstrap));
|
|
235
|
+
cpSync(resolve(ROOT_DIR, 'scripts/bootstrapAgentGovernance.mjs'), fixtureBootstrap);
|
|
236
|
+
|
|
237
|
+
const upgradeRoot = resolve(root, 'node_modules/.cabloy-upgrade');
|
|
238
|
+
cpSync(GOVERNANCE_DIR, resolve(upgradeRoot, 'repo-agent-governance'), { recursive: true });
|
|
239
|
+
const sourcePackage = JSON.parse(readFileSync(resolve(ROOT_DIR, 'package.json'), 'utf8'));
|
|
240
|
+
delete sourcePackage.scripts[name];
|
|
241
|
+
const projectPackage = structuredClone(sourcePackage);
|
|
242
|
+
projectPackage.scripts['project:custom'] = 'node project-owned.mjs';
|
|
243
|
+
if (Object.hasOwn(scenario, 'command')) projectPackage.scripts[name] = scenario.command;
|
|
244
|
+
if (scenario.incoming) sourcePackage.scripts[name] = incomingCommand;
|
|
245
|
+
writeFileSync(resolve(upgradeRoot, 'package.json'), JSON.stringify(sourcePackage));
|
|
246
|
+
const projectPackagePath = resolve(root, 'package.json');
|
|
247
|
+
const before = `${JSON.stringify(projectPackage)}\n`;
|
|
248
|
+
writeFileSync(projectPackagePath, before);
|
|
249
|
+
|
|
250
|
+
const result = runNode(root, fixtureBootstrap);
|
|
251
|
+
assert.equal(result.status, 0, result.output);
|
|
252
|
+
const customized = Object.hasOwn(scenario, 'command') && scenario.command !== incomingCommand;
|
|
253
|
+
const adopted = scenario.incoming && !Object.hasOwn(scenario, 'command');
|
|
254
|
+
const updated = JSON.parse(readFileSync(projectPackagePath, 'utf8'));
|
|
255
|
+
if (adopted) {
|
|
256
|
+
assert.match(result.output, /Added missing.*contract:generate:isolated/);
|
|
257
|
+
assert.deepEqual(updated, {
|
|
258
|
+
...projectPackage,
|
|
259
|
+
scripts: { ...projectPackage.scripts, [name]: incomingCommand },
|
|
260
|
+
});
|
|
261
|
+
} else {
|
|
262
|
+
assert.equal(readFileSync(projectPackagePath, 'utf8'), before);
|
|
263
|
+
}
|
|
264
|
+
if (customized) {
|
|
265
|
+
assert.match(result.output, /Preserved customized.*contract:generate:isolated/);
|
|
266
|
+
assert.ok(result.output.includes(incomingCommand), result.output);
|
|
267
|
+
} else {
|
|
268
|
+
assert.doesNotMatch(result.output, /Preserved customized.*contract:generate:isolated/);
|
|
269
|
+
}
|
|
270
|
+
const first = readFileSync(projectPackagePath, 'utf8');
|
|
271
|
+
const second = runNode(root, fixtureBootstrap);
|
|
272
|
+
assert.equal(second.status, 0, second.output);
|
|
273
|
+
assert.equal(readFileSync(projectPackagePath, 'utf8'), first);
|
|
274
|
+
assert.doesNotMatch(second.output, /Added missing.*contract:generate:isolated/);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
214
279
|
test('governance adoption adopts byte-identical legacy outputs', () => {
|
|
215
280
|
withFixture(root => {
|
|
216
281
|
assert.equal(runGovernance(root, 'render').status, 0);
|
|
@@ -54,6 +54,8 @@ Before regenerating frontend artifacts:
|
|
|
54
54
|
|
|
55
55
|
Use the Zova OpenAPI and REST-generation path rather than manually updating request code first. Follow the canonical [Contract Loop Playbook](/fullstack/contract-loop-playbook#forward-chain-playbook) for the active edition’s complete generation and verification sequence.
|
|
56
56
|
|
|
57
|
+
In Cabloy Basic, if no development Swagger service should be started or reused, use `npm run contract:generate:isolated -- <module-name>`. It verifies a fresh test-mode Vona service's signed readiness response and invokes the existing Zova generator against its Swagger source; generated SDK output remains shared workspace output and concurrent generation must be serialized.
|
|
58
|
+
|
|
57
59
|
Do not treat `build:rest:*` as a standalone current command recipe. Resolve the active edition’s full build path from the canonical playbook and current root scripts so SSR bundle and REST output stay synchronized.
|
|
58
60
|
|
|
59
61
|
For Start, resolve Start-specific flavor paths from that repository.
|
|
@@ -59,20 +59,39 @@ Representative use case:
|
|
|
59
59
|
|
|
60
60
|
Example: generate OpenAPI-based frontend services for module `training-student`.
|
|
61
61
|
|
|
62
|
+
The direct command is the fast path when a development Swagger service is already available:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm run zova :openapi:generate training-student
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
In Cabloy Basic, when generation should use a fresh isolated test-mode Vona service instead:
|
|
69
|
+
|
|
62
70
|
```bash
|
|
71
|
+
npm run contract:generate:isolated -- training-student
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The isolated command uses the same generator and preserves module ownership validation, but manages a disposable service, claim, and test resources without starting Playwright. Generated files under `zova/src/**` are shared workspace output, not run-owned temporary files; serialize concurrent SDK generation and do not expect automatic rollback.
|
|
75
|
+
|
|
76
|
+
The project-level source resolver accepts `OPENAPI_SOURCE_URL` as a complete OpenAPI document URL. It takes precedence over `API_BASE_URL` for generation only and does not change runtime `OPENAPI_BASE_URL_*` request targets:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
OPENAPI_SOURCE_URL=https://example.test/swagger/json?version=V31 \
|
|
63
80
|
npm run zova :openapi:generate training-student
|
|
64
81
|
```
|
|
65
82
|
|
|
83
|
+
The override changes the project default source. Explicit module sources in module config or project `modules` config retain their existing precedence; remove or align those overrides when the module must use the isolated service. Do not commit source URLs containing credentials or access tokens. Existing project `openapi.config.ts` files are not overwritten by `:openapi:config`; merge the resolver when upgrading an older project manually.
|
|
84
|
+
|
|
66
85
|
## Build the rest-contract output
|
|
67
86
|
|
|
68
|
-
|
|
87
|
+
SDK generation and flavor builds are separate steps. When refreshed frontend output must be handed back to Vona, use the paired SSR/REST build for each affected flavor, then synchronize dependencies. For Cabloy Basic Admin:
|
|
69
88
|
|
|
70
89
|
```bash
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
npm run build:zova:admin
|
|
91
|
+
npm run deps:vona
|
|
73
92
|
```
|
|
74
93
|
|
|
75
|
-
|
|
94
|
+
Also run `npm run build:zova:web` before dependency synchronization when Web is affected. Do not use `build:rest:*` alone for this reverse handoff; the SSR bundle and REST output must move together. See [Contract Loop Playbook](/fullstack/contract-loop-playbook#reverse-chain-playbook).
|
|
76
95
|
|
|
77
96
|
## Important convention
|
|
78
97
|
|
|
@@ -221,6 +221,14 @@ npm run zova :openapi:config <module-name>
|
|
|
221
221
|
npm run zova :openapi:generate <module-name>
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
+
The direct command is the fast path when a development Swagger service is already available. In Cabloy Basic, when the workflow should own a fresh test-mode service instead, use:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
npm run contract:generate:isolated -- <module-name>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
This isolated path reuses the existing Zova generator and module ownership checks, but starts Vona in an isolated test run without Playwright. Its claim-backed test resources are cleaned after success and retained on failure; generated SDK files remain shared workspace output, so serialize concurrent generation and do not assume rollback.
|
|
231
|
+
|
|
224
232
|
When the target is a module-local SDK, keep ownership constrained with `operations.match` so the module only generates the API surface it actually owns.
|
|
225
233
|
|
|
226
234
|
### Stage 4: Thin frontend follow-up
|
|
@@ -135,18 +135,24 @@ Representative frontend-side contract-generation commands include:
|
|
|
135
135
|
```bash
|
|
136
136
|
npm run zova :openapi:config training-student
|
|
137
137
|
npm run zova :openapi:generate training-student
|
|
138
|
-
cd zova && npm run build:rest:cabloyBasicAdmin
|
|
139
|
-
cd zova && npm run build:rest:cabloyBasicWeb
|
|
140
138
|
```
|
|
141
139
|
|
|
140
|
+
When no development service should be started or reused, use the isolated contract path instead:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm run contract:generate:isolated -- training-student
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This starts a disposable test-mode Vona service, verifies its signed readiness response and OpenAPI document, runs the same Zova generator, and cleans its claim-backed test resources after success. The generated SDK output is still shared workspace output; serialize concurrent generation and inspect or repair it explicitly rather than expecting rollback.
|
|
147
|
+
|
|
142
148
|
A practical forward-chain sequence is:
|
|
143
149
|
|
|
144
150
|
1. author or change the backend contract
|
|
145
151
|
2. emit or inspect backend OpenAPI output
|
|
146
|
-
3.
|
|
152
|
+
3. choose the source lifecycle: reuse a reachable development Swagger service for the direct command (`npm run dev` normally exposes `http://localhost:7102/swagger/json?version=V31`), or let the isolated command own a fresh test-mode service
|
|
147
153
|
4. configure frontend module ownership if needed
|
|
148
|
-
5. generate module-level OpenAPI SDK output
|
|
149
|
-
6. run the
|
|
154
|
+
5. generate module-level OpenAPI SDK output with the selected command
|
|
155
|
+
6. when frontend output must be handed back to Vona, run the paired flavor build (`npm run build:zova:admin`, plus `npm run build:zova:web` when affected), then `npm run deps:vona`; REST-only builds are not sufficient for this handoff
|
|
150
156
|
7. consume the generated contract from frontend code instead of re-declaring it manually
|
|
151
157
|
8. keep frontend follow-up thin by wrapping generated consumers with semantic facades instead of re-declaring the contract
|
|
152
158
|
9. when the custom API still belongs to an existing resource, reuse the existing resource-owner instead of creating a competing cache owner
|
|
@@ -162,7 +168,7 @@ If both `operations.match` and `operations.ignore` are empty, frontend SDK gener
|
|
|
162
168
|
A practical regeneration rule is:
|
|
163
169
|
|
|
164
170
|
- if the backend contract changed, prefer regenerating the SDK/rest layer before hand-editing frontend request code
|
|
165
|
-
- if `npm run zova :openapi:generate ...` fails because the local Swagger source is unavailable,
|
|
171
|
+
- if `npm run zova :openapi:generate ...` fails because the local Swagger source is unavailable, start the backend service and confirm the configured source is reachable, or use `npm run contract:generate:isolated -- <module-name>` instead
|
|
166
172
|
- if the generated consumer path is already correct, but frontend behavior still looks stale, stop patching generated files and diagnose consumer drift or local dependency drift instead
|
|
167
173
|
|
|
168
174
|
## `training-student` as a compact forward-chain specimen
|
|
@@ -41,6 +41,7 @@ Cabloy Basic exposes these shared root scripts:
|
|
|
41
41
|
- `npm run start:one`
|
|
42
42
|
- `npm run test`
|
|
43
43
|
- `npm run test:e2e`
|
|
44
|
+
- `npm run contract:generate:isolated`
|
|
44
45
|
- `npm run db:reset`
|
|
45
46
|
- `npm run tsc`
|
|
46
47
|
- `npm run docs:dev`
|
|
@@ -79,6 +80,8 @@ Judge preflight conflicts by actual resource overlap, not process names:
|
|
|
79
80
|
|
|
80
81
|
A process-list match or a free port alone is not proof of either a conflict or safe ownership. Use the managed root entrypoints and retain normal execution-authorization checks; isolation is not a blanket reset or cleanup authorization.
|
|
81
82
|
|
|
83
|
+
`npm run contract:generate:isolated -- <module-name>` starts a fresh test-mode Vona service on a run-owned loopback port, verifies its signed readiness response, fetches the Swagger document, and invokes the existing Zova OpenAPI generator without Playwright. The run owns its disposable test resources and claim-backed local roots; successful runs clean them after the service drains, while failures retain the claim and evidence for inspection. The generated SDK files under `zova/src/**` remain shared checkout output, so serialize concurrent generation for the same workspace/module and do not treat this command as an automatic rollback boundary.
|
|
84
|
+
|
|
82
85
|
For Vona integrated SSR, the allocated E2E origin is supplied to Zova at runtime as server `SSR_API_BASE_URL` and browser `API_BASE_URL`. Do not edit Zova `.env` files or rebuild solely to match the test port. The current root `npm run test:e2e` consumes prebuilt frontend artifacts; refresh affected SSR/REST output and synchronize dependencies when their inputs change. See [Runtime API origins in Vona integrated SSR](/fullstack/ssr-entry-modes#runtime-api-origins-in-vona-integrated-ssr) for configuration overrides and scope.
|
|
83
86
|
|
|
84
87
|
For deliberate **dev-mode** database preparation, `npm run db:reset` is a separate, destructive command: in a single process, it clears validated application-owned Redis keys, drops and recreates managed disposable development databases, reruns migrations and instance initialization, then runs module `seed()` hooks. It requires `database.testDatabase: true` in the dev configuration and a connection outside the disposable database. Ordinary `npm run dev` and `npm run dev:one` only reuse existing managed databases; if any required target is missing, startup fails promptly with an instruction to run `npm run db:reset`. They do not create databases, perform creation-time Redis cleanup, or seed. Never run `db:reset` against development data you need to retain. Upgrades do not reset existing development data or repeat its seeds. See [Development database preparation](#development-database-preparation) and [Migration and Changes](/backend/migration-and-changes#local-development-workflow).
|
|
@@ -35,6 +35,24 @@ function reconcileGovernancePackageJson(sourceRoot) {
|
|
|
35
35
|
projectPackage.scripts[name] = command;
|
|
36
36
|
changed = true;
|
|
37
37
|
}
|
|
38
|
+
// An older upgrader is already running when it installs this bootstrap, so adopt
|
|
39
|
+
// the new root command here too without replacing project-owned customizations.
|
|
40
|
+
const contractScriptName = 'contract:generate:isolated';
|
|
41
|
+
const contractCommand = sourcePackage.scripts?.[contractScriptName];
|
|
42
|
+
if (contractCommand) {
|
|
43
|
+
if (!Object.hasOwn(projectPackage.scripts ?? {}, contractScriptName)) {
|
|
44
|
+
projectPackage.scripts ??= {};
|
|
45
|
+
projectPackage.scripts[contractScriptName] = contractCommand;
|
|
46
|
+
changed = true;
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.log(`[init] Added missing package.json scripts.${contractScriptName}`);
|
|
49
|
+
} else if (projectPackage.scripts[contractScriptName] !== contractCommand) {
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.log(
|
|
52
|
+
`[init] Preserved customized package.json scripts.${contractScriptName}; incoming command: ${contractCommand}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
38
56
|
if (changed) {
|
|
39
57
|
writeFileSync(projectPackagePath, `${JSON.stringify(projectPackage, null, 2)}\n`);
|
|
40
58
|
// eslint-disable-next-line no-console
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
|
|
6
|
+
import type { GuardianResult, OwnedProcess } from './runIsolatedTest.ts';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getOpenapiGeneratorCommand,
|
|
10
|
+
getOpenapiSource,
|
|
11
|
+
validateOpenapiSource,
|
|
12
|
+
} from './generateOpenapiIsolated.ts';
|
|
13
|
+
import { getIsolatedVonaCommand, ROOT_DIR, withIsolatedVona } from './runIsolatedTest.ts';
|
|
14
|
+
|
|
15
|
+
test('builds the isolated Swagger source from the allocated port', () => {
|
|
16
|
+
assert.equal(getOpenapiSource(7123), 'http://127.0.0.1:7123/swagger/json?version=V31');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('forwards OpenAPI module arguments to the existing Zova generator', () => {
|
|
20
|
+
assert.deepEqual(getOpenapiGeneratorCommand(['training-student', '--nometadata']), {
|
|
21
|
+
command: process.execPath,
|
|
22
|
+
args: [
|
|
23
|
+
'./zova/packages-cli/cli/src/bin/zova.ts',
|
|
24
|
+
'--projectPath=zova',
|
|
25
|
+
':openapi:generate',
|
|
26
|
+
'training-student',
|
|
27
|
+
'--nometadata',
|
|
28
|
+
],
|
|
29
|
+
cwd: ROOT_DIR,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('rejects project overrides before provisioning isolated resources', () => {
|
|
34
|
+
for (const args of [['--projectPath=other'], ['--projectPath', 'other']]) {
|
|
35
|
+
assert.throws(() => getOpenapiGeneratorCommand(args), /projectPath overrides are unsupported/);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('launches only a loopback single-worker test-mode Vona service', () => {
|
|
40
|
+
const command = getIsolatedVonaCommand(7123, { CUSTOM: 'keep' }, 'test-key');
|
|
41
|
+
assert.equal(command.command, process.execPath);
|
|
42
|
+
assert.deepEqual(command.args, [
|
|
43
|
+
'./packages-cli/cli/src/bin/vona.ts',
|
|
44
|
+
':bin:dev',
|
|
45
|
+
'--workers=1',
|
|
46
|
+
'--flavor=normal',
|
|
47
|
+
'--mode=test',
|
|
48
|
+
]);
|
|
49
|
+
assert.equal(command.cwd, `${ROOT_DIR}/vona`);
|
|
50
|
+
assert.deepEqual(command.env, {
|
|
51
|
+
CUSTOM: 'keep',
|
|
52
|
+
VONA_TEST_READY_KEY: 'test-key',
|
|
53
|
+
VONA_TEST_SERVER_ORIGIN: 'true',
|
|
54
|
+
SERVER_SERVE_PROTOCOL: 'http',
|
|
55
|
+
SERVER_SERVE_HOST: '127.0.0.1:7123',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function lifecycleFixture(options: { readyFailure?: boolean; stopFailure?: boolean } = {}) {
|
|
60
|
+
const events: string[] = [];
|
|
61
|
+
let finish!: (result: GuardianResult) => void;
|
|
62
|
+
const closed = new Promise<GuardianResult>(resolve => {
|
|
63
|
+
finish = resolve;
|
|
64
|
+
});
|
|
65
|
+
const result: GuardianResult = {
|
|
66
|
+
type: 'drained',
|
|
67
|
+
failed: false,
|
|
68
|
+
result: { code: 0, signal: null },
|
|
69
|
+
};
|
|
70
|
+
const server: OwnedProcess = {
|
|
71
|
+
wait: () => closed,
|
|
72
|
+
stop: async () => {
|
|
73
|
+
events.push('stop');
|
|
74
|
+
if (options.stopFailure) throw new Error('shutdown failed');
|
|
75
|
+
finish(result);
|
|
76
|
+
return result;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
events,
|
|
81
|
+
finish,
|
|
82
|
+
result,
|
|
83
|
+
dependencies: {
|
|
84
|
+
start: async () => {
|
|
85
|
+
events.push('start');
|
|
86
|
+
return server;
|
|
87
|
+
},
|
|
88
|
+
ready: async () => {
|
|
89
|
+
events.push('ready');
|
|
90
|
+
if (options.readyFailure) throw new Error('readiness failed');
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
test('stops Vona after generation succeeds or fails and removes signal handlers', async () => {
|
|
97
|
+
for (const fail of [false, true]) {
|
|
98
|
+
const f = lifecycleFixture();
|
|
99
|
+
const listeners = process.listenerCount('SIGINT');
|
|
100
|
+
const run = withIsolatedVona(
|
|
101
|
+
7123,
|
|
102
|
+
{},
|
|
103
|
+
async () => {
|
|
104
|
+
f.events.push('generate');
|
|
105
|
+
if (fail) throw new Error('generator failed');
|
|
106
|
+
return 'generated';
|
|
107
|
+
},
|
|
108
|
+
f.dependencies,
|
|
109
|
+
);
|
|
110
|
+
if (fail) await assert.rejects(run, /generator failed/);
|
|
111
|
+
else assert.equal(await run, 'generated');
|
|
112
|
+
assert.deepEqual(f.events, ['start', 'ready', 'generate', 'stop']);
|
|
113
|
+
assert.equal(process.listenerCount('SIGINT'), listeners);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('readiness failure stops Vona without invoking the generator', async () => {
|
|
118
|
+
const f = lifecycleFixture({ readyFailure: true });
|
|
119
|
+
await assert.rejects(
|
|
120
|
+
withIsolatedVona(
|
|
121
|
+
7123,
|
|
122
|
+
{},
|
|
123
|
+
async () => {
|
|
124
|
+
f.events.push('generate');
|
|
125
|
+
},
|
|
126
|
+
f.dependencies,
|
|
127
|
+
),
|
|
128
|
+
/readiness failed/,
|
|
129
|
+
);
|
|
130
|
+
assert.deepEqual(f.events, ['start', 'ready', 'stop']);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('preserves both generation and shutdown errors', async () => {
|
|
134
|
+
const f = lifecycleFixture({ stopFailure: true });
|
|
135
|
+
await assert.rejects(
|
|
136
|
+
withIsolatedVona(
|
|
137
|
+
7123,
|
|
138
|
+
{},
|
|
139
|
+
async () => {
|
|
140
|
+
throw new Error('generator failed');
|
|
141
|
+
},
|
|
142
|
+
f.dependencies,
|
|
143
|
+
),
|
|
144
|
+
(error: unknown) => {
|
|
145
|
+
assert.ok(error instanceof AggregateError);
|
|
146
|
+
assert.deepEqual(
|
|
147
|
+
error.errors.map(item => item.message),
|
|
148
|
+
['generator failed', 'shutdown failed'],
|
|
149
|
+
);
|
|
150
|
+
return true;
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('an interrupted run or an early server exit cannot succeed', async () => {
|
|
156
|
+
for (const interrupt of [false, true]) {
|
|
157
|
+
const f = lifecycleFixture();
|
|
158
|
+
await assert.rejects(
|
|
159
|
+
withIsolatedVona(
|
|
160
|
+
7123,
|
|
161
|
+
{},
|
|
162
|
+
async () => {
|
|
163
|
+
if (interrupt) {
|
|
164
|
+
process.emit('SIGINT');
|
|
165
|
+
} else {
|
|
166
|
+
f.finish(f.result);
|
|
167
|
+
await Promise.resolve();
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
f.dependencies,
|
|
171
|
+
),
|
|
172
|
+
/exited early, or run was interrupted/,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('preflight distinguishes HTTP, JSON and OpenAPI shape failures', async () => {
|
|
178
|
+
let status = 200;
|
|
179
|
+
let body = JSON.stringify({ openapi: '3.1.0', info: {}, paths: {} });
|
|
180
|
+
const server = createServer((_req, res) => {
|
|
181
|
+
res.writeHead(status, status === 302 ? { location: '/redirect-target' } : {});
|
|
182
|
+
res.end(body);
|
|
183
|
+
});
|
|
184
|
+
server.listen(0, '127.0.0.1');
|
|
185
|
+
await once(server, 'listening');
|
|
186
|
+
const address = server.address();
|
|
187
|
+
assert.ok(address && typeof address !== 'string');
|
|
188
|
+
const source = getOpenapiSource(address.port);
|
|
189
|
+
try {
|
|
190
|
+
await validateOpenapiSource(source);
|
|
191
|
+
status = 302;
|
|
192
|
+
await assert.rejects(validateOpenapiSource(source), /source request failed/);
|
|
193
|
+
status = 503;
|
|
194
|
+
await assert.rejects(validateOpenapiSource(source), /HTTP 503/);
|
|
195
|
+
status = 200;
|
|
196
|
+
body = 'not-json';
|
|
197
|
+
await assert.rejects(validateOpenapiSource(source), /invalid JSON/);
|
|
198
|
+
for (const value of [null, [], {}, { openapi: '3.1.0', paths: [] }]) {
|
|
199
|
+
body = JSON.stringify(value);
|
|
200
|
+
await assert.rejects(validateOpenapiSource(source), /invalid document/);
|
|
201
|
+
}
|
|
202
|
+
} finally {
|
|
203
|
+
await new Promise<void>((resolve, reject) =>
|
|
204
|
+
server.close(error => (error ? reject(error) : resolve())),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
});
|