@united-workforce/cli 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -11
- package/dist/.build-fingerprint +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +17 -7
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts +2 -0
- package/dist/__tests__/agent-resolution-llm-free.test.d.ts.map +1 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js +30 -0
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -0
- package/dist/__tests__/build-step-entry.test.d.ts +2 -0
- package/dist/__tests__/build-step-entry.test.d.ts.map +1 -0
- package/dist/__tests__/build-step-entry.test.js +173 -0
- package/dist/__tests__/build-step-entry.test.js.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts +2 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.d.ts.map +1 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js +93 -0
- package/dist/__tests__/clear-thread-failed-attempts.test.js.map +1 -0
- package/dist/__tests__/concurrency.test.d.ts +2 -0
- package/dist/__tests__/concurrency.test.d.ts.map +1 -0
- package/dist/__tests__/concurrency.test.js +196 -0
- package/dist/__tests__/concurrency.test.js.map +1 -0
- package/dist/__tests__/config.test.js +26 -302
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/current-role.test.js +7 -6
- package/dist/__tests__/current-role.test.js.map +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js +43 -30
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/format-text-default.test.d.ts +2 -0
- package/dist/__tests__/format-text-default.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-default.test.js +43 -0
- package/dist/__tests__/format-text-default.test.js.map +1 -0
- package/dist/__tests__/format-text-registry.test.d.ts +2 -0
- package/dist/__tests__/format-text-registry.test.d.ts.map +1 -0
- package/dist/__tests__/format-text-registry.test.js +158 -0
- package/dist/__tests__/format-text-registry.test.js.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts +2 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.d.ts.map +1 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js +40 -0
- package/dist/__tests__/issue-180-workflow-ref-removed.test.js.map +1 -0
- package/dist/__tests__/log-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/log-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/log-text-renderer.test.js +265 -0
- package/dist/__tests__/log-text-renderer.test.js.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.js +9 -50
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -1
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js +102 -0
- package/dist/__tests__/output-mapper-thread-list-startedat.test.js.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts +2 -0
- package/dist/__tests__/output-mapper-workflow-add.test.d.ts.map +1 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js +22 -0
- package/dist/__tests__/output-mapper-workflow-add.test.js.map +1 -0
- package/dist/__tests__/pid-recycling.test.d.ts +2 -0
- package/dist/__tests__/pid-recycling.test.d.ts.map +1 -0
- package/dist/__tests__/pid-recycling.test.js +273 -0
- package/dist/__tests__/pid-recycling.test.js.map +1 -0
- package/dist/__tests__/prompt.test.js +365 -2
- package/dist/__tests__/prompt.test.js.map +1 -1
- package/dist/__tests__/resolve-head-hash.test.js +12 -4
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -1
- package/dist/__tests__/setup-agent-discovery.test.js +21 -30
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-complexity.test.js +2 -168
- package/dist/__tests__/setup-complexity.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.d.ts +2 -0
- package/dist/__tests__/setup-no-llm.test.d.ts.map +1 -0
- package/dist/__tests__/setup-no-llm.test.js +52 -0
- package/dist/__tests__/setup-no-llm.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +27 -28
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.d.ts +2 -0
- package/dist/__tests__/step-ask.test.d.ts.map +1 -0
- package/dist/__tests__/step-ask.test.js +507 -0
- package/dist/__tests__/step-ask.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.js +1 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-timing.test.js +2 -0
- package/dist/__tests__/step-timing.test.js.map +1 -1
- package/dist/__tests__/store-global-cas.test.js +2 -2
- package/dist/__tests__/store-global-cas.test.js.map +1 -1
- package/dist/__tests__/store-unified-threads.test.js +28 -26
- package/dist/__tests__/store-unified-threads.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-status.test.js +25 -19
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -1
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js +110 -0
- package/dist/__tests__/thread-cancel-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +354 -17
- package/dist/__tests__/thread-list-filters.test.js.map +1 -1
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts +2 -0
- package/dist/__tests__/thread-list-template-ms-date.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js +102 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts +2 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js +157 -0
- package/dist/__tests__/thread-list-workflow-corrupt.test.js.map +1 -0
- package/dist/__tests__/thread-poke.test.d.ts +2 -0
- package/dist/__tests__/thread-poke.test.d.ts.map +1 -0
- package/dist/__tests__/thread-poke.test.js +422 -0
- package/dist/__tests__/thread-poke.test.js.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.js +10 -9
- package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +21 -15
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-show-status.test.js +17 -28
- package/dist/__tests__/thread-show-status.test.js.map +1 -1
- package/dist/__tests__/thread-start-cwd-cli.test.js +15 -3
- package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -1
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts +2 -0
- package/dist/__tests__/thread-stop-text-renderer.test.d.ts.map +1 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js +148 -0
- package/dist/__tests__/thread-stop-text-renderer.test.js.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.js +13 -16
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread-suspended-display.test.js +10 -22
- package/dist/__tests__/thread-suspended-display.test.js.map +1 -1
- package/dist/__tests__/thread-test-helpers.d.ts +7 -0
- package/dist/__tests__/thread-test-helpers.d.ts.map +1 -1
- package/dist/__tests__/thread-test-helpers.js +13 -0
- package/dist/__tests__/thread-test-helpers.js.map +1 -1
- package/dist/__tests__/thread.test.js +15 -13
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/__tests__/validate-semantic.test.js +105 -23
- package/dist/__tests__/validate-semantic.test.js.map +1 -1
- package/dist/__tests__/workflow-list-recursive.test.d.ts +2 -0
- package/dist/__tests__/workflow-list-recursive.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-list-recursive.test.js +286 -0
- package/dist/__tests__/workflow-list-recursive.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +46 -28
- package/dist/__tests__/workflow-resolution.test.js.map +1 -1
- package/dist/__tests__/workflow-show-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-show-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-show-resolution.test.js +213 -0
- package/dist/__tests__/workflow-show-resolution.test.js.map +1 -0
- package/dist/__tests__/workflow-validate.test.d.ts +2 -0
- package/dist/__tests__/workflow-validate.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-validate.test.js +707 -0
- package/dist/__tests__/workflow-validate.test.js.map +1 -0
- package/dist/__tests__/write-envelope.test.d.ts +2 -0
- package/dist/__tests__/write-envelope.test.d.ts.map +1 -0
- package/dist/__tests__/write-envelope.test.js +201 -0
- package/dist/__tests__/write-envelope.test.js.map +1 -0
- package/dist/background/background.d.ts +22 -1
- package/dist/background/background.d.ts.map +1 -1
- package/dist/background/background.js +83 -6
- package/dist/background/background.js.map +1 -1
- package/dist/background/index.d.ts +1 -1
- package/dist/background/index.d.ts.map +1 -1
- package/dist/background/index.js +1 -1
- package/dist/background/index.js.map +1 -1
- package/dist/background/types.d.ts +1 -0
- package/dist/background/types.d.ts.map +1 -1
- package/dist/cli.js +120 -62
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts +3 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +17 -31
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +57 -31
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +12 -39
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +72 -303
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +44 -1
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +255 -11
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +16 -3
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +423 -142
- package/dist/commands/thread.js.map +1 -1
- package/dist/commands/workflow.d.ts +9 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +126 -6
- package/dist/commands/workflow.js.map +1 -1
- package/dist/concurrency/concurrency.d.ts +34 -0
- package/dist/concurrency/concurrency.d.ts.map +1 -0
- package/dist/concurrency/concurrency.js +216 -0
- package/dist/concurrency/concurrency.js.map +1 -0
- package/dist/concurrency/index.d.ts +3 -0
- package/dist/concurrency/index.d.ts.map +1 -0
- package/dist/concurrency/index.js +2 -0
- package/dist/concurrency/index.js.map +1 -0
- package/dist/concurrency/types.d.ts +19 -0
- package/dist/concurrency/types.d.ts.map +1 -0
- package/dist/concurrency/types.js +2 -0
- package/dist/concurrency/types.js.map +1 -0
- package/dist/format.d.ts +69 -2
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +198 -1
- package/dist/format.js.map +1 -1
- package/dist/moderator/__tests__/evaluate.test.js +31 -17
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -1
- package/dist/moderator/evaluate.d.ts.map +1 -1
- package/dist/moderator/evaluate.js +4 -16
- package/dist/moderator/evaluate.js.map +1 -1
- package/dist/moderator/index.d.ts +1 -2
- package/dist/moderator/index.d.ts.map +1 -1
- package/dist/moderator/index.js +0 -1
- package/dist/moderator/index.js.map +1 -1
- package/dist/moderator/types.d.ts +6 -10
- package/dist/moderator/types.d.ts.map +1 -1
- package/dist/moderator/types.js +1 -3
- package/dist/moderator/types.js.map +1 -1
- package/dist/output-mappers.d.ts +122 -0
- package/dist/output-mappers.d.ts.map +1 -0
- package/dist/output-mappers.js +134 -0
- package/dist/output-mappers.js.map +1 -0
- package/dist/schemas.d.ts +6 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +34 -5
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +28 -9
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +75 -16
- package/dist/store.js.map +1 -1
- package/dist/text-renderers.d.ts +30 -0
- package/dist/text-renderers.d.ts.map +1 -0
- package/dist/text-renderers.js +251 -0
- package/dist/text-renderers.js.map +1 -0
- package/dist/validate-semantic.d.ts.map +1 -1
- package/dist/validate-semantic.js +95 -61
- package/dist/validate-semantic.js.map +1 -1
- package/dist/validate.d.ts +6 -0
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +24 -0
- package/dist/validate.js.map +1 -1
- package/examples/brainstorm.yaml +130 -0
- package/examples/debate.yaml +169 -0
- package/examples/socratic-questioning.yaml +112 -0
- package/package.json +9 -10
- package/src/__tests__/adapter-json-roundtrip.test.ts +16 -7
- package/src/__tests__/agent-resolution-llm-free.test.ts +39 -0
- package/src/__tests__/build-step-entry.test.ts +203 -0
- package/src/__tests__/clear-thread-failed-attempts.test.ts +122 -0
- package/src/__tests__/concurrency.test.ts +266 -0
- package/src/__tests__/config.test.ts +33 -321
- package/src/__tests__/current-role.test.ts +7 -6
- package/src/__tests__/e2e-mock-agent.test.ts +65 -30
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +1 -0
- package/src/__tests__/fixtures/{e2e-mustache.workflow.yaml → e2e-liquid.workflow.yaml} +3 -2
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +1 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +2 -2
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +6 -10
- package/src/__tests__/format-text-default.test.ts +49 -0
- package/src/__tests__/format-text-registry.test.ts +173 -0
- package/src/__tests__/issue-180-workflow-ref-removed.test.ts +43 -0
- package/src/__tests__/log-text-renderer.test.ts +294 -0
- package/src/__tests__/moderator-evaluate.test.ts +9 -52
- package/src/__tests__/output-mapper-thread-list-startedat.test.ts +124 -0
- package/src/__tests__/output-mapper-workflow-add.test.ts +24 -0
- package/src/__tests__/pid-recycling.test.ts +329 -0
- package/src/__tests__/prompt.test.ts +443 -2
- package/src/__tests__/resolve-head-hash.test.ts +11 -4
- package/src/__tests__/setup-agent-discovery.test.ts +26 -51
- package/src/__tests__/setup-complexity.test.ts +1 -203
- package/src/__tests__/setup-no-llm.test.ts +68 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +27 -31
- package/src/__tests__/step-ask.test.ts +677 -0
- package/src/__tests__/step-show-json.test.ts +1 -0
- package/src/__tests__/step-timing.test.ts +2 -0
- package/src/__tests__/store-global-cas.test.ts +2 -2
- package/src/__tests__/store-unified-threads.test.ts +30 -27
- package/src/__tests__/thread-cancel-status.test.ts +27 -20
- package/src/__tests__/thread-cancel-text-renderer.test.ts +125 -0
- package/src/__tests__/thread-list-filters.test.ts +443 -17
- package/src/__tests__/thread-list-template-ms-date.test.ts +110 -0
- package/src/__tests__/thread-list-workflow-corrupt.test.ts +198 -0
- package/src/__tests__/thread-poke.test.ts +554 -0
- package/src/__tests__/thread-read-xml-tags.test.ts +9 -11
- package/src/__tests__/thread-resume.test.ts +20 -15
- package/src/__tests__/thread-show-status.test.ts +17 -29
- package/src/__tests__/thread-start-cwd-cli.test.ts +15 -3
- package/src/__tests__/thread-stop-text-renderer.test.ts +168 -0
- package/src/__tests__/thread-suspend-step.test.ts +13 -16
- package/src/__tests__/thread-suspended-display.test.ts +10 -22
- package/src/__tests__/thread-test-helpers.ts +15 -1
- package/src/__tests__/thread.test.ts +14 -14
- package/src/__tests__/validate-semantic.test.ts +118 -33
- package/src/__tests__/workflow-list-recursive.test.ts +370 -0
- package/src/__tests__/workflow-resolution.test.ts +48 -29
- package/src/__tests__/workflow-show-resolution.test.ts +286 -0
- package/src/__tests__/workflow-validate.test.ts +828 -0
- package/src/__tests__/write-envelope.test.ts +257 -0
- package/src/background/background.ts +88 -6
- package/src/background/index.ts +2 -0
- package/src/background/types.ts +1 -0
- package/src/cli.ts +184 -77
- package/src/commands/config.ts +16 -33
- package/src/commands/prompt.ts +57 -31
- package/src/commands/setup.ts +80 -358
- package/src/commands/step.ts +339 -12
- package/src/commands/thread.ts +511 -171
- package/src/commands/workflow.ts +155 -4
- package/src/concurrency/concurrency.ts +245 -0
- package/src/concurrency/index.ts +10 -0
- package/src/concurrency/types.ts +19 -0
- package/src/format.ts +282 -2
- package/src/moderator/__tests__/evaluate.test.ts +34 -17
- package/src/moderator/evaluate.ts +5 -17
- package/src/moderator/index.ts +1 -6
- package/src/moderator/types.ts +6 -14
- package/src/output-mappers.ts +254 -0
- package/src/schemas.ts +51 -5
- package/src/store.ts +86 -20
- package/src/text-renderers.ts +355 -0
- package/src/validate-semantic.ts +125 -73
- package/src/validate.ts +27 -0
- package/dist/__tests__/setup-validate.test.d.ts +0 -2
- package/dist/__tests__/setup-validate.test.d.ts.map +0 -1
- package/dist/__tests__/setup-validate.test.js +0 -108
- package/dist/__tests__/setup-validate.test.js.map +0 -1
- package/src/__tests__/setup-validate.test.ts +0 -148
- /package/src/__tests__/fixtures/{e2e-mustache.mock.yaml → e2e-liquid.mock.yaml} +0 -0
|
@@ -0,0 +1,677 @@
|
|
|
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 type { CasRef, ThreadId, ThreadIndexEntry } from "@united-workforce/protocol";
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
10
|
+
import { registerUwfSchemas } from "../schemas.js";
|
|
11
|
+
import { seedThreads } from "./thread-test-helpers.js";
|
|
12
|
+
|
|
13
|
+
const OUTPUT_SCHEMA = {
|
|
14
|
+
type: "object" as const,
|
|
15
|
+
properties: {
|
|
16
|
+
$status: { type: "string" as const },
|
|
17
|
+
note: { type: "string" as const },
|
|
18
|
+
},
|
|
19
|
+
required: ["$status"],
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const DETAIL_SCHEMA = {
|
|
24
|
+
title: "ask-detail",
|
|
25
|
+
type: "object" as const,
|
|
26
|
+
required: ["sessionId", "model", "duration", "turnCount", "turns"],
|
|
27
|
+
properties: {
|
|
28
|
+
sessionId: { type: "string" as const },
|
|
29
|
+
model: { type: "string" as const },
|
|
30
|
+
duration: { type: "integer" as const },
|
|
31
|
+
turnCount: { type: "integer" as const },
|
|
32
|
+
turns: {
|
|
33
|
+
type: "array" as const,
|
|
34
|
+
items: { type: "string" as const, format: "ocas_ref" },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const THREAD_ID = "01ASKSTEPTEST000000000" as ThreadId;
|
|
41
|
+
const STEP_SESSION_ID = "ses-original-step-001";
|
|
42
|
+
|
|
43
|
+
let tmpDir: string;
|
|
44
|
+
let savedOcasHome: string | undefined;
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
48
|
+
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-ask-test-"));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(async () => {
|
|
52
|
+
if (savedOcasHome === undefined) {
|
|
53
|
+
delete process.env.OCAS_HOME;
|
|
54
|
+
} else {
|
|
55
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
56
|
+
}
|
|
57
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
type SetupOpts = {
|
|
61
|
+
threadStatus: ThreadIndexEntry["status"];
|
|
62
|
+
withDetail: boolean;
|
|
63
|
+
// The agent name (path or alias) to record in the head StepNode.agent field.
|
|
64
|
+
// Defaults to mockAgentPath.
|
|
65
|
+
stepAgentNameOverride: string | null;
|
|
66
|
+
// Pre-cached fork session-id. When provided, the cache file is written
|
|
67
|
+
// before running so the test can verify reuse semantics.
|
|
68
|
+
preCachedForkSessionId: string | null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type SetupResult = {
|
|
72
|
+
casDir: string;
|
|
73
|
+
stepHash: CasRef;
|
|
74
|
+
startHash: CasRef;
|
|
75
|
+
workflowHash: CasRef;
|
|
76
|
+
detailHash: CasRef | null;
|
|
77
|
+
mockAgentPath: string;
|
|
78
|
+
failingAgentPath: string;
|
|
79
|
+
promptCapturePath: string;
|
|
80
|
+
modeCapturePath: string;
|
|
81
|
+
forkSessionCapturePath: string;
|
|
82
|
+
askSessionCapturePath: string;
|
|
83
|
+
envCapturePath: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
async function setupAskFixture(opts: Partial<SetupOpts> = {}): Promise<SetupResult> {
|
|
87
|
+
const cfg: SetupOpts = {
|
|
88
|
+
threadStatus: opts.threadStatus ?? "idle",
|
|
89
|
+
withDetail: opts.withDetail ?? true,
|
|
90
|
+
stepAgentNameOverride: opts.stepAgentNameOverride ?? null,
|
|
91
|
+
preCachedForkSessionId: opts.preCachedForkSessionId ?? null,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const casDir = join(tmpDir, "cas");
|
|
95
|
+
await mkdir(casDir, { recursive: true });
|
|
96
|
+
|
|
97
|
+
const store = await openStore(casDir);
|
|
98
|
+
await bootstrap(store);
|
|
99
|
+
const schemas = await registerUwfSchemas(store);
|
|
100
|
+
const outputSchemaHash = await putSchema(store, OUTPUT_SCHEMA);
|
|
101
|
+
const detailSchemaHash = await putSchema(store, DETAIL_SCHEMA);
|
|
102
|
+
|
|
103
|
+
const workflowHash = await store.cas.put(schemas.workflow, {
|
|
104
|
+
name: "test-ask",
|
|
105
|
+
description: "ask command integration test",
|
|
106
|
+
roles: {
|
|
107
|
+
worker: {
|
|
108
|
+
description: "Worker",
|
|
109
|
+
goal: "Work",
|
|
110
|
+
capabilities: [],
|
|
111
|
+
procedure: "work",
|
|
112
|
+
output: "result",
|
|
113
|
+
frontmatter: outputSchemaHash,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
graph: {
|
|
117
|
+
$START: {
|
|
118
|
+
new: { role: "worker", prompt: "Start work", location: null },
|
|
119
|
+
},
|
|
120
|
+
worker: { ok: { role: "$END", prompt: "done", location: null } },
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const startHash = await store.cas.put(schemas.startNode, {
|
|
125
|
+
workflow: workflowHash,
|
|
126
|
+
prompt: "Test ask task",
|
|
127
|
+
cwd: tmpDir,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Set OCAS_HOME so seedThreads + in-test createUwfStore calls resolve to this CAS dir.
|
|
131
|
+
process.env.OCAS_HOME = casDir;
|
|
132
|
+
|
|
133
|
+
// Capture file paths
|
|
134
|
+
const promptCapturePath = join(tmpDir, "captured-prompt.txt");
|
|
135
|
+
const modeCapturePath = join(tmpDir, "captured-mode.txt");
|
|
136
|
+
const forkSessionCapturePath = join(tmpDir, "captured-fork-session.txt");
|
|
137
|
+
const askSessionCapturePath = join(tmpDir, "captured-ask-session.txt");
|
|
138
|
+
const envCapturePath = join(tmpDir, "captured-env.txt");
|
|
139
|
+
const mockAgentPath = join(tmpDir, "mock-agent.sh");
|
|
140
|
+
const failingAgentPath = join(tmpDir, "failing-agent.sh");
|
|
141
|
+
|
|
142
|
+
// Build a detail node with sessionId so step ask can extract it
|
|
143
|
+
let detailHash: CasRef | null = null;
|
|
144
|
+
if (cfg.withDetail) {
|
|
145
|
+
const turnHash = await store.cas.put(detailSchemaHash, {
|
|
146
|
+
sessionId: STEP_SESSION_ID,
|
|
147
|
+
model: "test-model",
|
|
148
|
+
duration: 1000,
|
|
149
|
+
turnCount: 0,
|
|
150
|
+
turns: [],
|
|
151
|
+
});
|
|
152
|
+
detailHash = turnHash;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Build the StepNode at thread head
|
|
156
|
+
const outputHash = await store.cas.put(outputSchemaHash, { $status: "ok" });
|
|
157
|
+
const stepHash = await store.cas.put(schemas.stepNode, {
|
|
158
|
+
start: startHash,
|
|
159
|
+
prev: null,
|
|
160
|
+
role: "worker",
|
|
161
|
+
output: outputHash,
|
|
162
|
+
detail: detailHash,
|
|
163
|
+
agent: cfg.stepAgentNameOverride ?? mockAgentPath,
|
|
164
|
+
edgePrompt: "Start work",
|
|
165
|
+
startedAtMs: 1716600000000,
|
|
166
|
+
completedAtMs: 1716600001000,
|
|
167
|
+
cwd: tmpDir,
|
|
168
|
+
assembledPrompt: null,
|
|
169
|
+
usage: null,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Seed thread index entry
|
|
173
|
+
await seedThreads(tmpDir, {
|
|
174
|
+
[THREAD_ID]: {
|
|
175
|
+
head: stepHash,
|
|
176
|
+
status: cfg.threadStatus,
|
|
177
|
+
suspendedRole: null,
|
|
178
|
+
suspendMessage: null,
|
|
179
|
+
completedAt: cfg.threadStatus === "end" ? 1716600001000 : null,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Pre-seed the ask session cache so reuse tests have something to find.
|
|
184
|
+
if (cfg.preCachedForkSessionId !== null) {
|
|
185
|
+
const cachePath = join(tmpDir, "cache", "mock-sessions.json");
|
|
186
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
187
|
+
await writeFile(
|
|
188
|
+
cachePath,
|
|
189
|
+
`${JSON.stringify({ [`${stepHash}:ask`]: cfg.preCachedForkSessionId }, null, 2)}\n`,
|
|
190
|
+
"utf8",
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Mock agent: dispatches based on `--mode` (ask|fork|run) and captures inputs.
|
|
195
|
+
// - --mode ask --session <id> --prompt <text>: writes to ask capture; echoes a fixed answer to stdout
|
|
196
|
+
// - --mode fork --session <id>: writes to fork capture; prints "forked-from-<id>" sessionId on stdout
|
|
197
|
+
// - default (uwf-* style invocation): captures and echoes adapter JSON (not used in this suite)
|
|
198
|
+
await writeFile(
|
|
199
|
+
mockAgentPath,
|
|
200
|
+
`#!/bin/sh
|
|
201
|
+
mode=""
|
|
202
|
+
prompt=""
|
|
203
|
+
session=""
|
|
204
|
+
detail=""
|
|
205
|
+
while [ $# -gt 0 ]; do
|
|
206
|
+
case "$1" in
|
|
207
|
+
--mode) mode="$2"; shift 2 ;;
|
|
208
|
+
--prompt) prompt="$2"; shift 2 ;;
|
|
209
|
+
--session) session="$2"; shift 2 ;;
|
|
210
|
+
--detail) detail="$2"; shift 2 ;;
|
|
211
|
+
*) shift ;;
|
|
212
|
+
esac
|
|
213
|
+
done
|
|
214
|
+
printf '%s' "$mode" > '${modeCapturePath}'
|
|
215
|
+
printf '%s' "$prompt" > '${promptCapturePath}'
|
|
216
|
+
printf 'OCAS_HOME=%s\\n' "$OCAS_HOME" > '${envCapturePath}'
|
|
217
|
+
case "$mode" in
|
|
218
|
+
fork)
|
|
219
|
+
printf '%s' "$session" > '${forkSessionCapturePath}'
|
|
220
|
+
new_id="forked-from-$session"
|
|
221
|
+
printf '%s\\n' "$new_id"
|
|
222
|
+
;;
|
|
223
|
+
ask)
|
|
224
|
+
printf '%s' "$session" > '${askSessionCapturePath}'
|
|
225
|
+
# Print a deterministic answer that the cmdStepAsk path will hand back.
|
|
226
|
+
printf 'MOCK_ANSWER prompt=%s session=%s detail=%s\\n' "$prompt" "$session" "$detail"
|
|
227
|
+
;;
|
|
228
|
+
*)
|
|
229
|
+
echo "{\\"stepHash\\":\\"unused\\"}"
|
|
230
|
+
;;
|
|
231
|
+
esac
|
|
232
|
+
`,
|
|
233
|
+
{ mode: 0o755 },
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await writeFile(
|
|
237
|
+
failingAgentPath,
|
|
238
|
+
`#!/bin/sh
|
|
239
|
+
echo "boom" >&2
|
|
240
|
+
exit 7
|
|
241
|
+
`,
|
|
242
|
+
{ mode: 0o755 },
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// Minimal config so loadWorkflowConfig succeeds.
|
|
246
|
+
const configPath = join(tmpDir, "config.yaml");
|
|
247
|
+
await writeFile(
|
|
248
|
+
configPath,
|
|
249
|
+
`defaultAgent: uwf-hermes\nagentOverrides: null\nagents:\n uwf-hermes:\n command: uwf-hermes\n`,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
casDir,
|
|
254
|
+
stepHash,
|
|
255
|
+
startHash,
|
|
256
|
+
workflowHash,
|
|
257
|
+
detailHash,
|
|
258
|
+
mockAgentPath,
|
|
259
|
+
failingAgentPath,
|
|
260
|
+
promptCapturePath,
|
|
261
|
+
modeCapturePath,
|
|
262
|
+
forkSessionCapturePath,
|
|
263
|
+
askSessionCapturePath,
|
|
264
|
+
envCapturePath,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function runUwf(
|
|
269
|
+
args: string[],
|
|
270
|
+
casDir: string,
|
|
271
|
+
): { stdout: string; stderr: string; status: number } {
|
|
272
|
+
const cliPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "dist", "cli.js");
|
|
273
|
+
try {
|
|
274
|
+
const stdout = execFileSync(process.execPath, [cliPath, ...args], {
|
|
275
|
+
encoding: "utf8",
|
|
276
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
277
|
+
env: {
|
|
278
|
+
...process.env,
|
|
279
|
+
UWF_HOME: tmpDir,
|
|
280
|
+
OCAS_HOME: casDir,
|
|
281
|
+
},
|
|
282
|
+
cwd: tmpDir,
|
|
283
|
+
timeout: 30000,
|
|
284
|
+
});
|
|
285
|
+
return { stdout, stderr: "", status: 0 };
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const err = error as NodeJS.ErrnoException & {
|
|
288
|
+
stdout?: string | Buffer;
|
|
289
|
+
stderr?: string | Buffer;
|
|
290
|
+
status?: number;
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
stdout: typeof err.stdout === "string" ? err.stdout : (err.stdout?.toString("utf8") ?? ""),
|
|
294
|
+
stderr: typeof err.stderr === "string" ? err.stderr : (err.stderr?.toString("utf8") ?? ""),
|
|
295
|
+
status: err.status ?? 1,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── Group 1: CLI argument validation ───────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
describe("uwf step ask - CLI argument validation", () => {
|
|
303
|
+
test("1.1 missing step-hash exits non-zero", async () => {
|
|
304
|
+
const { casDir } = await setupAskFixture();
|
|
305
|
+
const result = runUwf(["step", "ask"], casDir);
|
|
306
|
+
expect(result.status).not.toBe(0);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("1.2 missing -p flag exits non-zero", async () => {
|
|
310
|
+
const { casDir, stepHash } = await setupAskFixture();
|
|
311
|
+
const result = runUwf(["step", "ask", stepHash], casDir);
|
|
312
|
+
expect(result.status).not.toBe(0);
|
|
313
|
+
expect(result.stderr.toLowerCase()).toMatch(/required|missing|prompt/);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("1.3 step-hash and -p accepted as valid invocation", async () => {
|
|
317
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
318
|
+
const result = runUwf(
|
|
319
|
+
["step", "ask", stepHash, "-p", "why?", "--agent", mockAgentPath],
|
|
320
|
+
casDir,
|
|
321
|
+
);
|
|
322
|
+
expect(result.status).toBe(0);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ── Group 2: CAS validation errors ────────────────────────────────────────
|
|
327
|
+
|
|
328
|
+
describe("uwf step ask - CAS validation errors", () => {
|
|
329
|
+
test("2.1 non-existent CAS hash exits non-zero with 'not found'", async () => {
|
|
330
|
+
const { casDir, mockAgentPath } = await setupAskFixture();
|
|
331
|
+
const result = runUwf(
|
|
332
|
+
["step", "ask", "0000000000000", "-p", "why?", "--agent", mockAgentPath],
|
|
333
|
+
casDir,
|
|
334
|
+
);
|
|
335
|
+
expect(result.status).not.toBe(0);
|
|
336
|
+
expect(result.stderr.toLowerCase()).toContain("not found");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("2.2 hash that is not a StepNode exits non-zero", async () => {
|
|
340
|
+
const { casDir, startHash, mockAgentPath } = await setupAskFixture();
|
|
341
|
+
// Use the StartNode hash — it exists but is not a StepNode
|
|
342
|
+
const result = runUwf(
|
|
343
|
+
["step", "ask", startHash, "-p", "why?", "--agent", mockAgentPath],
|
|
344
|
+
casDir,
|
|
345
|
+
);
|
|
346
|
+
expect(result.status).not.toBe(0);
|
|
347
|
+
expect(result.stderr.toLowerCase()).toContain("not a stepnode");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("2.3 step with no detail ref exits non-zero", async () => {
|
|
351
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture({ withDetail: false });
|
|
352
|
+
const result = runUwf(
|
|
353
|
+
["step", "ask", stepHash, "-p", "why?", "--agent", mockAgentPath],
|
|
354
|
+
casDir,
|
|
355
|
+
);
|
|
356
|
+
expect(result.status).not.toBe(0);
|
|
357
|
+
expect(result.stderr.toLowerCase()).toMatch(/no detail|detail.*missing|missing.*detail/);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// ── Group 3: Successful ask (core behavior) ───────────────────────────────
|
|
362
|
+
|
|
363
|
+
describe("uwf step ask - successful ask (core)", () => {
|
|
364
|
+
test("3.1 stdout contains agent's response text", async () => {
|
|
365
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
366
|
+
const result = runUwf(
|
|
367
|
+
["step", "ask", stepHash, "-p", "why tar not zip?", "--agent", mockAgentPath],
|
|
368
|
+
casDir,
|
|
369
|
+
);
|
|
370
|
+
expect(result.status).toBe(0);
|
|
371
|
+
expect(result.stdout).toContain("MOCK_ANSWER");
|
|
372
|
+
expect(result.stdout).toContain("why tar not zip?");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("3.2 thread index entry (head, status) is identical before and after ask", async () => {
|
|
376
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
377
|
+
|
|
378
|
+
// Before ask: snapshot the thread state
|
|
379
|
+
const { createUwfStore, getThread } = await import("../store.js");
|
|
380
|
+
const before = await createUwfStore(tmpDir);
|
|
381
|
+
const beforeEntry = getThread(before.varStore, THREAD_ID);
|
|
382
|
+
expect(beforeEntry).not.toBeNull();
|
|
383
|
+
|
|
384
|
+
const result = runUwf(
|
|
385
|
+
["step", "ask", stepHash, "-p", "anything", "--agent", mockAgentPath],
|
|
386
|
+
casDir,
|
|
387
|
+
);
|
|
388
|
+
expect(result.status).toBe(0);
|
|
389
|
+
|
|
390
|
+
// After ask: thread state should be unchanged
|
|
391
|
+
const after = await createUwfStore(tmpDir);
|
|
392
|
+
const afterEntry = getThread(after.varStore, THREAD_ID);
|
|
393
|
+
expect(afterEntry).not.toBeNull();
|
|
394
|
+
expect(afterEntry?.head).toBe(beforeEntry?.head);
|
|
395
|
+
expect(afterEntry?.status).toBe(beforeEntry?.status);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test("3.3 no new StepNode is written to CAS (step count unchanged)", async () => {
|
|
399
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
400
|
+
|
|
401
|
+
// Count StepNodes before
|
|
402
|
+
const { createUwfStore } = await import("../store.js");
|
|
403
|
+
const before = await createUwfStore(tmpDir);
|
|
404
|
+
const stepSchemaHash = before.schemas.stepNode;
|
|
405
|
+
|
|
406
|
+
function countStepNodes(uwfStore: typeof before): number {
|
|
407
|
+
const candidates = [stepHash];
|
|
408
|
+
let count = 0;
|
|
409
|
+
for (const h of candidates) {
|
|
410
|
+
const node = uwfStore.store.cas.get(h);
|
|
411
|
+
if (node !== null && node.type === stepSchemaHash) count++;
|
|
412
|
+
}
|
|
413
|
+
return count;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const beforeCount = countStepNodes(before);
|
|
417
|
+
expect(beforeCount).toBe(1);
|
|
418
|
+
|
|
419
|
+
const result = runUwf(
|
|
420
|
+
["step", "ask", stepHash, "-p", "anything", "--agent", mockAgentPath],
|
|
421
|
+
casDir,
|
|
422
|
+
);
|
|
423
|
+
expect(result.status).toBe(0);
|
|
424
|
+
|
|
425
|
+
// After ask: still only the seeded StepNode exists at head; no new step appended.
|
|
426
|
+
const after = await createUwfStore(tmpDir);
|
|
427
|
+
const headNode = after.store.cas.get(stepHash);
|
|
428
|
+
expect(headNode).not.toBeNull();
|
|
429
|
+
expect(headNode?.type).toBe(after.schemas.stepNode);
|
|
430
|
+
|
|
431
|
+
// Confirm thread head still points to the original step hash
|
|
432
|
+
const { getThread } = await import("../store.js");
|
|
433
|
+
const entry = getThread(after.varStore, THREAD_ID);
|
|
434
|
+
expect(entry?.head).toBe(stepHash);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ── Group 4: Fork cache semantics ─────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
describe("uwf step ask - fork cache", { timeout: 15_000 }, () => {
|
|
441
|
+
test("4.1 first ask creates a fork session and caches it", async () => {
|
|
442
|
+
const { casDir, stepHash, mockAgentPath, forkSessionCapturePath } = await setupAskFixture();
|
|
443
|
+
|
|
444
|
+
const result = runUwf(
|
|
445
|
+
["step", "ask", stepHash, "-p", "first ask", "--agent", mockAgentPath],
|
|
446
|
+
casDir,
|
|
447
|
+
);
|
|
448
|
+
expect(result.status).toBe(0);
|
|
449
|
+
|
|
450
|
+
// The mock agent in fork mode receives the source session id
|
|
451
|
+
const forkArg = await readFile(forkSessionCapturePath, "utf8");
|
|
452
|
+
expect(forkArg).toBe(STEP_SESSION_ID);
|
|
453
|
+
|
|
454
|
+
// Cache file should now contain the ask key
|
|
455
|
+
const cachePath = join(tmpDir, "cache", "mock-sessions.json");
|
|
456
|
+
const raw = await readFile(cachePath, "utf8");
|
|
457
|
+
const parsed = JSON.parse(raw) as Record<string, string>;
|
|
458
|
+
expect(parsed[`${stepHash}:ask`]).toBeDefined();
|
|
459
|
+
expect(parsed[`${stepHash}:ask`]).toBe(`forked-from-${STEP_SESSION_ID}`);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("4.2 second ask on same step reuses the cached fork session", async () => {
|
|
463
|
+
const cachedFork = "ses-already-forked-once";
|
|
464
|
+
const { casDir, stepHash, mockAgentPath, modeCapturePath, askSessionCapturePath } =
|
|
465
|
+
await setupAskFixture({ preCachedForkSessionId: cachedFork });
|
|
466
|
+
|
|
467
|
+
const result = runUwf(
|
|
468
|
+
["step", "ask", stepHash, "-p", "second ask", "--agent", mockAgentPath],
|
|
469
|
+
casDir,
|
|
470
|
+
);
|
|
471
|
+
expect(result.status).toBe(0);
|
|
472
|
+
|
|
473
|
+
// The mock agent must have been invoked in `ask` mode (no fork performed).
|
|
474
|
+
const mode = await readFile(modeCapturePath, "utf8");
|
|
475
|
+
expect(mode).toBe("ask");
|
|
476
|
+
|
|
477
|
+
// The ask invocation should have received the cached fork session id.
|
|
478
|
+
const askArg = await readFile(askSessionCapturePath, "utf8");
|
|
479
|
+
expect(askArg).toBe(cachedFork);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("4.3 different step hash creates an independent fork", async () => {
|
|
483
|
+
// Run a first ask on the base step → caches forkA
|
|
484
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
485
|
+
|
|
486
|
+
const r1 = runUwf(
|
|
487
|
+
["step", "ask", stepHash, "-p", "ask on step A", "--agent", mockAgentPath],
|
|
488
|
+
casDir,
|
|
489
|
+
);
|
|
490
|
+
expect(r1.status).toBe(0);
|
|
491
|
+
|
|
492
|
+
// Build a second StepNode (different hash) with a different sessionId so
|
|
493
|
+
// its detail-derived ask session is independent of the first.
|
|
494
|
+
const { createUwfStore } = await import("../store.js");
|
|
495
|
+
const uwf = await createUwfStore(tmpDir);
|
|
496
|
+
const detailSchemaHash = await putSchema(uwf.store, DETAIL_SCHEMA);
|
|
497
|
+
const outputSchemaHash = await putSchema(uwf.store, OUTPUT_SCHEMA);
|
|
498
|
+
const otherDetailHash = await uwf.store.cas.put(detailSchemaHash, {
|
|
499
|
+
sessionId: "ses-original-step-002",
|
|
500
|
+
model: "test-model",
|
|
501
|
+
duration: 1000,
|
|
502
|
+
turnCount: 0,
|
|
503
|
+
turns: [],
|
|
504
|
+
});
|
|
505
|
+
const otherOutputHash = await uwf.store.cas.put(outputSchemaHash, {
|
|
506
|
+
$status: "ok",
|
|
507
|
+
note: "alt",
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Reuse the same start ref the first step points to so the new step is a valid sibling.
|
|
511
|
+
const head = uwf.store.cas.get(stepHash);
|
|
512
|
+
const startRefFromHead = (head?.payload as { start: CasRef }).start;
|
|
513
|
+
const properOtherStep = await uwf.store.cas.put(uwf.schemas.stepNode, {
|
|
514
|
+
start: startRefFromHead,
|
|
515
|
+
prev: null,
|
|
516
|
+
role: "worker",
|
|
517
|
+
output: otherOutputHash,
|
|
518
|
+
detail: otherDetailHash,
|
|
519
|
+
agent: mockAgentPath,
|
|
520
|
+
edgePrompt: "Start work",
|
|
521
|
+
startedAtMs: 1716600002000,
|
|
522
|
+
completedAtMs: 1716600003000,
|
|
523
|
+
cwd: tmpDir,
|
|
524
|
+
assembledPrompt: null,
|
|
525
|
+
usage: null,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// sanity check we constructed a separate hash
|
|
529
|
+
expect(properOtherStep).not.toBe(stepHash);
|
|
530
|
+
|
|
531
|
+
const r2 = runUwf(
|
|
532
|
+
["step", "ask", properOtherStep, "-p", "ask on step B", "--agent", mockAgentPath],
|
|
533
|
+
casDir,
|
|
534
|
+
);
|
|
535
|
+
expect(r2.status).toBe(0);
|
|
536
|
+
|
|
537
|
+
const cachePath = join(tmpDir, "cache", "mock-sessions.json");
|
|
538
|
+
const raw = await readFile(cachePath, "utf8");
|
|
539
|
+
const parsed = JSON.parse(raw) as Record<string, string>;
|
|
540
|
+
expect(parsed[`${stepHash}:ask`]).toBeDefined();
|
|
541
|
+
expect(parsed[`${properOtherStep}:ask`]).toBeDefined();
|
|
542
|
+
expect(parsed[`${stepHash}:ask`]).not.toBe(parsed[`${properOtherStep}:ask`]);
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// ── Group 5: Fallback (agent has no fork support) ─────────────────────────
|
|
547
|
+
|
|
548
|
+
describe("uwf step ask - fallback path", () => {
|
|
549
|
+
test("5.1 fallback agent (no fork support) still answers via stdout", async () => {
|
|
550
|
+
// Use a fallback agent that ONLY supports `ask` mode without ever being asked
|
|
551
|
+
// to fork. The CLI should detect missing fork support and inject context instead.
|
|
552
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture();
|
|
553
|
+
|
|
554
|
+
// Create a fallback agent script that fails with non-zero exit on "fork" mode.
|
|
555
|
+
// Fallback path must NOT call mode=fork; it should call mode=ask directly.
|
|
556
|
+
const fallbackPath = join(tmpDir, "fallback-agent.sh");
|
|
557
|
+
const promptCapture = join(tmpDir, "fallback-prompt.txt");
|
|
558
|
+
const sessionCapture = join(tmpDir, "fallback-session.txt");
|
|
559
|
+
const modeCapture = join(tmpDir, "fallback-mode.txt");
|
|
560
|
+
await writeFile(
|
|
561
|
+
fallbackPath,
|
|
562
|
+
`#!/bin/sh
|
|
563
|
+
mode=""
|
|
564
|
+
prompt=""
|
|
565
|
+
session=""
|
|
566
|
+
detail=""
|
|
567
|
+
while [ $# -gt 0 ]; do
|
|
568
|
+
case "$1" in
|
|
569
|
+
--mode) mode="$2"; shift 2 ;;
|
|
570
|
+
--prompt) prompt="$2"; shift 2 ;;
|
|
571
|
+
--session) session="$2"; shift 2 ;;
|
|
572
|
+
--detail) detail="$2"; shift 2 ;;
|
|
573
|
+
*) shift ;;
|
|
574
|
+
esac
|
|
575
|
+
done
|
|
576
|
+
printf '%s' "$mode" > '${modeCapture}'
|
|
577
|
+
printf '%s' "$prompt" > '${promptCapture}'
|
|
578
|
+
printf '%s' "$session" > '${sessionCapture}'
|
|
579
|
+
case "$mode" in
|
|
580
|
+
fork) echo "fork not supported" >&2; exit 99 ;;
|
|
581
|
+
ask) printf 'FALLBACK_ANSWER for: %s (detail=%s)\\n' "$prompt" "$detail" ;;
|
|
582
|
+
*) echo "unknown" >&2; exit 1 ;;
|
|
583
|
+
esac
|
|
584
|
+
`,
|
|
585
|
+
{ mode: 0o755 },
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
const result = runUwf(
|
|
589
|
+
["step", "ask", stepHash, "-p", "explain context", "--agent", fallbackPath, "--no-fork"],
|
|
590
|
+
casDir,
|
|
591
|
+
);
|
|
592
|
+
expect(result.status).toBe(0);
|
|
593
|
+
expect(result.stdout).toContain("FALLBACK_ANSWER");
|
|
594
|
+
expect(result.stdout).toContain("explain context");
|
|
595
|
+
|
|
596
|
+
// The fallback agent should be invoked in `ask` mode, with NO session id
|
|
597
|
+
// (since no fork happened). The detail ref must be passed for context injection.
|
|
598
|
+
const mode = await readFile(modeCapture, "utf8");
|
|
599
|
+
expect(mode).toBe("ask");
|
|
600
|
+
const session = await readFile(sessionCapture, "utf8");
|
|
601
|
+
expect(session).toBe("");
|
|
602
|
+
|
|
603
|
+
// Make sure mockAgentPath's mock never ran.
|
|
604
|
+
void mockAgentPath;
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("5.2 fallback ask still does NOT mutate thread state", async () => {
|
|
608
|
+
const { casDir, stepHash } = await setupAskFixture();
|
|
609
|
+
|
|
610
|
+
const fallbackPath = join(tmpDir, "fallback-agent.sh");
|
|
611
|
+
await writeFile(
|
|
612
|
+
fallbackPath,
|
|
613
|
+
`#!/bin/sh
|
|
614
|
+
mode=""
|
|
615
|
+
prompt=""
|
|
616
|
+
while [ $# -gt 0 ]; do
|
|
617
|
+
case "$1" in
|
|
618
|
+
--mode) mode="$2"; shift 2 ;;
|
|
619
|
+
--prompt) prompt="$2"; shift 2 ;;
|
|
620
|
+
*) shift ;;
|
|
621
|
+
esac
|
|
622
|
+
done
|
|
623
|
+
case "$mode" in
|
|
624
|
+
fork) echo "fork not supported" >&2; exit 99 ;;
|
|
625
|
+
ask) printf 'OK %s\\n' "$prompt" ;;
|
|
626
|
+
*) exit 1 ;;
|
|
627
|
+
esac
|
|
628
|
+
`,
|
|
629
|
+
{ mode: 0o755 },
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
const { createUwfStore, getThread } = await import("../store.js");
|
|
633
|
+
const before = await createUwfStore(tmpDir);
|
|
634
|
+
const beforeEntry = getThread(before.varStore, THREAD_ID);
|
|
635
|
+
|
|
636
|
+
const result = runUwf(
|
|
637
|
+
["step", "ask", stepHash, "-p", "any", "--agent", fallbackPath, "--no-fork"],
|
|
638
|
+
casDir,
|
|
639
|
+
);
|
|
640
|
+
expect(result.status).toBe(0);
|
|
641
|
+
|
|
642
|
+
const after = await createUwfStore(tmpDir);
|
|
643
|
+
const afterEntry = getThread(after.varStore, THREAD_ID);
|
|
644
|
+
expect(afterEntry?.head).toBe(beforeEntry?.head);
|
|
645
|
+
expect(afterEntry?.status).toBe(beforeEntry?.status);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// ── Group 6: Agent resolution ─────────────────────────────────────────────
|
|
650
|
+
|
|
651
|
+
describe("uwf step ask - agent resolution", () => {
|
|
652
|
+
test("6.1 without --agent flag, agent is resolved from step's agent field", async () => {
|
|
653
|
+
// Step's agent field points at mockAgentPath by default.
|
|
654
|
+
const { casDir, stepHash, modeCapturePath, promptCapturePath } = await setupAskFixture();
|
|
655
|
+
const result = runUwf(["step", "ask", stepHash, "-p", "explain"], casDir);
|
|
656
|
+
expect(result.status).toBe(0);
|
|
657
|
+
|
|
658
|
+
// The mockAgentPath must have been invoked in ask mode with the user prompt.
|
|
659
|
+
const mode = await readFile(modeCapturePath, "utf8");
|
|
660
|
+
expect(mode).toBe("ask");
|
|
661
|
+
const captured = await readFile(promptCapturePath, "utf8");
|
|
662
|
+
expect(captured).toBe("explain");
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("6.2 --agent override beats step's recorded agent", async () => {
|
|
666
|
+
// Record a non-existent agent in step.agent. Provide a working one via --agent.
|
|
667
|
+
const { casDir, stepHash, mockAgentPath } = await setupAskFixture({
|
|
668
|
+
stepAgentNameOverride: "uwf-does-not-exist",
|
|
669
|
+
});
|
|
670
|
+
const result = runUwf(
|
|
671
|
+
["step", "ask", stepHash, "-p", "explain", "--agent", mockAgentPath],
|
|
672
|
+
casDir,
|
|
673
|
+
);
|
|
674
|
+
expect(result.status).toBe(0);
|
|
675
|
+
expect(result.stdout).toContain("MOCK_ANSWER");
|
|
676
|
+
});
|
|
677
|
+
});
|
|
@@ -97,6 +97,7 @@ describe("protocol types", () => {
|
|
|
97
97
|
assembledPrompt: null,
|
|
98
98
|
cwd: "/test/path",
|
|
99
99
|
usage: null,
|
|
100
|
+
previousAttempts: null,
|
|
100
101
|
};
|
|
101
102
|
expect(record.startedAtMs).toBe(1000);
|
|
102
103
|
expect(record.completedAtMs).toBe(2000);
|
|
@@ -112,6 +113,7 @@ describe("protocol types", () => {
|
|
|
112
113
|
timestamp: 123,
|
|
113
114
|
durationMs: 5000,
|
|
114
115
|
usage: null,
|
|
116
|
+
previousAttempts: null,
|
|
115
117
|
};
|
|
116
118
|
expect(entry.durationMs).toBe(5000);
|
|
117
119
|
});
|