@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
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
|
5
5
|
import { createThreadIndexEntry } from "@united-workforce/protocol";
|
|
6
6
|
import { extractUlidTimestamp, generateUlid } from "@united-workforce/util";
|
|
7
7
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
8
|
-
import { createMarker, deleteMarker } from "../background/index.js";
|
|
8
|
+
import { createMarker, deleteMarker, getProcessStartTime } from "../background/index.js";
|
|
9
9
|
import { cmdThreadList } from "../commands/thread.js";
|
|
10
10
|
import { parseTimeInput } from "../commands/thread-time-parser.js";
|
|
11
11
|
import type { UwfStore } from "../store.js";
|
|
@@ -13,19 +13,13 @@ import {
|
|
|
13
13
|
completeThread as completeThreadInStore,
|
|
14
14
|
createUwfStore,
|
|
15
15
|
loadAllThreads,
|
|
16
|
+
saveWorkflowRegistry,
|
|
16
17
|
setThread,
|
|
17
18
|
} from "../store.js";
|
|
19
|
+
import { makeUwfStore } from "./thread-test-helpers.js";
|
|
18
20
|
|
|
19
21
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
20
22
|
|
|
21
|
-
async function makeUwfStore(storageRoot: string): Promise<UwfStore> {
|
|
22
|
-
const casDir = join(storageRoot, "cas");
|
|
23
|
-
await mkdir(casDir, { recursive: true });
|
|
24
|
-
// Set OCAS_HOME to use the test's CAS directory
|
|
25
|
-
process.env.OCAS_HOME = casDir;
|
|
26
|
-
return createUwfStore(storageRoot);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
23
|
async function createTestWorkflow(uwf: UwfStore): Promise<CasRef> {
|
|
30
24
|
const workflowPayload = {
|
|
31
25
|
name: "test-workflow",
|
|
@@ -66,6 +60,7 @@ async function markThreadRunning(storageRoot: string, threadId: ThreadId, workfl
|
|
|
66
60
|
workflow,
|
|
67
61
|
pid: process.pid, // Use current process PID so isPidAlive returns true
|
|
68
62
|
startedAt: Date.now(),
|
|
63
|
+
processStartTime: getProcessStartTime(process.pid),
|
|
69
64
|
});
|
|
70
65
|
}
|
|
71
66
|
|
|
@@ -76,18 +71,25 @@ async function completeThread(
|
|
|
76
71
|
_headHash: CasRef,
|
|
77
72
|
) {
|
|
78
73
|
const uwfIdx = await createUwfStore(storageRoot);
|
|
79
|
-
completeThreadInStore(uwfIdx.varStore, threadId, "
|
|
74
|
+
completeThreadInStore(uwfIdx.varStore, threadId, "end");
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
// ── test setup ────────────────────────────────────────────────────────────────
|
|
83
78
|
|
|
84
79
|
let tmpDir: string;
|
|
80
|
+
let savedOcasHome: string | undefined;
|
|
85
81
|
|
|
86
82
|
beforeEach(async () => {
|
|
83
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
87
84
|
tmpDir = await mkdtemp(join(tmpdir(), "thread-list-filters-test-"));
|
|
88
85
|
});
|
|
89
86
|
|
|
90
87
|
afterEach(async () => {
|
|
88
|
+
if (savedOcasHome === undefined) {
|
|
89
|
+
delete process.env.OCAS_HOME;
|
|
90
|
+
} else {
|
|
91
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
92
|
+
}
|
|
91
93
|
await rm(tmpDir, { recursive: true, force: true });
|
|
92
94
|
});
|
|
93
95
|
|
|
@@ -135,7 +137,7 @@ describe("cmdThreadList status filter", () => {
|
|
|
135
137
|
if (thread3Head === undefined) throw new Error("thread3 head not found");
|
|
136
138
|
await completeThread(tmpDir, thread3, workflowHash, thread3Head);
|
|
137
139
|
|
|
138
|
-
const result = await cmdThreadList(tmpDir, ["idle", "
|
|
140
|
+
const result = await cmdThreadList(tmpDir, ["idle", "end"], null, null, null, null);
|
|
139
141
|
|
|
140
142
|
// Clean up marker
|
|
141
143
|
await deleteMarker(tmpDir, thread2);
|
|
@@ -160,14 +162,14 @@ describe("cmdThreadList status filter", () => {
|
|
|
160
162
|
if (thread3Head === undefined) throw new Error("thread3 head not found");
|
|
161
163
|
await completeThread(tmpDir, thread3, workflowHash, thread3Head);
|
|
162
164
|
|
|
163
|
-
const result = await cmdThreadList(tmpDir, ["
|
|
165
|
+
const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null);
|
|
164
166
|
|
|
165
167
|
expect(result).toHaveLength(1);
|
|
166
168
|
expect(result[0]?.thread).toBe(thread3);
|
|
167
|
-
expect(result[0]?.status).toBe("
|
|
169
|
+
expect(result[0]?.status).toBe("end");
|
|
168
170
|
});
|
|
169
171
|
|
|
170
|
-
test("should return
|
|
172
|
+
test("should return only active threads when no filter and no --all", async () => {
|
|
171
173
|
const uwf = await makeUwfStore(tmpDir);
|
|
172
174
|
const workflowHash = await createTestWorkflow(uwf);
|
|
173
175
|
|
|
@@ -185,8 +187,290 @@ describe("cmdThreadList status filter", () => {
|
|
|
185
187
|
|
|
186
188
|
const result = await cmdThreadList(tmpDir, null, null, null, null, null);
|
|
187
189
|
|
|
190
|
+
// Default behavior (issue #147): only active threads (idle + running)
|
|
191
|
+
expect(result).toHaveLength(2);
|
|
192
|
+
expect(result.map((r) => r.thread).sort()).toEqual([thread1, thread2].sort());
|
|
193
|
+
|
|
194
|
+
// Clean up marker
|
|
195
|
+
await deleteMarker(tmpDir, thread2);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("should return all threads when --all (showAll=true)", async () => {
|
|
199
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
200
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
201
|
+
|
|
202
|
+
const thread1 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
203
|
+
const thread2 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
204
|
+
const thread3 = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
205
|
+
|
|
206
|
+
await markThreadRunning(tmpDir, thread2, workflowHash);
|
|
207
|
+
|
|
208
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
209
|
+
const index = loadAllThreads(uwfIdx.varStore);
|
|
210
|
+
const thread3Head = index[thread3]!.head;
|
|
211
|
+
if (thread3Head === undefined) throw new Error("thread3 head not found");
|
|
212
|
+
await completeThread(tmpDir, thread3, workflowHash, thread3Head);
|
|
213
|
+
|
|
214
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
|
|
215
|
+
|
|
188
216
|
expect(result).toHaveLength(3);
|
|
189
217
|
expect(result.map((r) => r.thread).sort()).toEqual([thread1, thread2, thread3].sort());
|
|
218
|
+
|
|
219
|
+
// Clean up marker
|
|
220
|
+
await deleteMarker(tmpDir, thread2);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ── default behavior tests (issue #147) ───────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
describe("cmdThreadList default behavior (issue #147)", () => {
|
|
227
|
+
test("default returns only idle + running threads", async () => {
|
|
228
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
229
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
230
|
+
|
|
231
|
+
const threadA = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
|
|
232
|
+
const threadB = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
233
|
+
const threadC = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
234
|
+
const threadD = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
235
|
+
|
|
236
|
+
await markThreadRunning(tmpDir, threadB, workflowHash);
|
|
237
|
+
|
|
238
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
239
|
+
const index = loadAllThreads(uwfIdx.varStore);
|
|
240
|
+
const threadCHead = index[threadC]!.head;
|
|
241
|
+
if (threadCHead === undefined) throw new Error("threadC head not found");
|
|
242
|
+
await completeThread(tmpDir, threadC, workflowHash, threadCHead);
|
|
243
|
+
|
|
244
|
+
// Cancel threadD
|
|
245
|
+
const threadDHead = index[threadD]!.head;
|
|
246
|
+
if (threadDHead === undefined) throw new Error("threadD head not found");
|
|
247
|
+
const uwfCancel = await createUwfStore(tmpDir);
|
|
248
|
+
completeThreadInStore(uwfCancel.varStore, threadD, "cancelled");
|
|
249
|
+
|
|
250
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null);
|
|
251
|
+
|
|
252
|
+
expect(result).toHaveLength(2);
|
|
253
|
+
expect(result.map((r) => r.thread).sort()).toEqual([threadA, threadB].sort());
|
|
254
|
+
|
|
255
|
+
await deleteMarker(tmpDir, threadB);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("default excludes completed threads", async () => {
|
|
259
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
260
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
261
|
+
|
|
262
|
+
const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 6000);
|
|
263
|
+
const completedThreads: ThreadId[] = [];
|
|
264
|
+
for (let i = 0; i < 5; i++) {
|
|
265
|
+
const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (5 - i) * 1000);
|
|
266
|
+
completedThreads.push(t);
|
|
267
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
268
|
+
const index = loadAllThreads(uwfIdx.varStore);
|
|
269
|
+
const head = index[t]!.head;
|
|
270
|
+
if (head === undefined) throw new Error("head not found");
|
|
271
|
+
await completeThread(tmpDir, t, workflowHash, head);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null);
|
|
275
|
+
|
|
276
|
+
expect(result).toHaveLength(1);
|
|
277
|
+
expect(result[0]?.thread).toBe(idleThread);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("default excludes cancelled threads", async () => {
|
|
281
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
282
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
283
|
+
|
|
284
|
+
const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
|
|
285
|
+
await markThreadRunning(tmpDir, runningThread, workflowHash);
|
|
286
|
+
|
|
287
|
+
const cancelled: ThreadId[] = [];
|
|
288
|
+
for (let i = 0; i < 3; i++) {
|
|
289
|
+
const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (3 - i) * 1000);
|
|
290
|
+
cancelled.push(t);
|
|
291
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
292
|
+
completeThreadInStore(uwfIdx.varStore, t, "cancelled");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null);
|
|
296
|
+
|
|
297
|
+
expect(result).toHaveLength(1);
|
|
298
|
+
expect(result[0]?.thread).toBe(runningThread);
|
|
299
|
+
|
|
300
|
+
await deleteMarker(tmpDir, runningThread);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("--all (showAll=true) returns every status", async () => {
|
|
304
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
305
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
306
|
+
|
|
307
|
+
const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 4000);
|
|
308
|
+
const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
309
|
+
await markThreadRunning(tmpDir, runningThread, workflowHash);
|
|
310
|
+
|
|
311
|
+
const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
312
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
313
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
314
|
+
const ch = idx[completedThread]!.head;
|
|
315
|
+
if (ch === undefined) throw new Error("completedThread head not found");
|
|
316
|
+
await completeThread(tmpDir, completedThread, workflowHash, ch);
|
|
317
|
+
|
|
318
|
+
const cancelledThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
319
|
+
completeThreadInStore(uwfIdx.varStore, cancelledThread, "cancelled");
|
|
320
|
+
|
|
321
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
|
|
322
|
+
|
|
323
|
+
expect(result).toHaveLength(4);
|
|
324
|
+
expect(result.map((r) => r.thread).sort()).toEqual(
|
|
325
|
+
[idleThread, runningThread, completedThread, cancelledThread].sort(),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
await deleteMarker(tmpDir, runningThread);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("explicit --status overrides default (still returns just the filtered statuses)", async () => {
|
|
332
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
333
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
334
|
+
|
|
335
|
+
const _idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
336
|
+
const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
337
|
+
await markThreadRunning(tmpDir, runningThread, workflowHash);
|
|
338
|
+
|
|
339
|
+
const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
340
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
341
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
342
|
+
const ch = idx[completedThread]!.head;
|
|
343
|
+
if (ch === undefined) throw new Error("completedThread head not found");
|
|
344
|
+
await completeThread(tmpDir, completedThread, workflowHash, ch);
|
|
345
|
+
|
|
346
|
+
const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null);
|
|
347
|
+
|
|
348
|
+
expect(result).toHaveLength(1);
|
|
349
|
+
expect(result[0]?.thread).toBe(completedThread);
|
|
350
|
+
expect(result[0]?.status).toBe("end");
|
|
351
|
+
|
|
352
|
+
await deleteMarker(tmpDir, runningThread);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("--status active keeps working", async () => {
|
|
356
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
357
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
358
|
+
|
|
359
|
+
const idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
360
|
+
const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
361
|
+
await markThreadRunning(tmpDir, runningThread, workflowHash);
|
|
362
|
+
|
|
363
|
+
const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
364
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
365
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
366
|
+
const ch = idx[completedThread]!.head;
|
|
367
|
+
if (ch === undefined) throw new Error("completedThread head not found");
|
|
368
|
+
await completeThread(tmpDir, completedThread, workflowHash, ch);
|
|
369
|
+
|
|
370
|
+
const result = await cmdThreadList(tmpDir, ["idle", "running"], null, null, null, null);
|
|
371
|
+
|
|
372
|
+
expect(result).toHaveLength(2);
|
|
373
|
+
expect(result.map((r) => r.thread).sort()).toEqual([idleThread, runningThread].sort());
|
|
374
|
+
|
|
375
|
+
await deleteMarker(tmpDir, runningThread);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("--status + --all — explicit status wins", async () => {
|
|
379
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
380
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
381
|
+
|
|
382
|
+
const _idleThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 3000);
|
|
383
|
+
const runningThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 2000);
|
|
384
|
+
await markThreadRunning(tmpDir, runningThread, workflowHash);
|
|
385
|
+
|
|
386
|
+
const completedThread = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - 1000);
|
|
387
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
388
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
389
|
+
const ch = idx[completedThread]!.head;
|
|
390
|
+
if (ch === undefined) throw new Error("completedThread head not found");
|
|
391
|
+
await completeThread(tmpDir, completedThread, workflowHash, ch);
|
|
392
|
+
|
|
393
|
+
const result = await cmdThreadList(tmpDir, ["end"], null, null, null, null, true);
|
|
394
|
+
|
|
395
|
+
expect(result).toHaveLength(1);
|
|
396
|
+
expect(result[0]?.thread).toBe(completedThread);
|
|
397
|
+
|
|
398
|
+
await deleteMarker(tmpDir, runningThread);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("default returns empty when no threads", async () => {
|
|
402
|
+
await makeUwfStore(tmpDir);
|
|
403
|
+
|
|
404
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null);
|
|
405
|
+
|
|
406
|
+
expect(result).toHaveLength(0);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("default + time range filter composes correctly", async () => {
|
|
410
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
411
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
412
|
+
|
|
413
|
+
const ts1 = Date.UTC(2026, 4, 20, 0, 0, 0);
|
|
414
|
+
const ts2 = Date.UTC(2026, 4, 21, 0, 0, 0);
|
|
415
|
+
const ts3 = Date.UTC(2026, 4, 22, 0, 0, 0);
|
|
416
|
+
const ts4 = Date.UTC(2026, 4, 23, 0, 0, 0);
|
|
417
|
+
const ts5 = Date.UTC(2026, 4, 24, 0, 0, 0);
|
|
418
|
+
|
|
419
|
+
const _t1 = await createTestThread(uwf, tmpDir, workflowHash, ts1);
|
|
420
|
+
const t2 = await createTestThread(uwf, tmpDir, workflowHash, ts2);
|
|
421
|
+
const t3 = await createTestThread(uwf, tmpDir, workflowHash, ts3);
|
|
422
|
+
const t4 = await createTestThread(uwf, tmpDir, workflowHash, ts4);
|
|
423
|
+
const _t5 = await createTestThread(uwf, tmpDir, workflowHash, ts5);
|
|
424
|
+
|
|
425
|
+
// Mark t3 running
|
|
426
|
+
await markThreadRunning(tmpDir, t3, workflowHash);
|
|
427
|
+
|
|
428
|
+
// Complete t4 (should be excluded by default)
|
|
429
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
430
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
431
|
+
const t4head = idx[t4]!.head;
|
|
432
|
+
if (t4head === undefined) throw new Error("t4 head not found");
|
|
433
|
+
await completeThread(tmpDir, t4, workflowHash, t4head);
|
|
434
|
+
|
|
435
|
+
// afterMs in middle of range to exclude _t1
|
|
436
|
+
const afterMs = Date.UTC(2026, 4, 20, 12, 0, 0);
|
|
437
|
+
const result = await cmdThreadList(tmpDir, null, afterMs, null, null, null);
|
|
438
|
+
|
|
439
|
+
// Expected: t2 (idle), t3 (running), _t5 (idle); excludes t4 (completed) and _t1 (filtered by time)
|
|
440
|
+
expect(result).toHaveLength(3);
|
|
441
|
+
const ids = result.map((r) => r.thread).sort();
|
|
442
|
+
expect(ids).toEqual([t2, t3, _t5].sort());
|
|
443
|
+
|
|
444
|
+
await deleteMarker(tmpDir, t3);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("default + pagination composes correctly", async () => {
|
|
448
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
449
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
450
|
+
|
|
451
|
+
// Create 10 idle threads + 5 completed threads
|
|
452
|
+
const idleThreads: ThreadId[] = [];
|
|
453
|
+
for (let i = 0; i < 10; i++) {
|
|
454
|
+
idleThreads.push(
|
|
455
|
+
await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (15 - i) * 1000),
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
for (let i = 0; i < 5; i++) {
|
|
459
|
+
const t = await createTestThread(uwf, tmpDir, workflowHash, Date.now() - (5 - i) * 1000);
|
|
460
|
+
const uwfIdx = await createUwfStore(tmpDir);
|
|
461
|
+
const idx = loadAllThreads(uwfIdx.varStore);
|
|
462
|
+
const head = idx[t]!.head;
|
|
463
|
+
if (head === undefined) throw new Error("head not found");
|
|
464
|
+
await completeThread(tmpDir, t, workflowHash, head);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const result = await cmdThreadList(tmpDir, null, null, null, 2, 3);
|
|
468
|
+
|
|
469
|
+
expect(result).toHaveLength(3);
|
|
470
|
+
// All results should be idle (default excludes completed)
|
|
471
|
+
for (const r of result) {
|
|
472
|
+
expect(r.status).toBe("idle");
|
|
473
|
+
}
|
|
190
474
|
});
|
|
191
475
|
});
|
|
192
476
|
|
|
@@ -382,11 +666,11 @@ describe("combined filters", () => {
|
|
|
382
666
|
await completeThread(tmpDir, thread, workflowHash, headHash);
|
|
383
667
|
}
|
|
384
668
|
|
|
385
|
-
const result = await cmdThreadList(tmpDir, ["
|
|
669
|
+
const result = await cmdThreadList(tmpDir, ["end"], null, null, 3, 5);
|
|
386
670
|
|
|
387
671
|
expect(result).toHaveLength(5);
|
|
388
672
|
for (const r of result) {
|
|
389
|
-
expect(r.status).toBe("
|
|
673
|
+
expect(r.status).toBe("end");
|
|
390
674
|
}
|
|
391
675
|
});
|
|
392
676
|
|
|
@@ -570,3 +854,145 @@ describe("ISO date parsing", () => {
|
|
|
570
854
|
expect(() => parseTimeInput("invalid", nowMs)).toThrow();
|
|
571
855
|
});
|
|
572
856
|
});
|
|
857
|
+
|
|
858
|
+
// ── corrupt thread resilience (#250) ──────────────────────────────────────────
|
|
859
|
+
|
|
860
|
+
describe("corrupt thread resilience (#250)", () => {
|
|
861
|
+
test("thread list returns corrupt entry when CAS node is missing", async () => {
|
|
862
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
863
|
+
|
|
864
|
+
// Create a valid thread
|
|
865
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
866
|
+
const now = Date.now();
|
|
867
|
+
const _validId = await createTestThread(uwf, tmpDir, workflowHash, now);
|
|
868
|
+
|
|
869
|
+
// Create another thread with a unique start node, then delete its workflow CAS to corrupt it
|
|
870
|
+
const corruptThreadId = generateUlid(now + 1000) as ThreadId;
|
|
871
|
+
const startPayload = {
|
|
872
|
+
workflow: workflowHash,
|
|
873
|
+
prompt: "corrupt thread prompt — unique to avoid CAS hash collision",
|
|
874
|
+
cwd: tmpDir,
|
|
875
|
+
};
|
|
876
|
+
const headHash = await uwf.store.cas.put(uwf.schemas.startNode, startPayload);
|
|
877
|
+
setThread(uwf.varStore, corruptThreadId, createThreadIndexEntry(headHash));
|
|
878
|
+
|
|
879
|
+
// Delete the workflow CAS node — start node still exists but workflow ref dangles
|
|
880
|
+
uwf.store.cas.delete(workflowHash);
|
|
881
|
+
|
|
882
|
+
// thread list should NOT throw — it should return both threads
|
|
883
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, true);
|
|
884
|
+
|
|
885
|
+
// Both threads should appear (the valid one is now also corrupt since workflow is shared)
|
|
886
|
+
// In practice: both become corrupt because they share the same workflow CAS node
|
|
887
|
+
// This matches the real scenario from issue #250 — gc deleted a shared node
|
|
888
|
+
expect(result.length).toBeGreaterThanOrEqual(2);
|
|
889
|
+
const corruptItems = result.filter((r) => r.status === "corrupt");
|
|
890
|
+
expect(corruptItems.length).toBeGreaterThanOrEqual(1);
|
|
891
|
+
for (const item of corruptItems) {
|
|
892
|
+
expect(item.statusDisplay).toBe("corrupt");
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test("corrupt threads appear in default filter (without --all)", async () => {
|
|
897
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
898
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
899
|
+
|
|
900
|
+
// Create a thread then corrupt it
|
|
901
|
+
const corruptId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
|
|
902
|
+
const corruptEntry = loadAllThreads(uwf.varStore)[corruptId];
|
|
903
|
+
uwf.store.cas.delete(corruptEntry.head);
|
|
904
|
+
|
|
905
|
+
// Default filter (no --all, no --status) should include corrupt
|
|
906
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
|
|
907
|
+
expect(result).toHaveLength(1);
|
|
908
|
+
expect(result[0].status).toBe("corrupt");
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// ── orphan thread detection (#286) ────────────────────────────────────────────
|
|
913
|
+
|
|
914
|
+
describe("orphan thread detection (#286)", () => {
|
|
915
|
+
test("thread list includes workflowName when workflow is in registry", async () => {
|
|
916
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
917
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
918
|
+
|
|
919
|
+
// Register the workflow in registry
|
|
920
|
+
saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
|
|
921
|
+
|
|
922
|
+
const threadId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
|
|
923
|
+
|
|
924
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
|
|
925
|
+
expect(result).toHaveLength(1);
|
|
926
|
+
expect(result[0].thread).toBe(threadId);
|
|
927
|
+
expect(result[0].workflowName).toBe("test-workflow");
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test("thread list returns workflowName: null for orphaned threads", async () => {
|
|
931
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
932
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
933
|
+
|
|
934
|
+
// Do NOT register the workflow — thread is orphaned
|
|
935
|
+
const threadId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
|
|
936
|
+
|
|
937
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
|
|
938
|
+
expect(result).toHaveLength(1);
|
|
939
|
+
expect(result[0].thread).toBe(threadId);
|
|
940
|
+
expect(result[0].workflowName).toBeNull();
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
test("mixed registered and orphaned threads in the same list", async () => {
|
|
944
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
945
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
946
|
+
|
|
947
|
+
// Register the workflow
|
|
948
|
+
saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
|
|
949
|
+
|
|
950
|
+
// Create a thread using the registered workflow
|
|
951
|
+
const now = Date.now();
|
|
952
|
+
const registeredId = await createTestThread(uwf, tmpDir, workflowHash, now);
|
|
953
|
+
|
|
954
|
+
// Create a second workflow (different hash), not registered
|
|
955
|
+
const orphanWorkflowPayload = {
|
|
956
|
+
name: "orphan-workflow",
|
|
957
|
+
roles: {
|
|
958
|
+
role1: {
|
|
959
|
+
goal: "orphan goal",
|
|
960
|
+
outputSchema: { type: "object" as const, properties: {} },
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
graph: { start: "role1" },
|
|
964
|
+
conditions: {},
|
|
965
|
+
};
|
|
966
|
+
const orphanHash = await uwf.store.cas.put(uwf.schemas.workflow, orphanWorkflowPayload);
|
|
967
|
+
const orphanId = await createTestThread(uwf, tmpDir, orphanHash, now + 1000);
|
|
968
|
+
|
|
969
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
|
|
970
|
+
expect(result).toHaveLength(2);
|
|
971
|
+
|
|
972
|
+
// Sorted newest first, so orphan (later timestamp) comes first
|
|
973
|
+
const orphanItem = result.find((r) => r.thread === orphanId);
|
|
974
|
+
const registeredItem = result.find((r) => r.thread === registeredId);
|
|
975
|
+
|
|
976
|
+
expect(orphanItem).toBeDefined();
|
|
977
|
+
expect(orphanItem!.workflowName).toBeNull();
|
|
978
|
+
|
|
979
|
+
expect(registeredItem).toBeDefined();
|
|
980
|
+
expect(registeredItem!.workflowName).toBe("test-workflow");
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
test("corrupt threads have workflowName: null", async () => {
|
|
984
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
985
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
986
|
+
saveWorkflowRegistry(uwf.varStore, "test-workflow", workflowHash);
|
|
987
|
+
|
|
988
|
+
// Create a thread then corrupt it by deleting its head CAS node
|
|
989
|
+
const corruptId = await createTestThread(uwf, tmpDir, workflowHash, Date.now());
|
|
990
|
+
const corruptEntry = loadAllThreads(uwf.varStore)[corruptId];
|
|
991
|
+
uwf.store.cas.delete(corruptEntry.head);
|
|
992
|
+
|
|
993
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, null, false);
|
|
994
|
+
expect(result).toHaveLength(1);
|
|
995
|
+
expect(result[0].status).toBe("corrupt");
|
|
996
|
+
expect(result[0].workflowName).toBeNull();
|
|
997
|
+
});
|
|
998
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { OUTPUT_TEMPLATES } from "@united-workforce/protocol";
|
|
2
|
+
import { Liquid } from "liquidjs";
|
|
3
|
+
import { describe, expect, test } from "vitest";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Issue #351 — `uwf thread list --format text` rendered the `STARTED` column
|
|
7
|
+
* as `58414-12-06` because `THREAD_LIST_TEMPLATE` piped `item.startedAt` (Unix
|
|
8
|
+
* **ms** per `THREAD_LIST_OUTPUT_SCHEMA`) directly into LiquidJS's `| date`
|
|
9
|
+
* filter, which expects Unix **seconds**.
|
|
10
|
+
*
|
|
11
|
+
* This integration test renders the template against a known ms timestamp
|
|
12
|
+
* and asserts the year falls within the realistic 20xx range, confirming
|
|
13
|
+
* the ms→s conversion is in place at the protocol layer.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
function makeEngine(): Liquid {
|
|
17
|
+
return new Liquid({ cache: false, strictFilters: false, strictVariables: false });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("THREAD_LIST_TEMPLATE rendering — issue #351 ms→s for `| date`", () => {
|
|
21
|
+
test("renders item.startedAt=1781229932779 as a 2026 calendar date (not 58414)", async () => {
|
|
22
|
+
const engine = makeEngine();
|
|
23
|
+
const out = await engine.parseAndRender(OUTPUT_TEMPLATES["thread-list"], {
|
|
24
|
+
items: [
|
|
25
|
+
{
|
|
26
|
+
threadId: "01K5HMKZQB7VDA8E2K9P3R5XBC",
|
|
27
|
+
workflowHash: "WF1234567890A",
|
|
28
|
+
workflowName: null,
|
|
29
|
+
status: "idle",
|
|
30
|
+
currentRole: "planner",
|
|
31
|
+
startedAt: 1781229932779,
|
|
32
|
+
completedAt: null,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(out).not.toContain("58414");
|
|
38
|
+
expect(out).toMatch(/\b20\d{2}-\d{2}-\d{2}\b/);
|
|
39
|
+
// The STARTED cell must NOT begin with a 5-digit year.
|
|
40
|
+
expect(out).not.toMatch(/\b\d{5}-\d{2}-\d{2}\b/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("renders `-` for items with startedAt=null (null guard preserved)", async () => {
|
|
44
|
+
const engine = makeEngine();
|
|
45
|
+
const out = await engine.parseAndRender(OUTPUT_TEMPLATES["thread-list"], {
|
|
46
|
+
items: [
|
|
47
|
+
{
|
|
48
|
+
threadId: "01K5HMKZQB7VDA8E2K9P3R5XBC",
|
|
49
|
+
workflowHash: "WF1234567890A",
|
|
50
|
+
workflowName: null,
|
|
51
|
+
status: "idle",
|
|
52
|
+
currentRole: "planner",
|
|
53
|
+
startedAt: null,
|
|
54
|
+
completedAt: null,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(out).not.toContain("58414");
|
|
60
|
+
expect(out).not.toContain("Invalid Date");
|
|
61
|
+
expect(out).not.toContain("1970-01-01");
|
|
62
|
+
// Last token of the row is the rendered STARTED cell — must be `-`.
|
|
63
|
+
const dataRow = out
|
|
64
|
+
.split("\n")
|
|
65
|
+
.find((line: string) => line.includes("01K5HMKZQB7VDA8E2K9P3R5XBC"));
|
|
66
|
+
expect(dataRow).toBeDefined();
|
|
67
|
+
expect(dataRow?.trimEnd().endsWith("-")).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("renders multiple ms timestamps across years 2020–2030 with correct year prefix", async () => {
|
|
71
|
+
const engine = makeEngine();
|
|
72
|
+
const items = [
|
|
73
|
+
{
|
|
74
|
+
threadId: "ID1",
|
|
75
|
+
workflowHash: "WF",
|
|
76
|
+
workflowName: null,
|
|
77
|
+
status: "idle",
|
|
78
|
+
currentRole: null,
|
|
79
|
+
startedAt: Date.UTC(2020, 0, 1, 0, 0, 0),
|
|
80
|
+
completedAt: null,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
threadId: "ID2",
|
|
84
|
+
workflowHash: "WF",
|
|
85
|
+
workflowName: null,
|
|
86
|
+
status: "idle",
|
|
87
|
+
currentRole: null,
|
|
88
|
+
startedAt: Date.UTC(2026, 5, 12, 5, 25, 0),
|
|
89
|
+
completedAt: null,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
threadId: "ID3",
|
|
93
|
+
workflowHash: "WF",
|
|
94
|
+
workflowName: null,
|
|
95
|
+
status: "idle",
|
|
96
|
+
currentRole: null,
|
|
97
|
+
startedAt: Date.UTC(2030, 11, 31, 23, 59, 0),
|
|
98
|
+
completedAt: null,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const out = await engine.parseAndRender(OUTPUT_TEMPLATES["thread-list"], { items });
|
|
103
|
+
|
|
104
|
+
expect(out).toContain("2020-");
|
|
105
|
+
expect(out).toContain("2026-");
|
|
106
|
+
expect(out).toContain("2030-");
|
|
107
|
+
expect(out).not.toContain("58414");
|
|
108
|
+
expect(out).not.toMatch(/\b\d{5}-\d{2}-\d{2}\b/);
|
|
109
|
+
});
|
|
110
|
+
});
|