@united-workforce/cli 0.2.1-rc.9 → 0.4.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 +15 -8
- package/dist/__tests__/adapter-json-roundtrip.test.js +1 -1
- 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__/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 +20 -23
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- 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__/moderator-evaluate.test.js +9 -50
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
- 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 +271 -0
- package/dist/__tests__/pid-recycling.test.js.map +1 -0
- package/dist/__tests__/prompt.test.js +321 -0
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +4 -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 +24 -27
- 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 +499 -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 +9 -9
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +6 -6
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-list-filters.test.js +344 -9
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- 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 +412 -0
- package/dist/__tests__/thread-poke.test.js.map +1 -0
- package/dist/__tests__/thread-resume.test.js +10 -14
- 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-suspend-step.test.js +8 -14
- 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.js +4 -4
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +49 -21
- 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 +283 -0
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +36 -21
- 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 +210 -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 +687 -0
- package/dist/__tests__/workflow-validate.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 +66 -31
- 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 +7 -33
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +15 -2
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +7 -39
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +27 -302
- 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 +379 -140
- 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 +130 -6
- package/dist/commands/workflow.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/schemas.d.ts +2 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -3
- 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/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +83 -66
- 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/package.json +8 -10
- package/src/__tests__/adapter-json-roundtrip.test.ts +1 -1
- 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__/config.test.ts +33 -321
- package/src/__tests__/current-role.test.ts +7 -6
- package/src/__tests__/e2e-mock-agent.test.ts +20 -23
- 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__/issue-180-workflow-ref-removed.test.ts +43 -0
- package/src/__tests__/moderator-evaluate.test.ts +9 -52
- package/src/__tests__/pid-recycling.test.ts +328 -0
- package/src/__tests__/prompt.test.ts +397 -0
- package/src/__tests__/resolve-head-hash.test.ts +4 -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 +24 -30
- package/src/__tests__/step-ask.test.ts +670 -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 +9 -9
- package/src/__tests__/thread-cancel-status.test.ts +6 -6
- package/src/__tests__/thread-list-filters.test.ts +434 -8
- package/src/__tests__/thread-poke.test.ts +545 -0
- package/src/__tests__/thread-resume.test.ts +10 -14
- package/src/__tests__/thread-show-status.test.ts +17 -29
- package/src/__tests__/thread-suspend-step.test.ts +8 -14
- package/src/__tests__/thread-suspended-display.test.ts +10 -22
- package/src/__tests__/thread.test.ts +4 -4
- package/src/__tests__/validate-semantic.test.ts +59 -31
- package/src/__tests__/workflow-list-recursive.test.ts +370 -0
- package/src/__tests__/workflow-resolution.test.ts +39 -21
- package/src/__tests__/workflow-show-resolution.test.ts +285 -0
- package/src/__tests__/workflow-validate.test.ts +806 -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 +97 -47
- package/src/commands/config.ts +7 -35
- package/src/commands/prompt.ts +15 -2
- package/src/commands/setup.ts +29 -357
- package/src/commands/step.ts +339 -12
- package/src/commands/thread.ts +463 -169
- package/src/commands/workflow.ts +159 -4
- 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/schemas.ts +13 -3
- package/src/store.ts +86 -20
- package/src/validate-semantic.ts +109 -78
- 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
|
@@ -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 type { UwfStore } from "../store.js";
|
|
10
|
+
import { createUwfStore, discoverProjectWorkflows } from "../store.js";
|
|
11
|
+
|
|
12
|
+
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
|
|
15
|
+
const casDir = join(storageRoot, "cas");
|
|
16
|
+
await mkdir(casDir, { recursive: true });
|
|
17
|
+
process.env.OCAS_HOME = casDir;
|
|
18
|
+
return createUwfStore(storageRoot);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeMinimalPayload(name: string, description: string): WorkflowPayload {
|
|
22
|
+
return {
|
|
23
|
+
version: 1,
|
|
24
|
+
name,
|
|
25
|
+
description,
|
|
26
|
+
roles: {
|
|
27
|
+
worker: {
|
|
28
|
+
description: "worker role",
|
|
29
|
+
goal: "do work",
|
|
30
|
+
capabilities: [],
|
|
31
|
+
procedure: "",
|
|
32
|
+
output: "",
|
|
33
|
+
frontmatter: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
$status: { const: "done" },
|
|
37
|
+
},
|
|
38
|
+
required: ["$status"],
|
|
39
|
+
} as unknown as CasRef,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
graph: {
|
|
43
|
+
$START: {
|
|
44
|
+
new: { role: "worker", prompt: "start working", location: null },
|
|
45
|
+
resume: { role: "worker", prompt: "resume working", location: null },
|
|
46
|
+
},
|
|
47
|
+
worker: { done: { role: "$END", prompt: "done", location: null } },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function createWorkflowYaml(name: string, version: string | null = null): Promise<string> {
|
|
53
|
+
const payload = makeMinimalPayload(
|
|
54
|
+
name,
|
|
55
|
+
version !== null ? `Test workflow (${version})` : "Test workflow",
|
|
56
|
+
);
|
|
57
|
+
return stringify(payload);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── fixture ───────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
let tmpDir: string;
|
|
63
|
+
let storageRoot: string;
|
|
64
|
+
let projectRoot: string;
|
|
65
|
+
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
tmpDir = await mkdtemp(join(tmpdir(), "uwf-wf-list-recursive-"));
|
|
68
|
+
storageRoot = join(tmpDir, "storage");
|
|
69
|
+
projectRoot = join(tmpDir, "project");
|
|
70
|
+
await mkdir(storageRoot, { recursive: true });
|
|
71
|
+
await mkdir(projectRoot, { recursive: true });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(async () => {
|
|
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
|
+
});
|
|
@@ -19,6 +19,7 @@ async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
|
|
|
19
19
|
|
|
20
20
|
function makeMinimalPayload(name: string, description: string): WorkflowPayload {
|
|
21
21
|
return {
|
|
22
|
+
version: 1,
|
|
22
23
|
name,
|
|
23
24
|
description,
|
|
24
25
|
roles: {
|
|
@@ -180,9 +181,9 @@ describe("Strategy 2: File Path Resolution", () => {
|
|
|
180
181
|
// ── Strategy 3: Local Discovery (Parent Traversal) ────────────────────────────
|
|
181
182
|
|
|
182
183
|
describe("Strategy 3: Local Discovery", () => {
|
|
183
|
-
test("should find workflow in current directory .
|
|
184
|
+
test("should find workflow in current directory .workflows/", async () => {
|
|
184
185
|
await makeUwfStore(storageRoot);
|
|
185
|
-
const workflowDir = join(projectRoot, ".
|
|
186
|
+
const workflowDir = join(projectRoot, ".workflows");
|
|
186
187
|
await mkdir(workflowDir, { recursive: true });
|
|
187
188
|
await writeFile(join(workflowDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
|
188
189
|
|
|
@@ -197,9 +198,9 @@ describe("Strategy 3: Local Discovery", () => {
|
|
|
197
198
|
}
|
|
198
199
|
});
|
|
199
200
|
|
|
200
|
-
test("should find workflow in parent directory .
|
|
201
|
+
test("should find workflow in parent directory .workflows/", async () => {
|
|
201
202
|
await makeUwfStore(storageRoot);
|
|
202
|
-
const workflowDir = join(projectRoot, ".
|
|
203
|
+
const workflowDir = join(projectRoot, ".workflows");
|
|
203
204
|
await mkdir(workflowDir, { recursive: true });
|
|
204
205
|
await writeFile(join(workflowDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
|
205
206
|
|
|
@@ -219,19 +220,19 @@ describe("Strategy 3: Local Discovery", () => {
|
|
|
219
220
|
await expect(cmdThreadStart(storageRoot, "nonexistent", "prompt", deepPath)).rejects.toThrow();
|
|
220
221
|
});
|
|
221
222
|
|
|
222
|
-
test("should prefer .
|
|
223
|
+
test("should prefer .workflows/ over .workflow/ directory", async () => {
|
|
223
224
|
await makeUwfStore(storageRoot);
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
await mkdir(
|
|
227
|
-
await mkdir(
|
|
225
|
+
const primaryDir = join(projectRoot, ".workflows");
|
|
226
|
+
const legacyDir = join(projectRoot, ".workflow");
|
|
227
|
+
await mkdir(primaryDir, { recursive: true });
|
|
228
|
+
await mkdir(legacyDir, { recursive: true });
|
|
228
229
|
|
|
229
230
|
await writeFile(
|
|
230
|
-
join(
|
|
231
|
+
join(primaryDir, "solve-issue.yaml"),
|
|
231
232
|
await createWorkflowYaml("solve-issue", "1"),
|
|
232
233
|
);
|
|
233
234
|
await writeFile(
|
|
234
|
-
join(
|
|
235
|
+
join(legacyDir, "solve-issue.yaml"),
|
|
235
236
|
await createWorkflowYaml("solve-issue", "2"),
|
|
236
237
|
);
|
|
237
238
|
|
|
@@ -245,9 +246,9 @@ describe("Strategy 3: Local Discovery", () => {
|
|
|
245
246
|
}
|
|
246
247
|
});
|
|
247
248
|
|
|
248
|
-
test("should support .yml extension in local discovery", async () => {
|
|
249
|
+
test("should support .yml extension in local discovery under .workflows/", async () => {
|
|
249
250
|
await makeUwfStore(storageRoot);
|
|
250
|
-
const workflowDir = join(projectRoot, ".
|
|
251
|
+
const workflowDir = join(projectRoot, ".workflows");
|
|
251
252
|
await mkdir(workflowDir, { recursive: true });
|
|
252
253
|
await writeFile(join(workflowDir, "solve-issue.yml"), await createWorkflowYaml("solve-issue"));
|
|
253
254
|
|
|
@@ -256,9 +257,9 @@ describe("Strategy 3: Local Discovery", () => {
|
|
|
256
257
|
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
|
257
258
|
});
|
|
258
259
|
|
|
259
|
-
test("should find workflow in folder-based layout (name
|
|
260
|
+
test("should find workflow in folder-based layout (.workflows/<name>/index.yaml)", async () => {
|
|
260
261
|
await makeUwfStore(storageRoot);
|
|
261
|
-
const workflowDir = join(projectRoot, ".
|
|
262
|
+
const workflowDir = join(projectRoot, ".workflows", "solve-issue");
|
|
262
263
|
await mkdir(workflowDir, { recursive: true });
|
|
263
264
|
await writeFile(join(workflowDir, "index.yaml"), await createWorkflowYaml("solve-issue"));
|
|
264
265
|
|
|
@@ -273,9 +274,9 @@ describe("Strategy 3: Local Discovery", () => {
|
|
|
273
274
|
}
|
|
274
275
|
});
|
|
275
276
|
|
|
276
|
-
test("should prefer flat file over folder-based layout", async () => {
|
|
277
|
+
test("should prefer flat file over folder-based layout under .workflows/", async () => {
|
|
277
278
|
await makeUwfStore(storageRoot);
|
|
278
|
-
const workflowDir = join(projectRoot, ".
|
|
279
|
+
const workflowDir = join(projectRoot, ".workflows");
|
|
279
280
|
await mkdir(workflowDir, { recursive: true });
|
|
280
281
|
await writeFile(
|
|
281
282
|
join(workflowDir, "solve-issue.yaml"),
|
|
@@ -298,6 +299,23 @@ describe("Strategy 3: Local Discovery", () => {
|
|
|
298
299
|
expect((node.payload as WorkflowPayload).description).toBe("Test workflow (flat)");
|
|
299
300
|
}
|
|
300
301
|
});
|
|
302
|
+
|
|
303
|
+
test("should resolve from legacy .workflow/ when .workflows/ is absent", async () => {
|
|
304
|
+
await makeUwfStore(storageRoot);
|
|
305
|
+
const legacyDir = join(projectRoot, ".workflow");
|
|
306
|
+
await mkdir(legacyDir, { recursive: true });
|
|
307
|
+
await writeFile(join(legacyDir, "solve-issue.yaml"), await createWorkflowYaml("solve-issue"));
|
|
308
|
+
|
|
309
|
+
const result = await cmdThreadStart(storageRoot, "solve-issue", "prompt", projectRoot);
|
|
310
|
+
|
|
311
|
+
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
|
312
|
+
const uwf = await makeUwfStore(storageRoot);
|
|
313
|
+
const node = uwf.store.cas.get(result.workflow);
|
|
314
|
+
expect(node).not.toBeNull();
|
|
315
|
+
if (node !== null) {
|
|
316
|
+
expect((node.payload as WorkflowPayload).name).toBe("solve-issue");
|
|
317
|
+
}
|
|
318
|
+
});
|
|
301
319
|
});
|
|
302
320
|
|
|
303
321
|
// ── Strategy 4: Global Registry Fallback ──────────────────────────────────────
|
|
@@ -329,8 +347,8 @@ describe("Resolution Priority", () => {
|
|
|
329
347
|
test("should use explicit file path over local discovery", async () => {
|
|
330
348
|
await makeUwfStore(storageRoot);
|
|
331
349
|
|
|
332
|
-
// Setup: Create workflow in .
|
|
333
|
-
const workflowDir = join(projectRoot, ".
|
|
350
|
+
// Setup: Create workflow in .workflows/ AND as explicit file
|
|
351
|
+
const workflowDir = join(projectRoot, ".workflows");
|
|
334
352
|
await mkdir(workflowDir, { recursive: true });
|
|
335
353
|
await writeFile(
|
|
336
354
|
join(workflowDir, "solve-issue.yaml"),
|
|
@@ -358,8 +376,8 @@ describe("Resolution Priority", () => {
|
|
|
358
376
|
const globalHash = await storeWorkflow(uwf, "solve-issue");
|
|
359
377
|
saveWorkflowRegistry(uwf.varStore, "solve-issue", globalHash);
|
|
360
378
|
|
|
361
|
-
// Setup: Create local .
|
|
362
|
-
const workflowDir = join(projectRoot, ".
|
|
379
|
+
// Setup: Create local .workflows/
|
|
380
|
+
const workflowDir = join(projectRoot, ".workflows");
|
|
363
381
|
await mkdir(workflowDir, { recursive: true });
|
|
364
382
|
const localYaml = await createWorkflowYaml("solve-issue", "local");
|
|
365
383
|
await writeFile(join(workflowDir, "solve-issue.yaml"), localYaml);
|