@united-workforce/cli 0.3.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 +45 -11
- package/dist/.build-fingerprint +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +17 -7
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
- package/dist/__tests__/build-step-entry.test.d.ts +2 -0
- package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
- package/dist/__tests__/build-step-entry.test.js +173 -0
- package/dist/__tests__/build-step-entry.test.js.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
- 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__/config.test.js +26 -302
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/current-role.test.js +7 -6
- package/dist/__tests__/current-role.test.js.map +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js +43 -30
- 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__/issue-180-workflow-ref-removed.test.d.ts +2 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.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__/moderator-evaluate.test.js +9 -50
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
- 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.d.ts +2 -0
- package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
- package/dist/__tests__/pid-recycling.test.js +273 -0
- package/dist/__tests__/pid-recycling.test.js.map +1 -0
- package/dist/__tests__/prompt.test.js +365 -2
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +12 -4
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/setup-agent-discovery.test.js +21 -30
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-complexity.test.js +2 -168
- package/dist/__tests__/setup-complexity.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
- package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
- package/dist/__tests__/setup-no-llm.test.js +52 -0
- package/dist/__tests__/setup-no-llm.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +27 -28
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.d.ts +2 -0
- package/dist/__tests__/step-ask.test.d.ts.map +1 -0
- package/dist/__tests__/step-ask.test.js +507 -0
- package/dist/__tests__/step-ask.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.js +1 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-timing.test.js +2 -0
- package/dist/__tests__/step-timing.test.js.map +1 -1
- package/dist/__tests__/store-global-cas.test.js +2 -2
- package/dist/__tests__/store-global-cas.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +28 -26
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +25 -19
- 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 +354 -17
- 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.d.ts +2 -0
- package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
- package/dist/__tests__/thread-poke.test.js +422 -0
- package/dist/__tests__/thread-poke.test.js.map +1 -0
- 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 +21 -15
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-show-status.test.js +17 -28
- package/dist/__tests__/thread-show-status.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 +13 -16
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread-suspended-display.test.js +10 -22
- package/dist/__tests__/thread-suspended-display.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 +15 -13
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +105 -23
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
- package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-list-recursive.test.js +286 -0
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +46 -28
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-show-resolution.test.js +213 -0
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
- package/dist/__tests__/workflow-validate.test.d.ts +2 -0
- package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-validate.test.js +707 -0
- package/dist/__tests__/workflow-validate.test.js.map +1 -0
- 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/background/background.d.ts +22 -1
- package/dist/background/background.d.ts.map +1 -1
- package/dist/background/background.js +83 -6
- package/dist/background/background.js.map +1 -1
- package/dist/background/index.d.ts +1 -1
- package/dist/background/index.d.ts.map +1 -1
- package/dist/background/index.js +1 -1
- package/dist/background/index.js.map +1 -1
- package/dist/background/types.d.ts +1 -0
- package/dist/background/types.d.ts.map +1 -1
- package/dist/cli.js +120 -62
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +3 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +17 -31
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +57 -31
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +12 -39
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +72 -303
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +44 -1
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +255 -11
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +16 -3
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +423 -142
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +9 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +126 -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/moderator/__tests__/evaluate.test.js +31 -17
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
- package/dist/moderator/evaluate.d.ts.map +1 -1
- package/dist/moderator/evaluate.js +4 -16
- package/dist/moderator/evaluate.js.map +1 -1
- package/dist/moderator/index.d.ts +1 -2
- package/dist/moderator/index.d.ts.map +1 -1
- package/dist/moderator/index.js +0 -1
- package/dist/moderator/index.js.map +1 -1
- package/dist/moderator/types.d.ts +6 -10
- package/dist/moderator/types.d.ts.map +1 -1
- package/dist/moderator/types.js +1 -3
- package/dist/moderator/types.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 +6 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +34 -5
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +28 -9
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +75 -16
- package/dist/store.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 +95 -61
- package/dist/validate-semantic.js.map +1 -1
- package/dist/validate.d.ts +6 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +24 -0
- package/dist/validate.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 +9 -10
- package/src/__tests__/adapter-json-roundtrip.test.ts +16 -7
- package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
- package/src/__tests__/build-step-entry.test.ts +203 -0
- package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
- package/src/__tests__/concurrency.test.ts +266 -0
- package/src/__tests__/config.test.ts +33 -321
- package/src/__tests__/current-role.test.ts +7 -6
- package/src/__tests__/e2e-mock-agent.test.ts +65 -30
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
- package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
- package/src/__tests__/format-text-default.test.ts +49 -0
- package/src/__tests__/format-text-registry.test.ts +173 -0
- package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
- package/src/__tests__/log-text-renderer.test.ts +294 -0
- package/src/__tests__/moderator-evaluate.test.ts +9 -52
- 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 +329 -0
- package/src/__tests__/prompt.test.ts +443 -2
- package/src/__tests__/resolve-head-hash.test.ts +11 -4
- package/src/__tests__/setup-agent-discovery.test.ts +26 -51
- package/src/__tests__/setup-complexity.test.ts +1 -203
- package/src/__tests__/setup-no-llm.test.ts +68 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +27 -31
- package/src/__tests__/step-ask.test.ts +677 -0
- package/src/__tests__/step-show-json.test.ts +1 -0
- package/src/__tests__/step-timing.test.ts +2 -0
- package/src/__tests__/store-global-cas.test.ts +2 -2
- package/src/__tests__/store-unified-threads.test.ts +30 -27
- package/src/__tests__/thread-cancel-status.test.ts +27 -20
- package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
- package/src/__tests__/thread-list-filters.test.ts +443 -17
- 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 +554 -0
- package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
- package/src/__tests__/thread-resume.test.ts +20 -15
- package/src/__tests__/thread-show-status.test.ts +17 -29
- 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 +13 -16
- package/src/__tests__/thread-suspended-display.test.ts +10 -22
- package/src/__tests__/thread-test-helpers.ts +15 -1
- package/src/__tests__/thread.test.ts +14 -14
- package/src/__tests__/validate-semantic.test.ts +118 -33
- package/src/__tests__/workflow-list-recursive.test.ts +370 -0
- package/src/__tests__/workflow-resolution.test.ts +48 -29
- package/src/__tests__/workflow-show-resolution.test.ts +286 -0
- package/src/__tests__/workflow-validate.test.ts +828 -0
- package/src/__tests__/write-envelope.test.ts +257 -0
- package/src/background/background.ts +88 -6
- package/src/background/index.ts +2 -0
- package/src/background/types.ts +1 -0
- package/src/cli.ts +184 -77
- package/src/commands/config.ts +16 -33
- package/src/commands/prompt.ts +57 -31
- package/src/commands/setup.ts +80 -358
- package/src/commands/step.ts +339 -12
- package/src/commands/thread.ts +511 -171
- package/src/commands/workflow.ts +155 -4
- 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/moderator/__tests__/evaluate.test.ts +34 -17
- package/src/moderator/evaluate.ts +5 -17
- package/src/moderator/index.ts +1 -6
- package/src/moderator/types.ts +6 -14
- package/src/output-mappers.ts +254 -0
- package/src/schemas.ts +51 -5
- package/src/store.ts +86 -20
- package/src/text-renderers.ts +355 -0
- package/src/validate-semantic.ts +125 -73
- package/src/validate.ts +27 -0
- package/dist/__tests__/setup-validate.test.d.ts +0 -2
- package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
- package/dist/__tests__/setup-validate.test.js +0 -108
- package/dist/__tests__/setup-validate.test.js.map +0 -1
- package/src/__tests__/setup-validate.test.ts +0 -148
- /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
|
@@ -5,6 +5,7 @@ import { validateWorkflow } from "../validate-semantic.js";
|
|
|
5
5
|
/** Build a valid two-role workflow that passes all checks. */
|
|
6
6
|
function makeWorkflow(overrides?: Partial<WorkflowPayload>): WorkflowPayload {
|
|
7
7
|
const base: WorkflowPayload = {
|
|
8
|
+
version: 1,
|
|
8
9
|
name: "test-workflow",
|
|
9
10
|
description: "A test workflow",
|
|
10
11
|
roles: {
|
|
@@ -55,10 +56,10 @@ function makeWorkflow(overrides?: Partial<WorkflowPayload>): WorkflowPayload {
|
|
|
55
56
|
new: { role: "writer", prompt: "Begin writing", location: null },
|
|
56
57
|
resume: { role: "writer", prompt: "Review previous output and continue", location: null },
|
|
57
58
|
},
|
|
58
|
-
writer: { done: { role: "reviewer", prompt: "Review this: {{
|
|
59
|
+
writer: { done: { role: "reviewer", prompt: "Review this: {{ plan }}", location: null } },
|
|
59
60
|
reviewer: {
|
|
60
|
-
approved: { role: "$END", prompt: "Done: {{
|
|
61
|
-
rejected: { role: "writer", prompt: "Fix: {{
|
|
61
|
+
approved: { role: "$END", prompt: "Done: {{ summary }}", location: null },
|
|
62
|
+
rejected: { role: "writer", prompt: "Fix: {{ reason }}", location: null },
|
|
62
63
|
},
|
|
63
64
|
},
|
|
64
65
|
};
|
|
@@ -208,8 +209,6 @@ describe("Suite 2: Graph Structure", () => {
|
|
|
208
209
|
|
|
209
210
|
describe("Suite 3: Status-Edge Consistency", () => {
|
|
210
211
|
test("3.1 user role using _ graph key is treated as an unknown status", () => {
|
|
211
|
-
// "_" is no longer special-cased — it's just a status key that does not
|
|
212
|
-
// match the role's $status enum, so it surfaces as extra/missing keys.
|
|
213
212
|
const wf = makeWorkflow();
|
|
214
213
|
wf.graph.writer = { _: { role: "reviewer", prompt: "Review", location: null } };
|
|
215
214
|
const errors = validateWorkflow(wf);
|
|
@@ -288,7 +287,7 @@ describe("Suite 3b: Enum-Based $status is Rejected", () => {
|
|
|
288
287
|
};
|
|
289
288
|
wf.graph.reviewer = {
|
|
290
289
|
approved: { role: "$END", prompt: "Done", location: null },
|
|
291
|
-
rejected: { role: "writer", prompt: "Fix: {{
|
|
290
|
+
rejected: { role: "writer", prompt: "Fix: {{ comments }}", location: null },
|
|
292
291
|
};
|
|
293
292
|
const errors = validateWorkflow(wf);
|
|
294
293
|
expect(errors.some((e) => e.includes("must define") && e.includes("const"))).toBe(true);
|
|
@@ -307,7 +306,9 @@ describe("Suite 3b: Enum-Based $status is Rejected", () => {
|
|
|
307
306
|
required: ["$status", "plan"],
|
|
308
307
|
} as unknown as string,
|
|
309
308
|
};
|
|
310
|
-
wf.graph.writer = {
|
|
309
|
+
wf.graph.writer = {
|
|
310
|
+
ready: { role: "reviewer", prompt: "Review: {{ plan }}", location: null },
|
|
311
|
+
};
|
|
311
312
|
const errors = validateWorkflow(wf);
|
|
312
313
|
expect(errors.some((e) => e.includes("must define") && e.includes("const"))).toBe(true);
|
|
313
314
|
});
|
|
@@ -352,7 +353,7 @@ describe("Suite 3c: Const-Based Flat Schema", () => {
|
|
|
352
353
|
expect(errors.some((e) => e.includes("extra status keys") && e.includes("extra"))).toBe(true);
|
|
353
354
|
});
|
|
354
355
|
|
|
355
|
-
test("3c.3 flat schema with const $status validates
|
|
356
|
+
test("3c.3 flat schema with const $status validates template vars", () => {
|
|
356
357
|
const wf = makeWorkflow();
|
|
357
358
|
wf.roles.writer = {
|
|
358
359
|
...wf.roles.writer,
|
|
@@ -366,56 +367,48 @@ describe("Suite 3c: Const-Based Flat Schema", () => {
|
|
|
366
367
|
} as unknown as string,
|
|
367
368
|
};
|
|
368
369
|
wf.graph.writer = {
|
|
369
|
-
done: { role: "reviewer", prompt: "Review: {{
|
|
370
|
+
done: { role: "reviewer", prompt: "Review: {{ nonexistent }}", location: null },
|
|
370
371
|
};
|
|
371
372
|
const errors = validateWorkflow(wf);
|
|
372
|
-
expect(
|
|
373
|
-
errors.some(
|
|
374
|
-
(e) => e.includes('prompt variable "nonexistent"') && e.includes('role "writer"'),
|
|
375
|
-
),
|
|
376
|
-
).toBe(true);
|
|
373
|
+
expect(errors.some((e) => e.includes("nonexistent") && e.includes('role "writer"'))).toBe(true);
|
|
377
374
|
});
|
|
378
375
|
});
|
|
379
376
|
|
|
380
|
-
describe("Suite 4:
|
|
381
|
-
test("4.1 prompt references nonexistent variable (
|
|
377
|
+
describe("Suite 4: Template Variable Existence (LiquidJS strict-render)", () => {
|
|
378
|
+
test("4.1 prompt references nonexistent variable (flat schema)", () => {
|
|
382
379
|
const wf = makeWorkflow();
|
|
383
380
|
wf.graph.writer = {
|
|
384
|
-
done: { role: "reviewer", prompt: "Review: {{
|
|
381
|
+
done: { role: "reviewer", prompt: "Review: {{ branch }}", location: null },
|
|
385
382
|
};
|
|
386
383
|
const errors = validateWorkflow(wf);
|
|
387
|
-
expect(
|
|
388
|
-
errors.some(
|
|
389
|
-
(e) => e.includes('prompt variable "branch"') && e.includes('role "writer" frontmatter'),
|
|
390
|
-
),
|
|
391
|
-
).toBe(true);
|
|
384
|
+
expect(errors.some((e) => e.includes("branch") && e.includes('role "writer"'))).toBe(true);
|
|
392
385
|
});
|
|
393
386
|
|
|
394
387
|
test("4.2 prompt references nonexistent variable (multi-exit)", () => {
|
|
395
388
|
const wf = makeWorkflow();
|
|
396
389
|
wf.graph.reviewer = {
|
|
397
|
-
approved: { role: "$END", prompt: "Done: {{
|
|
398
|
-
rejected: { role: "writer", prompt: "Fix: {{
|
|
390
|
+
approved: { role: "$END", prompt: "Done: {{ branch }}", location: null },
|
|
391
|
+
rejected: { role: "writer", prompt: "Fix: {{ reason }}", location: null },
|
|
399
392
|
};
|
|
400
393
|
const errors = validateWorkflow(wf);
|
|
401
394
|
expect(
|
|
402
|
-
errors.some((e) =>
|
|
403
|
-
e.includes('prompt variable "branch" not found in role "reviewer" variant "approved"'),
|
|
404
|
-
),
|
|
395
|
+
errors.some((e) => e.includes("branch") && e.includes("reviewer") && e.includes("approved")),
|
|
405
396
|
).toBe(true);
|
|
406
397
|
});
|
|
407
398
|
|
|
408
|
-
test("4.3 valid
|
|
399
|
+
test("4.3 valid template variables pass", () => {
|
|
409
400
|
const wf = makeWorkflow();
|
|
410
401
|
const errors = validateWorkflow(wf);
|
|
411
402
|
expect(errors).toEqual([]);
|
|
412
403
|
});
|
|
413
404
|
|
|
414
|
-
test("4.4 $status
|
|
405
|
+
test("4.4 $status in template is rejected ($ prefix invalid in LiquidJS)", () => {
|
|
415
406
|
const wf = makeWorkflow();
|
|
416
|
-
wf.graph.writer = {
|
|
407
|
+
wf.graph.writer = {
|
|
408
|
+
done: { role: "reviewer", prompt: "Status: {{ $status }}", location: null },
|
|
409
|
+
};
|
|
417
410
|
const errors = validateWorkflow(wf);
|
|
418
|
-
expect(errors).
|
|
411
|
+
expect(errors.length).toBeGreaterThan(0);
|
|
419
412
|
});
|
|
420
413
|
});
|
|
421
414
|
|
|
@@ -469,6 +462,41 @@ describe("Suite 5: oneOf Discriminant Validity", () => {
|
|
|
469
462
|
});
|
|
470
463
|
});
|
|
471
464
|
|
|
465
|
+
describe("Suite 7: $SUSPEND is no longer a valid graph target", () => {
|
|
466
|
+
test("7.1 edge targeting $SUSPEND is rejected with a migration hint", () => {
|
|
467
|
+
const wf = makeWorkflow();
|
|
468
|
+
wf.graph.writer = {
|
|
469
|
+
done: { role: "$SUSPEND", prompt: "Need more info", location: null },
|
|
470
|
+
};
|
|
471
|
+
const errors = validateWorkflow(wf);
|
|
472
|
+
expect(
|
|
473
|
+
errors.some(
|
|
474
|
+
(e) =>
|
|
475
|
+
e.includes("$SUSPEND") &&
|
|
476
|
+
e.includes("no longer a valid graph target") &&
|
|
477
|
+
e.includes('Emit $status: "$SUSPEND"'),
|
|
478
|
+
),
|
|
479
|
+
).toBe(true);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("7.2 $SUSPEND as a graph node is rejected", () => {
|
|
483
|
+
const wf = makeWorkflow();
|
|
484
|
+
(wf.graph as Record<string, unknown>).$SUSPEND = {
|
|
485
|
+
done: { role: "$END", prompt: "done", location: null },
|
|
486
|
+
};
|
|
487
|
+
const errors = validateWorkflow(wf);
|
|
488
|
+
expect(
|
|
489
|
+
errors.some((e) => e.includes("$SUSPEND") && e.includes("no longer a valid graph node")),
|
|
490
|
+
).toBe(true);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("7.3 a role emitting $SUSPEND from its output (not the graph) passes", () => {
|
|
494
|
+
const wf = makeWorkflow();
|
|
495
|
+
const errors = validateWorkflow(wf);
|
|
496
|
+
expect(errors.some((e) => e.includes("$SUSPEND"))).toBe(false);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
472
500
|
describe("Suite 6: Multiple Errors Collection", () => {
|
|
473
501
|
test("6.1 multiple errors collected", () => {
|
|
474
502
|
const wf = makeWorkflow();
|
|
@@ -487,9 +515,66 @@ describe("Suite 6: Multiple Errors Collection", () => {
|
|
|
487
515
|
};
|
|
488
516
|
// unknown graph reference
|
|
489
517
|
wf.graph.nonexistent = { done: { role: "$END", prompt: "done", location: null } };
|
|
490
|
-
// bad
|
|
491
|
-
wf.graph.writer = { done: { role: "reviewer", prompt: "{{
|
|
518
|
+
// bad template var
|
|
519
|
+
wf.graph.writer = { done: { role: "reviewer", prompt: "{{ badvar }}", location: null } };
|
|
492
520
|
const errors = validateWorkflow(wf);
|
|
493
521
|
expect(errors.length).toBeGreaterThanOrEqual(3);
|
|
494
522
|
});
|
|
495
523
|
});
|
|
524
|
+
|
|
525
|
+
describe("Suite 7: Reserved Frontmatter Properties", () => {
|
|
526
|
+
test("7.1 flat schema with _body property is rejected", () => {
|
|
527
|
+
const wf = makeWorkflow();
|
|
528
|
+
wf.roles.writer = {
|
|
529
|
+
...wf.roles.writer,
|
|
530
|
+
frontmatter: {
|
|
531
|
+
type: "object",
|
|
532
|
+
properties: {
|
|
533
|
+
$status: { const: "done" },
|
|
534
|
+
_body: { type: "string" },
|
|
535
|
+
},
|
|
536
|
+
required: ["$status"],
|
|
537
|
+
} as unknown as string,
|
|
538
|
+
};
|
|
539
|
+
wf.graph.writer = { done: { role: "reviewer", prompt: "ok", location: null } };
|
|
540
|
+
const errors = validateWorkflow(wf);
|
|
541
|
+
expect(errors.some((e) => e.includes("_body") && e.includes("reserved"))).toBe(true);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("7.2 oneOf schema with _body in a variant is rejected", () => {
|
|
545
|
+
const wf = makeWorkflow();
|
|
546
|
+
wf.roles.writer = {
|
|
547
|
+
...wf.roles.writer,
|
|
548
|
+
frontmatter: {
|
|
549
|
+
oneOf: [
|
|
550
|
+
{
|
|
551
|
+
properties: {
|
|
552
|
+
$status: { const: "done" },
|
|
553
|
+
_body: { type: "string" },
|
|
554
|
+
},
|
|
555
|
+
required: ["$status"],
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
properties: {
|
|
559
|
+
$status: { const: "failed" },
|
|
560
|
+
reason: { type: "string" },
|
|
561
|
+
},
|
|
562
|
+
required: ["$status", "reason"],
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
} as unknown as string,
|
|
566
|
+
};
|
|
567
|
+
wf.graph.writer = {
|
|
568
|
+
done: { role: "reviewer", prompt: "ok", location: null },
|
|
569
|
+
failed: { role: "$END", prompt: "failed", location: null },
|
|
570
|
+
};
|
|
571
|
+
const errors = validateWorkflow(wf);
|
|
572
|
+
expect(errors.some((e) => e.includes("_body") && e.includes("reserved"))).toBe(true);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("7.3 schema without _body passes", () => {
|
|
576
|
+
const wf = makeWorkflow();
|
|
577
|
+
const errors = validateWorkflow(wf);
|
|
578
|
+
expect(errors.some((e) => e.includes("_body"))).toBe(false);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { CasRef, WorkflowPayload } from "@united-workforce/protocol";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
6
|
+
import { stringify } from "yaml";
|
|
7
|
+
import { cmdThreadStart } from "../commands/thread.js";
|
|
8
|
+
import { cmdWorkflowList } from "../commands/workflow.js";
|
|
9
|
+
import { discoverProjectWorkflows } from "../store.js";
|
|
10
|
+
import { makeUwfStore } from "./thread-test-helpers.js";
|
|
11
|
+
|
|
12
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
function makeMinimalPayload(name: string, description: string): WorkflowPayload {
|
|
15
|
+
return {
|
|
16
|
+
version: 1,
|
|
17
|
+
name,
|
|
18
|
+
description,
|
|
19
|
+
roles: {
|
|
20
|
+
worker: {
|
|
21
|
+
description: "worker role",
|
|
22
|
+
goal: "do work",
|
|
23
|
+
capabilities: [],
|
|
24
|
+
procedure: "",
|
|
25
|
+
output: "",
|
|
26
|
+
frontmatter: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
$status: { const: "done" },
|
|
30
|
+
},
|
|
31
|
+
required: ["$status"],
|
|
32
|
+
} as unknown as CasRef,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
graph: {
|
|
36
|
+
$START: {
|
|
37
|
+
new: { role: "worker", prompt: "start working", location: null },
|
|
38
|
+
resume: { role: "worker", prompt: "resume working", location: null },
|
|
39
|
+
},
|
|
40
|
+
worker: { done: { role: "$END", prompt: "done", location: null } },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function createWorkflowYaml(name: string, version: string | null = null): Promise<string> {
|
|
46
|
+
const payload = makeMinimalPayload(
|
|
47
|
+
name,
|
|
48
|
+
version !== null ? `Test workflow (${version})` : "Test workflow",
|
|
49
|
+
);
|
|
50
|
+
return stringify(payload);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── fixture ───────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
let tmpDir: string;
|
|
56
|
+
let storageRoot: string;
|
|
57
|
+
let projectRoot: string;
|
|
58
|
+
let savedOcasHome: string | undefined;
|
|
59
|
+
|
|
60
|
+
beforeEach(async () => {
|
|
61
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
62
|
+
tmpDir = await mkdtemp(join(tmpdir(), "uwf-wf-list-recursive-"));
|
|
63
|
+
storageRoot = join(tmpDir, "storage");
|
|
64
|
+
projectRoot = join(tmpDir, "project");
|
|
65
|
+
await mkdir(storageRoot, { recursive: true });
|
|
66
|
+
await mkdir(projectRoot, { recursive: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(async () => {
|
|
70
|
+
if (savedOcasHome === undefined) {
|
|
71
|
+
delete process.env.OCAS_HOME;
|
|
72
|
+
} else {
|
|
73
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
74
|
+
}
|
|
75
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── discoverProjectWorkflows — parent traversal ───────────────────────────────
|
|
79
|
+
|
|
80
|
+
describe("discoverProjectWorkflows — parent traversal", () => {
|
|
81
|
+
test("T1: finds workflows in cwd's .workflows/", async () => {
|
|
82
|
+
const wfDir = join(projectRoot, ".workflows");
|
|
83
|
+
await mkdir(wfDir, { recursive: true });
|
|
84
|
+
await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
|
85
|
+
|
|
86
|
+
const entries = await discoverProjectWorkflows(projectRoot);
|
|
87
|
+
|
|
88
|
+
expect(entries.map((e) => e.name)).toContain("solve-issue");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("T2: finds workflows in ancestor's .workflows/ when called from subdirectory", async () => {
|
|
92
|
+
const wfDir = join(projectRoot, ".workflows");
|
|
93
|
+
await mkdir(wfDir, { recursive: true });
|
|
94
|
+
await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
|
95
|
+
|
|
96
|
+
const subdir = join(projectRoot, "packages", "cli", "src");
|
|
97
|
+
await mkdir(subdir, { recursive: true });
|
|
98
|
+
|
|
99
|
+
const entries = await discoverProjectWorkflows(subdir);
|
|
100
|
+
|
|
101
|
+
expect(entries.map((e) => e.name)).toContain("solve-issue");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("T3: returns [] when no .workflows/ or .workflow/ exists in any ancestor", async () => {
|
|
105
|
+
// Use a deep path under tmpDir that has no .workflows/ or .workflow/ on the way up.
|
|
106
|
+
// (Traversal will stop at filesystem root and find nothing.)
|
|
107
|
+
const deepPath = join(tmpDir, "isolated", "no", "workflow", "here");
|
|
108
|
+
await mkdir(deepPath, { recursive: true });
|
|
109
|
+
|
|
110
|
+
const entries = await discoverProjectWorkflows(deepPath);
|
|
111
|
+
|
|
112
|
+
expect(entries).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("T4: .workflows/ entries win over .workflow/ within the same directory", async () => {
|
|
116
|
+
const primaryDir = join(projectRoot, ".workflows");
|
|
117
|
+
const legacyDir = join(projectRoot, ".workflow");
|
|
118
|
+
await mkdir(primaryDir, { recursive: true });
|
|
119
|
+
await mkdir(legacyDir, { recursive: true });
|
|
120
|
+
|
|
121
|
+
await writeFile(
|
|
122
|
+
join(primaryDir, "solve-issue.yaml"),
|
|
123
|
+
await createWorkflowYaml("solve-issue", "new"),
|
|
124
|
+
);
|
|
125
|
+
await writeFile(
|
|
126
|
+
join(legacyDir, "solve-issue.yaml"),
|
|
127
|
+
await createWorkflowYaml("solve-issue", "legacy"),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const entries = await discoverProjectWorkflows(projectRoot);
|
|
131
|
+
|
|
132
|
+
const match = entries.find((e) => e.name === "solve-issue");
|
|
133
|
+
expect(match).toBeDefined();
|
|
134
|
+
expect(match?.filePath).toBe(join(primaryDir, "solve-issue.yaml"));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("T5: nearest .workflows/ wins over ancestor's .workflows/", async () => {
|
|
138
|
+
const ancestorWf = join(projectRoot, ".workflows");
|
|
139
|
+
await mkdir(ancestorWf, { recursive: true });
|
|
140
|
+
await writeFile(join(ancestorWf, "foo.yaml"), await createWorkflowYaml("foo", "ancestor"));
|
|
141
|
+
|
|
142
|
+
const nearDir = join(projectRoot, "pkg");
|
|
143
|
+
const nearWf = join(nearDir, ".workflows");
|
|
144
|
+
await mkdir(nearWf, { recursive: true });
|
|
145
|
+
await writeFile(join(nearWf, "foo.yaml"), await createWorkflowYaml("foo", "near"));
|
|
146
|
+
|
|
147
|
+
const entries = await discoverProjectWorkflows(nearDir);
|
|
148
|
+
|
|
149
|
+
const match = entries.find((e) => e.name === "foo");
|
|
150
|
+
expect(match).toBeDefined();
|
|
151
|
+
expect(match?.filePath).toBe(join(nearWf, "foo.yaml"));
|
|
152
|
+
// Should not include duplicates from ancestor
|
|
153
|
+
expect(entries.filter((e) => e.name === "foo")).toHaveLength(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("T6: returns all entries from the nearest .workflows/ when called from a deep subdir", async () => {
|
|
157
|
+
const wfDir = join(projectRoot, ".workflows");
|
|
158
|
+
await mkdir(wfDir, { recursive: true });
|
|
159
|
+
await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
|
160
|
+
await writeFile(join(wfDir, "review-code.yaml"), await createWorkflowYaml("review-code"));
|
|
161
|
+
|
|
162
|
+
const deep = join(projectRoot, "a", "b", "c", "d");
|
|
163
|
+
await mkdir(deep, { recursive: true });
|
|
164
|
+
|
|
165
|
+
const entries = await discoverProjectWorkflows(deep);
|
|
166
|
+
|
|
167
|
+
const names = entries.map((e) => e.name).sort();
|
|
168
|
+
expect(names).toEqual(["review-code", "solve-issue"]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("T7: discovers folder-based layout (name/index.yaml) via parent traversal under .workflows/", async () => {
|
|
172
|
+
const folderDir = join(projectRoot, ".workflows", "solve-issue");
|
|
173
|
+
await mkdir(folderDir, { recursive: true });
|
|
174
|
+
await writeFile(join(folderDir, "index.yaml"), await createWorkflowYaml("solve-issue"));
|
|
175
|
+
|
|
176
|
+
const subdir = join(projectRoot, "deep", "sub");
|
|
177
|
+
await mkdir(subdir, { recursive: true });
|
|
178
|
+
|
|
179
|
+
const entries = await discoverProjectWorkflows(subdir);
|
|
180
|
+
|
|
181
|
+
const match = entries.find((e) => e.name === "solve-issue");
|
|
182
|
+
expect(match).toBeDefined();
|
|
183
|
+
expect(match?.filePath).toBe(join(folderDir, "index.yaml"));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("T8: .workflow/ (legacy) is still discovered when .workflows/ does not exist", async () => {
|
|
187
|
+
const legacyDir = join(projectRoot, ".workflow");
|
|
188
|
+
await mkdir(legacyDir, { recursive: true });
|
|
189
|
+
await writeFile(join(legacyDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
|
190
|
+
|
|
191
|
+
const entries = await discoverProjectWorkflows(projectRoot);
|
|
192
|
+
|
|
193
|
+
const match = entries.find((e) => e.name === "solve-issue");
|
|
194
|
+
expect(match).toBeDefined();
|
|
195
|
+
expect(match?.filePath).toBe(join(legacyDir, "solve-issue.yaml"));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("T9: nearest directory with EITHER variant stops traversal", async () => {
|
|
199
|
+
// Setup: ancestor .workflows/ + near .workflow/ only — near wins, ancestor not merged.
|
|
200
|
+
const ancestorWf = join(tmpDir, ".workflows");
|
|
201
|
+
await mkdir(ancestorWf, { recursive: true });
|
|
202
|
+
await writeFile(join(ancestorWf, "leak.yaml"), await createWorkflowYaml("leak"));
|
|
203
|
+
|
|
204
|
+
const nearLegacyDir = join(projectRoot, ".workflow");
|
|
205
|
+
await mkdir(nearLegacyDir, { recursive: true });
|
|
206
|
+
await writeFile(join(nearLegacyDir, "local.yaml"), await createWorkflowYaml("local"));
|
|
207
|
+
|
|
208
|
+
const entries = await discoverProjectWorkflows(projectRoot);
|
|
209
|
+
const names = entries.map((e) => e.name);
|
|
210
|
+
expect(names).toContain("local");
|
|
211
|
+
expect(names).not.toContain("leak");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ── discoverProjectWorkflows — .git boundary ─────────────────────────────────
|
|
216
|
+
|
|
217
|
+
describe("discoverProjectWorkflows — .git boundary", () => {
|
|
218
|
+
test("G1: .git directory stops traversal", async () => {
|
|
219
|
+
// Setup: tmpDir/repo/.git/ (dir), tmpDir/.workflows/leak.yaml, start from tmpDir/repo/sub/deep/
|
|
220
|
+
const repoDir = join(tmpDir, "repo");
|
|
221
|
+
const gitDir = join(repoDir, ".git");
|
|
222
|
+
await mkdir(gitDir, { recursive: true });
|
|
223
|
+
|
|
224
|
+
// Workflow above repo root — should NOT be reachable
|
|
225
|
+
const leakDir = join(tmpDir, ".workflows");
|
|
226
|
+
await mkdir(leakDir, { recursive: true });
|
|
227
|
+
await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
|
|
228
|
+
|
|
229
|
+
const startFrom = join(repoDir, "sub", "deep");
|
|
230
|
+
await mkdir(startFrom, { recursive: true });
|
|
231
|
+
|
|
232
|
+
const entries = await discoverProjectWorkflows(startFrom);
|
|
233
|
+
expect(entries).toEqual([]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("G2: .git file (worktree) stops traversal", async () => {
|
|
237
|
+
// Setup: tmpDir/repo/.git as a FILE, tmpDir/.workflows/leak.yaml, start from tmpDir/repo/pkg/
|
|
238
|
+
const repoDir = join(tmpDir, "repo");
|
|
239
|
+
await mkdir(repoDir, { recursive: true });
|
|
240
|
+
await writeFile(join(repoDir, ".git"), "gitdir: /some/other/path/.git/worktrees/repo");
|
|
241
|
+
|
|
242
|
+
const leakDir = join(tmpDir, ".workflows");
|
|
243
|
+
await mkdir(leakDir, { recursive: true });
|
|
244
|
+
await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
|
|
245
|
+
|
|
246
|
+
const startFrom = join(repoDir, "pkg");
|
|
247
|
+
await mkdir(startFrom, { recursive: true });
|
|
248
|
+
|
|
249
|
+
const entries = await discoverProjectWorkflows(startFrom);
|
|
250
|
+
expect(entries).toEqual([]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("G3: workflow at .git boundary IS found (primary .workflows/)", async () => {
|
|
254
|
+
// Setup: tmpDir/repo/.git/ (dir), tmpDir/repo/.workflows/local.yaml, start from tmpDir/repo/sub/
|
|
255
|
+
const repoDir = join(tmpDir, "repo");
|
|
256
|
+
const gitDir = join(repoDir, ".git");
|
|
257
|
+
await mkdir(gitDir, { recursive: true });
|
|
258
|
+
|
|
259
|
+
const wfDir = join(repoDir, ".workflows");
|
|
260
|
+
await mkdir(wfDir, { recursive: true });
|
|
261
|
+
await writeFile(join(wfDir, "local.yaml"), await createWorkflowYaml("local"));
|
|
262
|
+
|
|
263
|
+
const startFrom = join(repoDir, "sub");
|
|
264
|
+
await mkdir(startFrom, { recursive: true });
|
|
265
|
+
|
|
266
|
+
const entries = await discoverProjectWorkflows(startFrom);
|
|
267
|
+
expect(entries.map((e) => e.name)).toContain("local");
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("G4: workflow below .git is found, above is not", async () => {
|
|
271
|
+
// Setup: tmpDir/repo/.git/ + tmpDir/repo/.workflows/local.yaml + tmpDir/.workflows/leak.yaml
|
|
272
|
+
const repoDir = join(tmpDir, "repo");
|
|
273
|
+
const gitDir = join(repoDir, ".git");
|
|
274
|
+
await mkdir(gitDir, { recursive: true });
|
|
275
|
+
|
|
276
|
+
const localWfDir = join(repoDir, ".workflows");
|
|
277
|
+
await mkdir(localWfDir, { recursive: true });
|
|
278
|
+
await writeFile(join(localWfDir, "local.yaml"), await createWorkflowYaml("local"));
|
|
279
|
+
|
|
280
|
+
const leakDir = join(tmpDir, ".workflows");
|
|
281
|
+
await mkdir(leakDir, { recursive: true });
|
|
282
|
+
await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
|
|
283
|
+
|
|
284
|
+
const startFrom = join(repoDir, "sub");
|
|
285
|
+
await mkdir(startFrom, { recursive: true });
|
|
286
|
+
|
|
287
|
+
const entries = await discoverProjectWorkflows(startFrom);
|
|
288
|
+
expect(entries.map((e) => e.name)).toEqual(["local"]);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// ── findWorkflowInParents (via cmdThreadStart) — .git boundary ───────────────
|
|
293
|
+
|
|
294
|
+
describe("findWorkflowInParents via cmdThreadStart — .git boundary", () => {
|
|
295
|
+
test("G5: .git stops traversal — workflow above boundary is not found", async () => {
|
|
296
|
+
await makeUwfStore(storageRoot);
|
|
297
|
+
const repoDir = join(tmpDir, "repo");
|
|
298
|
+
const gitDir = join(repoDir, ".git");
|
|
299
|
+
await mkdir(gitDir, { recursive: true });
|
|
300
|
+
|
|
301
|
+
// Workflow above .git boundary
|
|
302
|
+
const leakDir = join(tmpDir, ".workflows");
|
|
303
|
+
await mkdir(leakDir, { recursive: true });
|
|
304
|
+
await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
|
|
305
|
+
|
|
306
|
+
const startFrom = join(repoDir, "sub");
|
|
307
|
+
await mkdir(startFrom, { recursive: true });
|
|
308
|
+
|
|
309
|
+
// cmdThreadStart should fail — "leak" is above the .git boundary
|
|
310
|
+
await expect(cmdThreadStart(storageRoot, "leak", "prompt", startFrom)).rejects.toThrow();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("G6: workflow at .git boundary IS found via cmdThreadStart", async () => {
|
|
314
|
+
await makeUwfStore(storageRoot);
|
|
315
|
+
const repoDir = join(tmpDir, "repo");
|
|
316
|
+
const gitDir = join(repoDir, ".git");
|
|
317
|
+
await mkdir(gitDir, { recursive: true });
|
|
318
|
+
|
|
319
|
+
const wfDir = join(repoDir, ".workflows");
|
|
320
|
+
await mkdir(wfDir, { recursive: true });
|
|
321
|
+
await writeFile(join(wfDir, "local.yaml"), await createWorkflowYaml("local"));
|
|
322
|
+
|
|
323
|
+
const startFrom = join(repoDir, "sub");
|
|
324
|
+
await mkdir(startFrom, { recursive: true });
|
|
325
|
+
|
|
326
|
+
const result = await cmdThreadStart(storageRoot, "local", "prompt", startFrom);
|
|
327
|
+
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// ── cmdWorkflowList — parent traversal ───────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
describe("cmdWorkflowList — parent traversal", () => {
|
|
334
|
+
test("B9: lists local workflows discovered from a subdirectory", async () => {
|
|
335
|
+
await makeUwfStore(storageRoot);
|
|
336
|
+
const wfDir = join(projectRoot, ".workflows");
|
|
337
|
+
await mkdir(wfDir, { recursive: true });
|
|
338
|
+
await writeFile(join(wfDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
|
339
|
+
|
|
340
|
+
const subdir = join(projectRoot, "packages", "foo", "src");
|
|
341
|
+
await mkdir(subdir, { recursive: true });
|
|
342
|
+
|
|
343
|
+
const result = await cmdWorkflowList(storageRoot, subdir);
|
|
344
|
+
|
|
345
|
+
const match = result.find((e) => e.name === "solve-issue");
|
|
346
|
+
expect(match).toBeDefined();
|
|
347
|
+
expect(match?.hash).toBe("(local)");
|
|
348
|
+
expect(match?.origin).toBe("local");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("aligns with cmdThreadStart discovery from same subdirectory", async () => {
|
|
352
|
+
await makeUwfStore(storageRoot);
|
|
353
|
+
const wfDir = join(projectRoot, ".workflows");
|
|
354
|
+
await mkdir(wfDir, { recursive: true });
|
|
355
|
+
await writeFile(join(wfDir, "foo.yaml"), await createWorkflowYaml("foo"));
|
|
356
|
+
|
|
357
|
+
const subdir = join(projectRoot, "packages", "foo", "src");
|
|
358
|
+
await mkdir(subdir, { recursive: true });
|
|
359
|
+
|
|
360
|
+
// cmdThreadStart already resolves foo successfully from subdir (existing behavior)
|
|
361
|
+
const startResult = await cmdThreadStart(storageRoot, "foo", "prompt", subdir);
|
|
362
|
+
expect(startResult.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
|
363
|
+
|
|
364
|
+
// cmdWorkflowList must ALSO include foo (newly aligned behavior)
|
|
365
|
+
const listResult = await cmdWorkflowList(storageRoot, subdir);
|
|
366
|
+
const match = listResult.find((e) => e.name === "foo");
|
|
367
|
+
expect(match).toBeDefined();
|
|
368
|
+
expect(match?.origin).toBe("local");
|
|
369
|
+
});
|
|
370
|
+
});
|