@united-workforce/cli 0.4.0 → 0.5.0
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 +30 -3
- package/dist/.build-fingerprint +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +16 -6
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- package/dist/__tests__/concurrency.test.d.ts +2 -0
- package/dist/__tests__/concurrency.test.d.ts.map +1 -0
- package/dist/__tests__/concurrency.test.js +196 -0
- package/dist/__tests__/concurrency.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +23 -7
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/format-text-default.test.d.ts +2 -0
- package/dist/__tests__/format-text-default.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-default.test.js +43 -0
- package/dist/__tests__/format-text-default.test.js.map +1 -0
- package/dist/__tests__/format-text-registry.test.d.ts +2 -0
- package/dist/__tests__/format-text-registry.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-registry.test.js +158 -0
- package/dist/__tests__/format-text-registry.test.js.map +1 -0
- package/dist/__tests__/log-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/log-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/log-text-renderer.test.js +265 -0
- package/dist/__tests__/log-text-renderer.test.js.map +1 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js +102 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js +22 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js.map +1 -0
- package/dist/__tests__/pid-recycling.test.js +9 -7
- package/dist/__tests__/pid-recycling.test.js.map +1 -1
- package/dist/__tests__/prompt.test.js +46 -4
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +8 -0
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/solve-issue-tea-worktree.test.js +3 -1
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.js +9 -1
- package/dist/__tests__/step-ask.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +19 -17
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +19 -13
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js +110 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +10 -8
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts +2 -0
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js +102 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts +2 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js +157 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js.map +1 -0
- package/dist/__tests__/thread-poke.test.js +11 -1
- package/dist/__tests__/thread-poke.test.js.map +1 -1
- package/dist/__tests__/thread-read-xml-tags.test.js +10 -9
- package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +11 -1
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-start-cwd-cli.test.js +15 -3
- package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js +148 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.js +5 -2
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread-test-helpers.d.ts +7 -0
- package/dist/__tests__/thread-test-helpers.d.ts.map +1 -1
- package/dist/__tests__/thread-test-helpers.js +13 -0
- package/dist/__tests__/thread-test-helpers.js.map +1 -1
- package/dist/__tests__/thread.test.js +11 -9
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +56 -2
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.js +10 -7
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -1
- package/dist/__tests__/workflow-resolution.test.js +10 -7
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.js +10 -7
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-validate.test.js +75 -55
- package/dist/__tests__/workflow-validate.test.js.map +1 -1
- package/dist/__tests__/write-envelope.test.d.ts +2 -0
- package/dist/__tests__/write-envelope.test.d.ts.map +1 -0
- package/dist/__tests__/write-envelope.test.js +201 -0
- package/dist/__tests__/write-envelope.test.js.map +1 -0
- package/dist/cli.js +58 -35
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +12 -0
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +42 -29
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +9 -4
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +51 -7
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +44 -2
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +1 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +2 -6
- package/dist/commands/workflow.js.map +1 -1
- package/dist/concurrency/concurrency.d.ts +34 -0
- package/dist/concurrency/concurrency.d.ts.map +1 -0
- package/dist/concurrency/concurrency.js +216 -0
- package/dist/concurrency/concurrency.js.map +1 -0
- package/dist/concurrency/index.d.ts +3 -0
- package/dist/concurrency/index.d.ts.map +1 -0
- package/dist/concurrency/index.js +2 -0
- package/dist/concurrency/index.js.map +1 -0
- package/dist/concurrency/types.d.ts +19 -0
- package/dist/concurrency/types.d.ts.map +1 -0
- package/dist/concurrency/types.js +2 -0
- package/dist/concurrency/types.js.map +1 -0
- package/dist/format.d.ts +69 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +198 -1
- package/dist/format.js.map +1 -1
- package/dist/output-mappers.d.ts +122 -0
- package/dist/output-mappers.d.ts.map +1 -0
- package/dist/output-mappers.js +134 -0
- package/dist/output-mappers.js.map +1 -0
- package/dist/schemas.d.ts +4 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +31 -4
- package/dist/schemas.js.map +1 -1
- package/dist/text-renderers.d.ts +30 -0
- package/dist/text-renderers.d.ts.map +1 -0
- package/dist/text-renderers.js +251 -0
- package/dist/text-renderers.js.map +1 -0
- package/dist/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +28 -11
- package/dist/validate-semantic.js.map +1 -1
- package/examples/brainstorm.yaml +130 -0
- package/examples/debate.yaml +169 -0
- package/examples/socratic-questioning.yaml +112 -0
- package/package.json +5 -4
- package/src/__tests__/adapter-json-roundtrip.test.ts +15 -6
- package/src/__tests__/concurrency.test.ts +266 -0
- package/src/__tests__/e2e-mock-agent.test.ts +45 -7
- package/src/__tests__/format-text-default.test.ts +49 -0
- package/src/__tests__/format-text-registry.test.ts +173 -0
- package/src/__tests__/log-text-renderer.test.ts +294 -0
- package/src/__tests__/output-mapper-thread-list-startedat.test.ts +124 -0
- package/src/__tests__/output-mapper-workflow-add.test.ts +24 -0
- package/src/__tests__/pid-recycling.test.ts +9 -8
- package/src/__tests__/prompt.test.ts +48 -4
- package/src/__tests__/resolve-head-hash.test.ts +7 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +3 -1
- package/src/__tests__/step-ask.test.ts +8 -1
- package/src/__tests__/store-unified-threads.test.ts +21 -18
- package/src/__tests__/thread-cancel-status.test.ts +21 -14
- package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
- package/src/__tests__/thread-list-filters.test.ts +9 -9
- package/src/__tests__/thread-list-template-ms-date.test.ts +110 -0
- package/src/__tests__/thread-list-workflow-corrupt.test.ts +198 -0
- package/src/__tests__/thread-poke.test.ts +10 -1
- package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
- package/src/__tests__/thread-resume.test.ts +10 -1
- package/src/__tests__/thread-start-cwd-cli.test.ts +15 -3
- package/src/__tests__/thread-stop-text-renderer.test.ts +168 -0
- package/src/__tests__/thread-suspend-step.test.ts +5 -2
- package/src/__tests__/thread-test-helpers.ts +15 -1
- package/src/__tests__/thread.test.ts +10 -10
- package/src/__tests__/validate-semantic.test.ts +59 -2
- package/src/__tests__/workflow-list-recursive.test.ts +9 -9
- package/src/__tests__/workflow-resolution.test.ts +9 -8
- package/src/__tests__/workflow-show-resolution.test.ts +9 -8
- package/src/__tests__/workflow-validate.test.ts +78 -56
- package/src/__tests__/write-envelope.test.ts +257 -0
- package/src/cli.ts +92 -35
- package/src/commands/config.ts +11 -0
- package/src/commands/prompt.ts +42 -29
- package/src/commands/setup.ts +57 -7
- package/src/commands/thread.ts +48 -2
- package/src/commands/workflow.ts +3 -7
- package/src/concurrency/concurrency.ts +245 -0
- package/src/concurrency/index.ts +10 -0
- package/src/concurrency/types.ts +19 -0
- package/src/format.ts +282 -2
- package/src/output-mappers.ts +254 -0
- package/src/schemas.ts +39 -3
- package/src/text-renderers.ts +355 -0
- package/src/validate-semantic.ts +33 -12
|
@@ -233,38 +233,42 @@ afterEach(async () => {
|
|
|
233
233
|
});
|
|
234
234
|
|
|
235
235
|
// ── Suite A: Success Path ────────────────────────────────────────────────────
|
|
236
|
+
//
|
|
237
|
+
// Note: Issue #308 makes validate an envelope-emitting command. Default
|
|
238
|
+
// `--format text` renders `✓ valid` (or `✗ invalid (N errors)`) to stdout.
|
|
239
|
+
// Tests below assert the new envelope contract.
|
|
236
240
|
|
|
237
241
|
describe("workflow validate — Suite A: Success Path", () => {
|
|
238
|
-
test("A.1 valid single-role workflow exits 0
|
|
242
|
+
test("A.1 valid single-role workflow exits 0 with text envelope", async () => {
|
|
239
243
|
const file = join(tmpDir, "test-wf.yaml");
|
|
240
244
|
await writeFile(file, stringify(makeMinimalPayload("test-wf")));
|
|
241
245
|
|
|
242
246
|
const result = runValidate(file);
|
|
243
247
|
|
|
244
248
|
expect(result.exitCode).toBe(0);
|
|
245
|
-
expect(result.stdout).toBe("");
|
|
249
|
+
expect(result.stdout.trim()).toBe("✓ valid");
|
|
246
250
|
expect(result.stderr).toBe("");
|
|
247
251
|
});
|
|
248
252
|
|
|
249
|
-
test("A.2 valid multi-role workflow with Liquid vars exits 0
|
|
253
|
+
test("A.2 valid multi-role workflow with Liquid vars exits 0 with text envelope", async () => {
|
|
250
254
|
const file = join(tmpDir, "writer-flow.yaml");
|
|
251
255
|
await writeFile(file, stringify(makeMultiRolePayload("writer-flow")));
|
|
252
256
|
|
|
253
257
|
const result = runValidate(file);
|
|
254
258
|
|
|
255
259
|
expect(result.exitCode).toBe(0);
|
|
256
|
-
expect(result.stdout).toBe("");
|
|
260
|
+
expect(result.stdout.trim()).toBe("✓ valid");
|
|
257
261
|
expect(result.stderr).toBe("");
|
|
258
262
|
});
|
|
259
263
|
|
|
260
|
-
test("A.3 valid oneOf multi-exit workflow exits 0
|
|
264
|
+
test("A.3 valid oneOf multi-exit workflow exits 0 with text envelope", async () => {
|
|
261
265
|
const file = join(tmpDir, "review-flow.yaml");
|
|
262
266
|
await writeFile(file, stringify(makeOneOfPayload("review-flow")));
|
|
263
267
|
|
|
264
268
|
const result = runValidate(file);
|
|
265
269
|
|
|
266
270
|
expect(result.exitCode).toBe(0);
|
|
267
|
-
expect(result.stdout).toBe("");
|
|
271
|
+
expect(result.stdout.trim()).toBe("✓ valid");
|
|
268
272
|
expect(result.stderr).toBe("");
|
|
269
273
|
});
|
|
270
274
|
|
|
@@ -307,16 +311,16 @@ graph:
|
|
|
307
311
|
const result = runValidate(file);
|
|
308
312
|
|
|
309
313
|
expect(result.exitCode).toBe(0);
|
|
310
|
-
expect(result.stdout).toBe("");
|
|
314
|
+
expect(result.stdout.trim()).toBe("✓ valid");
|
|
311
315
|
expect(result.stderr).toBe("");
|
|
312
316
|
});
|
|
313
317
|
|
|
314
|
-
test("A.5 --format
|
|
318
|
+
test("A.5 --format raw-json emits bare valid envelope value", async () => {
|
|
315
319
|
const file = join(tmpDir, "test-wf.yaml");
|
|
316
320
|
await writeFile(file, stringify(makeMinimalPayload("test-wf")));
|
|
317
321
|
|
|
318
322
|
// --format is a global option on `program`, must come before the subcommand
|
|
319
|
-
const args = [CLI_PATH, "--format", "
|
|
323
|
+
const args = [CLI_PATH, "--format", "raw-json", "workflow", "validate", file];
|
|
320
324
|
let result: RunResult;
|
|
321
325
|
try {
|
|
322
326
|
const stdout = execFileSync(process.execPath, args, {
|
|
@@ -340,7 +344,7 @@ graph:
|
|
|
340
344
|
}
|
|
341
345
|
|
|
342
346
|
expect(result.exitCode).toBe(0);
|
|
343
|
-
expect(result.stdout).
|
|
347
|
+
expect(JSON.parse(result.stdout)).toEqual({ valid: true, errors: [] });
|
|
344
348
|
expect(result.stderr).toBe("");
|
|
345
349
|
});
|
|
346
350
|
});
|
|
@@ -415,9 +419,10 @@ describe("workflow validate — Suite D: Filename Consistency", () => {
|
|
|
415
419
|
const result = runValidate(file);
|
|
416
420
|
|
|
417
421
|
expect(result.exitCode).toBe(1);
|
|
418
|
-
|
|
419
|
-
expect(result.
|
|
420
|
-
expect(result.
|
|
422
|
+
// text envelope contains the error in stdout
|
|
423
|
+
expect(result.stdout).toContain("workflow name mismatch:");
|
|
424
|
+
expect(result.stdout).toContain("foo-bar");
|
|
425
|
+
expect(result.stdout).toContain("baz-qux");
|
|
421
426
|
});
|
|
422
427
|
|
|
423
428
|
test("D.2 index.yaml accepts directory name as workflow name", async () => {
|
|
@@ -429,12 +434,15 @@ describe("workflow validate — Suite D: Filename Consistency", () => {
|
|
|
429
434
|
const result = runValidate(file);
|
|
430
435
|
|
|
431
436
|
expect(result.exitCode).toBe(0);
|
|
432
|
-
expect(result.stdout).toBe("");
|
|
437
|
+
expect(result.stdout.trim()).toBe("✓ valid");
|
|
433
438
|
expect(result.stderr).toBe("");
|
|
434
439
|
});
|
|
435
440
|
});
|
|
436
441
|
|
|
437
442
|
// ── Suite E: Semantic Errors ─────────────────────────────────────────────────
|
|
443
|
+
//
|
|
444
|
+
// Issue #308: errors are now rendered to stdout via the validate-result
|
|
445
|
+
// envelope template (`✗ invalid (N errors)\n - <error>...`). Exit code is 1.
|
|
438
446
|
|
|
439
447
|
describe("workflow validate — Suite E: Semantic Errors", () => {
|
|
440
448
|
test("E.1 graph prompt references variable absent from frontmatter", async () => {
|
|
@@ -478,9 +486,9 @@ describe("workflow validate — Suite E: Semantic Errors", () => {
|
|
|
478
486
|
const result = runValidate(file);
|
|
479
487
|
|
|
480
488
|
expect(result.exitCode).toBe(1);
|
|
481
|
-
expect(result.
|
|
482
|
-
expect(result.
|
|
483
|
-
expect(result.
|
|
489
|
+
expect(result.stdout).toContain("✗ invalid");
|
|
490
|
+
expect(result.stdout).toContain('template variable "prNumber"');
|
|
491
|
+
expect(result.stdout).toContain("commenter");
|
|
484
492
|
});
|
|
485
493
|
|
|
486
494
|
test("E.2 multi-exit oneOf variant prompt references variable not in that variant", async () => {
|
|
@@ -549,8 +557,8 @@ describe("workflow validate — Suite E: Semantic Errors", () => {
|
|
|
549
557
|
const result = runValidate(file);
|
|
550
558
|
|
|
551
559
|
expect(result.exitCode).toBe(1);
|
|
552
|
-
expect(result.
|
|
553
|
-
expect(result.
|
|
560
|
+
expect(result.stdout).toContain('template variable "reason"');
|
|
561
|
+
expect(result.stdout).toContain('variant "approved"');
|
|
554
562
|
});
|
|
555
563
|
|
|
556
564
|
test("E.3 graph references unknown role", async () => {
|
|
@@ -564,7 +572,7 @@ describe("workflow validate — Suite E: Semantic Errors", () => {
|
|
|
564
572
|
const result = runValidate(file);
|
|
565
573
|
|
|
566
574
|
expect(result.exitCode).toBe(1);
|
|
567
|
-
expect(result.
|
|
575
|
+
expect(result.stdout).toContain('unknown role "bogus"');
|
|
568
576
|
});
|
|
569
577
|
|
|
570
578
|
test("E.4 $START missing resume edge", async () => {
|
|
@@ -581,7 +589,7 @@ describe("workflow validate — Suite E: Semantic Errors", () => {
|
|
|
581
589
|
const result = runValidate(file);
|
|
582
590
|
|
|
583
591
|
expect(result.exitCode).toBe(1);
|
|
584
|
-
expect(result.
|
|
592
|
+
expect(result.stdout).toContain('$START must have edges with statuses "new" and "resume"');
|
|
585
593
|
});
|
|
586
594
|
|
|
587
595
|
test("E.5 unreachable role exits 1", async () => {
|
|
@@ -609,7 +617,7 @@ describe("workflow validate — Suite E: Semantic Errors", () => {
|
|
|
609
617
|
const result = runValidate(file);
|
|
610
618
|
|
|
611
619
|
expect(result.exitCode).toBe(1);
|
|
612
|
-
expect(result.
|
|
620
|
+
expect(result.stdout).toContain("is not reachable from $START");
|
|
613
621
|
});
|
|
614
622
|
|
|
615
623
|
test("E.6 $SUSPEND used as edge target exits 1", async () => {
|
|
@@ -623,7 +631,7 @@ describe("workflow validate — Suite E: Semantic Errors", () => {
|
|
|
623
631
|
const result = runValidate(file);
|
|
624
632
|
|
|
625
633
|
expect(result.exitCode).toBe(1);
|
|
626
|
-
expect(result.
|
|
634
|
+
expect(result.stdout).toContain("$SUSPEND");
|
|
627
635
|
});
|
|
628
636
|
|
|
629
637
|
test("E.7 multiple semantic errors are all reported", async () => {
|
|
@@ -646,16 +654,24 @@ describe("workflow validate — Suite E: Semantic Errors", () => {
|
|
|
646
654
|
const result = runValidate(file);
|
|
647
655
|
|
|
648
656
|
expect(result.exitCode).toBe(1);
|
|
649
|
-
expect(result.
|
|
650
|
-
expect(result.
|
|
651
|
-
expect(result.
|
|
652
|
-
expect(result.
|
|
657
|
+
expect(result.stdout).toContain("✗ invalid");
|
|
658
|
+
expect(result.stdout).toContain('unknown role "bogus"');
|
|
659
|
+
expect(result.stdout).toContain("$START must have edges");
|
|
660
|
+
expect(result.stdout).toContain("missing");
|
|
653
661
|
// each error is bullet-prefixed with ` - `
|
|
654
|
-
expect(result.
|
|
662
|
+
expect(result.stdout).toContain(" - ");
|
|
655
663
|
});
|
|
656
664
|
});
|
|
657
665
|
|
|
658
666
|
// ── Suite F: Isolation From CAS / Store ──────────────────────────────────────
|
|
667
|
+
//
|
|
668
|
+
// Issue #308: validate now uses the unified envelope writer, which requires
|
|
669
|
+
// the CAS store (for `@uwf/output/validate-result` schema lookup and the
|
|
670
|
+
// `@ocas/template/text/<hash>` template). The store is initialized
|
|
671
|
+
// idempotently on startup. These tests assert that:
|
|
672
|
+
// - validate works without explicit OCAS_HOME
|
|
673
|
+
// - validate runs idempotently (second run modifies nothing on success)
|
|
674
|
+
// - validate does not modify the workflow registry on success
|
|
659
675
|
|
|
660
676
|
describe("workflow validate — Suite F: Isolation From CAS / Store", () => {
|
|
661
677
|
test("F.1 runs without OCAS_HOME set", async () => {
|
|
@@ -673,38 +689,32 @@ describe("workflow validate — Suite F: Isolation From CAS / Store", () => {
|
|
|
673
689
|
const result = runValidate(file, [], env);
|
|
674
690
|
|
|
675
691
|
expect(result.exitCode).toBe(0);
|
|
676
|
-
expect(result.stdout).toBe("");
|
|
692
|
+
expect(result.stdout.trim()).toBe("✓ valid");
|
|
677
693
|
expect(result.stderr).toBe("");
|
|
678
694
|
});
|
|
679
695
|
|
|
680
|
-
test("F.2 runs even when
|
|
696
|
+
test("F.2 runs even when ocas store is empty/uninitialized (skip on win32)", {
|
|
681
697
|
skip: process.platform === "win32",
|
|
682
698
|
}, async () => {
|
|
683
699
|
const file = join(tmpDir, "ro-home-wf.yaml");
|
|
684
700
|
await writeFile(file, stringify(makeMinimalPayload("ro-home-wf")));
|
|
685
701
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
702
|
+
// Use a writable but empty OCAS_HOME — schema registration writes
|
|
703
|
+
// happen at startup but the validate command should still succeed.
|
|
704
|
+
const ocasHome = join(tmpDir, "fresh-ocas");
|
|
705
|
+
await mkdir(ocasHome, { recursive: true });
|
|
706
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
707
|
+
delete env.UWF_HOME;
|
|
708
|
+
env.OCAS_HOME = ocasHome;
|
|
689
709
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const result = runValidate(file, [], env);
|
|
697
|
-
|
|
698
|
-
expect(result.exitCode).toBe(0);
|
|
699
|
-
expect(result.stdout).toBe("");
|
|
700
|
-
expect(result.stderr).toBe("");
|
|
701
|
-
} finally {
|
|
702
|
-
// restore permissions so afterEach can clean up
|
|
703
|
-
await chmod(ro, 0o755);
|
|
704
|
-
}
|
|
710
|
+
const result = runValidate(file, [], env);
|
|
711
|
+
|
|
712
|
+
expect(result.exitCode).toBe(0);
|
|
713
|
+
expect(result.stdout.trim()).toBe("✓ valid");
|
|
714
|
+
expect(result.stderr).toBe("");
|
|
705
715
|
});
|
|
706
716
|
|
|
707
|
-
test("F.3 does not modify registry on success", async () => {
|
|
717
|
+
test("F.3 does not modify workflow registry on success", async () => {
|
|
708
718
|
const file = join(tmpDir, "reg-wf.yaml");
|
|
709
719
|
await writeFile(file, stringify(makeMinimalPayload("reg-wf")));
|
|
710
720
|
|
|
@@ -712,18 +722,25 @@ describe("workflow validate — Suite F: Isolation From CAS / Store", () => {
|
|
|
712
722
|
await mkdir(ocasHome, { recursive: true });
|
|
713
723
|
const env: NodeJS.ProcessEnv = { ...process.env, OCAS_HOME: ocasHome, UWF_HOME: ocasHome };
|
|
714
724
|
|
|
725
|
+
// First run primes the schema/template registrations
|
|
726
|
+
const first = runValidate(file, [], env);
|
|
727
|
+
expect(first.exitCode).toBe(0);
|
|
728
|
+
expect(first.stdout.trim()).toBe("✓ valid");
|
|
729
|
+
|
|
730
|
+
// Capture state after registrations are present
|
|
715
731
|
const beforeListing = await listingSnapshot(ocasHome);
|
|
716
732
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
expect(
|
|
720
|
-
expect(
|
|
733
|
+
// Second run must not modify the registry (no @uwf/registry/<name> binding)
|
|
734
|
+
const second = runValidate(file, [], env);
|
|
735
|
+
expect(second.exitCode).toBe(0);
|
|
736
|
+
expect(second.stdout.trim()).toBe("✓ valid");
|
|
737
|
+
expect(second.stderr).toBe("");
|
|
721
738
|
|
|
722
739
|
const afterListing = await listingSnapshot(ocasHome);
|
|
723
740
|
expect(afterListing).toEqual(beforeListing);
|
|
724
741
|
});
|
|
725
742
|
|
|
726
|
-
test("F.4
|
|
743
|
+
test("F.4 second run on the same workflow is idempotent (no further CAS writes)", async () => {
|
|
727
744
|
const file = join(tmpDir, "cas-iso-wf.yaml");
|
|
728
745
|
await writeFile(file, stringify(makeMinimalPayload("cas-iso-wf")));
|
|
729
746
|
|
|
@@ -731,10 +748,15 @@ describe("workflow validate — Suite F: Isolation From CAS / Store", () => {
|
|
|
731
748
|
await mkdir(ocasHome, { recursive: true });
|
|
732
749
|
const env: NodeJS.ProcessEnv = { ...process.env, OCAS_HOME: ocasHome, UWF_HOME: ocasHome };
|
|
733
750
|
|
|
751
|
+
// First run: schema/template registration is allowed to write to CAS
|
|
752
|
+
const first = runValidate(file, [], env);
|
|
753
|
+
expect(first.exitCode).toBe(0);
|
|
754
|
+
|
|
734
755
|
const beforeListing = await listingSnapshot(ocasHome);
|
|
735
756
|
|
|
736
|
-
|
|
737
|
-
|
|
757
|
+
// Second run: registrations are idempotent → no new writes
|
|
758
|
+
const second = runValidate(file, [], env);
|
|
759
|
+
expect(second.exitCode).toBe(0);
|
|
738
760
|
|
|
739
761
|
const afterListing = await listingSnapshot(ocasHome);
|
|
740
762
|
expect(afterListing).toEqual(beforeListing);
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { openStore } from "@ocas/fs";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
6
|
+
import { isOutputFormat, type OutputFormat, SUPPORTED_FORMATS, writeEnvelope } from "../format.js";
|
|
7
|
+
import { registerUwfSchemas, type UwfSchemaHashes } from "../schemas.js";
|
|
8
|
+
|
|
9
|
+
let tmp: string;
|
|
10
|
+
let store: Awaited<ReturnType<typeof openStore>>;
|
|
11
|
+
let schemas: UwfSchemaHashes;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
tmp = await mkdtemp(join(tmpdir(), "uwf-write-envelope-"));
|
|
15
|
+
store = await openStore(tmp);
|
|
16
|
+
schemas = await registerUwfSchemas(store);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
await rm(tmp, { recursive: true, force: true });
|
|
21
|
+
vi.restoreAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function captureStdout<T>(fn: () => Promise<T>): { result: Promise<T>; output: string[] } {
|
|
25
|
+
const buf: string[] = [];
|
|
26
|
+
const spy = vi.spyOn(process.stdout, "write").mockImplementation(((
|
|
27
|
+
chunk: string | Uint8Array,
|
|
28
|
+
): boolean => {
|
|
29
|
+
buf.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
|
|
30
|
+
return true;
|
|
31
|
+
}) as typeof process.stdout.write);
|
|
32
|
+
return {
|
|
33
|
+
result: (async () => {
|
|
34
|
+
try {
|
|
35
|
+
return await fn();
|
|
36
|
+
} finally {
|
|
37
|
+
spy.mockRestore();
|
|
38
|
+
}
|
|
39
|
+
})(),
|
|
40
|
+
output: buf,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("isOutputFormat type guard", () => {
|
|
45
|
+
test("accepts every supported format", () => {
|
|
46
|
+
for (const fmt of SUPPORTED_FORMATS) {
|
|
47
|
+
expect(isOutputFormat(fmt)).toBe(true);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("rejects unknown formats", () => {
|
|
52
|
+
expect(isOutputFormat("xml")).toBe(false);
|
|
53
|
+
expect(isOutputFormat("")).toBe(false);
|
|
54
|
+
expect(isOutputFormat("JSON")).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("SUPPORTED_FORMATS", () => {
|
|
59
|
+
test("contains exactly the five formats specified in cli-envelope-writer.md", () => {
|
|
60
|
+
expect([...SUPPORTED_FORMATS].sort()).toEqual(["json", "raw-json", "raw-yaml", "text", "yaml"]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("writeEnvelope — json format", () => {
|
|
65
|
+
test("emits {type,value} JSON envelope with trailing newline", async () => {
|
|
66
|
+
const payload = { valid: true, errors: [] };
|
|
67
|
+
const { result, output } = captureStdout(async () =>
|
|
68
|
+
writeEnvelope(payload, "validate-result", { format: "json", store, schemas }),
|
|
69
|
+
);
|
|
70
|
+
await result;
|
|
71
|
+
|
|
72
|
+
const out = output.join("");
|
|
73
|
+
expect(out.endsWith("\n")).toBe(true);
|
|
74
|
+
const parsed = JSON.parse(out);
|
|
75
|
+
expect(parsed).toEqual({
|
|
76
|
+
type: schemas.outputs["validate-result"],
|
|
77
|
+
value: { valid: true, errors: [] },
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("writeEnvelope — yaml format", () => {
|
|
83
|
+
test("emits envelope yaml with type then value keys", async () => {
|
|
84
|
+
const payload = { valid: false, errors: ["a", "b"] };
|
|
85
|
+
const { result, output } = captureStdout(async () =>
|
|
86
|
+
writeEnvelope(payload, "validate-result", { format: "yaml", store, schemas }),
|
|
87
|
+
);
|
|
88
|
+
await result;
|
|
89
|
+
|
|
90
|
+
const out = output.join("");
|
|
91
|
+
expect(out.endsWith("\n")).toBe(true);
|
|
92
|
+
expect(out).toContain(`type: ${schemas.outputs["validate-result"]}`);
|
|
93
|
+
expect(out).toContain("value:");
|
|
94
|
+
expect(out).toContain("valid: false");
|
|
95
|
+
// type must precede value
|
|
96
|
+
expect(out.indexOf("type:")).toBeLessThan(out.indexOf("value:"));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("writeEnvelope — raw-json format", () => {
|
|
101
|
+
test("emits bare value JSON without envelope (legacy 0.5.0 shape)", async () => {
|
|
102
|
+
const payload = { valid: true, errors: [] };
|
|
103
|
+
const { result, output } = captureStdout(async () =>
|
|
104
|
+
writeEnvelope(payload, "validate-result", { format: "raw-json", store, schemas }),
|
|
105
|
+
);
|
|
106
|
+
await result;
|
|
107
|
+
|
|
108
|
+
const out = output.join("");
|
|
109
|
+
expect(out.endsWith("\n")).toBe(true);
|
|
110
|
+
const parsed = JSON.parse(out);
|
|
111
|
+
expect(parsed).toEqual({ valid: true, errors: [] });
|
|
112
|
+
// Must NOT contain envelope keys
|
|
113
|
+
expect(parsed.type).toBeUndefined();
|
|
114
|
+
expect(parsed.value).toBeUndefined();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("writeEnvelope — raw-yaml format", () => {
|
|
119
|
+
test("emits bare value YAML without envelope (legacy 0.5.0 shape)", async () => {
|
|
120
|
+
const payload = { valid: true, errors: [] };
|
|
121
|
+
const { result, output } = captureStdout(async () =>
|
|
122
|
+
writeEnvelope(payload, "validate-result", { format: "raw-yaml", store, schemas }),
|
|
123
|
+
);
|
|
124
|
+
await result;
|
|
125
|
+
|
|
126
|
+
const out = output.join("");
|
|
127
|
+
expect(out.endsWith("\n")).toBe(true);
|
|
128
|
+
expect(out).toContain("valid: true");
|
|
129
|
+
expect(out).toContain("errors:");
|
|
130
|
+
expect(out).not.toContain("type:");
|
|
131
|
+
expect(out).not.toContain("value:");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("writeEnvelope — text format (Liquid template)", () => {
|
|
136
|
+
test("renders validate-result valid case as `✓ valid`", async () => {
|
|
137
|
+
const payload = { valid: true, errors: [] };
|
|
138
|
+
const { result, output } = captureStdout(async () =>
|
|
139
|
+
writeEnvelope(payload, "validate-result", { format: "text", store, schemas }),
|
|
140
|
+
);
|
|
141
|
+
await result;
|
|
142
|
+
|
|
143
|
+
const out = output.join("");
|
|
144
|
+
expect(out.trim()).toBe("✓ valid");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("renders validate-result invalid case with bulleted errors", async () => {
|
|
148
|
+
const payload = {
|
|
149
|
+
valid: false,
|
|
150
|
+
errors: ['unknown role "bogus"', "$START missing resume edge"],
|
|
151
|
+
};
|
|
152
|
+
const { result, output } = captureStdout(async () =>
|
|
153
|
+
writeEnvelope(payload, "validate-result", { format: "text", store, schemas }),
|
|
154
|
+
);
|
|
155
|
+
await result;
|
|
156
|
+
|
|
157
|
+
const out = output.join("");
|
|
158
|
+
expect(out).toContain("✗ invalid (2 errors)");
|
|
159
|
+
expect(out).toContain(' - unknown role "bogus"');
|
|
160
|
+
expect(out).toContain(" - $START missing resume edge");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("renders workflow-add as Registered/Hash key-value lines", async () => {
|
|
164
|
+
const payload = { name: "review-pr", hash: "2TBP6T37TZAJZ" };
|
|
165
|
+
const { result, output } = captureStdout(async () =>
|
|
166
|
+
writeEnvelope(payload, "workflow-add", { format: "text", store, schemas }),
|
|
167
|
+
);
|
|
168
|
+
await result;
|
|
169
|
+
|
|
170
|
+
const out = output.join("");
|
|
171
|
+
expect(out).toBe("Registered review-pr\nHash 2TBP6T37TZAJZ\n");
|
|
172
|
+
// No JSON envelope leakage
|
|
173
|
+
expect(out).not.toContain("{");
|
|
174
|
+
expect(out).not.toContain("undefined");
|
|
175
|
+
// Single trailing newline
|
|
176
|
+
expect(out.endsWith("\n")).toBe(true);
|
|
177
|
+
expect(out.endsWith("\n\n")).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("workflow-add tolerates empty hash field without throwing", async () => {
|
|
181
|
+
const payload = { name: "review-pr", hash: "" };
|
|
182
|
+
const { result, output } = captureStdout(async () =>
|
|
183
|
+
writeEnvelope(payload, "workflow-add", { format: "text", store, schemas }),
|
|
184
|
+
);
|
|
185
|
+
await result;
|
|
186
|
+
|
|
187
|
+
const out = output.join("");
|
|
188
|
+
// Renders without throwing; empty hash leaves trailing whitespace
|
|
189
|
+
expect(out).toContain("Registered review-pr");
|
|
190
|
+
expect(out).toContain("Hash ");
|
|
191
|
+
expect(out).not.toContain("undefined");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("writeEnvelope — workflow-add formats", () => {
|
|
196
|
+
test("json format wraps payload in {type,value} envelope", async () => {
|
|
197
|
+
const payload = { name: "review-pr", hash: "2TBP6T37TZAJZ" };
|
|
198
|
+
const { result, output } = captureStdout(async () =>
|
|
199
|
+
writeEnvelope(payload, "workflow-add", { format: "json", store, schemas }),
|
|
200
|
+
);
|
|
201
|
+
await result;
|
|
202
|
+
|
|
203
|
+
const out = output.join("");
|
|
204
|
+
const parsed = JSON.parse(out);
|
|
205
|
+
expect(parsed).toEqual({
|
|
206
|
+
type: schemas.outputs["workflow-add"],
|
|
207
|
+
value: { name: "review-pr", hash: "2TBP6T37TZAJZ" },
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("raw-json format emits bare payload (legacy 0.5.0 shape)", async () => {
|
|
212
|
+
const payload = { name: "review-pr", hash: "2TBP6T37TZAJZ" };
|
|
213
|
+
const { result, output } = captureStdout(async () =>
|
|
214
|
+
writeEnvelope(payload, "workflow-add", { format: "raw-json", store, schemas }),
|
|
215
|
+
);
|
|
216
|
+
await result;
|
|
217
|
+
|
|
218
|
+
const out = output.join("");
|
|
219
|
+
expect(out).toBe('{"name":"review-pr","hash":"2TBP6T37TZAJZ"}\n');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("yaml format emits envelope with type and value keys", async () => {
|
|
223
|
+
const payload = { name: "review-pr", hash: "2TBP6T37TZAJZ" };
|
|
224
|
+
const { result, output } = captureStdout(async () =>
|
|
225
|
+
writeEnvelope(payload, "workflow-add", { format: "yaml", store, schemas }),
|
|
226
|
+
);
|
|
227
|
+
await result;
|
|
228
|
+
|
|
229
|
+
const out = output.join("");
|
|
230
|
+
expect(out).toContain(`type: ${schemas.outputs["workflow-add"]}`);
|
|
231
|
+
expect(out).toContain("value:");
|
|
232
|
+
expect(out).toContain("name: review-pr");
|
|
233
|
+
expect(out).toContain("hash: 2TBP6T37TZAJZ");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("writeEnvelope — schema lookup", () => {
|
|
238
|
+
test("throws when schema short name is unknown", async () => {
|
|
239
|
+
await expect(
|
|
240
|
+
// @ts-expect-error invalid schema name on purpose
|
|
241
|
+
writeEnvelope({}, "not-a-real-schema", { format: "json", store, schemas }),
|
|
242
|
+
).rejects.toThrow(/output schema not registered/);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("each format calls in to the same registered schema hash", async () => {
|
|
246
|
+
const payload = { valid: true, errors: [] };
|
|
247
|
+
const formats: OutputFormat[] = ["json", "yaml"];
|
|
248
|
+
for (const fmt of formats) {
|
|
249
|
+
const { result, output } = captureStdout(async () =>
|
|
250
|
+
writeEnvelope(payload, "validate-result", { format: fmt, store, schemas }),
|
|
251
|
+
);
|
|
252
|
+
await result;
|
|
253
|
+
const out = output.join("");
|
|
254
|
+
expect(out).toContain(schemas.outputs["validate-result"]);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|