@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
|
@@ -12,28 +12,26 @@ import { parse } from "yaml";
|
|
|
12
12
|
*/
|
|
13
13
|
describe("solve-issue workflow: Gitea API PR creation", () => {
|
|
14
14
|
// Navigate up from packages/cli/src/__tests__ to repo root
|
|
15
|
-
const workflowPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "
|
|
16
|
-
test("committer procedure should
|
|
15
|
+
const workflowPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "examples", "solve-issue.yaml");
|
|
16
|
+
test("committer procedure should create PR via tea pr create", async () => {
|
|
17
17
|
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
18
18
|
const workflow = parse(yamlContent);
|
|
19
19
|
expect(workflow.roles.committer).toBeDefined();
|
|
20
20
|
const committerProcedure = workflow.roles.committer?.procedure;
|
|
21
21
|
expect(committerProcedure).toBeDefined();
|
|
22
|
-
// Verify the procedure uses
|
|
23
|
-
expect(committerProcedure).toContain("
|
|
24
|
-
expect(committerProcedure).toContain("
|
|
25
|
-
expect(committerProcedure).toContain("
|
|
26
|
-
// Verify it explicitly warns against tea pr create
|
|
27
|
-
expect(committerProcedure).toMatch(/do NOT use.*tea pr create/i);
|
|
22
|
+
// Verify the procedure uses tea pr create for PR creation
|
|
23
|
+
expect(committerProcedure).toContain("tea pr create");
|
|
24
|
+
expect(committerProcedure).toContain("git push");
|
|
25
|
+
expect(committerProcedure).toContain("Fixes #N");
|
|
28
26
|
});
|
|
29
|
-
test("committer procedure should
|
|
27
|
+
test("committer procedure should extract owner/repo from git remote", async () => {
|
|
30
28
|
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
31
29
|
const workflow = parse(yamlContent);
|
|
32
30
|
const committerProcedure = workflow.roles.committer?.procedure;
|
|
33
31
|
expect(committerProcedure).toBeDefined();
|
|
34
|
-
// Verify the procedure
|
|
35
|
-
expect(committerProcedure).
|
|
36
|
-
expect(committerProcedure).
|
|
32
|
+
// Verify the procedure extracts owner/repo from remote
|
|
33
|
+
expect(committerProcedure).toContain("git remote get-url origin");
|
|
34
|
+
expect(committerProcedure).toContain("hook_failed");
|
|
37
35
|
});
|
|
38
36
|
test("committer procedure should include error handling for curl failures", async () => {
|
|
39
37
|
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
@@ -72,36 +70,35 @@ describe("solve-issue workflow: Gitea API PR creation", () => {
|
|
|
72
70
|
expect(committedVariant).toBeDefined();
|
|
73
71
|
expect(committedVariant.required).toContain("$status");
|
|
74
72
|
});
|
|
75
|
-
test("developer procedure should include
|
|
73
|
+
test("developer procedure should include worktree setup", async () => {
|
|
76
74
|
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
77
75
|
const workflow = parse(yamlContent);
|
|
78
76
|
const developerProcedure = workflow.roles.developer?.procedure;
|
|
79
77
|
expect(developerProcedure).toBeDefined();
|
|
80
|
-
// Verify the procedure includes
|
|
81
|
-
expect(developerProcedure).toContain("
|
|
82
|
-
expect(developerProcedure).toContain("git
|
|
83
|
-
expect(developerProcedure).toContain("
|
|
84
|
-
expect(developerProcedure).toMatch(/ls -la|verify.*exist/i);
|
|
78
|
+
// Verify the procedure includes worktree setup
|
|
79
|
+
expect(developerProcedure).toContain("IMPORTANT");
|
|
80
|
+
expect(developerProcedure).toContain("git worktree add");
|
|
81
|
+
expect(developerProcedure).toContain("pnpm install");
|
|
85
82
|
});
|
|
86
|
-
test("reviewer procedure should
|
|
83
|
+
test("reviewer procedure should verify branch and run checks", async () => {
|
|
87
84
|
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
88
85
|
const workflow = parse(yamlContent);
|
|
89
86
|
const reviewerProcedure = workflow.roles.reviewer?.procedure;
|
|
90
87
|
expect(reviewerProcedure).toBeDefined();
|
|
91
|
-
// Verify the procedure includes
|
|
92
|
-
expect(reviewerProcedure).toContain("
|
|
93
|
-
expect(reviewerProcedure).
|
|
94
|
-
expect(reviewerProcedure).toContain("
|
|
88
|
+
// Verify the procedure includes branch verification and build checks
|
|
89
|
+
expect(reviewerProcedure).toContain("git branch --show-current");
|
|
90
|
+
expect(reviewerProcedure).toContain("pnpm run build");
|
|
91
|
+
expect(reviewerProcedure).toContain("pnpm run check");
|
|
95
92
|
});
|
|
96
|
-
test("developer procedure should include
|
|
93
|
+
test("developer procedure should include changeset and failure handling", async () => {
|
|
97
94
|
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
98
95
|
const workflow = parse(yamlContent);
|
|
99
96
|
const developerProcedure = workflow.roles.developer?.procedure;
|
|
100
97
|
expect(developerProcedure).toBeDefined();
|
|
101
|
-
// Verify the procedure includes
|
|
102
|
-
expect(developerProcedure).
|
|
103
|
-
expect(developerProcedure).toMatch(/3 test cycles|after 3 attempts/i);
|
|
98
|
+
// Verify the procedure includes changeset requirement and failure path
|
|
99
|
+
expect(developerProcedure).toContain(".changeset/");
|
|
104
100
|
expect(developerProcedure).toContain("$status=failed");
|
|
101
|
+
expect(developerProcedure).toContain("pnpm test");
|
|
105
102
|
});
|
|
106
103
|
});
|
|
107
104
|
//# sourceMappingURL=solve-issue-tea-worktree.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"solve-issue-tea-worktree.test.js","sourceRoot":"","sources":["../../src/__tests__/solve-issue-tea-worktree.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,MAAM,CAAC;AAE7B;;;;;;GAMG;AAEH,QAAQ,CAAC,6CAA6C,EAAE,GAAG,EAAE;IAC3D,2DAA2D;IAC3D,MAAM,YAAY,GAAG,IAAI,CACvB,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EACvC,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,
|
|
1
|
+
{"version":3,"file":"solve-issue-tea-worktree.test.js","sourceRoot":"","sources":["../../src/__tests__/solve-issue-tea-worktree.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"step-ask.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/step-ask.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { bootstrap, putSchema } from "@ocas/core";
|
|
7
|
+
import { openStore } from "@ocas/fs";
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
9
|
+
import { registerUwfSchemas } from "../schemas.js";
|
|
10
|
+
import { seedThreads } from "./thread-test-helpers.js";
|
|
11
|
+
const OUTPUT_SCHEMA = {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
$status: { type: "string" },
|
|
15
|
+
note: { type: "string" },
|
|
16
|
+
},
|
|
17
|
+
required: ["$status"],
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
};
|
|
20
|
+
const DETAIL_SCHEMA = {
|
|
21
|
+
title: "ask-detail",
|
|
22
|
+
type: "object",
|
|
23
|
+
required: ["sessionId", "model", "duration", "turnCount", "turns"],
|
|
24
|
+
properties: {
|
|
25
|
+
sessionId: { type: "string" },
|
|
26
|
+
model: { type: "string" },
|
|
27
|
+
duration: { type: "integer" },
|
|
28
|
+
turnCount: { type: "integer" },
|
|
29
|
+
turns: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: { type: "string", format: "ocas_ref" },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
};
|
|
36
|
+
const THREAD_ID = "01ASKSTEPTEST000000000";
|
|
37
|
+
const STEP_SESSION_ID = "ses-original-step-001";
|
|
38
|
+
let tmpDir;
|
|
39
|
+
beforeEach(async () => {
|
|
40
|
+
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-ask-test-"));
|
|
41
|
+
});
|
|
42
|
+
afterEach(async () => {
|
|
43
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
async function setupAskFixture(opts = {}) {
|
|
46
|
+
const cfg = {
|
|
47
|
+
threadStatus: opts.threadStatus ?? "idle",
|
|
48
|
+
withDetail: opts.withDetail ?? true,
|
|
49
|
+
stepAgentNameOverride: opts.stepAgentNameOverride ?? null,
|
|
50
|
+
preCachedForkSessionId: opts.preCachedForkSessionId ?? null,
|
|
51
|
+
};
|
|
52
|
+
const casDir = join(tmpDir, "cas");
|
|
53
|
+
await mkdir(casDir, { recursive: true });
|
|
54
|
+
const store = await openStore(casDir);
|
|
55
|
+
await bootstrap(store);
|
|
56
|
+
const schemas = await registerUwfSchemas(store);
|
|
57
|
+
const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
|
|
58
|
+
const detailSchemaHash = await putSchema(store, DETAIL_SCHEMA);
|
|
59
|
+
const workflowHash = await store.cas.put(schemas.workflow, {
|
|
60
|
+
name: "test-ask",
|
|
61
|
+
description: "ask command integration test",
|
|
62
|
+
roles: {
|
|
63
|
+
worker: {
|
|
64
|
+
description: "Worker",
|
|
65
|
+
goal: "Work",
|
|
66
|
+
capabilities: [],
|
|
67
|
+
procedure: "work",
|
|
68
|
+
output: "result",
|
|
69
|
+
frontmatter: outputSchemaHash,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
graph: {
|
|
73
|
+
$START: {
|
|
74
|
+
new: { role: "worker", prompt: "Start work", location: null },
|
|
75
|
+
},
|
|
76
|
+
worker: { ok: { role: "$END", prompt: "done", location: null } },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
const startHash = await store.cas.put(schemas.startNode, {
|
|
80
|
+
workflow: workflowHash,
|
|
81
|
+
prompt: "Test ask task",
|
|
82
|
+
cwd: tmpDir,
|
|
83
|
+
});
|
|
84
|
+
// Set OCAS_HOME so seedThreads + in-test createUwfStore calls resolve to this CAS dir.
|
|
85
|
+
process.env.OCAS_HOME = casDir;
|
|
86
|
+
// Capture file paths
|
|
87
|
+
const promptCapturePath = join(tmpDir, "captured-prompt.txt");
|
|
88
|
+
const modeCapturePath = join(tmpDir, "captured-mode.txt");
|
|
89
|
+
const forkSessionCapturePath = join(tmpDir, "captured-fork-session.txt");
|
|
90
|
+
const askSessionCapturePath = join(tmpDir, "captured-ask-session.txt");
|
|
91
|
+
const envCapturePath = join(tmpDir, "captured-env.txt");
|
|
92
|
+
const mockAgentPath = join(tmpDir, "mock-agent.sh");
|
|
93
|
+
const failingAgentPath = join(tmpDir, "failing-agent.sh");
|
|
94
|
+
// Build a detail node with sessionId so step ask can extract it
|
|
95
|
+
let detailHash = null;
|
|
96
|
+
if (cfg.withDetail) {
|
|
97
|
+
const turnHash = await store.cas.put(detailSchemaHash, {
|
|
98
|
+
sessionId: STEP_SESSION_ID,
|
|
99
|
+
model: "test-model",
|
|
100
|
+
duration: 1000,
|
|
101
|
+
turnCount: 0,
|
|
102
|
+
turns: [],
|
|
103
|
+
});
|
|
104
|
+
detailHash = turnHash;
|
|
105
|
+
}
|
|
106
|
+
// Build the StepNode at thread head
|
|
107
|
+
const outputHash = await store.cas.put(outputSchemaHash, { $status: "ok" });
|
|
108
|
+
const stepHash = await store.cas.put(schemas.stepNode, {
|
|
109
|
+
start: startHash,
|
|
110
|
+
prev: null,
|
|
111
|
+
role: "worker",
|
|
112
|
+
output: outputHash,
|
|
113
|
+
detail: detailHash,
|
|
114
|
+
agent: cfg.stepAgentNameOverride ?? mockAgentPath,
|
|
115
|
+
edgePrompt: "Start work",
|
|
116
|
+
startedAtMs: 1716600000000,
|
|
117
|
+
completedAtMs: 1716600001000,
|
|
118
|
+
cwd: tmpDir,
|
|
119
|
+
assembledPrompt: null,
|
|
120
|
+
usage: null,
|
|
121
|
+
});
|
|
122
|
+
// Seed thread index entry
|
|
123
|
+
await seedThreads(tmpDir, {
|
|
124
|
+
[THREAD_ID]: {
|
|
125
|
+
head: stepHash,
|
|
126
|
+
status: cfg.threadStatus,
|
|
127
|
+
suspendedRole: null,
|
|
128
|
+
suspendMessage: null,
|
|
129
|
+
completedAt: cfg.threadStatus === "end" ? 1716600001000 : null,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
// Pre-seed the ask session cache so reuse tests have something to find.
|
|
133
|
+
if (cfg.preCachedForkSessionId !== null) {
|
|
134
|
+
const cachePath = join(tmpDir, "cache", "mock-sessions.json");
|
|
135
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
136
|
+
await writeFile(cachePath, `${JSON.stringify({ [`${stepHash}:ask`]: cfg.preCachedForkSessionId }, null, 2)}\n`, "utf8");
|
|
137
|
+
}
|
|
138
|
+
// Mock agent: dispatches based on `--mode` (ask|fork|run) and captures inputs.
|
|
139
|
+
// - --mode ask --session <id> --prompt <text>: writes to ask capture; echoes a fixed answer to stdout
|
|
140
|
+
// - --mode fork --session <id>: writes to fork capture; prints "forked-from-<id>" sessionId on stdout
|
|
141
|
+
// - default (uwf-* style invocation): captures and echoes adapter JSON (not used in this suite)
|
|
142
|
+
await writeFile(mockAgentPath, `#!/bin/sh
|
|
143
|
+
mode=""
|
|
144
|
+
prompt=""
|
|
145
|
+
session=""
|
|
146
|
+
detail=""
|
|
147
|
+
while [ $# -gt 0 ]; do
|
|
148
|
+
case "$1" in
|
|
149
|
+
--mode) mode="$2"; shift 2 ;;
|
|
150
|
+
--prompt) prompt="$2"; shift 2 ;;
|
|
151
|
+
--session) session="$2"; shift 2 ;;
|
|
152
|
+
--detail) detail="$2"; shift 2 ;;
|
|
153
|
+
*) shift ;;
|
|
154
|
+
esac
|
|
155
|
+
done
|
|
156
|
+
printf '%s' "$mode" > '${modeCapturePath}'
|
|
157
|
+
printf '%s' "$prompt" > '${promptCapturePath}'
|
|
158
|
+
printf 'OCAS_HOME=%s\\n' "$OCAS_HOME" > '${envCapturePath}'
|
|
159
|
+
case "$mode" in
|
|
160
|
+
fork)
|
|
161
|
+
printf '%s' "$session" > '${forkSessionCapturePath}'
|
|
162
|
+
new_id="forked-from-$session"
|
|
163
|
+
printf '%s\\n' "$new_id"
|
|
164
|
+
;;
|
|
165
|
+
ask)
|
|
166
|
+
printf '%s' "$session" > '${askSessionCapturePath}'
|
|
167
|
+
# Print a deterministic answer that the cmdStepAsk path will hand back.
|
|
168
|
+
printf 'MOCK_ANSWER prompt=%s session=%s detail=%s\\n' "$prompt" "$session" "$detail"
|
|
169
|
+
;;
|
|
170
|
+
*)
|
|
171
|
+
echo "{\\"stepHash\\":\\"unused\\"}"
|
|
172
|
+
;;
|
|
173
|
+
esac
|
|
174
|
+
`, { mode: 0o755 });
|
|
175
|
+
await writeFile(failingAgentPath, `#!/bin/sh
|
|
176
|
+
echo "boom" >&2
|
|
177
|
+
exit 7
|
|
178
|
+
`, { mode: 0o755 });
|
|
179
|
+
// Minimal config so loadWorkflowConfig succeeds.
|
|
180
|
+
const configPath = join(tmpDir, "config.yaml");
|
|
181
|
+
await writeFile(configPath, `defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`);
|
|
182
|
+
return {
|
|
183
|
+
casDir,
|
|
184
|
+
stepHash,
|
|
185
|
+
startHash,
|
|
186
|
+
workflowHash,
|
|
187
|
+
detailHash,
|
|
188
|
+
mockAgentPath,
|
|
189
|
+
failingAgentPath,
|
|
190
|
+
promptCapturePath,
|
|
191
|
+
modeCapturePath,
|
|
192
|
+
forkSessionCapturePath,
|
|
193
|
+
askSessionCapturePath,
|
|
194
|
+
envCapturePath,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function runUwf(args, casDir) {
|
|
198
|
+
const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
|
|
199
|
+
try {
|
|
200
|
+
const stdout = execFileSync(process.execPath, [cliPath, ...args], {
|
|
201
|
+
encoding: "utf8",
|
|
202
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
203
|
+
env: {
|
|
204
|
+
...process.env,
|
|
205
|
+
UWF_HOME: tmpDir,
|
|
206
|
+
OCAS_HOME: casDir,
|
|
207
|
+
},
|
|
208
|
+
cwd: tmpDir,
|
|
209
|
+
timeout: 30000,
|
|
210
|
+
});
|
|
211
|
+
return { stdout, stderr: "", status: 0 };
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
const err = error;
|
|
215
|
+
return {
|
|
216
|
+
stdout: typeof err.stdout === "string" ? err.stdout : (err.stdout?.toString("utf8") ?? ""),
|
|
217
|
+
stderr: typeof err.stderr === "string" ? err.stderr : (err.stderr?.toString("utf8") ?? ""),
|
|
218
|
+
status: err.status ?? 1,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ── Group 1: CLI argument validation ───────────────────────────────────────
|
|
223
|
+
describe("uwf step ask - CLI argument validation", () => {
|
|
224
|
+
test("1.1 missing step-hash exits non-zero", async () => {
|
|
225
|
+
const { casDir } = await setupAskFixture();
|
|
226
|
+
const result = runUwf(["step", "ask"], casDir);
|
|
227
|
+
expect(result.status).not.toBe(0);
|
|
228
|
+
});
|
|
229
|
+
test("1.2 missing -p flag exits non-zero", async () => {
|
|
230
|
+
const { casDir, stepHash } = await setupAskFixture();
|
|
231
|
+
const result = runUwf(["step", "ask", stepHash], casDir);
|
|
232
|
+
expect(result.status).not.toBe(0);
|
|
233
|
+
expect(result.stderr.toLowerCase()).toMatch(/required|missing|prompt/);
|
|
234
|
+
});
|
|
235
|
+
test("1.3 step-hash and -p accepted as valid invocation", async () => {
|
|
236
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
237
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "why?", "--agent", mockAgentPath], casDir);
|
|
238
|
+
expect(result.status).toBe(0);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
// ── Group 2: CAS validation errors ────────────────────────────────────────
|
|
242
|
+
describe("uwf step ask - CAS validation errors", () => {
|
|
243
|
+
test("2.1 non-existent CAS hash exits non-zero with 'not found'", async () => {
|
|
244
|
+
const { casDir, mockAgentPath } = await setupAskFixture();
|
|
245
|
+
const result = runUwf(["step", "ask", "0000000000000", "-p", "why?", "--agent", mockAgentPath], casDir);
|
|
246
|
+
expect(result.status).not.toBe(0);
|
|
247
|
+
expect(result.stderr.toLowerCase()).toContain("not found");
|
|
248
|
+
});
|
|
249
|
+
test("2.2 hash that is not a StepNode exits non-zero", async () => {
|
|
250
|
+
const { casDir, startHash, mockAgentPath } = await setupAskFixture();
|
|
251
|
+
// Use the StartNode hash — it exists but is not a StepNode
|
|
252
|
+
const result = runUwf(["step", "ask", startHash, "-p", "why?", "--agent", mockAgentPath], casDir);
|
|
253
|
+
expect(result.status).not.toBe(0);
|
|
254
|
+
expect(result.stderr.toLowerCase()).toContain("not a stepnode");
|
|
255
|
+
});
|
|
256
|
+
test("2.3 step with no detail ref exits non-zero", async () => {
|
|
257
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture({ withDetail: false });
|
|
258
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "why?", "--agent", mockAgentPath], casDir);
|
|
259
|
+
expect(result.status).not.toBe(0);
|
|
260
|
+
expect(result.stderr.toLowerCase()).toMatch(/no detail|detail.*missing|missing.*detail/);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
// ── Group 3: Successful ask (core behavior) ───────────────────────────────
|
|
264
|
+
describe("uwf step ask - successful ask (core)", () => {
|
|
265
|
+
test("3.1 stdout contains agent's response text", async () => {
|
|
266
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
267
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "why tar not zip?", "--agent", mockAgentPath], casDir);
|
|
268
|
+
expect(result.status).toBe(0);
|
|
269
|
+
expect(result.stdout).toContain("MOCK_ANSWER");
|
|
270
|
+
expect(result.stdout).toContain("why tar not zip?");
|
|
271
|
+
});
|
|
272
|
+
test("3.2 thread index entry (head, status) is identical before and after ask", async () => {
|
|
273
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
274
|
+
// Before ask: snapshot the thread state
|
|
275
|
+
const { createUwfStore, getThread } = await import("../store.js");
|
|
276
|
+
const before = await createUwfStore(tmpDir);
|
|
277
|
+
const beforeEntry = getThread(before.varStore, THREAD_ID);
|
|
278
|
+
expect(beforeEntry).not.toBeNull();
|
|
279
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "anything", "--agent", mockAgentPath], casDir);
|
|
280
|
+
expect(result.status).toBe(0);
|
|
281
|
+
// After ask: thread state should be unchanged
|
|
282
|
+
const after = await createUwfStore(tmpDir);
|
|
283
|
+
const afterEntry = getThread(after.varStore, THREAD_ID);
|
|
284
|
+
expect(afterEntry).not.toBeNull();
|
|
285
|
+
expect(afterEntry?.head).toBe(beforeEntry?.head);
|
|
286
|
+
expect(afterEntry?.status).toBe(beforeEntry?.status);
|
|
287
|
+
});
|
|
288
|
+
test("3.3 no new StepNode is written to CAS (step count unchanged)", async () => {
|
|
289
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
290
|
+
// Count StepNodes before
|
|
291
|
+
const { createUwfStore } = await import("../store.js");
|
|
292
|
+
const before = await createUwfStore(tmpDir);
|
|
293
|
+
const stepSchemaHash = before.schemas.stepNode;
|
|
294
|
+
function countStepNodes(uwfStore) {
|
|
295
|
+
const candidates = [stepHash];
|
|
296
|
+
let count = 0;
|
|
297
|
+
for (const h of candidates) {
|
|
298
|
+
const node = uwfStore.store.cas.get(h);
|
|
299
|
+
if (node !== null && node.type === stepSchemaHash)
|
|
300
|
+
count++;
|
|
301
|
+
}
|
|
302
|
+
return count;
|
|
303
|
+
}
|
|
304
|
+
const beforeCount = countStepNodes(before);
|
|
305
|
+
expect(beforeCount).toBe(1);
|
|
306
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "anything", "--agent", mockAgentPath], casDir);
|
|
307
|
+
expect(result.status).toBe(0);
|
|
308
|
+
// After ask: still only the seeded StepNode exists at head; no new step appended.
|
|
309
|
+
const after = await createUwfStore(tmpDir);
|
|
310
|
+
const headNode = after.store.cas.get(stepHash);
|
|
311
|
+
expect(headNode).not.toBeNull();
|
|
312
|
+
expect(headNode?.type).toBe(after.schemas.stepNode);
|
|
313
|
+
// Confirm thread head still points to the original step hash
|
|
314
|
+
const { getThread } = await import("../store.js");
|
|
315
|
+
const entry = getThread(after.varStore, THREAD_ID);
|
|
316
|
+
expect(entry?.head).toBe(stepHash);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
// ── Group 4: Fork cache semantics ─────────────────────────────────────────
|
|
320
|
+
describe("uwf step ask - fork cache", { timeout: 15_000 }, () => {
|
|
321
|
+
test("4.1 first ask creates a fork session and caches it", async () => {
|
|
322
|
+
const { casDir, stepHash, mockAgentPath, forkSessionCapturePath } = await setupAskFixture();
|
|
323
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "first ask", "--agent", mockAgentPath], casDir);
|
|
324
|
+
expect(result.status).toBe(0);
|
|
325
|
+
// The mock agent in fork mode receives the source session id
|
|
326
|
+
const forkArg = await readFile(forkSessionCapturePath, "utf8");
|
|
327
|
+
expect(forkArg).toBe(STEP_SESSION_ID);
|
|
328
|
+
// Cache file should now contain the ask key
|
|
329
|
+
const cachePath = join(tmpDir, "cache", "mock-sessions.json");
|
|
330
|
+
const raw = await readFile(cachePath, "utf8");
|
|
331
|
+
const parsed = JSON.parse(raw);
|
|
332
|
+
expect(parsed[`${stepHash}:ask`]).toBeDefined();
|
|
333
|
+
expect(parsed[`${stepHash}:ask`]).toBe(`forked-from-${STEP_SESSION_ID}`);
|
|
334
|
+
});
|
|
335
|
+
test("4.2 second ask on same step reuses the cached fork session", async () => {
|
|
336
|
+
const cachedFork = "ses-already-forked-once";
|
|
337
|
+
const { casDir, stepHash, mockAgentPath, modeCapturePath, askSessionCapturePath } = await setupAskFixture({ preCachedForkSessionId: cachedFork });
|
|
338
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "second ask", "--agent", mockAgentPath], casDir);
|
|
339
|
+
expect(result.status).toBe(0);
|
|
340
|
+
// The mock agent must have been invoked in `ask` mode (no fork performed).
|
|
341
|
+
const mode = await readFile(modeCapturePath, "utf8");
|
|
342
|
+
expect(mode).toBe("ask");
|
|
343
|
+
// The ask invocation should have received the cached fork session id.
|
|
344
|
+
const askArg = await readFile(askSessionCapturePath, "utf8");
|
|
345
|
+
expect(askArg).toBe(cachedFork);
|
|
346
|
+
});
|
|
347
|
+
test("4.3 different step hash creates an independent fork", async () => {
|
|
348
|
+
// Run a first ask on the base step → caches forkA
|
|
349
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
350
|
+
const r1 = runUwf(["step", "ask", stepHash, "-p", "ask on step A", "--agent", mockAgentPath], casDir);
|
|
351
|
+
expect(r1.status).toBe(0);
|
|
352
|
+
// Build a second StepNode (different hash) with a different sessionId so
|
|
353
|
+
// its detail-derived ask session is independent of the first.
|
|
354
|
+
const { createUwfStore } = await import("../store.js");
|
|
355
|
+
const uwf = await createUwfStore(tmpDir);
|
|
356
|
+
const detailSchemaHash = await putSchema(uwf.store, DETAIL_SCHEMA);
|
|
357
|
+
const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA);
|
|
358
|
+
const otherDetailHash = await uwf.store.cas.put(detailSchemaHash, {
|
|
359
|
+
sessionId: "ses-original-step-002",
|
|
360
|
+
model: "test-model",
|
|
361
|
+
duration: 1000,
|
|
362
|
+
turnCount: 0,
|
|
363
|
+
turns: [],
|
|
364
|
+
});
|
|
365
|
+
const otherOutputHash = await uwf.store.cas.put(outputSchemaHash, {
|
|
366
|
+
$status: "ok",
|
|
367
|
+
note: "alt",
|
|
368
|
+
});
|
|
369
|
+
// Reuse the same start ref the first step points to so the new step is a valid sibling.
|
|
370
|
+
const head = uwf.store.cas.get(stepHash);
|
|
371
|
+
const startRefFromHead = (head?.payload).start;
|
|
372
|
+
const properOtherStep = await uwf.store.cas.put(uwf.schemas.stepNode, {
|
|
373
|
+
start: startRefFromHead,
|
|
374
|
+
prev: null,
|
|
375
|
+
role: "worker",
|
|
376
|
+
output: otherOutputHash,
|
|
377
|
+
detail: otherDetailHash,
|
|
378
|
+
agent: mockAgentPath,
|
|
379
|
+
edgePrompt: "Start work",
|
|
380
|
+
startedAtMs: 1716600002000,
|
|
381
|
+
completedAtMs: 1716600003000,
|
|
382
|
+
cwd: tmpDir,
|
|
383
|
+
assembledPrompt: null,
|
|
384
|
+
usage: null,
|
|
385
|
+
});
|
|
386
|
+
// sanity check we constructed a separate hash
|
|
387
|
+
expect(properOtherStep).not.toBe(stepHash);
|
|
388
|
+
const r2 = runUwf(["step", "ask", properOtherStep, "-p", "ask on step B", "--agent", mockAgentPath], casDir);
|
|
389
|
+
expect(r2.status).toBe(0);
|
|
390
|
+
const cachePath = join(tmpDir, "cache", "mock-sessions.json");
|
|
391
|
+
const raw = await readFile(cachePath, "utf8");
|
|
392
|
+
const parsed = JSON.parse(raw);
|
|
393
|
+
expect(parsed[`${stepHash}:ask`]).toBeDefined();
|
|
394
|
+
expect(parsed[`${properOtherStep}:ask`]).toBeDefined();
|
|
395
|
+
expect(parsed[`${stepHash}:ask`]).not.toBe(parsed[`${properOtherStep}:ask`]);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
// ── Group 5: Fallback (agent has no fork support) ─────────────────────────
|
|
399
|
+
describe("uwf step ask - fallback path", () => {
|
|
400
|
+
test("5.1 fallback agent (no fork support) still answers via stdout", async () => {
|
|
401
|
+
// Use a fallback agent that ONLY supports `ask` mode without ever being asked
|
|
402
|
+
// to fork. The CLI should detect missing fork support and inject context instead.
|
|
403
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
404
|
+
// Create a fallback agent script that fails with non-zero exit on "fork" mode.
|
|
405
|
+
// Fallback path must NOT call mode=fork; it should call mode=ask directly.
|
|
406
|
+
const fallbackPath = join(tmpDir, "fallback-agent.sh");
|
|
407
|
+
const promptCapture = join(tmpDir, "fallback-prompt.txt");
|
|
408
|
+
const sessionCapture = join(tmpDir, "fallback-session.txt");
|
|
409
|
+
const modeCapture = join(tmpDir, "fallback-mode.txt");
|
|
410
|
+
await writeFile(fallbackPath, `#!/bin/sh
|
|
411
|
+
mode=""
|
|
412
|
+
prompt=""
|
|
413
|
+
session=""
|
|
414
|
+
detail=""
|
|
415
|
+
while [ $# -gt 0 ]; do
|
|
416
|
+
case "$1" in
|
|
417
|
+
--mode) mode="$2"; shift 2 ;;
|
|
418
|
+
--prompt) prompt="$2"; shift 2 ;;
|
|
419
|
+
--session) session="$2"; shift 2 ;;
|
|
420
|
+
--detail) detail="$2"; shift 2 ;;
|
|
421
|
+
*) shift ;;
|
|
422
|
+
esac
|
|
423
|
+
done
|
|
424
|
+
printf '%s' "$mode" > '${modeCapture}'
|
|
425
|
+
printf '%s' "$prompt" > '${promptCapture}'
|
|
426
|
+
printf '%s' "$session" > '${sessionCapture}'
|
|
427
|
+
case "$mode" in
|
|
428
|
+
fork) echo "fork not supported" >&2; exit 99 ;;
|
|
429
|
+
ask) printf 'FALLBACK_ANSWER for: %s (detail=%s)\\n' "$prompt" "$detail" ;;
|
|
430
|
+
*) echo "unknown" >&2; exit 1 ;;
|
|
431
|
+
esac
|
|
432
|
+
`, { mode: 0o755 });
|
|
433
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "explain context", "--agent", fallbackPath, "--no-fork"], casDir);
|
|
434
|
+
expect(result.status).toBe(0);
|
|
435
|
+
expect(result.stdout).toContain("FALLBACK_ANSWER");
|
|
436
|
+
expect(result.stdout).toContain("explain context");
|
|
437
|
+
// The fallback agent should be invoked in `ask` mode, with NO session id
|
|
438
|
+
// (since no fork happened). The detail ref must be passed for context injection.
|
|
439
|
+
const mode = await readFile(modeCapture, "utf8");
|
|
440
|
+
expect(mode).toBe("ask");
|
|
441
|
+
const session = await readFile(sessionCapture, "utf8");
|
|
442
|
+
expect(session).toBe("");
|
|
443
|
+
// Make sure mockAgentPath's mock never ran.
|
|
444
|
+
void mockAgentPath;
|
|
445
|
+
});
|
|
446
|
+
test("5.2 fallback ask still does NOT mutate thread state", async () => {
|
|
447
|
+
const { casDir, stepHash } = await setupAskFixture();
|
|
448
|
+
const fallbackPath = join(tmpDir, "fallback-agent.sh");
|
|
449
|
+
await writeFile(fallbackPath, `#!/bin/sh
|
|
450
|
+
mode=""
|
|
451
|
+
prompt=""
|
|
452
|
+
while [ $# -gt 0 ]; do
|
|
453
|
+
case "$1" in
|
|
454
|
+
--mode) mode="$2"; shift 2 ;;
|
|
455
|
+
--prompt) prompt="$2"; shift 2 ;;
|
|
456
|
+
*) shift ;;
|
|
457
|
+
esac
|
|
458
|
+
done
|
|
459
|
+
case "$mode" in
|
|
460
|
+
fork) echo "fork not supported" >&2; exit 99 ;;
|
|
461
|
+
ask) printf 'OK %s\\n' "$prompt" ;;
|
|
462
|
+
*) exit 1 ;;
|
|
463
|
+
esac
|
|
464
|
+
`, { mode: 0o755 });
|
|
465
|
+
const { createUwfStore, getThread } = await import("../store.js");
|
|
466
|
+
const before = await createUwfStore(tmpDir);
|
|
467
|
+
const beforeEntry = getThread(before.varStore, THREAD_ID);
|
|
468
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "any", "--agent", fallbackPath, "--no-fork"], casDir);
|
|
469
|
+
expect(result.status).toBe(0);
|
|
470
|
+
const after = await createUwfStore(tmpDir);
|
|
471
|
+
const afterEntry = getThread(after.varStore, THREAD_ID);
|
|
472
|
+
expect(afterEntry?.head).toBe(beforeEntry?.head);
|
|
473
|
+
expect(afterEntry?.status).toBe(beforeEntry?.status);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
// ── Group 6: Agent resolution ─────────────────────────────────────────────
|
|
477
|
+
describe("uwf step ask - agent resolution", () => {
|
|
478
|
+
test("6.1 without --agent flag, agent is resolved from step's agent field", async () => {
|
|
479
|
+
// Step's agent field points at mockAgentPath by default.
|
|
480
|
+
const { casDir, stepHash, modeCapturePath, promptCapturePath } = await setupAskFixture();
|
|
481
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "explain"], casDir);
|
|
482
|
+
expect(result.status).toBe(0);
|
|
483
|
+
// The mockAgentPath must have been invoked in ask mode with the user prompt.
|
|
484
|
+
const mode = await readFile(modeCapturePath, "utf8");
|
|
485
|
+
expect(mode).toBe("ask");
|
|
486
|
+
const captured = await readFile(promptCapturePath, "utf8");
|
|
487
|
+
expect(captured).toBe("explain");
|
|
488
|
+
});
|
|
489
|
+
test("6.2 --agent override beats step's recorded agent", async () => {
|
|
490
|
+
// Record a non-existent agent in step.agent. Provide a working one via --agent.
|
|
491
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture({
|
|
492
|
+
stepAgentNameOverride: "uwf-does-not-exist",
|
|
493
|
+
});
|
|
494
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "explain", "--agent", mockAgentPath], casDir);
|
|
495
|
+
expect(result.status).toBe(0);
|
|
496
|
+
expect(result.stdout).toContain("MOCK_ANSWER");
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
//# sourceMappingURL=step-ask.test.js.map
|