gsd-pi 2.41.0-dev.3557dc4 → 2.41.0-dev.5a170d0

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 (123) hide show
  1. package/README.md +1 -1
  2. package/dist/resources/extensions/gsd/auto/loop.js +80 -0
  3. package/dist/resources/extensions/gsd/auto/phases.js +2 -2
  4. package/dist/resources/extensions/gsd/auto/session.js +6 -0
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
  6. package/dist/resources/extensions/gsd/auto.js +28 -1
  7. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
  8. package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
  9. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
  10. package/dist/resources/extensions/gsd/context-injector.js +74 -0
  11. package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
  12. package/dist/resources/extensions/gsd/custom-verification.js +145 -0
  13. package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
  14. package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
  15. package/dist/resources/extensions/gsd/definition-loader.js +352 -0
  16. package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
  17. package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
  18. package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
  19. package/dist/resources/extensions/gsd/engine-types.js +8 -0
  20. package/dist/resources/extensions/gsd/execution-policy.js +8 -0
  21. package/dist/resources/extensions/gsd/graph.js +225 -0
  22. package/dist/resources/extensions/gsd/run-manager.js +134 -0
  23. package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
  24. package/dist/resources/skills/create-workflow/SKILL.md +103 -0
  25. package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  26. package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
  27. package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  28. package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  29. package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  30. package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  31. package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  32. package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  33. package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  34. package/dist/web/standalone/.next/BUILD_ID +1 -1
  35. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  36. package/dist/web/standalone/.next/build-manifest.json +2 -2
  37. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  38. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  39. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.html +1 -1
  55. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  62. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  63. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  64. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  65. package/package.json +1 -1
  66. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -0
  68. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  69. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -0
  70. package/src/resources/extensions/gsd/auto/loop.ts +91 -0
  71. package/src/resources/extensions/gsd/auto/phases.ts +2 -2
  72. package/src/resources/extensions/gsd/auto/session.ts +6 -0
  73. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  74. package/src/resources/extensions/gsd/auto.ts +31 -1
  75. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
  76. package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
  77. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
  78. package/src/resources/extensions/gsd/context-injector.ts +100 -0
  79. package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
  80. package/src/resources/extensions/gsd/custom-verification.ts +180 -0
  81. package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
  82. package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
  83. package/src/resources/extensions/gsd/definition-loader.ts +462 -0
  84. package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
  85. package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
  86. package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
  87. package/src/resources/extensions/gsd/engine-types.ts +71 -0
  88. package/src/resources/extensions/gsd/execution-policy.ts +43 -0
  89. package/src/resources/extensions/gsd/graph.ts +312 -0
  90. package/src/resources/extensions/gsd/run-manager.ts +180 -0
  91. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +100 -118
  92. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -2
  93. package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
  94. package/src/resources/extensions/gsd/tests/captures.test.ts +12 -1
  95. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
  96. package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
  97. package/src/resources/extensions/gsd/tests/continue-here.test.ts +20 -20
  98. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
  99. package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
  100. package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
  101. package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
  102. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
  103. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
  104. package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
  105. package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
  106. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
  107. package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
  108. package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
  109. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
  110. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +195 -105
  111. package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
  112. package/src/resources/skills/create-workflow/SKILL.md +103 -0
  113. package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  114. package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
  115. package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  116. package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  117. package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  118. package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  119. package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  120. package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  121. package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  122. /package/dist/web/standalone/.next/static/{JBSIr4fSfHXs5g5x2ZBSC → K7GYOOPvQWX6TKYEKhODM}/_buildManifest.js +0 -0
  123. /package/dist/web/standalone/.next/static/{JBSIr4fSfHXs5g5x2ZBSC → K7GYOOPvQWX6TKYEKhODM}/_ssgManifest.js +0 -0
@@ -0,0 +1,778 @@
1
+ /**
2
+ * Unit tests for definition-loader.ts.
3
+ *
4
+ * Covers V1 YAML schema validation (valid + various rejection cases),
5
+ * filesystem loading, snake_case → camelCase conversion, forward
6
+ * compatibility with unknown fields, parameter substitution, and the
7
+ * four gap validations (duplicate IDs, dangling deps, self-deps, cycles).
8
+ */
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ import {
17
+ loadDefinition,
18
+ validateDefinition,
19
+ substituteParams,
20
+ substitutePromptString,
21
+ } from "../definition-loader.ts";
22
+ import type { WorkflowDefinition } from "../definition-loader.ts";
23
+
24
+ // ─── Helpers ─────────────────────────────────────────────────────────────
25
+
26
+ function makeTmpDir(): string {
27
+ return mkdtempSync(join(tmpdir(), "gsd-defloader-test-"));
28
+ }
29
+
30
+ /** Write a YAML string into a temp definitions directory. Returns the dir path. */
31
+ function writeDefYaml(yaml: string, name = "test-workflow"): string {
32
+ const dir = makeTmpDir();
33
+ writeFileSync(join(dir, `${name}.yaml`), yaml, "utf-8");
34
+ return dir;
35
+ }
36
+
37
+ const VALID_3STEP_YAML = `
38
+ version: 1
39
+ name: "test-workflow"
40
+ description: "A test workflow"
41
+ params:
42
+ topic: "AI"
43
+ steps:
44
+ - id: research
45
+ name: "Research the topic"
46
+ prompt: "Research {{topic}} and write findings to research.md"
47
+ requires: []
48
+ produces:
49
+ - research.md
50
+ - id: outline
51
+ name: "Create outline"
52
+ prompt: "Based on research.md, create an outline in outline.md"
53
+ requires: [research]
54
+ produces:
55
+ - outline.md
56
+ - id: draft
57
+ name: "Write draft"
58
+ prompt: "Write a draft based on outline.md"
59
+ requires: [outline]
60
+ produces:
61
+ - draft.md
62
+ `;
63
+
64
+ // ─── loadDefinition: valid YAML ──────────────────────────────────────────
65
+
66
+ test("loadDefinition: valid 3-step YAML returns correct structure", () => {
67
+ const dir = writeDefYaml(VALID_3STEP_YAML);
68
+ try {
69
+ const def = loadDefinition(dir, "test-workflow");
70
+
71
+ assert.equal(def.version, 1);
72
+ assert.equal(def.name, "test-workflow");
73
+ assert.equal(def.description, "A test workflow");
74
+ assert.deepEqual(def.params, { topic: "AI" });
75
+ assert.equal(def.steps.length, 3);
76
+
77
+ // Step 1: research
78
+ assert.equal(def.steps[0].id, "research");
79
+ assert.equal(def.steps[0].name, "Research the topic");
80
+ assert.equal(def.steps[0].prompt, "Research {{topic}} and write findings to research.md");
81
+ assert.deepEqual(def.steps[0].requires, []);
82
+ assert.deepEqual(def.steps[0].produces, ["research.md"]);
83
+
84
+ // Step 2: outline — depends on research
85
+ assert.equal(def.steps[1].id, "outline");
86
+ assert.deepEqual(def.steps[1].requires, ["research"]);
87
+
88
+ // Step 3: draft — depends on outline
89
+ assert.equal(def.steps[2].id, "draft");
90
+ assert.deepEqual(def.steps[2].requires, ["outline"]);
91
+ assert.deepEqual(def.steps[2].produces, ["draft.md"]);
92
+ } finally {
93
+ try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
94
+ }
95
+ });
96
+
97
+ // ─── validateDefinition: rejection cases ─────────────────────────────────
98
+
99
+ test("validateDefinition: missing version → error", () => {
100
+ const result = validateDefinition({
101
+ name: "test",
102
+ steps: [{ id: "a", name: "A", prompt: "do A" }],
103
+ });
104
+ assert.equal(result.valid, false);
105
+ assert.ok(result.errors.some((e) => e.includes("version")));
106
+ });
107
+
108
+ test("validateDefinition: version 2 (unsupported) → error", () => {
109
+ const result = validateDefinition({
110
+ version: 2,
111
+ name: "test",
112
+ steps: [{ id: "a", name: "A", prompt: "do A" }],
113
+ });
114
+ assert.equal(result.valid, false);
115
+ assert.ok(result.errors.some((e) => e.includes("Unsupported version: 2")));
116
+ });
117
+
118
+ test("validateDefinition: missing step id → error", () => {
119
+ const result = validateDefinition({
120
+ version: 1,
121
+ name: "test",
122
+ steps: [{ name: "A", prompt: "do A" }],
123
+ });
124
+ assert.equal(result.valid, false);
125
+ assert.ok(result.errors.some((e) => e.includes("index 0") && e.includes("id")));
126
+ });
127
+
128
+ test("validateDefinition: missing step prompt → error", () => {
129
+ const result = validateDefinition({
130
+ version: 1,
131
+ name: "test",
132
+ steps: [{ id: "a", name: "A" }],
133
+ });
134
+ assert.equal(result.valid, false);
135
+ assert.ok(result.errors.some((e) => e.includes("index 0") && e.includes("prompt")));
136
+ });
137
+
138
+ test("validateDefinition: produces with '..' path traversal → error", () => {
139
+ const result = validateDefinition({
140
+ version: 1,
141
+ name: "test",
142
+ steps: [{ id: "a", name: "A", prompt: "do A", produces: ["../secret.txt"] }],
143
+ });
144
+ assert.equal(result.valid, false);
145
+ assert.ok(result.errors.some((e) => e.includes("..") && e.includes("produces")));
146
+ });
147
+
148
+ test("validateDefinition: unknown fields (context_from, iterate) → accepted silently", () => {
149
+ const result = validateDefinition({
150
+ version: 1,
151
+ name: "test",
152
+ future_top_level_field: true,
153
+ steps: [{
154
+ id: "a",
155
+ name: "A",
156
+ prompt: "do A",
157
+ context_from: ["other-step"],
158
+ iterate: { source: "file.md", pattern: "^## (.+)" },
159
+ some_future_field: 42,
160
+ }],
161
+ });
162
+ assert.equal(result.valid, true);
163
+ assert.equal(result.errors.length, 0);
164
+ });
165
+
166
+ test("validateDefinition: collects multiple errors in one pass", () => {
167
+ const result = validateDefinition({
168
+ // missing version and name
169
+ steps: [
170
+ { id: "a" }, // missing name and prompt
171
+ { name: "B", prompt: "do B" }, // missing id
172
+ ],
173
+ });
174
+ assert.equal(result.valid, false);
175
+ // Should have errors for: version, name, step 0 name, step 0 prompt, step 1 id
176
+ assert.ok(result.errors.length >= 4, `Expected ≥4 errors, got ${result.errors.length}: ${result.errors.join("; ")}`);
177
+ });
178
+
179
+ test("validateDefinition: null input → error", () => {
180
+ const result = validateDefinition(null);
181
+ assert.equal(result.valid, false);
182
+ assert.ok(result.errors.some((e) => e.includes("non-null object")));
183
+ });
184
+
185
+ test("validateDefinition: empty steps array → error", () => {
186
+ const result = validateDefinition({
187
+ version: 1,
188
+ name: "test",
189
+ steps: [],
190
+ });
191
+ assert.equal(result.valid, false);
192
+ assert.ok(result.errors.some((e) => e.includes("at least one step")));
193
+ });
194
+
195
+ test("validateDefinition: missing name → error", () => {
196
+ const result = validateDefinition({
197
+ version: 1,
198
+ steps: [{ id: "a", name: "A", prompt: "do A" }],
199
+ });
200
+ assert.equal(result.valid, false);
201
+ assert.ok(result.errors.some((e) => e.includes("name")));
202
+ });
203
+
204
+ test("validateDefinition: step is not an object → error", () => {
205
+ const result = validateDefinition({
206
+ version: 1,
207
+ name: "test",
208
+ steps: ["not-an-object"],
209
+ });
210
+ assert.equal(result.valid, false);
211
+ assert.ok(result.errors.some((e) => e.includes("index 0") && e.includes("not an object")));
212
+ });
213
+
214
+ test("validateDefinition: missing step name → error", () => {
215
+ const result = validateDefinition({
216
+ version: 1,
217
+ name: "test",
218
+ steps: [{ id: "a", prompt: "do A" }],
219
+ });
220
+ assert.equal(result.valid, false);
221
+ assert.ok(result.errors.some((e) => e.includes("index 0") && e.includes("name")));
222
+ });
223
+
224
+ // ─── loadDefinition: error cases ─────────────────────────────────────────
225
+
226
+ test("loadDefinition: missing file → descriptive error", () => {
227
+ const dir = makeTmpDir();
228
+ try {
229
+ assert.throws(
230
+ () => loadDefinition(dir, "nonexistent"),
231
+ (err: Error) => {
232
+ assert.ok(err.message.includes("not found"));
233
+ assert.ok(err.message.includes("nonexistent.yaml"));
234
+ return true;
235
+ },
236
+ );
237
+ } finally {
238
+ try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
239
+ }
240
+ });
241
+
242
+ test("loadDefinition: invalid YAML schema → descriptive error", () => {
243
+ const dir = writeDefYaml(`
244
+ version: 2
245
+ name: "bad"
246
+ steps:
247
+ - id: a
248
+ name: "A"
249
+ prompt: "do A"
250
+ `);
251
+ try {
252
+ assert.throws(
253
+ () => loadDefinition(dir, "test-workflow"),
254
+ (err: Error) => {
255
+ assert.ok(err.message.includes("Invalid workflow definition"));
256
+ assert.ok(err.message.includes("Unsupported version"));
257
+ return true;
258
+ },
259
+ );
260
+ } finally {
261
+ try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
262
+ }
263
+ });
264
+
265
+ // ─── loadDefinition: snake_case → camelCase conversion ───────────────────
266
+
267
+ test("loadDefinition: depends_on in YAML maps to requires in TypeScript", () => {
268
+ const dir = writeDefYaml(`
269
+ version: 1
270
+ name: "dep-test"
271
+ steps:
272
+ - id: first
273
+ name: "First"
274
+ prompt: "do first"
275
+ - id: second
276
+ name: "Second"
277
+ prompt: "do second"
278
+ depends_on: [first]
279
+ `);
280
+ try {
281
+ const def = loadDefinition(dir, "test-workflow");
282
+ assert.deepEqual(def.steps[1].requires, ["first"]);
283
+ } finally {
284
+ try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
285
+ }
286
+ });
287
+
288
+ test("loadDefinition: context_from in YAML maps to contextFrom in TypeScript", () => {
289
+ const dir = writeDefYaml(`
290
+ version: 1
291
+ name: "ctx-test"
292
+ steps:
293
+ - id: first
294
+ name: "First"
295
+ prompt: "do first"
296
+ - id: second
297
+ name: "Second"
298
+ prompt: "do second"
299
+ context_from: [first]
300
+ `);
301
+ try {
302
+ const def = loadDefinition(dir, "test-workflow");
303
+ assert.deepEqual(def.steps[1].contextFrom, ["first"]);
304
+ } finally {
305
+ try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
306
+ }
307
+ });
308
+
309
+ // ─── validateDefinition: iterate field validation ────────────────────────
310
+
311
+ test("validateDefinition: valid iterate config accepted", () => {
312
+ const result = validateDefinition({
313
+ version: 1,
314
+ name: "test",
315
+ steps: [{
316
+ id: "a",
317
+ name: "A",
318
+ prompt: "do A",
319
+ iterate: { source: "outline.md", pattern: "^## (.+)" },
320
+ }],
321
+ });
322
+ assert.equal(result.valid, true);
323
+ assert.equal(result.errors.length, 0);
324
+ });
325
+
326
+ test("validateDefinition: iterate missing source → error", () => {
327
+ const result = validateDefinition({
328
+ version: 1,
329
+ name: "test",
330
+ steps: [{
331
+ id: "a",
332
+ name: "A",
333
+ prompt: "do A",
334
+ iterate: { pattern: "^## (.+)" },
335
+ }],
336
+ });
337
+ assert.equal(result.valid, false);
338
+ assert.ok(result.errors.some((e) => e.includes("source")));
339
+ });
340
+
341
+ test("validateDefinition: iterate source with .. → error", () => {
342
+ const result = validateDefinition({
343
+ version: 1,
344
+ name: "test",
345
+ steps: [{
346
+ id: "a",
347
+ name: "A",
348
+ prompt: "do A",
349
+ iterate: { source: "../escape.md", pattern: "(.+)" },
350
+ }],
351
+ });
352
+ assert.equal(result.valid, false);
353
+ assert.ok(result.errors.some((e) => e.includes("path traversal") || e.includes("..")));
354
+ });
355
+
356
+ test("validateDefinition: iterate invalid regex → error", () => {
357
+ const result = validateDefinition({
358
+ version: 1,
359
+ name: "test",
360
+ steps: [{
361
+ id: "a",
362
+ name: "A",
363
+ prompt: "do A",
364
+ iterate: { source: "f.md", pattern: "[invalid" },
365
+ }],
366
+ });
367
+ assert.equal(result.valid, false);
368
+ assert.ok(result.errors.some((e) => e.includes("regex")));
369
+ });
370
+
371
+ test("validateDefinition: iterate pattern without capture group → error", () => {
372
+ const result = validateDefinition({
373
+ version: 1,
374
+ name: "test",
375
+ steps: [{
376
+ id: "a",
377
+ name: "A",
378
+ prompt: "do A",
379
+ iterate: { source: "f.md", pattern: "^## .+" },
380
+ }],
381
+ });
382
+ assert.equal(result.valid, false);
383
+ assert.ok(result.errors.some((e) => e.includes("capture group")));
384
+ });
385
+
386
+ // ─── validateDefinition: verify field validation ─────────────────────────
387
+
388
+ test("validateDefinition: valid content-heuristic verify → accepted", () => {
389
+ const result = validateDefinition({
390
+ version: 1,
391
+ name: "test",
392
+ steps: [{
393
+ id: "a",
394
+ name: "A",
395
+ prompt: "do A",
396
+ verify: { policy: "content-heuristic", minSize: 100, pattern: "^## " },
397
+ }],
398
+ });
399
+ assert.equal(result.valid, true);
400
+ assert.equal(result.errors.length, 0);
401
+ });
402
+
403
+ test("validateDefinition: valid shell-command verify → accepted", () => {
404
+ const result = validateDefinition({
405
+ version: 1,
406
+ name: "test",
407
+ steps: [{
408
+ id: "a",
409
+ name: "A",
410
+ prompt: "do A",
411
+ verify: { policy: "shell-command", command: "cat output.md | grep '^## '" },
412
+ }],
413
+ });
414
+ assert.equal(result.valid, true);
415
+ assert.equal(result.errors.length, 0);
416
+ });
417
+
418
+ test("validateDefinition: valid prompt-verify → accepted", () => {
419
+ const result = validateDefinition({
420
+ version: 1,
421
+ name: "test",
422
+ steps: [{
423
+ id: "a",
424
+ name: "A",
425
+ prompt: "do A",
426
+ verify: { policy: "prompt-verify", prompt: "Does the output contain at least 3 sections?" },
427
+ }],
428
+ });
429
+ assert.equal(result.valid, true);
430
+ assert.equal(result.errors.length, 0);
431
+ });
432
+
433
+ test("validateDefinition: valid human-review verify → accepted", () => {
434
+ const result = validateDefinition({
435
+ version: 1,
436
+ name: "test",
437
+ steps: [{
438
+ id: "a",
439
+ name: "A",
440
+ prompt: "do A",
441
+ verify: { policy: "human-review" },
442
+ }],
443
+ });
444
+ assert.equal(result.valid, true);
445
+ assert.equal(result.errors.length, 0);
446
+ });
447
+
448
+ test("validateDefinition: invalid verify policy name → rejected", () => {
449
+ const result = validateDefinition({
450
+ version: 1,
451
+ name: "test",
452
+ steps: [{
453
+ id: "a",
454
+ name: "A",
455
+ prompt: "do A",
456
+ verify: { policy: "magic-check" },
457
+ }],
458
+ });
459
+ assert.equal(result.valid, false);
460
+ assert.ok(result.errors.some((e) => e.includes("verify.policy must be one of")));
461
+ });
462
+
463
+ test("validateDefinition: shell-command missing command → rejected", () => {
464
+ const result = validateDefinition({
465
+ version: 1,
466
+ name: "test",
467
+ steps: [{
468
+ id: "a",
469
+ name: "A",
470
+ prompt: "do A",
471
+ verify: { policy: "shell-command" },
472
+ }],
473
+ });
474
+ assert.equal(result.valid, false);
475
+ assert.ok(result.errors.some((e) => e.includes('requires a non-empty "command"')));
476
+ });
477
+
478
+ test("validateDefinition: prompt-verify missing prompt → rejected", () => {
479
+ const result = validateDefinition({
480
+ version: 1,
481
+ name: "test",
482
+ steps: [{
483
+ id: "a",
484
+ name: "A",
485
+ prompt: "do A",
486
+ verify: { policy: "prompt-verify" },
487
+ }],
488
+ });
489
+ assert.equal(result.valid, false);
490
+ assert.ok(result.errors.some((e) => e.includes('requires a non-empty "prompt"')));
491
+ });
492
+
493
+ // ─── Gap validations: duplicate IDs ──────────────────────────────────────
494
+
495
+ test("validateDefinition: duplicate step IDs → error", () => {
496
+ const result = validateDefinition({
497
+ version: 1,
498
+ name: "test",
499
+ steps: [
500
+ { id: "dup", name: "A", prompt: "do A" },
501
+ { id: "dup", name: "B", prompt: "do B" },
502
+ ],
503
+ });
504
+ assert.equal(result.valid, false);
505
+ assert.ok(result.errors.some((e) => e.includes("Duplicate step id")));
506
+ assert.ok(result.errors.some((e) => e.includes("dup")));
507
+ });
508
+
509
+ // ─── Gap validations: dangling dependencies ──────────────────────────────
510
+
511
+ test("validateDefinition: dangling dependency → error", () => {
512
+ const result = validateDefinition({
513
+ version: 1,
514
+ name: "test",
515
+ steps: [
516
+ { id: "a", name: "A", prompt: "do A" },
517
+ { id: "b", name: "B", prompt: "do B", requires: ["nonexistent"] },
518
+ ],
519
+ });
520
+ assert.equal(result.valid, false);
521
+ assert.ok(result.errors.some((e) => e.includes("requires unknown step")));
522
+ assert.ok(result.errors.some((e) => e.includes("nonexistent")));
523
+ });
524
+
525
+ test("validateDefinition: dangling dependency via depends_on → error", () => {
526
+ const result = validateDefinition({
527
+ version: 1,
528
+ name: "test",
529
+ steps: [
530
+ { id: "a", name: "A", prompt: "do A" },
531
+ { id: "b", name: "B", prompt: "do B", depends_on: ["ghost"] },
532
+ ],
533
+ });
534
+ assert.equal(result.valid, false);
535
+ assert.ok(result.errors.some((e) => e.includes("requires unknown step")));
536
+ assert.ok(result.errors.some((e) => e.includes("ghost")));
537
+ });
538
+
539
+ // ─── Gap validations: self-referencing dependencies ──────────────────────
540
+
541
+ test("validateDefinition: self-referencing dependency → error", () => {
542
+ const result = validateDefinition({
543
+ version: 1,
544
+ name: "test",
545
+ steps: [
546
+ { id: "a", name: "A", prompt: "do A", requires: ["a"] },
547
+ ],
548
+ });
549
+ assert.equal(result.valid, false);
550
+ assert.ok(result.errors.some((e) => e.includes("depends on itself")));
551
+ });
552
+
553
+ // ─── Gap validations: cycle detection ────────────────────────────────────
554
+
555
+ test("validateDefinition: simple cycle (A→B→A) → error", () => {
556
+ const result = validateDefinition({
557
+ version: 1,
558
+ name: "test",
559
+ steps: [
560
+ { id: "a", name: "A", prompt: "do A", requires: ["b"] },
561
+ { id: "b", name: "B", prompt: "do B", requires: ["a"] },
562
+ ],
563
+ });
564
+ assert.equal(result.valid, false);
565
+ assert.ok(result.errors.some((e) => e.includes("Cycle detected")));
566
+ });
567
+
568
+ test("validateDefinition: complex cycle (A→B→C→A) → error", () => {
569
+ const result = validateDefinition({
570
+ version: 1,
571
+ name: "test",
572
+ steps: [
573
+ { id: "a", name: "A", prompt: "do A", requires: ["c"] },
574
+ { id: "b", name: "B", prompt: "do B", requires: ["a"] },
575
+ { id: "c", name: "C", prompt: "do C", requires: ["b"] },
576
+ ],
577
+ });
578
+ assert.equal(result.valid, false);
579
+ assert.ok(result.errors.some((e) => e.includes("Cycle detected")));
580
+ });
581
+
582
+ test("validateDefinition: diamond dependency (no cycle) → accepted", () => {
583
+ // A→B, A→C, B→D, C→D — classic diamond, no cycle
584
+ const result = validateDefinition({
585
+ version: 1,
586
+ name: "test",
587
+ steps: [
588
+ { id: "a", name: "A", prompt: "do A" },
589
+ { id: "b", name: "B", prompt: "do B", requires: ["a"] },
590
+ { id: "c", name: "C", prompt: "do C", requires: ["a"] },
591
+ { id: "d", name: "D", prompt: "do D", requires: ["b", "c"] },
592
+ ],
593
+ });
594
+ assert.equal(result.valid, true, `Expected valid but got errors: ${result.errors.join("; ")}`);
595
+ assert.equal(result.errors.length, 0);
596
+ });
597
+
598
+ test("validateDefinition: linear chain (no cycle) → accepted", () => {
599
+ const result = validateDefinition({
600
+ version: 1,
601
+ name: "test",
602
+ steps: [
603
+ { id: "a", name: "A", prompt: "do A" },
604
+ { id: "b", name: "B", prompt: "do B", requires: ["a"] },
605
+ { id: "c", name: "C", prompt: "do C", requires: ["b"] },
606
+ { id: "d", name: "D", prompt: "do D", requires: ["c"] },
607
+ ],
608
+ });
609
+ assert.equal(result.valid, true);
610
+ });
611
+
612
+ // ─── substituteParams ────────────────────────────────────────────────────
613
+
614
+ test("substituteParams: replaces placeholders with defaults", () => {
615
+ const def: WorkflowDefinition = {
616
+ version: 1,
617
+ name: "test",
618
+ params: { topic: "AI", format: "markdown" },
619
+ steps: [
620
+ { id: "a", name: "A", prompt: "Write about {{topic}} in {{format}}", requires: [], produces: [] },
621
+ ],
622
+ };
623
+ const result = substituteParams(def);
624
+ assert.equal(result.steps[0].prompt, "Write about AI in markdown");
625
+ });
626
+
627
+ test("substituteParams: overrides win over defaults", () => {
628
+ const def: WorkflowDefinition = {
629
+ version: 1,
630
+ name: "test",
631
+ params: { topic: "AI" },
632
+ steps: [
633
+ { id: "a", name: "A", prompt: "Write about {{topic}}", requires: [], produces: [] },
634
+ ],
635
+ };
636
+ const result = substituteParams(def, { topic: "Robotics" });
637
+ assert.equal(result.steps[0].prompt, "Write about Robotics");
638
+ });
639
+
640
+ test("substituteParams: rejects values containing '..'", () => {
641
+ const def: WorkflowDefinition = {
642
+ version: 1,
643
+ name: "test",
644
+ params: { path: "safe" },
645
+ steps: [
646
+ { id: "a", name: "A", prompt: "Read {{path}}", requires: [], produces: [] },
647
+ ],
648
+ };
649
+ assert.throws(
650
+ () => substituteParams(def, { path: "../etc/passwd" }),
651
+ (err: Error) => {
652
+ assert.ok(err.message.includes(".."));
653
+ assert.ok(err.message.includes("path traversal"));
654
+ return true;
655
+ },
656
+ );
657
+ });
658
+
659
+ test("substituteParams: errors on unresolved placeholders", () => {
660
+ const def: WorkflowDefinition = {
661
+ version: 1,
662
+ name: "test",
663
+ steps: [
664
+ { id: "a", name: "A", prompt: "Write about {{topic}}", requires: [], produces: [] },
665
+ ],
666
+ };
667
+ assert.throws(
668
+ () => substituteParams(def),
669
+ (err: Error) => {
670
+ assert.ok(err.message.includes("Unresolved"));
671
+ assert.ok(err.message.includes("topic"));
672
+ return true;
673
+ },
674
+ );
675
+ });
676
+
677
+ test("substituteParams: does not mutate the original definition", () => {
678
+ const def: WorkflowDefinition = {
679
+ version: 1,
680
+ name: "test",
681
+ params: { topic: "AI" },
682
+ steps: [
683
+ { id: "a", name: "A", prompt: "Write about {{topic}}", requires: [], produces: [] },
684
+ ],
685
+ };
686
+ const original = def.steps[0].prompt;
687
+ substituteParams(def);
688
+ assert.equal(def.steps[0].prompt, original, "Original definition should not be mutated");
689
+ });
690
+
691
+ // ─── substitutePromptString ──────────────────────────────────────────────
692
+
693
+ test("substitutePromptString: replaces known placeholders, leaves unknown", () => {
694
+ const result = substitutePromptString(
695
+ "Hello {{name}}, write about {{topic}}",
696
+ { name: "Agent" },
697
+ );
698
+ assert.equal(result, "Hello Agent, write about {{topic}}");
699
+ });
700
+
701
+ test("substitutePromptString: no placeholders → unchanged", () => {
702
+ const result = substitutePromptString("No placeholders here", {});
703
+ assert.equal(result, "No placeholders here");
704
+ });
705
+
706
+ // ─── Edge cases ──────────────────────────────────────────────────────────
707
+
708
+ test("validateDefinition: steps is not an array → error", () => {
709
+ const result = validateDefinition({
710
+ version: 1,
711
+ name: "test",
712
+ steps: "not-an-array",
713
+ });
714
+ assert.equal(result.valid, false);
715
+ assert.ok(result.errors.some((e) => e.includes("steps") && e.includes("array")));
716
+ });
717
+
718
+ test("validateDefinition: valid minimal step (no requires/produces) → accepted", () => {
719
+ const result = validateDefinition({
720
+ version: 1,
721
+ name: "test",
722
+ steps: [{ id: "a", name: "A", prompt: "do A" }],
723
+ });
724
+ assert.equal(result.valid, true);
725
+ assert.equal(result.errors.length, 0);
726
+ });
727
+
728
+ test("loadDefinition: loads without params field → params is undefined", () => {
729
+ const dir = writeDefYaml(`
730
+ version: 1
731
+ name: "no-params"
732
+ steps:
733
+ - id: a
734
+ name: "A"
735
+ prompt: "do A"
736
+ `);
737
+ try {
738
+ const def = loadDefinition(dir, "test-workflow");
739
+ assert.equal(def.params, undefined);
740
+ } finally {
741
+ try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
742
+ }
743
+ });
744
+
745
+ test("loadDefinition: loads without description → description is undefined", () => {
746
+ const dir = writeDefYaml(`
747
+ version: 1
748
+ name: "no-desc"
749
+ steps:
750
+ - id: a
751
+ name: "A"
752
+ prompt: "do A"
753
+ `);
754
+ try {
755
+ const def = loadDefinition(dir, "test-workflow");
756
+ assert.equal(def.description, undefined);
757
+ } finally {
758
+ try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
759
+ }
760
+ });
761
+
762
+ test("loadDefinition: step with no requires/produces defaults to empty arrays", () => {
763
+ const dir = writeDefYaml(`
764
+ version: 1
765
+ name: "defaults"
766
+ steps:
767
+ - id: a
768
+ name: "A"
769
+ prompt: "do A"
770
+ `);
771
+ try {
772
+ const def = loadDefinition(dir, "test-workflow");
773
+ assert.deepEqual(def.steps[0].requires, []);
774
+ assert.deepEqual(def.steps[0].produces, []);
775
+ } finally {
776
+ try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); } catch { /* Windows EPERM */ }
777
+ }
778
+ });