@united-workforce/cli 0.1.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/LICENSE +21 -0
- package/README.md +221 -0
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +147 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +685 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/current-role.test.d.ts +2 -0
- package/dist/__tests__/current-role.test.d.ts.map +1 -0
- package/dist/__tests__/current-role.test.js +401 -0
- package/dist/__tests__/current-role.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.d.ts +2 -0
- package/dist/__tests__/e2e-mock-agent.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +401 -0
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -0
- package/dist/__tests__/include-tag.test.d.ts +2 -0
- package/dist/__tests__/include-tag.test.d.ts.map +1 -0
- package/dist/__tests__/include-tag.test.js +69 -0
- package/dist/__tests__/include-tag.test.js.map +1 -0
- package/dist/__tests__/log.test.d.ts +2 -0
- package/dist/__tests__/log.test.d.ts.map +1 -0
- package/dist/__tests__/log.test.js +161 -0
- package/dist/__tests__/log.test.js.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.d.ts +2 -0
- package/dist/__tests__/moderator-evaluate.test.d.ts.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.js +170 -0
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -0
- package/dist/__tests__/preload.d.ts +3 -0
- package/dist/__tests__/preload.d.ts.map +1 -0
- package/dist/__tests__/preload.js +6 -0
- package/dist/__tests__/preload.js.map +1 -0
- package/dist/__tests__/prompt.test.d.ts +2 -0
- package/dist/__tests__/prompt.test.d.ts.map +1 -0
- package/dist/__tests__/prompt.test.js +111 -0
- package/dist/__tests__/prompt.test.js.map +1 -0
- package/dist/__tests__/resolve-head-hash.test.d.ts +2 -0
- package/dist/__tests__/resolve-head-hash.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-head-hash.test.js +66 -0
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.d.ts +2 -0
- package/dist/__tests__/setup-agent-discovery.test.d.ts.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.js +119 -0
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -0
- package/dist/__tests__/setup-complexity.test.d.ts +2 -0
- package/dist/__tests__/setup-complexity.test.d.ts.map +1 -0
- package/dist/__tests__/setup-complexity.test.js +314 -0
- package/dist/__tests__/setup-complexity.test.js.map +1 -0
- package/dist/__tests__/setup-validate.test.d.ts +2 -0
- package/dist/__tests__/setup-validate.test.d.ts.map +1 -0
- package/dist/__tests__/setup-validate.test.js +108 -0
- package/dist/__tests__/setup-validate.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.d.ts +2 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.d.ts.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +107 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -0
- package/dist/__tests__/spawn-agent-json.test.d.ts +2 -0
- package/dist/__tests__/spawn-agent-json.test.d.ts.map +1 -0
- package/dist/__tests__/spawn-agent-json.test.js +79 -0
- package/dist/__tests__/spawn-agent-json.test.js.map +1 -0
- package/dist/__tests__/step-read.test.d.ts +2 -0
- package/dist/__tests__/step-read.test.d.ts.map +1 -0
- package/dist/__tests__/step-read.test.js +561 -0
- package/dist/__tests__/step-read.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.d.ts +2 -0
- package/dist/__tests__/step-show-json.test.d.ts.map +1 -0
- package/dist/__tests__/step-show-json.test.js +311 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -0
- package/dist/__tests__/step-timing.test.d.ts +2 -0
- package/dist/__tests__/step-timing.test.d.ts.map +1 -0
- package/dist/__tests__/step-timing.test.js +345 -0
- package/dist/__tests__/step-timing.test.js.map +1 -0
- package/dist/__tests__/store-global-cas.test.d.ts +2 -0
- package/dist/__tests__/store-global-cas.test.d.ts.map +1 -0
- package/dist/__tests__/store-global-cas.test.js +235 -0
- package/dist/__tests__/store-global-cas.test.js.map +1 -0
- package/dist/__tests__/store-storage-root.test.d.ts +2 -0
- package/dist/__tests__/store-storage-root.test.d.ts.map +1 -0
- package/dist/__tests__/store-storage-root.test.js +43 -0
- package/dist/__tests__/store-storage-root.test.js.map +1 -0
- package/dist/__tests__/store-unified-threads.test.d.ts +2 -0
- package/dist/__tests__/store-unified-threads.test.d.ts.map +1 -0
- package/dist/__tests__/store-unified-threads.test.js +189 -0
- package/dist/__tests__/store-unified-threads.test.js.map +1 -0
- package/dist/__tests__/thread-cancel-status.test.d.ts +2 -0
- package/dist/__tests__/thread-cancel-status.test.d.ts.map +1 -0
- package/dist/__tests__/thread-cancel-status.test.js +111 -0
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.d.ts +2 -0
- package/dist/__tests__/thread-list-filters.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +442 -0
- package/dist/__tests__/thread-list-filters.test.js.map +1 -0
- package/dist/__tests__/thread-location.test.d.ts +2 -0
- package/dist/__tests__/thread-location.test.d.ts.map +1 -0
- package/dist/__tests__/thread-location.test.js +159 -0
- package/dist/__tests__/thread-location.test.js.map +1 -0
- package/dist/__tests__/thread-read-quota.test.d.ts +2 -0
- package/dist/__tests__/thread-read-quota.test.d.ts.map +1 -0
- package/dist/__tests__/thread-read-quota.test.js +546 -0
- package/dist/__tests__/thread-read-quota.test.js.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.d.ts +2 -0
- package/dist/__tests__/thread-read-xml-tags.test.d.ts.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.js +610 -0
- package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -0
- package/dist/__tests__/thread-resume.test.d.ts +2 -0
- package/dist/__tests__/thread-resume.test.d.ts.map +1 -0
- package/dist/__tests__/thread-resume.test.js +592 -0
- package/dist/__tests__/thread-resume.test.js.map +1 -0
- package/dist/__tests__/thread-show-status.test.d.ts +2 -0
- package/dist/__tests__/thread-show-status.test.d.ts.map +1 -0
- package/dist/__tests__/thread-show-status.test.js +267 -0
- package/dist/__tests__/thread-show-status.test.js.map +1 -0
- package/dist/__tests__/thread-start-cwd-cli.test.d.ts +2 -0
- package/dist/__tests__/thread-start-cwd-cli.test.d.ts.map +1 -0
- package/dist/__tests__/thread-start-cwd-cli.test.js +130 -0
- package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -0
- package/dist/__tests__/thread-step-count.test.d.ts +2 -0
- package/dist/__tests__/thread-step-count.test.d.ts.map +1 -0
- package/dist/__tests__/thread-step-count.test.js +55 -0
- package/dist/__tests__/thread-step-count.test.js.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.d.ts +2 -0
- package/dist/__tests__/thread-suspend-step.test.d.ts.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.js +155 -0
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -0
- package/dist/__tests__/thread-suspended-display.test.d.ts +2 -0
- package/dist/__tests__/thread-suspended-display.test.d.ts.map +1 -0
- package/dist/__tests__/thread-suspended-display.test.js +247 -0
- package/dist/__tests__/thread-suspended-display.test.js.map +1 -0
- package/dist/__tests__/thread-test-helpers.d.ts +4 -0
- package/dist/__tests__/thread-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/thread-test-helpers.js +23 -0
- package/dist/__tests__/thread-test-helpers.js.map +1 -0
- package/dist/__tests__/thread.test.d.ts +2 -0
- package/dist/__tests__/thread.test.d.ts.map +1 -0
- package/dist/__tests__/thread.test.js +883 -0
- package/dist/__tests__/thread.test.js.map +1 -0
- package/dist/__tests__/validate-semantic.test.d.ts +2 -0
- package/dist/__tests__/validate-semantic.test.d.ts.map +1 -0
- package/dist/__tests__/validate-semantic.test.js +408 -0
- package/dist/__tests__/validate-semantic.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +308 -0
- package/dist/__tests__/workflow-resolution.test.js.map +1 -0
- package/dist/background/background.d.ts +38 -0
- package/dist/background/background.d.ts.map +1 -0
- package/dist/background/background.js +123 -0
- package/dist/background/background.js.map +1 -0
- package/dist/background/index.d.ts +3 -0
- package/dist/background/index.d.ts.map +1 -0
- package/dist/background/index.js +2 -0
- package/dist/background/index.js.map +1 -0
- package/dist/background/types.d.ts +9 -0
- package/dist/background/types.d.ts.map +1 -0
- package/dist/background/types.js +2 -0
- package/dist/background/types.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +535 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.d.ts +41 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +252 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/log.d.ts +26 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +79 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/prompt.d.ts +6 -0
- package/dist/commands/prompt.d.ts.map +1 -0
- package/dist/commands/prompt.js +67 -0
- package/dist/commands/prompt.js.map +1 -0
- package/dist/commands/setup.d.ts +73 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +522 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/shared.d.ts +31 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +154 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/step.d.ts +18 -0
- package/dist/commands/step.d.ts.map +1 -0
- package/dist/commands/step.js +257 -0
- package/dist/commands/step.js.map +1 -0
- package/dist/commands/thread-time-parser.d.ts +6 -0
- package/dist/commands/thread-time-parser.d.ts.map +1 -0
- package/dist/commands/thread-time-parser.js +22 -0
- package/dist/commands/thread-time-parser.js.map +1 -0
- package/dist/commands/thread.d.ts +38 -0
- package/dist/commands/thread.d.ts.map +1 -0
- package/dist/commands/thread.js +1087 -0
- package/dist/commands/thread.js.map +1 -0
- package/dist/commands/workflow.d.ts +24 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +138 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +10 -0
- package/dist/format.js.map +1 -0
- package/dist/include.d.ts +12 -0
- package/dist/include.d.ts.map +1 -0
- package/dist/include.js +35 -0
- package/dist/include.js.map +1 -0
- package/dist/moderator/__tests__/evaluate.test.d.ts +2 -0
- package/dist/moderator/__tests__/evaluate.test.d.ts.map +1 -0
- package/dist/moderator/__tests__/evaluate.test.js +167 -0
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -0
- package/dist/moderator/evaluate.d.ts +6 -0
- package/dist/moderator/evaluate.d.ts.map +1 -0
- package/dist/moderator/evaluate.js +65 -0
- package/dist/moderator/evaluate.js.map +1 -0
- package/dist/moderator/index.d.ts +4 -0
- package/dist/moderator/index.d.ts.map +1 -0
- package/dist/moderator/index.js +3 -0
- package/dist/moderator/index.js.map +1 -0
- package/dist/moderator/types.d.ts +25 -0
- package/dist/moderator/types.d.ts.map +1 -0
- package/dist/moderator/types.js +4 -0
- package/dist/moderator/types.js.map +1 -0
- package/dist/schemas.d.ts +16 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +17 -0
- package/dist/schemas.js.map +1 -0
- package/dist/store.d.ts +77 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +392 -0
- package/dist/store.js.map +1 -0
- package/dist/validate-semantic.d.ts +7 -0
- package/dist/validate-semantic.d.ts.map +1 -0
- package/dist/validate-semantic.js +263 -0
- package/dist/validate-semantic.js.map +1 -0
- package/dist/validate.d.ts +16 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +115 -0
- package/dist/validate.js.map +1 -0
- package/package.json +44 -0
- package/src/__tests__/adapter-json-roundtrip.test.ts +181 -0
- package/src/__tests__/config.test.ts +740 -0
- package/src/__tests__/current-role.test.ts +438 -0
- package/src/__tests__/e2e-mock-agent.test.ts +498 -0
- package/src/__tests__/fixtures/e2e-completed-resume.mock.yaml +15 -0
- package/src/__tests__/fixtures/e2e-count.mock.yaml +19 -0
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +45 -0
- package/src/__tests__/fixtures/e2e-linear.mock.yaml +13 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +32 -0
- package/src/__tests__/fixtures/e2e-loop.mock.yaml +25 -0
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +36 -0
- package/src/__tests__/fixtures/e2e-mismatch.mock.yaml +16 -0
- package/src/__tests__/fixtures/e2e-mustache.mock.yaml +15 -0
- package/src/__tests__/fixtures/e2e-mustache.workflow.yaml +34 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +14 -0
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +24 -0
- package/src/__tests__/include-tag.test.ts +84 -0
- package/src/__tests__/log.test.ts +181 -0
- package/src/__tests__/moderator-evaluate.test.ts +186 -0
- package/src/__tests__/preload.ts +7 -0
- package/src/__tests__/prompt.test.ts +129 -0
- package/src/__tests__/resolve-head-hash.test.ts +86 -0
- package/src/__tests__/setup-agent-discovery.test.ts +167 -0
- package/src/__tests__/setup-complexity.test.ts +381 -0
- package/src/__tests__/setup-validate.test.ts +148 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +144 -0
- package/src/__tests__/spawn-agent-json.test.ts +100 -0
- package/src/__tests__/step-read.test.ts +632 -0
- package/src/__tests__/step-show-json.test.ts +373 -0
- package/src/__tests__/step-timing.test.ts +392 -0
- package/src/__tests__/store-global-cas.test.ts +308 -0
- package/src/__tests__/store-storage-root.test.ts +49 -0
- package/src/__tests__/store-unified-threads.test.ts +235 -0
- package/src/__tests__/thread-cancel-status.test.ts +138 -0
- package/src/__tests__/thread-list-filters.test.ts +572 -0
- package/src/__tests__/thread-location.test.ts +186 -0
- package/src/__tests__/thread-read-quota.test.ts +613 -0
- package/src/__tests__/thread-read-xml-tags.test.ts +717 -0
- package/src/__tests__/thread-resume.test.ts +710 -0
- package/src/__tests__/thread-show-status.test.ts +317 -0
- package/src/__tests__/thread-start-cwd-cli.test.ts +164 -0
- package/src/__tests__/thread-step-count.test.ts +70 -0
- package/src/__tests__/thread-suspend-step.test.ts +181 -0
- package/src/__tests__/thread-suspended-display.test.ts +287 -0
- package/src/__tests__/thread-test-helpers.ts +37 -0
- package/src/__tests__/thread.test.ts +1025 -0
- package/src/__tests__/validate-semantic.test.ts +474 -0
- package/src/__tests__/workflow-resolution.test.ts +421 -0
- package/src/background/background.ts +147 -0
- package/src/background/index.ts +11 -0
- package/src/background/types.ts +9 -0
- package/src/cli.ts +692 -0
- package/src/commands/config.ts +304 -0
- package/src/commands/log.ts +116 -0
- package/src/commands/prompt.ts +81 -0
- package/src/commands/setup.ts +603 -0
- package/src/commands/shared.ts +227 -0
- package/src/commands/step.ts +343 -0
- package/src/commands/thread-time-parser.ts +23 -0
- package/src/commands/thread.ts +1575 -0
- package/src/commands/workflow.ts +213 -0
- package/src/format.ts +12 -0
- package/src/include.ts +37 -0
- package/src/moderator/__tests__/evaluate.test.ts +199 -0
- package/src/moderator/evaluate.ts +80 -0
- package/src/moderator/index.ts +7 -0
- package/src/moderator/types.ts +24 -0
- package/src/schemas.ts +26 -0
- package/src/store.ts +479 -0
- package/src/validate-semantic.ts +304 -0
- package/src/validate.ts +137 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { openStore } from "@ocas/fs";
|
|
8
|
+
import type { CasRef, StartNodePayload, StepNodePayload } from "@united-workforce/protocol";
|
|
9
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
10
|
+
import { stringify } from "yaml";
|
|
11
|
+
import { cmdThreadStart } from "../commands/thread.js";
|
|
12
|
+
import { cmdWorkflowAdd } from "../commands/workflow.js";
|
|
13
|
+
import { createUwfStore, getThread } from "../store.js";
|
|
14
|
+
|
|
15
|
+
// ── paths ──────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const TEST_DIR = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const FIXTURES_DIR = join(TEST_DIR, "fixtures");
|
|
19
|
+
const CLI_PATH = join(TEST_DIR, "..", "..", "dist", "cli.js");
|
|
20
|
+
const REPO_ROOT = join(TEST_DIR, "..", "..", "..", "..");
|
|
21
|
+
const AGENT_MOCK_DIR = join(REPO_ROOT, "packages", "agent-mock");
|
|
22
|
+
const AGENT_MOCK_CLI = join(AGENT_MOCK_DIR, "dist", "cli.js");
|
|
23
|
+
|
|
24
|
+
// ── shared fixture state ─────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
let tmpDir: string;
|
|
27
|
+
let uwfHome: string;
|
|
28
|
+
let casDir: string;
|
|
29
|
+
let savedEnv: { uwf: string | undefined; ocas: string | undefined };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The mock agent runs from its built `dist/cli.js`. When the test suite runs
|
|
33
|
+
* standalone (no prior `pnpm run build`), build it on demand so the E2E run is
|
|
34
|
+
* self-contained.
|
|
35
|
+
*/
|
|
36
|
+
beforeAll(() => {
|
|
37
|
+
if (existsSync(AGENT_MOCK_CLI)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
execFileSync(
|
|
41
|
+
process.execPath,
|
|
42
|
+
[
|
|
43
|
+
join(REPO_ROOT, "node_modules", "typescript", "bin", "tsc"),
|
|
44
|
+
"--build",
|
|
45
|
+
"--force",
|
|
46
|
+
AGENT_MOCK_DIR,
|
|
47
|
+
],
|
|
48
|
+
{ cwd: REPO_ROOT, stdio: "ignore" },
|
|
49
|
+
);
|
|
50
|
+
}, 120000);
|
|
51
|
+
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
tmpDir = await mkdtemp(join(tmpdir(), "cli-e2e-mock-"));
|
|
54
|
+
uwfHome = join(tmpDir, "uwf");
|
|
55
|
+
casDir = join(tmpDir, "ocas");
|
|
56
|
+
await mkdir(uwfHome, { recursive: true });
|
|
57
|
+
await mkdir(casDir, { recursive: true });
|
|
58
|
+
// Programmatic CLI APIs (cmdWorkflowAdd, cmdThreadStart) read the global CAS
|
|
59
|
+
// directory from OCAS_HOME and the storage root from UWF_HOME.
|
|
60
|
+
savedEnv = { uwf: process.env.UWF_HOME, ocas: process.env.OCAS_HOME };
|
|
61
|
+
process.env.UWF_HOME = uwfHome;
|
|
62
|
+
process.env.OCAS_HOME = casDir;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
process.env.UWF_HOME = savedEnv.uwf;
|
|
67
|
+
process.env.OCAS_HOME = savedEnv.ocas;
|
|
68
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Write a `config.yaml` into UWF_HOME that wires the default agent to the mock
|
|
75
|
+
* agent. The mock data path is baked into the agent args so the CLI's
|
|
76
|
+
* `thread exec` (without an `--agent` override) resolves it from config.
|
|
77
|
+
*/
|
|
78
|
+
async function writeMockConfig(mockDataFixture: string): Promise<void> {
|
|
79
|
+
const config = {
|
|
80
|
+
defaultAgent: "mock",
|
|
81
|
+
defaultModel: "test",
|
|
82
|
+
providers: {},
|
|
83
|
+
models: {},
|
|
84
|
+
agentOverrides: null,
|
|
85
|
+
agents: {
|
|
86
|
+
mock: {
|
|
87
|
+
command: process.execPath,
|
|
88
|
+
args: [AGENT_MOCK_CLI, "--mock-data", join(FIXTURES_DIR, mockDataFixture)],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
await writeFile(join(uwfHome, "config.yaml"), stringify(config));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `cmdWorkflowAdd` enforces filename↔name consistency, so copy the fixture into
|
|
97
|
+
* UWF_HOME under `<workflow-name>.yaml` before registering it.
|
|
98
|
+
*/
|
|
99
|
+
async function addWorkflow(workflowFixture: string, workflowName: string): Promise<CasRef> {
|
|
100
|
+
const text = await readFile(join(FIXTURES_DIR, workflowFixture), "utf8");
|
|
101
|
+
const filePath = join(uwfHome, `${workflowName}.yaml`);
|
|
102
|
+
await writeFile(filePath, text);
|
|
103
|
+
const result = await cmdWorkflowAdd(uwfHome, filePath);
|
|
104
|
+
return result.hash;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type ExecResult = { stdout: string; stderr: string; exitCode: number };
|
|
108
|
+
|
|
109
|
+
function runExec(threadId: string, count: number | null = null): ExecResult {
|
|
110
|
+
const args = [CLI_PATH, "thread", "exec", threadId];
|
|
111
|
+
if (count !== null) {
|
|
112
|
+
args.push("--count", String(count));
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const stdout = execFileSync(process.execPath, args, {
|
|
116
|
+
encoding: "utf8",
|
|
117
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
118
|
+
env: { ...process.env, UWF_HOME: uwfHome, OCAS_HOME: casDir },
|
|
119
|
+
cwd: tmpDir,
|
|
120
|
+
timeout: 30000,
|
|
121
|
+
});
|
|
122
|
+
return { stdout, stderr: "", exitCode: 0 };
|
|
123
|
+
} catch (e: unknown) {
|
|
124
|
+
const err = e as NodeJS.ErrnoException & {
|
|
125
|
+
stdout?: string;
|
|
126
|
+
stderr?: string;
|
|
127
|
+
status?: number;
|
|
128
|
+
};
|
|
129
|
+
return { stdout: err.stdout ?? "", stderr: err.stderr ?? "", exitCode: err.status ?? 1 };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Invoke `uwf thread resume <threadId> -p <prompt>` through the built CLI. */
|
|
134
|
+
function runResume(threadId: string, prompt: string): ExecResult {
|
|
135
|
+
try {
|
|
136
|
+
const stdout = execFileSync(
|
|
137
|
+
process.execPath,
|
|
138
|
+
[CLI_PATH, "thread", "resume", threadId, "-p", prompt],
|
|
139
|
+
{
|
|
140
|
+
encoding: "utf8",
|
|
141
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
142
|
+
env: { ...process.env, UWF_HOME: uwfHome, OCAS_HOME: casDir },
|
|
143
|
+
cwd: tmpDir,
|
|
144
|
+
timeout: 30000,
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
return { stdout, stderr: "", exitCode: 0 };
|
|
148
|
+
} catch (e: unknown) {
|
|
149
|
+
const err = e as NodeJS.ErrnoException & {
|
|
150
|
+
stdout?: string;
|
|
151
|
+
stderr?: string;
|
|
152
|
+
status?: number;
|
|
153
|
+
};
|
|
154
|
+
return { stdout: err.stdout ?? "", stderr: err.stderr ?? "", exitCode: err.status ?? 1 };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
type StepOutputJson = {
|
|
159
|
+
thread: string;
|
|
160
|
+
head: string;
|
|
161
|
+
status: string;
|
|
162
|
+
currentRole: string | null;
|
|
163
|
+
suspendedRole: string | null;
|
|
164
|
+
suspendMessage: string | null;
|
|
165
|
+
done: boolean;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
function execStep(threadId: string): StepOutputJson {
|
|
169
|
+
const { stdout, stderr, exitCode } = runExec(threadId);
|
|
170
|
+
if (exitCode !== 0) {
|
|
171
|
+
throw new Error(`thread exec failed (code ${exitCode})\nstdout: ${stdout}\nstderr: ${stderr}`);
|
|
172
|
+
}
|
|
173
|
+
return JSON.parse(stdout.trim()) as StepOutputJson;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getStepNode(store: Awaited<ReturnType<typeof openStore>>, hash: string): StepNodePayload {
|
|
177
|
+
const node = store.cas.get(hash as CasRef);
|
|
178
|
+
expect(node).not.toBeNull();
|
|
179
|
+
return node!.payload as StepNodePayload;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getStatus(store: Awaited<ReturnType<typeof openStore>>, outputRef: CasRef): unknown {
|
|
183
|
+
const node = store.cas.get(outputRef);
|
|
184
|
+
expect(node).not.toBeNull();
|
|
185
|
+
return (node!.payload as Record<string, unknown>).$status;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── scenarios ─────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
describe("E2E mock-agent: full uwf pipeline", () => {
|
|
191
|
+
test("1. linear workflow runs planner then worker and reaches $END", async () => {
|
|
192
|
+
await writeMockConfig("e2e-linear.mock.yaml");
|
|
193
|
+
const workflowHash = await addWorkflow("e2e-linear.workflow.yaml", "test-linear");
|
|
194
|
+
|
|
195
|
+
const start = await cmdThreadStart(uwfHome, workflowHash, "Build the thing", uwfHome, tmpDir);
|
|
196
|
+
const threadId = start.thread;
|
|
197
|
+
|
|
198
|
+
// Capture the start node hash (thread head before any step).
|
|
199
|
+
const startHash = getThread((await createUwfStore(uwfHome)).varStore, threadId)?.head;
|
|
200
|
+
expect(startHash).toBeDefined();
|
|
201
|
+
|
|
202
|
+
// Step 1 → planner.
|
|
203
|
+
const step1 = execStep(threadId);
|
|
204
|
+
expect(step1.thread).toBe(threadId);
|
|
205
|
+
expect(step1.done).toBe(false);
|
|
206
|
+
expect(step1.status).toBe("idle");
|
|
207
|
+
expect(step1.currentRole).toBe("worker");
|
|
208
|
+
|
|
209
|
+
// Step 2 → worker → $END (thread archived to history).
|
|
210
|
+
const step2 = execStep(threadId);
|
|
211
|
+
expect(step2.done).toBe(true);
|
|
212
|
+
expect(step2.status).toBe("completed");
|
|
213
|
+
expect(step2.currentRole).toBeNull();
|
|
214
|
+
|
|
215
|
+
// Verify CAS chain integrity: start → step1 → step2.
|
|
216
|
+
const store = await openStore(casDir);
|
|
217
|
+
const s1 = getStepNode(store, step1.head);
|
|
218
|
+
const s2 = getStepNode(store, step2.head);
|
|
219
|
+
|
|
220
|
+
expect(s1.role).toBe("planner");
|
|
221
|
+
expect(s1.prev).toBeNull();
|
|
222
|
+
expect(s1.start).toBe(startHash);
|
|
223
|
+
|
|
224
|
+
expect(s2.role).toBe("worker");
|
|
225
|
+
expect(s2.prev).toBe(step1.head);
|
|
226
|
+
expect(s2.start).toBe(s1.start);
|
|
227
|
+
|
|
228
|
+
// Output frontmatter statuses persisted correctly.
|
|
229
|
+
expect(getStatus(store, s1.output)).toBe("ready");
|
|
230
|
+
expect(getStatus(store, s2.output)).toBe("done");
|
|
231
|
+
|
|
232
|
+
// Mock agent reports usage stats in step nodes.
|
|
233
|
+
expect(s1.usage).toEqual({ turns: 1, inputTokens: 0, outputTokens: 0, duration: 0 });
|
|
234
|
+
expect(s2.usage).toEqual({ turns: 1, inputTokens: 0, outputTokens: 0, duration: 0 });
|
|
235
|
+
|
|
236
|
+
// The start node points at the registered workflow.
|
|
237
|
+
const startNode = store.cas.get(startHash as CasRef);
|
|
238
|
+
expect((startNode!.payload as StartNodePayload).workflow).toBe(workflowHash);
|
|
239
|
+
|
|
240
|
+
// Thread is completed: status changed to "completed", head updated.
|
|
241
|
+
const uwf = await createUwfStore(uwfHome);
|
|
242
|
+
const finalEntry = getThread(uwf.varStore, threadId);
|
|
243
|
+
expect(finalEntry).not.toBeNull();
|
|
244
|
+
expect(finalEntry!.status).toBe("completed");
|
|
245
|
+
expect(finalEntry!.head).toBe(step2.head);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("2. branching workflow loops developer→reviewer→developer→reviewer→$END", {
|
|
249
|
+
timeout: 30_000,
|
|
250
|
+
}, async () => {
|
|
251
|
+
await writeMockConfig("e2e-loop.mock.yaml");
|
|
252
|
+
const workflowHash = await addWorkflow("e2e-loop.workflow.yaml", "test-loop");
|
|
253
|
+
|
|
254
|
+
const start = await cmdThreadStart(uwfHome, workflowHash, "Implement feature", uwfHome, tmpDir);
|
|
255
|
+
const threadId = start.thread;
|
|
256
|
+
|
|
257
|
+
// 4 steps: developer, reviewer (rejected → loop), developer, reviewer (approved → $END).
|
|
258
|
+
const s1 = execStep(threadId);
|
|
259
|
+
expect(s1.status).toBe("idle");
|
|
260
|
+
expect(s1.currentRole).toBe("reviewer");
|
|
261
|
+
|
|
262
|
+
const s2 = execStep(threadId);
|
|
263
|
+
expect(s2.status).toBe("idle");
|
|
264
|
+
// reviewer rejected → loops back to developer.
|
|
265
|
+
expect(s2.currentRole).toBe("developer");
|
|
266
|
+
|
|
267
|
+
const s3 = execStep(threadId);
|
|
268
|
+
expect(s3.status).toBe("idle");
|
|
269
|
+
expect(s3.currentRole).toBe("reviewer");
|
|
270
|
+
|
|
271
|
+
const s4 = execStep(threadId);
|
|
272
|
+
expect(s4.done).toBe(true);
|
|
273
|
+
expect(s4.status).toBe("completed");
|
|
274
|
+
|
|
275
|
+
// Verify the chain order and roles.
|
|
276
|
+
const store = await openStore(casDir);
|
|
277
|
+
const n1 = getStepNode(store, s1.head);
|
|
278
|
+
const n2 = getStepNode(store, s2.head);
|
|
279
|
+
const n3 = getStepNode(store, s3.head);
|
|
280
|
+
const n4 = getStepNode(store, s4.head);
|
|
281
|
+
|
|
282
|
+
expect([n1.role, n2.role, n3.role, n4.role]).toEqual([
|
|
283
|
+
"developer",
|
|
284
|
+
"reviewer",
|
|
285
|
+
"developer",
|
|
286
|
+
"reviewer",
|
|
287
|
+
]);
|
|
288
|
+
expect(n1.prev).toBeNull();
|
|
289
|
+
expect(n2.prev).toBe(s1.head);
|
|
290
|
+
expect(n3.prev).toBe(s2.head);
|
|
291
|
+
expect(n4.prev).toBe(s3.head);
|
|
292
|
+
|
|
293
|
+
// All steps share the same start node.
|
|
294
|
+
expect(new Set([n1.start, n2.start, n3.start, n4.start]).size).toBe(1);
|
|
295
|
+
|
|
296
|
+
// Statuses drove the loop routing.
|
|
297
|
+
expect(getStatus(store, n1.output)).toBe("review_needed");
|
|
298
|
+
expect(getStatus(store, n2.output)).toBe("rejected");
|
|
299
|
+
expect(getStatus(store, n3.output)).toBe("review_needed");
|
|
300
|
+
expect(getStatus(store, n4.output)).toBe("approved");
|
|
301
|
+
|
|
302
|
+
const uwf = await createUwfStore(uwfHome);
|
|
303
|
+
const finalEntry = getThread(uwf.varStore, threadId);
|
|
304
|
+
expect(finalEntry).not.toBeNull();
|
|
305
|
+
expect(finalEntry!.status).toBe("completed");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("3. role mismatch in mock data makes the agent exit with an error", {
|
|
309
|
+
timeout: 30_000,
|
|
310
|
+
}, async () => {
|
|
311
|
+
// Reuses the linear workflow but with a mock whose step[1].role is wrong.
|
|
312
|
+
await writeMockConfig("e2e-mismatch.mock.yaml");
|
|
313
|
+
const workflowHash = await addWorkflow("e2e-linear.workflow.yaml", "test-linear");
|
|
314
|
+
|
|
315
|
+
const start = await cmdThreadStart(uwfHome, workflowHash, "Build the thing", uwfHome, tmpDir);
|
|
316
|
+
const threadId = start.thread;
|
|
317
|
+
|
|
318
|
+
// Step 1 (planner) matches and succeeds.
|
|
319
|
+
const step1 = execStep(threadId);
|
|
320
|
+
expect(step1.status).toBe("idle");
|
|
321
|
+
expect(step1.currentRole).toBe("worker");
|
|
322
|
+
|
|
323
|
+
// Step 2: moderator routes to "worker" but mock step[1].role is "planner".
|
|
324
|
+
const result = runExec(threadId);
|
|
325
|
+
expect(result.exitCode).not.toBe(0);
|
|
326
|
+
expect(`${result.stdout}\n${result.stderr}`).toMatch(/expected role "planner"/);
|
|
327
|
+
|
|
328
|
+
// The thread remains active (no step node was written for the failed step).
|
|
329
|
+
const uwf = await createUwfStore(uwfHome);
|
|
330
|
+
const entry = getThread(uwf.varStore, threadId);
|
|
331
|
+
expect(entry).not.toBeNull();
|
|
332
|
+
expect(entry!.status).not.toBe("completed");
|
|
333
|
+
expect(entry!.head).toBe(step1.head);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("4. planner $SUSPEND then resume re-runs planner and reaches $END", {
|
|
337
|
+
timeout: 30_000,
|
|
338
|
+
}, async () => {
|
|
339
|
+
await writeMockConfig("e2e-suspend.mock.yaml");
|
|
340
|
+
const workflowHash = await addWorkflow("e2e-suspend.workflow.yaml", "test-suspend");
|
|
341
|
+
|
|
342
|
+
const start = await cmdThreadStart(uwfHome, workflowHash, "Analyze the task", uwfHome, tmpDir);
|
|
343
|
+
const threadId = start.thread;
|
|
344
|
+
|
|
345
|
+
// Step 1 → planner emits insufficient_info → thread suspends.
|
|
346
|
+
const step1 = execStep(threadId);
|
|
347
|
+
expect(step1.status).toBe("suspended");
|
|
348
|
+
expect(step1.done).toBe(false);
|
|
349
|
+
expect(step1.currentRole).toBeNull();
|
|
350
|
+
expect(step1.suspendedRole).toBe("planner");
|
|
351
|
+
expect(step1.suspendMessage).toBe("Need more info: missing requirements");
|
|
352
|
+
|
|
353
|
+
// Thread index entry reflects the suspension with rendered metadata.
|
|
354
|
+
const suspendedEntry = getThread((await createUwfStore(uwfHome)).varStore, threadId);
|
|
355
|
+
expect(suspendedEntry).not.toBeNull();
|
|
356
|
+
expect(suspendedEntry!.status).toBe("suspended");
|
|
357
|
+
expect(suspendedEntry!.suspendedRole).toBe("planner");
|
|
358
|
+
expect(suspendedEntry!.suspendMessage).toBe("Need more info: missing requirements");
|
|
359
|
+
|
|
360
|
+
// Resume re-runs the planner role; the second scripted step is `ready` → $END.
|
|
361
|
+
const resume = runResume(threadId, "Here are the requirements");
|
|
362
|
+
expect(resume.exitCode).toBe(0);
|
|
363
|
+
const resumeOut = JSON.parse(resume.stdout.trim()) as StepOutputJson;
|
|
364
|
+
expect(resumeOut.status).toBe("completed");
|
|
365
|
+
expect(resumeOut.done).toBe(true);
|
|
366
|
+
expect(resumeOut.currentRole).toBeNull();
|
|
367
|
+
expect(resumeOut.suspendedRole).toBeNull();
|
|
368
|
+
|
|
369
|
+
// CAS chain: suspended planner step → resumed planner step.
|
|
370
|
+
const store = await openStore(casDir);
|
|
371
|
+
const s1 = getStepNode(store, step1.head);
|
|
372
|
+
const s2 = getStepNode(store, resumeOut.head);
|
|
373
|
+
expect(s1.role).toBe("planner");
|
|
374
|
+
expect(s2.role).toBe("planner");
|
|
375
|
+
expect(s2.prev).toBe(step1.head);
|
|
376
|
+
expect(getStatus(store, s1.output)).toBe("insufficient_info");
|
|
377
|
+
expect(getStatus(store, s2.output)).toBe("ready");
|
|
378
|
+
|
|
379
|
+
const finalEntry = getThread((await createUwfStore(uwfHome)).varStore, threadId);
|
|
380
|
+
expect(finalEntry).not.toBeNull();
|
|
381
|
+
expect(finalEntry!.status).toBe("completed");
|
|
382
|
+
expect(finalEntry!.head).toBe(resumeOut.head);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("5. --count 3 runs the whole linear pipeline in one invocation", {
|
|
386
|
+
timeout: 30_000,
|
|
387
|
+
}, async () => {
|
|
388
|
+
await writeMockConfig("e2e-count.mock.yaml");
|
|
389
|
+
const workflowHash = await addWorkflow("e2e-count.workflow.yaml", "test-count");
|
|
390
|
+
|
|
391
|
+
const start = await cmdThreadStart(uwfHome, workflowHash, "Ship the feature", uwfHome, tmpDir);
|
|
392
|
+
const threadId = start.thread;
|
|
393
|
+
|
|
394
|
+
// Single invocation with --count 3 → moderator drives analyst → developer → reviewer → $END.
|
|
395
|
+
const { stdout, stderr, exitCode } = runExec(threadId, 3);
|
|
396
|
+
expect(exitCode, `stderr: ${stderr}`).toBe(0);
|
|
397
|
+
|
|
398
|
+
// Multi-step exec emits a JSON array (one entry per executed step).
|
|
399
|
+
const results = JSON.parse(stdout.trim()) as StepOutputJson[];
|
|
400
|
+
expect(Array.isArray(results)).toBe(true);
|
|
401
|
+
expect(results).toHaveLength(3);
|
|
402
|
+
|
|
403
|
+
expect(results[0].status).toBe("idle");
|
|
404
|
+
expect(results[0].currentRole).toBe("developer");
|
|
405
|
+
expect(results[1].status).toBe("idle");
|
|
406
|
+
expect(results[1].currentRole).toBe("reviewer");
|
|
407
|
+
expect(results[2].status).toBe("completed");
|
|
408
|
+
expect(results[2].done).toBe(true);
|
|
409
|
+
|
|
410
|
+
// Verify the CAS chain holds 3 step nodes in the correct order.
|
|
411
|
+
const store = await openStore(casDir);
|
|
412
|
+
const n1 = getStepNode(store, results[0].head);
|
|
413
|
+
const n2 = getStepNode(store, results[1].head);
|
|
414
|
+
const n3 = getStepNode(store, results[2].head);
|
|
415
|
+
expect([n1.role, n2.role, n3.role]).toEqual(["analyst", "developer", "reviewer"]);
|
|
416
|
+
expect(n1.prev).toBeNull();
|
|
417
|
+
expect(n2.prev).toBe(results[0].head);
|
|
418
|
+
expect(n3.prev).toBe(results[1].head);
|
|
419
|
+
expect(new Set([n1.start, n2.start, n3.start]).size).toBe(1);
|
|
420
|
+
|
|
421
|
+
const finalEntry = getThread((await createUwfStore(uwfHome)).varStore, threadId);
|
|
422
|
+
expect(finalEntry).not.toBeNull();
|
|
423
|
+
expect(finalEntry!.status).toBe("completed");
|
|
424
|
+
expect(finalEntry!.head).toBe(results[2].head);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("6. mustache edge prompt renders planner variables into the worker step", {
|
|
428
|
+
timeout: 30_000,
|
|
429
|
+
}, async () => {
|
|
430
|
+
await writeMockConfig("e2e-mustache.mock.yaml");
|
|
431
|
+
const workflowHash = await addWorkflow("e2e-mustache.workflow.yaml", "test-mustache");
|
|
432
|
+
|
|
433
|
+
const start = await cmdThreadStart(uwfHome, workflowHash, "Plan the task", uwfHome, tmpDir);
|
|
434
|
+
const threadId = start.thread;
|
|
435
|
+
|
|
436
|
+
// Step 1 → planner emits branch + repoPath.
|
|
437
|
+
const step1 = execStep(threadId);
|
|
438
|
+
expect(step1.status).toBe("idle");
|
|
439
|
+
expect(step1.currentRole).toBe("worker");
|
|
440
|
+
|
|
441
|
+
// Step 2 → worker; the moderator renders the templated edge prompt before spawning it.
|
|
442
|
+
const step2 = execStep(threadId);
|
|
443
|
+
expect(step2.done).toBe(true);
|
|
444
|
+
expect(step2.status).toBe("completed");
|
|
445
|
+
|
|
446
|
+
const store = await openStore(casDir);
|
|
447
|
+
const plannerStep = getStepNode(store, step1.head);
|
|
448
|
+
expect(getStatus(store, plannerStep.output)).toBe("ready");
|
|
449
|
+
|
|
450
|
+
// The worker step's edgePrompt is the mustache-rendered template.
|
|
451
|
+
const workerStep = getStepNode(store, step2.head);
|
|
452
|
+
expect(workerStep.role).toBe("worker");
|
|
453
|
+
expect(workerStep.edgePrompt).toContain("fix/42-auth");
|
|
454
|
+
expect(workerStep.edgePrompt).toContain("/tmp/my-repo");
|
|
455
|
+
expect(workerStep.edgePrompt).toBe("Work on branch fix/42-auth in /tmp/my-repo");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("7. completed thread can be resumed (衔尾蛇: end → start)", {
|
|
459
|
+
timeout: 30_000,
|
|
460
|
+
}, async () => {
|
|
461
|
+
// Reuse the suspend workflow (planner with ready → $END), but mock data
|
|
462
|
+
// goes straight to ready on first run, then ready again after resume.
|
|
463
|
+
await writeMockConfig("e2e-completed-resume.mock.yaml");
|
|
464
|
+
const workflowHash = await addWorkflow("e2e-suspend.workflow.yaml", "test-suspend");
|
|
465
|
+
|
|
466
|
+
const start = await cmdThreadStart(uwfHome, workflowHash, "Do the work", uwfHome, tmpDir);
|
|
467
|
+
const threadId = start.thread;
|
|
468
|
+
|
|
469
|
+
// Step 1: planner outputs ready → $END → thread completed.
|
|
470
|
+
const step1 = execStep(threadId);
|
|
471
|
+
expect(step1.done).toBe(true);
|
|
472
|
+
expect(step1.status).toBe("completed");
|
|
473
|
+
|
|
474
|
+
const uwf1 = await createUwfStore(uwfHome);
|
|
475
|
+
const entry1 = getThread(uwf1.varStore, threadId);
|
|
476
|
+
expect(entry1).not.toBeNull();
|
|
477
|
+
expect(entry1!.status).toBe("completed");
|
|
478
|
+
|
|
479
|
+
// Resume the completed thread — should re-evaluate $START → planner.
|
|
480
|
+
const resumeResult = runResume(threadId, "Additional context for round 2");
|
|
481
|
+
expect(resumeResult.exitCode).toBe(0);
|
|
482
|
+
|
|
483
|
+
// After resume step, planner ran again (step index 1 in mock) → ready → $END.
|
|
484
|
+
const uwf2 = await createUwfStore(uwfHome);
|
|
485
|
+
const entry2 = getThread(uwf2.varStore, threadId);
|
|
486
|
+
expect(entry2).not.toBeNull();
|
|
487
|
+
expect(entry2!.status).toBe("completed");
|
|
488
|
+
// Head should have advanced (not the same as step1).
|
|
489
|
+
expect(entry2!.head).not.toBe(step1.head);
|
|
490
|
+
|
|
491
|
+
// CAS chain: step2.prev === step1 head (chain is preserved across resume).
|
|
492
|
+
const store = await openStore(casDir);
|
|
493
|
+
const resumeOutput = JSON.parse(resumeResult.stdout.trim());
|
|
494
|
+
const step2Node = getStepNode(store, resumeOutput.head);
|
|
495
|
+
expect(step2Node.role).toBe("planner");
|
|
496
|
+
expect(step2Node.prev).toBe(step1.head);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
steps:
|
|
2
|
+
# Step 0: planner → ready → $END (thread completes)
|
|
3
|
+
- role: planner
|
|
4
|
+
output: |
|
|
5
|
+
---
|
|
6
|
+
$status: ready
|
|
7
|
+
---
|
|
8
|
+
Initial plan complete.
|
|
9
|
+
# Step 1: after resume, planner runs again from $START → ready → $END again
|
|
10
|
+
- role: planner
|
|
11
|
+
output: |
|
|
12
|
+
---
|
|
13
|
+
$status: ready
|
|
14
|
+
---
|
|
15
|
+
Revised plan after resume.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
steps:
|
|
2
|
+
- role: analyst
|
|
3
|
+
output: |
|
|
4
|
+
---
|
|
5
|
+
$status: analyzed
|
|
6
|
+
---
|
|
7
|
+
Analysis complete.
|
|
8
|
+
- role: developer
|
|
9
|
+
output: |
|
|
10
|
+
---
|
|
11
|
+
$status: implemented
|
|
12
|
+
---
|
|
13
|
+
Implementation complete.
|
|
14
|
+
- role: reviewer
|
|
15
|
+
output: |
|
|
16
|
+
---
|
|
17
|
+
$status: approved
|
|
18
|
+
---
|
|
19
|
+
Approved.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: test-count
|
|
2
|
+
description: 3-step linear pipeline (analyst -> developer -> reviewer -> $END)
|
|
3
|
+
roles:
|
|
4
|
+
analyst:
|
|
5
|
+
description: Analyzes the task
|
|
6
|
+
goal: Analyze the task
|
|
7
|
+
capabilities: []
|
|
8
|
+
procedure: Analyze it
|
|
9
|
+
output: Output the analysis and set $status to analyzed
|
|
10
|
+
frontmatter:
|
|
11
|
+
oneOf:
|
|
12
|
+
- properties:
|
|
13
|
+
$status: { const: analyzed }
|
|
14
|
+
required: [$status]
|
|
15
|
+
developer:
|
|
16
|
+
description: Implements the change
|
|
17
|
+
goal: Implement the change
|
|
18
|
+
capabilities: []
|
|
19
|
+
procedure: Write code
|
|
20
|
+
output: Output the implementation and set $status to implemented
|
|
21
|
+
frontmatter:
|
|
22
|
+
oneOf:
|
|
23
|
+
- properties:
|
|
24
|
+
$status: { const: implemented }
|
|
25
|
+
required: [$status]
|
|
26
|
+
reviewer:
|
|
27
|
+
description: Reviews the change
|
|
28
|
+
goal: Review the change
|
|
29
|
+
capabilities: []
|
|
30
|
+
procedure: Review code
|
|
31
|
+
output: Approve and set $status to approved
|
|
32
|
+
frontmatter:
|
|
33
|
+
oneOf:
|
|
34
|
+
- properties:
|
|
35
|
+
$status: { const: approved }
|
|
36
|
+
required: [$status]
|
|
37
|
+
graph:
|
|
38
|
+
$START:
|
|
39
|
+
_: { role: analyst, prompt: 'Analyze the task' }
|
|
40
|
+
analyst:
|
|
41
|
+
analyzed: { role: developer, prompt: 'Implement the change' }
|
|
42
|
+
developer:
|
|
43
|
+
implemented: { role: reviewer, prompt: 'Review the change' }
|
|
44
|
+
reviewer:
|
|
45
|
+
approved: { role: '$END', prompt: 'Done' }
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: test-linear
|
|
2
|
+
description: Simple 2-step linear test (planner -> worker -> $END)
|
|
3
|
+
roles:
|
|
4
|
+
planner:
|
|
5
|
+
description: Plans work
|
|
6
|
+
goal: Plan the task
|
|
7
|
+
capabilities: []
|
|
8
|
+
procedure: Plan it
|
|
9
|
+
output: Output a plan and set $status to ready
|
|
10
|
+
frontmatter:
|
|
11
|
+
oneOf:
|
|
12
|
+
- properties:
|
|
13
|
+
$status: { const: ready }
|
|
14
|
+
required: [$status]
|
|
15
|
+
worker:
|
|
16
|
+
description: Does work
|
|
17
|
+
goal: Do the work
|
|
18
|
+
capabilities: []
|
|
19
|
+
procedure: Do it
|
|
20
|
+
output: Output the result and set $status to done
|
|
21
|
+
frontmatter:
|
|
22
|
+
oneOf:
|
|
23
|
+
- properties:
|
|
24
|
+
$status: { const: done }
|
|
25
|
+
required: [$status]
|
|
26
|
+
graph:
|
|
27
|
+
$START:
|
|
28
|
+
_: { role: planner, prompt: 'Plan the task' }
|
|
29
|
+
planner:
|
|
30
|
+
ready: { role: worker, prompt: 'Do the work' }
|
|
31
|
+
worker:
|
|
32
|
+
done: { role: '$END', prompt: 'Done' }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
steps:
|
|
2
|
+
- role: developer
|
|
3
|
+
output: |
|
|
4
|
+
---
|
|
5
|
+
$status: review_needed
|
|
6
|
+
---
|
|
7
|
+
First implementation.
|
|
8
|
+
- role: reviewer
|
|
9
|
+
output: |
|
|
10
|
+
---
|
|
11
|
+
$status: rejected
|
|
12
|
+
---
|
|
13
|
+
Needs changes, sending back.
|
|
14
|
+
- role: developer
|
|
15
|
+
output: |
|
|
16
|
+
---
|
|
17
|
+
$status: review_needed
|
|
18
|
+
---
|
|
19
|
+
Second implementation addressing feedback.
|
|
20
|
+
- role: reviewer
|
|
21
|
+
output: |
|
|
22
|
+
---
|
|
23
|
+
$status: approved
|
|
24
|
+
---
|
|
25
|
+
Looks good, approved.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: test-loop
|
|
2
|
+
description: Branching test where the reviewer can reject and loop back to the developer
|
|
3
|
+
roles:
|
|
4
|
+
developer:
|
|
5
|
+
description: Implements changes
|
|
6
|
+
goal: Implement the change
|
|
7
|
+
capabilities: []
|
|
8
|
+
procedure: Write code
|
|
9
|
+
output: Summarize the change and set $status to review_needed
|
|
10
|
+
frontmatter:
|
|
11
|
+
oneOf:
|
|
12
|
+
- properties:
|
|
13
|
+
$status: { const: review_needed }
|
|
14
|
+
required: [$status]
|
|
15
|
+
reviewer:
|
|
16
|
+
description: Reviews changes
|
|
17
|
+
goal: Review the change
|
|
18
|
+
capabilities: []
|
|
19
|
+
procedure: Review code
|
|
20
|
+
output: Approve or reject; set $status to approved or rejected
|
|
21
|
+
frontmatter:
|
|
22
|
+
oneOf:
|
|
23
|
+
- properties:
|
|
24
|
+
$status: { const: rejected }
|
|
25
|
+
required: [$status]
|
|
26
|
+
- properties:
|
|
27
|
+
$status: { const: approved }
|
|
28
|
+
required: [$status]
|
|
29
|
+
graph:
|
|
30
|
+
$START:
|
|
31
|
+
_: { role: developer, prompt: 'Implement the change' }
|
|
32
|
+
developer:
|
|
33
|
+
review_needed: { role: reviewer, prompt: 'Review the change' }
|
|
34
|
+
reviewer:
|
|
35
|
+
rejected: { role: developer, prompt: 'Fix the issues and resubmit' }
|
|
36
|
+
approved: { role: '$END', prompt: 'Approved, done' }
|