@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,392 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { bootstrap, putSchema } from "@ocas/core";
|
|
5
|
+
import { openStore } from "@ocas/fs";
|
|
6
|
+
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
|
7
|
+
import { STEP_NODE_SCHEMA } from "@united-workforce/protocol";
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
9
|
+
import { cmdStepList } from "../commands/step.js";
|
|
10
|
+
import { cmdThreadRead } from "../commands/thread.js";
|
|
11
|
+
import { registerUwfSchemas } from "../schemas.js";
|
|
12
|
+
import { seedThreads } from "./thread-test-helpers.js";
|
|
13
|
+
|
|
14
|
+
// ── schemas ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const TURN_SCHEMA = {
|
|
17
|
+
title: "hermes-turn",
|
|
18
|
+
type: "object" as const,
|
|
19
|
+
required: ["index", "role", "content"],
|
|
20
|
+
properties: {
|
|
21
|
+
index: { type: "integer" as const },
|
|
22
|
+
role: { type: "string" as const },
|
|
23
|
+
content: { type: "string" as const },
|
|
24
|
+
toolCalls: {
|
|
25
|
+
anyOf: [
|
|
26
|
+
{ type: "array" as const, items: { type: "object" as const } },
|
|
27
|
+
{ type: "null" as const },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
reasoning: { anyOf: [{ type: "string" as const }, { type: "null" as const }] },
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const DETAIL_SCHEMA = {
|
|
36
|
+
title: "hermes-detail",
|
|
37
|
+
type: "object" as const,
|
|
38
|
+
required: ["sessionId", "model", "duration", "turnCount", "turns"],
|
|
39
|
+
properties: {
|
|
40
|
+
sessionId: { type: "string" as const },
|
|
41
|
+
model: { type: "string" as const },
|
|
42
|
+
duration: { type: "integer" as const },
|
|
43
|
+
turnCount: { type: "integer" as const },
|
|
44
|
+
turns: {
|
|
45
|
+
type: "array" as const,
|
|
46
|
+
items: { type: "string" as const, format: "ocas_ref" },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
async function registerDetailSchemas(store: Awaited<ReturnType<typeof openStore>>) {
|
|
55
|
+
await bootstrap(store);
|
|
56
|
+
const [turn, detail] = await Promise.all([
|
|
57
|
+
putSchema(store, TURN_SCHEMA),
|
|
58
|
+
putSchema(store, DETAIL_SCHEMA),
|
|
59
|
+
]);
|
|
60
|
+
return { turn, detail };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── fixture ──────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
let tmpDir: string;
|
|
66
|
+
let originalEnv: string | undefined;
|
|
67
|
+
|
|
68
|
+
beforeEach(async () => {
|
|
69
|
+
tmpDir = await mkdtemp(join(tmpdir(), "cli-uwf-step-timing-test-"));
|
|
70
|
+
originalEnv = process.env.OCAS_HOME;
|
|
71
|
+
process.env.OCAS_HOME = join(tmpDir, "cas");
|
|
72
|
+
await mkdir(process.env.OCAS_HOME, { recursive: true });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(async () => {
|
|
76
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
77
|
+
if (originalEnv === undefined) {
|
|
78
|
+
delete process.env.OCAS_HOME;
|
|
79
|
+
} else {
|
|
80
|
+
process.env.OCAS_HOME = originalEnv;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ── 1. Protocol types (compile-time) ─────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
describe("protocol types", () => {
|
|
87
|
+
test("StepRecord has startedAtMs and completedAtMs as required fields", () => {
|
|
88
|
+
// Type-level test: this block compiles only if fields exist and are number
|
|
89
|
+
const record: import("@united-workforce/protocol").StepRecord = {
|
|
90
|
+
role: "test",
|
|
91
|
+
output: "hash1" as CasRef,
|
|
92
|
+
detail: "hash2" as CasRef,
|
|
93
|
+
agent: "uwf-test",
|
|
94
|
+
edgePrompt: "",
|
|
95
|
+
startedAtMs: 1000,
|
|
96
|
+
completedAtMs: 2000,
|
|
97
|
+
assembledPrompt: null,
|
|
98
|
+
cwd: "/test/path",
|
|
99
|
+
usage: null,
|
|
100
|
+
};
|
|
101
|
+
expect(record.startedAtMs).toBe(1000);
|
|
102
|
+
expect(record.completedAtMs).toBe(2000);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("StepEntry has durationMs as required field", () => {
|
|
106
|
+
const entry: import("@united-workforce/protocol").StepEntry = {
|
|
107
|
+
hash: "hash" as CasRef,
|
|
108
|
+
role: "test",
|
|
109
|
+
output: {},
|
|
110
|
+
detail: "hash2" as CasRef,
|
|
111
|
+
agent: "uwf-test",
|
|
112
|
+
timestamp: 123,
|
|
113
|
+
durationMs: 5000,
|
|
114
|
+
usage: null,
|
|
115
|
+
};
|
|
116
|
+
expect(entry.durationMs).toBe(5000);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ── 2. JSON Schema ───────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
describe("StepNode JSON schema", () => {
|
|
123
|
+
test("schema requires startedAtMs and completedAtMs", () => {
|
|
124
|
+
const required = STEP_NODE_SCHEMA.required as string[];
|
|
125
|
+
expect(required).toContain("startedAtMs");
|
|
126
|
+
expect(required).toContain("completedAtMs");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("schema defines timing fields as integer", () => {
|
|
130
|
+
const props = STEP_NODE_SCHEMA.properties as Record<string, { type: string }>;
|
|
131
|
+
expect(props.startedAtMs.type).toBe("integer");
|
|
132
|
+
expect(props.completedAtMs.type).toBe("integer");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("StepNode with timing fields passes CAS validation", async () => {
|
|
136
|
+
const casDir = join(tmpDir, "cas");
|
|
137
|
+
await mkdir(casDir, { recursive: true });
|
|
138
|
+
const store = await openStore(casDir);
|
|
139
|
+
const schemas = await registerUwfSchemas(store);
|
|
140
|
+
|
|
141
|
+
const startHash = await store.cas.put(schemas.startNode, {
|
|
142
|
+
workflow: "placeholder0000" as CasRef,
|
|
143
|
+
prompt: "test",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const outputHash = await store.cas.put(schemas.text, "output text");
|
|
147
|
+
|
|
148
|
+
const detailSchemas = await registerDetailSchemas(store);
|
|
149
|
+
const detailHash = await store.cas.put(detailSchemas.detail, {
|
|
150
|
+
sessionId: "s1",
|
|
151
|
+
model: "m1",
|
|
152
|
+
duration: 100,
|
|
153
|
+
turnCount: 0,
|
|
154
|
+
turns: [],
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Should succeed — valid timing fields
|
|
158
|
+
const hash = await store.cas.put(schemas.stepNode, {
|
|
159
|
+
start: startHash,
|
|
160
|
+
prev: null,
|
|
161
|
+
role: "worker",
|
|
162
|
+
output: outputHash,
|
|
163
|
+
detail: detailHash,
|
|
164
|
+
agent: "uwf-test",
|
|
165
|
+
edgePrompt: "",
|
|
166
|
+
startedAtMs: 1000000000000,
|
|
167
|
+
completedAtMs: 1000000005000,
|
|
168
|
+
assembledPrompt: null,
|
|
169
|
+
});
|
|
170
|
+
expect(hash).toBeTruthy();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ── 3. step list — durationMs computed ───────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
describe("step list timing", () => {
|
|
177
|
+
test("step list includes durationMs = completedAtMs - startedAtMs", async () => {
|
|
178
|
+
const casDir = join(tmpDir, "cas");
|
|
179
|
+
await mkdir(casDir, { recursive: true });
|
|
180
|
+
const store = await openStore(casDir);
|
|
181
|
+
const schemas = await registerUwfSchemas(store);
|
|
182
|
+
const detailSchemas = await registerDetailSchemas(store);
|
|
183
|
+
|
|
184
|
+
const workflowHash = await store.cas.put(schemas.workflow, {
|
|
185
|
+
name: "test-wf",
|
|
186
|
+
description: "desc",
|
|
187
|
+
roles: {},
|
|
188
|
+
graph: {},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const startHash = await store.cas.put(schemas.startNode, {
|
|
192
|
+
workflow: workflowHash,
|
|
193
|
+
prompt: "test",
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const outputHash = await store.cas.put(schemas.text, "output");
|
|
197
|
+
const detailHash = await store.cas.put(detailSchemas.detail, {
|
|
198
|
+
sessionId: "s1",
|
|
199
|
+
model: "m1",
|
|
200
|
+
duration: 100,
|
|
201
|
+
turnCount: 0,
|
|
202
|
+
turns: [],
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const startedAt = 1716600000000;
|
|
206
|
+
const completedAt = 1716600003500;
|
|
207
|
+
|
|
208
|
+
const stepHash = await store.cas.put(schemas.stepNode, {
|
|
209
|
+
start: startHash,
|
|
210
|
+
prev: null,
|
|
211
|
+
role: "worker",
|
|
212
|
+
output: outputHash,
|
|
213
|
+
detail: detailHash,
|
|
214
|
+
agent: "uwf-test",
|
|
215
|
+
edgePrompt: "",
|
|
216
|
+
startedAtMs: startedAt,
|
|
217
|
+
completedAtMs: completedAt,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const threadId = "01HX2Q3R4S5T6V7W8X9YZ1" as ThreadId;
|
|
221
|
+
await seedThreads(tmpDir, { [threadId]: stepHash });
|
|
222
|
+
|
|
223
|
+
const result = await cmdStepList(tmpDir, threadId);
|
|
224
|
+
const stepEntries = result.steps.slice(1); // skip start entry
|
|
225
|
+
expect(stepEntries).toHaveLength(1);
|
|
226
|
+
|
|
227
|
+
const step = stepEntries[0] as import("@united-workforce/protocol").StepEntry;
|
|
228
|
+
expect(step.durationMs).toBe(3500);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ── 4. thread read — duration in header ──────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
describe("thread read timing", () => {
|
|
235
|
+
test("thread read header includes Duration", async () => {
|
|
236
|
+
const casDir = join(tmpDir, "cas");
|
|
237
|
+
await mkdir(casDir, { recursive: true });
|
|
238
|
+
const store = await openStore(casDir);
|
|
239
|
+
const schemas = await registerUwfSchemas(store);
|
|
240
|
+
const detailSchemas = await registerDetailSchemas(store);
|
|
241
|
+
|
|
242
|
+
const workflowHash = await store.cas.put(schemas.workflow, {
|
|
243
|
+
name: "test-wf",
|
|
244
|
+
description: "desc",
|
|
245
|
+
roles: {
|
|
246
|
+
worker: {
|
|
247
|
+
description: "Worker",
|
|
248
|
+
goal: "Do work",
|
|
249
|
+
capabilities: [],
|
|
250
|
+
procedure: "work",
|
|
251
|
+
output: "result",
|
|
252
|
+
frontmatter: "placeholder0000" as CasRef,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
graph: {
|
|
256
|
+
$START: { _: { role: "worker", prompt: "go", location: null } },
|
|
257
|
+
worker: { done: { role: "$END", prompt: "", location: null } },
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const startHash = await store.cas.put(schemas.startNode, {
|
|
262
|
+
workflow: workflowHash,
|
|
263
|
+
prompt: "test task",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const turnHash = await store.cas.put(detailSchemas.turn, {
|
|
267
|
+
index: 0,
|
|
268
|
+
role: "assistant",
|
|
269
|
+
content: "Done.",
|
|
270
|
+
toolCalls: null,
|
|
271
|
+
reasoning: null,
|
|
272
|
+
});
|
|
273
|
+
const detailHash = await store.cas.put(detailSchemas.detail, {
|
|
274
|
+
sessionId: "s1",
|
|
275
|
+
model: "m1",
|
|
276
|
+
duration: 100,
|
|
277
|
+
turnCount: 1,
|
|
278
|
+
turns: [turnHash],
|
|
279
|
+
});
|
|
280
|
+
const outputHash = await store.cas.put(schemas.text, "output");
|
|
281
|
+
|
|
282
|
+
const stepHash = await store.cas.put(schemas.stepNode, {
|
|
283
|
+
start: startHash,
|
|
284
|
+
prev: null,
|
|
285
|
+
role: "worker",
|
|
286
|
+
output: outputHash,
|
|
287
|
+
detail: detailHash,
|
|
288
|
+
agent: "uwf-test",
|
|
289
|
+
edgePrompt: "",
|
|
290
|
+
startedAtMs: 1716600000000,
|
|
291
|
+
completedAtMs: 1716600042000,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const threadId = "01HX2Q3R4S5T6V7W8X9YZ3" as ThreadId;
|
|
295
|
+
await seedThreads(tmpDir, { [threadId]: stepHash });
|
|
296
|
+
|
|
297
|
+
const markdown = await cmdThreadRead(tmpDir, threadId, 10000, null, false);
|
|
298
|
+
expect(markdown).toContain("**Duration:** 42.0s");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("thread read shows sub-second duration as ms", async () => {
|
|
302
|
+
const casDir = join(tmpDir, "cas");
|
|
303
|
+
await mkdir(casDir, { recursive: true });
|
|
304
|
+
const store = await openStore(casDir);
|
|
305
|
+
const schemas = await registerUwfSchemas(store);
|
|
306
|
+
const detailSchemas = await registerDetailSchemas(store);
|
|
307
|
+
|
|
308
|
+
const workflowHash = await store.cas.put(schemas.workflow, {
|
|
309
|
+
name: "test-wf",
|
|
310
|
+
description: "desc",
|
|
311
|
+
roles: {
|
|
312
|
+
worker: {
|
|
313
|
+
description: "Worker",
|
|
314
|
+
goal: "Do work",
|
|
315
|
+
capabilities: [],
|
|
316
|
+
procedure: "work",
|
|
317
|
+
output: "result",
|
|
318
|
+
frontmatter: "placeholder0000" as CasRef,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
graph: {
|
|
322
|
+
$START: { _: { role: "worker", prompt: "go", location: null } },
|
|
323
|
+
worker: { done: { role: "$END", prompt: "", location: null } },
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const startHash = await store.cas.put(schemas.startNode, {
|
|
328
|
+
workflow: workflowHash,
|
|
329
|
+
prompt: "test",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const turnHash = await store.cas.put(detailSchemas.turn, {
|
|
333
|
+
index: 0,
|
|
334
|
+
role: "assistant",
|
|
335
|
+
content: "Done.",
|
|
336
|
+
toolCalls: null,
|
|
337
|
+
reasoning: null,
|
|
338
|
+
});
|
|
339
|
+
const detailHash = await store.cas.put(detailSchemas.detail, {
|
|
340
|
+
sessionId: "s1",
|
|
341
|
+
model: "m1",
|
|
342
|
+
duration: 100,
|
|
343
|
+
turnCount: 1,
|
|
344
|
+
turns: [turnHash],
|
|
345
|
+
});
|
|
346
|
+
const outputHash = await store.cas.put(schemas.text, "output");
|
|
347
|
+
|
|
348
|
+
const stepHash = await store.cas.put(schemas.stepNode, {
|
|
349
|
+
start: startHash,
|
|
350
|
+
prev: null,
|
|
351
|
+
role: "worker",
|
|
352
|
+
output: outputHash,
|
|
353
|
+
detail: detailHash,
|
|
354
|
+
agent: "uwf-test",
|
|
355
|
+
edgePrompt: "",
|
|
356
|
+
startedAtMs: 1716600000000,
|
|
357
|
+
completedAtMs: 1716600000350,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const threadId = "01HX2Q3R4S5T6V7W8X9YZ4" as ThreadId;
|
|
361
|
+
await seedThreads(tmpDir, { [threadId]: stepHash });
|
|
362
|
+
|
|
363
|
+
const markdown = await cmdThreadRead(tmpDir, threadId, 10000, null, false);
|
|
364
|
+
expect(markdown).toContain("**Duration:** 350ms");
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ── 6. Breaking change — old data without timing fails ───────────────────────
|
|
369
|
+
|
|
370
|
+
describe("breaking change", () => {
|
|
371
|
+
test("StepNode schema rejects payload without timing fields", () => {
|
|
372
|
+
const required = STEP_NODE_SCHEMA.required as string[];
|
|
373
|
+
// Both fields must be in the required array
|
|
374
|
+
expect(required).toContain("startedAtMs");
|
|
375
|
+
expect(required).toContain("completedAtMs");
|
|
376
|
+
|
|
377
|
+
// Payload without timing fields would fail schema validation
|
|
378
|
+
// because the schema marks them as required
|
|
379
|
+
const payloadWithoutTiming = {
|
|
380
|
+
start: "hash1",
|
|
381
|
+
prev: null,
|
|
382
|
+
role: "worker",
|
|
383
|
+
output: "hash2",
|
|
384
|
+
detail: "hash3",
|
|
385
|
+
agent: "uwf-test",
|
|
386
|
+
edgePrompt: "",
|
|
387
|
+
};
|
|
388
|
+
// Verify the payload is missing required fields
|
|
389
|
+
expect(payloadWithoutTiming).not.toHaveProperty("startedAtMs");
|
|
390
|
+
expect(payloadWithoutTiming).not.toHaveProperty("completedAtMs");
|
|
391
|
+
});
|
|
392
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createThreadIndexEntry, type ThreadId } from "@united-workforce/protocol";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
createUwfStore,
|
|
8
|
+
getCasDir,
|
|
9
|
+
getGlobalCasDir,
|
|
10
|
+
getRegistryPath,
|
|
11
|
+
loadWorkflowRegistry,
|
|
12
|
+
saveWorkflowRegistry,
|
|
13
|
+
setThread,
|
|
14
|
+
} from "../store.js";
|
|
15
|
+
|
|
16
|
+
describe("Global CAS directory", () => {
|
|
17
|
+
let tmpDir: string;
|
|
18
|
+
let originalOcasDir: string | undefined;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tmpDir = join(tmpdir(), `uwf-test-global-cas-${Date.now()}`);
|
|
22
|
+
await mkdir(tmpDir, { recursive: true });
|
|
23
|
+
originalOcasDir = process.env.OCAS_HOME;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(async () => {
|
|
27
|
+
if (tmpDir) {
|
|
28
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
29
|
+
}
|
|
30
|
+
if (originalOcasDir === undefined) {
|
|
31
|
+
delete process.env.OCAS_HOME;
|
|
32
|
+
} else {
|
|
33
|
+
process.env.OCAS_HOME = originalOcasDir;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("getGlobalCasDir returns default path when no env var set", () => {
|
|
38
|
+
delete process.env.OCAS_HOME;
|
|
39
|
+
const casDir = getGlobalCasDir();
|
|
40
|
+
expect(casDir).toContain(".ocas");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("getGlobalCasDir respects OCAS_HOME environment variable", () => {
|
|
44
|
+
const customPath = join(tmpDir, "custom-cas");
|
|
45
|
+
process.env.OCAS_HOME = customPath;
|
|
46
|
+
const casDir = getGlobalCasDir();
|
|
47
|
+
expect(casDir).toBe(customPath);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("getGlobalCasDir ignores empty OCAS_HOME", () => {
|
|
51
|
+
process.env.OCAS_HOME = "";
|
|
52
|
+
const casDir = getGlobalCasDir();
|
|
53
|
+
expect(casDir).toContain(".ocas");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("getCasDir is deprecated but still works for backward compatibility", () => {
|
|
57
|
+
const storageRoot = join(tmpDir, "storage");
|
|
58
|
+
const casDir = getCasDir(storageRoot);
|
|
59
|
+
expect(casDir).toBe(join(storageRoot, "cas"));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("createUwfStore uses global CAS directory", async () => {
|
|
63
|
+
const globalCasDir = join(tmpDir, "global-cas");
|
|
64
|
+
process.env.OCAS_HOME = globalCasDir;
|
|
65
|
+
|
|
66
|
+
const storageRoot = join(tmpDir, "storage");
|
|
67
|
+
await mkdir(storageRoot, { recursive: true });
|
|
68
|
+
|
|
69
|
+
const uwf = await createUwfStore(storageRoot);
|
|
70
|
+
|
|
71
|
+
// Verify the store was created in the global CAS directory
|
|
72
|
+
expect(uwf.storageRoot).toBe(storageRoot);
|
|
73
|
+
expect(uwf.store).toBeDefined();
|
|
74
|
+
expect(uwf.schemas).toBeDefined();
|
|
75
|
+
expect(uwf.varStore).toBeDefined();
|
|
76
|
+
|
|
77
|
+
// The global CAS directory should be created
|
|
78
|
+
const { stat } = await import("node:fs/promises");
|
|
79
|
+
const stats = await stat(globalCasDir);
|
|
80
|
+
expect(stats.isDirectory()).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("createUwfStore creates global CAS directory if it does not exist", async () => {
|
|
84
|
+
const globalCasDir = join(tmpDir, "new-global-cas");
|
|
85
|
+
process.env.OCAS_HOME = globalCasDir;
|
|
86
|
+
|
|
87
|
+
const storageRoot = join(tmpDir, "storage");
|
|
88
|
+
await mkdir(storageRoot, { recursive: true });
|
|
89
|
+
|
|
90
|
+
await createUwfStore(storageRoot);
|
|
91
|
+
|
|
92
|
+
// Verify the directory was created
|
|
93
|
+
const { stat } = await import("node:fs/promises");
|
|
94
|
+
const stats = await stat(globalCasDir);
|
|
95
|
+
expect(stats.isDirectory()).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("multiple uwfStore instances share the same global CAS filesystem", async () => {
|
|
99
|
+
const globalCasDir = join(tmpDir, "shared-cas");
|
|
100
|
+
process.env.OCAS_HOME = globalCasDir;
|
|
101
|
+
|
|
102
|
+
const storageRoot1 = join(tmpDir, "storage1");
|
|
103
|
+
const storageRoot2 = join(tmpDir, "storage2");
|
|
104
|
+
await mkdir(storageRoot1, { recursive: true });
|
|
105
|
+
await mkdir(storageRoot2, { recursive: true });
|
|
106
|
+
|
|
107
|
+
const uwf1 = await createUwfStore(storageRoot1);
|
|
108
|
+
const uwf2 = await createUwfStore(storageRoot2);
|
|
109
|
+
|
|
110
|
+
// Both should use the same global CAS directory
|
|
111
|
+
expect(uwf1.store).toBeDefined();
|
|
112
|
+
expect(uwf2.store).toBeDefined();
|
|
113
|
+
|
|
114
|
+
// Store a node in the first store
|
|
115
|
+
const testData = { test: "data" };
|
|
116
|
+
const _hash = uwf1.store.cas.put(uwf1.schemas.text, JSON.stringify(testData));
|
|
117
|
+
|
|
118
|
+
// Both stores share the same CAS filesystem directory
|
|
119
|
+
// Since schemas are registered idempotently, they should have the same hash
|
|
120
|
+
expect(uwf2.schemas.text).toBe(uwf1.schemas.text);
|
|
121
|
+
|
|
122
|
+
// Verify the CAS files are written to the shared directory
|
|
123
|
+
const { readdir } = await import("node:fs/promises");
|
|
124
|
+
const files = await readdir(globalCasDir);
|
|
125
|
+
expect(files.length).toBeGreaterThan(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("workflow registry is stored in global CAS variable store", async () => {
|
|
129
|
+
const globalCasDir = join(tmpDir, "global-cas");
|
|
130
|
+
process.env.OCAS_HOME = globalCasDir;
|
|
131
|
+
|
|
132
|
+
const storageRoot = join(tmpDir, "storage");
|
|
133
|
+
await mkdir(storageRoot, { recursive: true });
|
|
134
|
+
|
|
135
|
+
const uwf = await createUwfStore(storageRoot);
|
|
136
|
+
const hash = await uwf.store.cas.put(uwf.schemas.text, "registry-test");
|
|
137
|
+
saveWorkflowRegistry(uwf.varStore, "test-workflow", hash);
|
|
138
|
+
|
|
139
|
+
const registry = loadWorkflowRegistry(uwf.varStore);
|
|
140
|
+
expect(registry["test-workflow"]).toBe(hash);
|
|
141
|
+
|
|
142
|
+
const { access } = await import("node:fs/promises");
|
|
143
|
+
await access(join(globalCasDir, "vars"));
|
|
144
|
+
|
|
145
|
+
const registryPath = join(storageRoot, "workflows.yaml");
|
|
146
|
+
await expect(access(registryPath)).rejects.toThrow();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("migrates workflows.yaml to variable store and renames file", async () => {
|
|
150
|
+
const globalCasDir = join(tmpDir, "global-cas");
|
|
151
|
+
process.env.OCAS_HOME = globalCasDir;
|
|
152
|
+
|
|
153
|
+
const storageRoot = join(tmpDir, "storage-migrate");
|
|
154
|
+
await mkdir(storageRoot, { recursive: true });
|
|
155
|
+
|
|
156
|
+
const uwfSeed = await createUwfStore(storageRoot);
|
|
157
|
+
const hash = await uwfSeed.store.cas.put(uwfSeed.schemas.text, "migrated-workflow");
|
|
158
|
+
|
|
159
|
+
const registryPath = getRegistryPath(storageRoot);
|
|
160
|
+
const { writeFile, access, readFile } = await import("node:fs/promises");
|
|
161
|
+
await writeFile(registryPath, `migrated-workflow: ${hash}\n`, "utf8");
|
|
162
|
+
|
|
163
|
+
const uwf = await createUwfStore(storageRoot);
|
|
164
|
+
const registry = loadWorkflowRegistry(uwf.varStore);
|
|
165
|
+
expect(registry["migrated-workflow"]).toBe(hash);
|
|
166
|
+
|
|
167
|
+
await expect(access(registryPath)).rejects.toThrow();
|
|
168
|
+
const migratedPath = `${registryPath}.migrated`;
|
|
169
|
+
const migratedContent = await readFile(migratedPath, "utf8");
|
|
170
|
+
expect(migratedContent).toContain("migrated-workflow");
|
|
171
|
+
expect(migratedContent).toContain(hash);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("migrates threads.yaml to variable store and renames file", async () => {
|
|
175
|
+
const globalCasDir = join(tmpDir, "global-cas-threads");
|
|
176
|
+
process.env.OCAS_HOME = globalCasDir;
|
|
177
|
+
|
|
178
|
+
const storageRoot = join(tmpDir, "storage-threads-migrate");
|
|
179
|
+
await mkdir(storageRoot, { recursive: true });
|
|
180
|
+
|
|
181
|
+
const threadId = "01JTEST0000000000000000AB" as ThreadId;
|
|
182
|
+
const uwfSeed = await createUwfStore(storageRoot);
|
|
183
|
+
const headHash = await uwfSeed.store.cas.put(uwfSeed.schemas.text, "migrated-thread-head");
|
|
184
|
+
const { writeFile, access, readFile } = await import("node:fs/promises");
|
|
185
|
+
const threadsPath = join(storageRoot, "threads.yaml");
|
|
186
|
+
await writeFile(threadsPath, `${threadId}: ${headHash}\n`, "utf8");
|
|
187
|
+
|
|
188
|
+
const uwf = await createUwfStore(storageRoot);
|
|
189
|
+
const entry = uwf.varStore.list({ exactName: `@uwf/thread/${threadId}` })[0];
|
|
190
|
+
expect(entry?.value).toBe(headHash);
|
|
191
|
+
|
|
192
|
+
await expect(access(threadsPath)).rejects.toThrow();
|
|
193
|
+
const migratedContent = await readFile(`${threadsPath}.migrated`, "utf8");
|
|
194
|
+
expect(migratedContent).toContain(threadId);
|
|
195
|
+
expect(migratedContent).toContain(headHash);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("thread metadata stored in ocas variable store", async () => {
|
|
199
|
+
const globalCasDir = join(tmpDir, "global-cas");
|
|
200
|
+
process.env.OCAS_HOME = globalCasDir;
|
|
201
|
+
|
|
202
|
+
const storageRoot = join(tmpDir, "storage");
|
|
203
|
+
await mkdir(storageRoot, { recursive: true });
|
|
204
|
+
|
|
205
|
+
const threadId = "01JTEST000000000000000123" as ThreadId;
|
|
206
|
+
const uwfSeed = await createUwfStore(storageRoot);
|
|
207
|
+
const headHash = await uwfSeed.store.cas.put(uwfSeed.schemas.text, "hash-456");
|
|
208
|
+
setThread(uwfSeed.varStore, threadId, createThreadIndexEntry(headHash));
|
|
209
|
+
|
|
210
|
+
const uwf = await createUwfStore(storageRoot);
|
|
211
|
+
const entry = uwf.varStore.list({ exactName: `@uwf/thread/${threadId}` })[0];
|
|
212
|
+
expect(entry?.value).toBe(headHash);
|
|
213
|
+
|
|
214
|
+
const { readFile } = await import("node:fs/promises");
|
|
215
|
+
const threadsPath = join(storageRoot, "threads.yaml");
|
|
216
|
+
await expect(readFile(threadsPath, "utf8")).rejects.toThrow();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("history is stored in global CAS variable store", async () => {
|
|
220
|
+
const globalCasDir = join(tmpDir, "global-cas");
|
|
221
|
+
process.env.OCAS_HOME = globalCasDir;
|
|
222
|
+
|
|
223
|
+
const storageRoot = join(tmpDir, "storage");
|
|
224
|
+
await mkdir(storageRoot, { recursive: true });
|
|
225
|
+
|
|
226
|
+
const uwf = await createUwfStore(storageRoot);
|
|
227
|
+
const threadId = "thread-123" as ThreadId;
|
|
228
|
+
const headHash = await uwf.store.cas.put(uwf.schemas.text, "history-head");
|
|
229
|
+
const { completeThread, setThread, getThread } = await import("../store.js");
|
|
230
|
+
const { createThreadIndexEntry } = await import("@united-workforce/protocol");
|
|
231
|
+
|
|
232
|
+
setThread(uwf.varStore, threadId, createThreadIndexEntry(headHash));
|
|
233
|
+
completeThread(uwf.varStore, threadId, "completed");
|
|
234
|
+
|
|
235
|
+
const entry = getThread(uwf.varStore, threadId);
|
|
236
|
+
expect(entry?.head).toBe(headHash);
|
|
237
|
+
expect(entry?.status).toBe("completed");
|
|
238
|
+
|
|
239
|
+
const { access } = await import("node:fs/promises");
|
|
240
|
+
await access(join(globalCasDir, "vars"));
|
|
241
|
+
|
|
242
|
+
const historyPath = join(storageRoot, "history.jsonl");
|
|
243
|
+
await expect(access(historyPath)).rejects.toThrow();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("migrates history.jsonl to variable store and renames file", async () => {
|
|
247
|
+
const globalCasDir = join(tmpDir, "global-cas-history");
|
|
248
|
+
process.env.OCAS_HOME = globalCasDir;
|
|
249
|
+
|
|
250
|
+
const storageRoot = join(tmpDir, "storage-history-migrate");
|
|
251
|
+
await mkdir(storageRoot, { recursive: true });
|
|
252
|
+
|
|
253
|
+
const threadId = "01JTEST0000000000000000CD" as ThreadId;
|
|
254
|
+
const uwfSeed = await createUwfStore(storageRoot);
|
|
255
|
+
const workflowHash = await uwfSeed.store.cas.put(uwfSeed.schemas.text, "migrated-workflow");
|
|
256
|
+
const headHash = await uwfSeed.store.cas.put(uwfSeed.schemas.text, "migrated-head");
|
|
257
|
+
const completedAt = 1780410000000;
|
|
258
|
+
const { writeFile, access, readFile } = await import("node:fs/promises");
|
|
259
|
+
const historyPath = join(storageRoot, "history.jsonl");
|
|
260
|
+
await writeFile(
|
|
261
|
+
historyPath,
|
|
262
|
+
`${JSON.stringify({
|
|
263
|
+
thread: threadId,
|
|
264
|
+
workflow: workflowHash,
|
|
265
|
+
head: headHash,
|
|
266
|
+
completedAt,
|
|
267
|
+
reason: "cancelled",
|
|
268
|
+
})}\n`,
|
|
269
|
+
"utf8",
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const uwf = await createUwfStore(storageRoot);
|
|
273
|
+
const { getThread } = await import("../store.js");
|
|
274
|
+
const entry = getThread(uwf.varStore, threadId);
|
|
275
|
+
expect(entry).not.toBeNull();
|
|
276
|
+
expect(entry?.head).toBe(headHash);
|
|
277
|
+
expect(entry?.status).toBe("cancelled");
|
|
278
|
+
expect(entry?.completedAt).toBe(completedAt);
|
|
279
|
+
|
|
280
|
+
await expect(access(historyPath)).rejects.toThrow();
|
|
281
|
+
const migratedContent = await readFile(`${historyPath}.migrated`, "utf8");
|
|
282
|
+
expect(migratedContent).toContain(threadId);
|
|
283
|
+
expect(migratedContent).toContain(workflowHash);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("CAS nodes are stored in global directory", async () => {
|
|
287
|
+
const globalCasDir = join(tmpDir, "global-cas");
|
|
288
|
+
process.env.OCAS_HOME = globalCasDir;
|
|
289
|
+
|
|
290
|
+
const storageRoot = join(tmpDir, "storage");
|
|
291
|
+
await mkdir(storageRoot, { recursive: true });
|
|
292
|
+
|
|
293
|
+
const uwf = await createUwfStore(storageRoot);
|
|
294
|
+
|
|
295
|
+
// Store a CAS node
|
|
296
|
+
const testPayload = JSON.stringify({ test: "node" });
|
|
297
|
+
const _hash = uwf.store.cas.put(uwf.schemas.text, testPayload);
|
|
298
|
+
|
|
299
|
+
// Verify the node is in global CAS directory
|
|
300
|
+
const { readdir } = await import("node:fs/promises");
|
|
301
|
+
const files = await readdir(globalCasDir);
|
|
302
|
+
expect(files.length).toBeGreaterThan(0);
|
|
303
|
+
|
|
304
|
+
// Verify the node is NOT in the old storageRoot/cas location
|
|
305
|
+
const oldCasDir = join(storageRoot, "cas");
|
|
306
|
+
await expect(readdir(oldCasDir)).rejects.toThrow();
|
|
307
|
+
});
|
|
308
|
+
});
|