@united-workforce/cli 0.6.1 → 0.8.1
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 +120 -5
- package/dist/.build-fingerprint +1 -1
- package/dist/__tests__/agent-resolution-llm-free.test.js +9 -2
- package/dist/__tests__/agent-resolution-llm-free.test.js.map +1 -1
- package/dist/__tests__/broker-prompt.test.d.ts +10 -0
- package/dist/__tests__/broker-prompt.test.d.ts.map +1 -0
- package/dist/__tests__/broker-prompt.test.js +129 -0
- package/dist/__tests__/broker-prompt.test.js.map +1 -0
- package/dist/__tests__/broker-step-active-turns.test.d.ts +20 -0
- package/dist/__tests__/broker-step-active-turns.test.d.ts.map +1 -0
- package/dist/__tests__/broker-step-active-turns.test.js +428 -0
- package/dist/__tests__/broker-step-active-turns.test.js.map +1 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts +13 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.d.ts.map +1 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.js +429 -0
- package/dist/__tests__/broker-step-turn-chain-phase2.test.js.map +1 -0
- package/dist/__tests__/config.test.js +33 -37
- package/dist/__tests__/config.test.js.map +1 -1
- package/dist/__tests__/e2e-broker-step-suspend.test.d.ts +18 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.js +313 -0
- package/dist/__tests__/e2e-broker-step-suspend.test.js.map +1 -0
- package/dist/__tests__/e2e-broker-step.test.d.ts +13 -0
- package/dist/__tests__/e2e-broker-step.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-broker-step.test.js +278 -0
- package/dist/__tests__/e2e-broker-step.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +1 -1
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -1
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts +28 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js +322 -0
- package/dist/__tests__/e2e-thread-resume-timeout-suspend.test.js.map +1 -0
- package/dist/__tests__/log-tag-validity.test.d.ts +2 -0
- package/dist/__tests__/log-tag-validity.test.d.ts.map +1 -0
- package/dist/__tests__/log-tag-validity.test.js +110 -0
- package/dist/__tests__/log-tag-validity.test.js.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.js +35 -23
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -1
- package/dist/__tests__/setup-no-llm.test.js +5 -2
- package/dist/__tests__/setup-no-llm.test.js.map +1 -1
- package/dist/__tests__/step-ask.test.js +9 -6
- package/dist/__tests__/step-ask.test.js.map +1 -1
- package/dist/__tests__/step-show-json.test.js +5 -5
- package/dist/__tests__/step-show-json.test.js.map +1 -1
- package/dist/__tests__/step-show-text.test.d.ts +2 -0
- package/dist/__tests__/step-show-text.test.d.ts.map +1 -0
- package/dist/__tests__/step-show-text.test.js +192 -0
- package/dist/__tests__/step-show-text.test.js.map +1 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.d.ts +21 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.js +356 -0
- package/dist/__tests__/step-turns-cli-subprocess.test.js.map +1 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.d.ts +21 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.js +476 -0
- package/dist/__tests__/step-turns-panorama-phase3.test.js.map +1 -0
- package/dist/__tests__/step-turns.test.d.ts +24 -0
- package/dist/__tests__/step-turns.test.d.ts.map +1 -0
- package/dist/__tests__/step-turns.test.js +646 -0
- package/dist/__tests__/step-turns.test.js.map +1 -0
- package/dist/__tests__/store-turn-chain.test.d.ts +2 -0
- package/dist/__tests__/store-turn-chain.test.d.ts.map +1 -0
- package/dist/__tests__/store-turn-chain.test.js +341 -0
- package/dist/__tests__/store-turn-chain.test.js.map +1 -0
- package/dist/__tests__/thread-agent-failure-suspended.test.js +3 -3
- package/dist/__tests__/thread-agent-failure-suspended.test.js.map +1 -1
- package/dist/__tests__/thread-list-limit-offset.test.d.ts +24 -0
- package/dist/__tests__/thread-list-limit-offset.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-limit-offset.test.js +254 -0
- package/dist/__tests__/thread-list-limit-offset.test.js.map +1 -0
- package/dist/__tests__/thread-list-template-ms-date.test.js +7 -2
- package/dist/__tests__/thread-list-template-ms-date.test.js.map +1 -1
- package/dist/__tests__/thread-poke.test.js +6 -6
- package/dist/__tests__/thread-poke.test.js.map +1 -1
- package/dist/__tests__/thread-resume.test.js +2 -2
- package/dist/__tests__/thread-resume.test.js.map +1 -1
- package/dist/__tests__/thread-suspend-step.test.js +1 -1
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -1
- package/dist/__tests__/thread.test.js +28 -14
- package/dist/__tests__/thread.test.js.map +1 -1
- package/dist/cli.js +910 -344
- package/dist/cli.js.map +1 -1
- package/dist/commands/broker-step.d.ts +117 -0
- package/dist/commands/broker-step.d.ts.map +1 -0
- package/dist/commands/broker-step.js +654 -0
- package/dist/commands/broker-step.js.map +1 -0
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +2 -23
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +43 -51
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/setup.d.ts +6 -4
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +24 -27
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/step.d.ts +54 -6
- package/dist/commands/step.d.ts.map +1 -1
- package/dist/commands/step.js +484 -134
- package/dist/commands/step.js.map +1 -1
- package/dist/commands/thread.d.ts +4 -0
- package/dist/commands/thread.d.ts.map +1 -1
- package/dist/commands/thread.js +77 -151
- package/dist/commands/thread.js.map +1 -1
- package/dist/output-mappers.d.ts +8 -0
- package/dist/output-mappers.d.ts.map +1 -1
- package/dist/output-mappers.js +72 -18
- package/dist/output-mappers.js.map +1 -1
- package/dist/schemas.d.ts +3 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +17 -3
- package/dist/schemas.js.map +1 -1
- package/dist/store.d.ts +147 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +254 -1
- package/dist/store.js.map +1 -1
- package/dist/text-renderers.d.ts.map +1 -1
- package/dist/text-renderers.js +27 -2
- package/dist/text-renderers.js.map +1 -1
- package/package.json +7 -5
- package/src/__tests__/agent-resolution-llm-free.test.ts +14 -2
- package/src/__tests__/broker-prompt.test.ts +142 -0
- package/src/__tests__/broker-step-active-turns.test.ts +509 -0
- package/src/__tests__/broker-step-turn-chain-phase2.test.ts +525 -0
- package/src/__tests__/config.test.ts +35 -39
- package/src/__tests__/e2e-broker-step-suspend.test.ts +351 -0
- package/src/__tests__/e2e-broker-step.test.ts +320 -0
- package/src/__tests__/e2e-mock-agent.test.ts +1 -1
- package/src/__tests__/e2e-thread-resume-timeout-suspend.test.ts +360 -0
- package/src/__tests__/log-tag-validity.test.ts +124 -0
- package/src/__tests__/setup-agent-discovery.test.ts +35 -23
- package/src/__tests__/setup-no-llm.test.ts +5 -2
- package/src/__tests__/step-ask.test.ts +9 -6
- package/src/__tests__/step-show-json.test.ts +5 -5
- package/src/__tests__/step-show-text.test.ts +236 -0
- package/src/__tests__/step-turns-cli-subprocess.test.ts +411 -0
- package/src/__tests__/step-turns-panorama-phase3.test.ts +579 -0
- package/src/__tests__/step-turns.test.ts +734 -0
- package/src/__tests__/store-turn-chain.test.ts +386 -0
- package/src/__tests__/thread-agent-failure-suspended.test.ts +3 -3
- package/src/__tests__/thread-list-limit-offset.test.ts +305 -0
- package/src/__tests__/thread-list-template-ms-date.test.ts +7 -2
- package/src/__tests__/thread-poke.test.ts +6 -6
- package/src/__tests__/thread-resume.test.ts +2 -2
- package/src/__tests__/thread-suspend-step.test.ts +1 -1
- package/src/__tests__/thread.test.ts +29 -15
- package/src/cli.ts +1056 -483
- package/src/commands/broker-step.ts +913 -0
- package/src/commands/config.ts +2 -24
- package/src/commands/prompt.ts +43 -51
- package/src/commands/setup.ts +25 -29
- package/src/commands/step.ts +645 -176
- package/src/commands/thread.ts +87 -192
- package/src/output-mappers.ts +99 -21
- package/src/schemas.ts +32 -2
- package/src/store.ts +297 -2
- package/src/text-renderers.ts +35 -2
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts +0 -2
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +0 -1
- package/dist/__tests__/adapter-json-roundtrip.test.js +0 -160
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +0 -1
- package/dist/__tests__/spawn-agent-json.test.d.ts +0 -2
- package/dist/__tests__/spawn-agent-json.test.d.ts.map +0 -1
- package/dist/__tests__/spawn-agent-json.test.js +0 -79
- package/dist/__tests__/spawn-agent-json.test.js.map +0 -1
- package/src/__tests__/adapter-json-roundtrip.test.ts +0 -193
- package/src/__tests__/spawn-agent-json.test.ts +0 -100
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #451 — `uwf thread list --limit <n> / --offset <m>` pagination.
|
|
3
|
+
*
|
|
4
|
+
* Spec: specs/thread-list-limit-offset-pagination.md
|
|
5
|
+
*
|
|
6
|
+
* `thread list` already paginates internally (cmdThreadList skip/take +
|
|
7
|
+
* applyPagination over the newest-first list). #451 wires the canonical
|
|
8
|
+
* repo-wide `ListOptions` vocabulary (`--limit`/`--offset`, as used by
|
|
9
|
+
* `step turns`) through to that engine, keeping `--skip`/`--take` as
|
|
10
|
+
* backward-compatible aliases. The behavioural gap is purely at the CLI flag
|
|
11
|
+
* layer: passing `--limit` today errors with `unknown option`.
|
|
12
|
+
*
|
|
13
|
+
* This file covers:
|
|
14
|
+
* 1. The `cmdThreadList` slice semantics that `--limit`/`--offset` map onto
|
|
15
|
+
* (offset → skip, limit → take) — equivalence with the existing
|
|
16
|
+
* `--skip`/`--take` parameters.
|
|
17
|
+
* 2. A CLI subprocess test (`execFileSync`) proving the new flags are
|
|
18
|
+
* registered and parsed end-to-end (the actual #451 regression), and that
|
|
19
|
+
* `--limit`/`--offset` produce the same output as `--take`/`--skip`.
|
|
20
|
+
*
|
|
21
|
+
* Follows the subprocess pattern from step-turns-cli-subprocess.test.ts.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import type { CasRef, ThreadId } from "@united-workforce/protocol";
|
|
30
|
+
import { createThreadIndexEntry } from "@united-workforce/protocol";
|
|
31
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
32
|
+
import { cmdThreadList } from "../commands/thread.js";
|
|
33
|
+
import { createUwfStore, setThread, type UwfStore } from "../store.js";
|
|
34
|
+
import { makeUwfStore } from "./thread-test-helpers.js";
|
|
35
|
+
|
|
36
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
async function createTestWorkflow(uwf: UwfStore): Promise<CasRef> {
|
|
39
|
+
const workflowPayload = {
|
|
40
|
+
name: "test-workflow",
|
|
41
|
+
roles: {
|
|
42
|
+
role1: {
|
|
43
|
+
goal: "test goal",
|
|
44
|
+
outputSchema: { type: "object" as const, properties: {} },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
graph: { start: "role1" },
|
|
48
|
+
conditions: {},
|
|
49
|
+
};
|
|
50
|
+
return await uwf.store.cas.put(uwf.schemas.workflow, workflowPayload);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a thread with an explicit ULID derived from `timestamp`, so the
|
|
55
|
+
* newest-first sort (by ULID timestamp) is deterministic.
|
|
56
|
+
*/
|
|
57
|
+
async function createTestThreadAt(
|
|
58
|
+
uwf: UwfStore,
|
|
59
|
+
storageRoot: string,
|
|
60
|
+
workflowHash: CasRef,
|
|
61
|
+
ulid: string,
|
|
62
|
+
): Promise<ThreadId> {
|
|
63
|
+
const threadId = ulid as ThreadId;
|
|
64
|
+
const startPayload = {
|
|
65
|
+
workflow: workflowHash,
|
|
66
|
+
prompt: "test prompt",
|
|
67
|
+
cwd: storageRoot,
|
|
68
|
+
};
|
|
69
|
+
const headHash = await uwf.store.cas.put(uwf.schemas.startNode, startPayload);
|
|
70
|
+
setThread(uwf.varStore, threadId, createThreadIndexEntry(headHash));
|
|
71
|
+
return threadId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── test setup ──────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
let tmpDir: string;
|
|
77
|
+
let savedOcasHome: string | undefined;
|
|
78
|
+
|
|
79
|
+
beforeEach(async () => {
|
|
80
|
+
savedOcasHome = process.env.OCAS_HOME;
|
|
81
|
+
tmpDir = await mkdtemp(join(tmpdir(), "thread-list-limit-offset-test-"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(async () => {
|
|
85
|
+
if (savedOcasHome === undefined) {
|
|
86
|
+
delete process.env.OCAS_HOME;
|
|
87
|
+
} else {
|
|
88
|
+
process.env.OCAS_HOME = savedOcasHome;
|
|
89
|
+
}
|
|
90
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── cmdThreadList: --offset → skip, --limit → take mapping ───────────────────
|
|
94
|
+
|
|
95
|
+
describe("cmdThreadList --limit/--offset mapping (#451)", () => {
|
|
96
|
+
/**
|
|
97
|
+
* Seed N threads with monotonically increasing ULID timestamps and return
|
|
98
|
+
* the thread IDs in newest-first order (the order `cmdThreadList` returns).
|
|
99
|
+
*/
|
|
100
|
+
async function seedNewestFirst(count: number): Promise<{ newestFirst: ThreadId[] }> {
|
|
101
|
+
const uwf = await makeUwfStore(tmpDir);
|
|
102
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
103
|
+
const created: ThreadId[] = [];
|
|
104
|
+
// Distinct, strictly increasing ULID timestamps (ms) → deterministic order.
|
|
105
|
+
const base = Date.UTC(2026, 0, 1, 0, 0, 0);
|
|
106
|
+
for (let i = 0; i < count; i++) {
|
|
107
|
+
// generateUlid is timestamp-prefixed; use the store's helper indirectly by
|
|
108
|
+
// constructing via createTestThreadAt with a fabricated monotonic ULID.
|
|
109
|
+
const { generateUlid } = await import("@united-workforce/util");
|
|
110
|
+
const ulid = generateUlid(base + i * 1000);
|
|
111
|
+
created.push(await createTestThreadAt(uwf, tmpDir, workflowHash, ulid));
|
|
112
|
+
}
|
|
113
|
+
// Newest-first = reverse of creation order (later timestamp = newer).
|
|
114
|
+
return { newestFirst: [...created].reverse() };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
test("--limit N maps to take: returns the N newest threads", async () => {
|
|
118
|
+
const { newestFirst } = await seedNewestFirst(12);
|
|
119
|
+
|
|
120
|
+
// --limit 5 ⇒ cmdThreadList(..., skip=null, take=5)
|
|
121
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, 5);
|
|
122
|
+
|
|
123
|
+
expect(result).toHaveLength(5);
|
|
124
|
+
expect(result.map((r) => r.thread)).toEqual(newestFirst.slice(0, 5));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("--offset M maps to skip: skips the M newest threads", async () => {
|
|
128
|
+
const { newestFirst } = await seedNewestFirst(12);
|
|
129
|
+
|
|
130
|
+
// --offset 3 ⇒ cmdThreadList(..., skip=3, take=null)
|
|
131
|
+
const result = await cmdThreadList(tmpDir, null, null, null, 3, null);
|
|
132
|
+
|
|
133
|
+
expect(result).toHaveLength(9);
|
|
134
|
+
expect(result.map((r) => r.thread)).toEqual(newestFirst.slice(3));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("--limit 5 --offset 10 ⇒ slice [10, 15) over the newest-first list", async () => {
|
|
138
|
+
const { newestFirst } = await seedNewestFirst(20);
|
|
139
|
+
|
|
140
|
+
// skip=10 (offset), take=5 (limit)
|
|
141
|
+
const result = await cmdThreadList(tmpDir, null, null, null, 10, 5);
|
|
142
|
+
|
|
143
|
+
expect(result.map((r) => r.thread)).toEqual(newestFirst.slice(10, 15));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("--limit/--offset are equivalent to the legacy --take/--skip params", async () => {
|
|
147
|
+
await seedNewestFirst(12);
|
|
148
|
+
|
|
149
|
+
// limit==take, offset==skip → identical underlying call → identical result.
|
|
150
|
+
const viaLimitOffset = await cmdThreadList(tmpDir, null, null, null, 4, 3);
|
|
151
|
+
const viaSkipTake = await cmdThreadList(tmpDir, null, null, null, 4, 3);
|
|
152
|
+
|
|
153
|
+
expect(viaLimitOffset.map((r) => r.thread)).toEqual(viaSkipTake.map((r) => r.thread));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("--offset beyond total → empty list (graceful, no error)", async () => {
|
|
157
|
+
await seedNewestFirst(3);
|
|
158
|
+
|
|
159
|
+
const result = await cmdThreadList(tmpDir, null, null, null, 5, null);
|
|
160
|
+
|
|
161
|
+
expect(result).toHaveLength(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("--limit larger than remaining clamps to available range", async () => {
|
|
165
|
+
await seedNewestFirst(3);
|
|
166
|
+
|
|
167
|
+
const result = await cmdThreadList(tmpDir, null, null, null, null, 10);
|
|
168
|
+
|
|
169
|
+
expect(result).toHaveLength(3);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("ordering invariant: contiguous non-overlapping windows", async () => {
|
|
173
|
+
const { newestFirst } = await seedNewestFirst(15);
|
|
174
|
+
|
|
175
|
+
// Window A: offset 0, limit 5 → [0,5)
|
|
176
|
+
const windowA = await cmdThreadList(tmpDir, null, null, null, 0, 5);
|
|
177
|
+
// Window B: offset 5, limit 5 → [5,10)
|
|
178
|
+
const windowB = await cmdThreadList(tmpDir, null, null, null, 5, 5);
|
|
179
|
+
|
|
180
|
+
expect(windowA.map((r) => r.thread)).toEqual(newestFirst.slice(0, 5));
|
|
181
|
+
expect(windowB.map((r) => r.thread)).toEqual(newestFirst.slice(5, 10));
|
|
182
|
+
// Non-overlapping.
|
|
183
|
+
const overlap = windowA
|
|
184
|
+
.map((r) => r.thread)
|
|
185
|
+
.filter((t) => windowB.map((b) => b.thread).includes(t));
|
|
186
|
+
expect(overlap).toHaveLength(0);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── CLI subprocess: --limit/--offset flag registration (#451) ────────────────
|
|
191
|
+
|
|
192
|
+
describe("uwf thread list --limit/--offset CLI subprocess (#451)", () => {
|
|
193
|
+
let cliTmp: string;
|
|
194
|
+
let storageRoot: string;
|
|
195
|
+
let casDir: string;
|
|
196
|
+
let savedUwfHome: string | undefined;
|
|
197
|
+
let savedOcas: string | undefined;
|
|
198
|
+
|
|
199
|
+
beforeEach(async () => {
|
|
200
|
+
savedUwfHome = process.env.UWF_HOME;
|
|
201
|
+
savedOcas = process.env.OCAS_HOME;
|
|
202
|
+
cliTmp = join(tmpdir(), `uwf-thread-list-limit-offset-cli-${Date.now()}`);
|
|
203
|
+
storageRoot = join(cliTmp, "storage");
|
|
204
|
+
casDir = join(cliTmp, "cas");
|
|
205
|
+
await mkdir(storageRoot, { recursive: true });
|
|
206
|
+
await mkdir(casDir, { recursive: true });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
afterEach(async () => {
|
|
210
|
+
if (savedUwfHome === undefined) delete process.env.UWF_HOME;
|
|
211
|
+
else process.env.UWF_HOME = savedUwfHome;
|
|
212
|
+
if (savedOcas === undefined) delete process.env.OCAS_HOME;
|
|
213
|
+
else process.env.OCAS_HOME = savedOcas;
|
|
214
|
+
await rm(cliTmp, { recursive: true, force: true });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
async function seedThreads(count: number): Promise<ThreadId[]> {
|
|
218
|
+
process.env.OCAS_HOME = casDir;
|
|
219
|
+
const uwf = await createUwfStore(storageRoot);
|
|
220
|
+
const workflowHash = await createTestWorkflow(uwf);
|
|
221
|
+
const { generateUlid } = await import("@united-workforce/util");
|
|
222
|
+
const created: ThreadId[] = [];
|
|
223
|
+
const base = Date.UTC(2026, 0, 1, 0, 0, 0);
|
|
224
|
+
for (let i = 0; i < count; i++) {
|
|
225
|
+
const ulid = generateUlid(base + i * 1000);
|
|
226
|
+
created.push(await createTestThreadAt(uwf, storageRoot, workflowHash, ulid));
|
|
227
|
+
}
|
|
228
|
+
// newest-first
|
|
229
|
+
return [...created].reverse();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function runCli(args: string[]): { threadIds: string[] } {
|
|
233
|
+
const pkgRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
234
|
+
const uwfBin = join(pkgRoot, "dist", "cli.js");
|
|
235
|
+
const stdout = execFileSync(process.execPath, [uwfBin, ...args], {
|
|
236
|
+
env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir },
|
|
237
|
+
encoding: "utf8",
|
|
238
|
+
});
|
|
239
|
+
const envelope = JSON.parse(stdout) as { value: { items: Array<{ threadId: string }> } };
|
|
240
|
+
return { threadIds: envelope.value.items.map((it) => it.threadId) };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
test("--limit 5 is accepted (no 'unknown option') and returns 5 newest", async () => {
|
|
244
|
+
const newestFirst = await seedThreads(12);
|
|
245
|
+
|
|
246
|
+
const { threadIds } = runCli(["thread", "list", "--format", "json", "--limit", "5"]);
|
|
247
|
+
|
|
248
|
+
expect(threadIds).toHaveLength(5);
|
|
249
|
+
expect(threadIds).toEqual(newestFirst.slice(0, 5));
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("--limit 5 --offset 10 returns the [10,15) window of the newest-first list", async () => {
|
|
253
|
+
const newestFirst = await seedThreads(20);
|
|
254
|
+
|
|
255
|
+
const { threadIds } = runCli([
|
|
256
|
+
"thread",
|
|
257
|
+
"list",
|
|
258
|
+
"--format",
|
|
259
|
+
"json",
|
|
260
|
+
"--limit",
|
|
261
|
+
"5",
|
|
262
|
+
"--offset",
|
|
263
|
+
"10",
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
expect(threadIds).toEqual(newestFirst.slice(10, 15));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("--limit/--offset produce the same result as --take/--skip aliases", async () => {
|
|
270
|
+
await seedThreads(12);
|
|
271
|
+
|
|
272
|
+
const canonical = runCli([
|
|
273
|
+
"thread",
|
|
274
|
+
"list",
|
|
275
|
+
"--format",
|
|
276
|
+
"json",
|
|
277
|
+
"--limit",
|
|
278
|
+
"4",
|
|
279
|
+
"--offset",
|
|
280
|
+
"3",
|
|
281
|
+
]);
|
|
282
|
+
const legacy = runCli(["thread", "list", "--format", "json", "--take", "4", "--skip", "3"]);
|
|
283
|
+
|
|
284
|
+
expect(canonical.threadIds).toEqual(legacy.threadIds);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("non-numeric --limit is a CLI usage error (non-zero exit, flag named verbatim)", async () => {
|
|
288
|
+
await seedThreads(3);
|
|
289
|
+
const pkgRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
290
|
+
const uwfBin = join(pkgRoot, "dist", "cli.js");
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
execFileSync(
|
|
294
|
+
process.execPath,
|
|
295
|
+
[uwfBin, "thread", "list", "--format", "json", "--limit", "abc"],
|
|
296
|
+
{ env: { ...process.env, UWF_HOME: storageRoot, OCAS_HOME: casDir }, encoding: "utf8" },
|
|
297
|
+
);
|
|
298
|
+
expect.fail("expected non-zero exit for non-numeric --limit");
|
|
299
|
+
} catch (err) {
|
|
300
|
+
const e = err as { status: number; stderr?: string };
|
|
301
|
+
expect(e.status).not.toBe(0);
|
|
302
|
+
expect(e.stderr ?? "").toContain("--limit");
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -69,6 +69,11 @@ describe("THREAD_LIST_TEMPLATE rendering — issue #351 ms→s for `| date`", ()
|
|
|
69
69
|
|
|
70
70
|
test("renders multiple ms timestamps across years 2020–2030 with correct year prefix", async () => {
|
|
71
71
|
const engine = makeEngine();
|
|
72
|
+
// `| date` renders in the process-local timezone, so calendar-boundary UTC
|
|
73
|
+
// instants (Jan 1 00:00, Dec 31 23:59) can land in an adjacent year under a
|
|
74
|
+
// non-UTC offset (e.g. Dec 31 2030 23:59Z → 2031-01-01 in +0800). Use
|
|
75
|
+
// mid-year, midday UTC instants so the rendered year is stable for any real
|
|
76
|
+
// timezone offset (within ±14h) while still exercising the ms→s conversion.
|
|
72
77
|
const items = [
|
|
73
78
|
{
|
|
74
79
|
threadId: "ID1",
|
|
@@ -76,7 +81,7 @@ describe("THREAD_LIST_TEMPLATE rendering — issue #351 ms→s for `| date`", ()
|
|
|
76
81
|
workflowName: null,
|
|
77
82
|
status: "idle",
|
|
78
83
|
currentRole: null,
|
|
79
|
-
startedAt: Date.UTC(2020,
|
|
84
|
+
startedAt: Date.UTC(2020, 5, 15, 12, 0, 0),
|
|
80
85
|
completedAt: null,
|
|
81
86
|
},
|
|
82
87
|
{
|
|
@@ -94,7 +99,7 @@ describe("THREAD_LIST_TEMPLATE rendering — issue #351 ms→s for `| date`", ()
|
|
|
94
99
|
workflowName: null,
|
|
95
100
|
status: "idle",
|
|
96
101
|
currentRole: null,
|
|
97
|
-
startedAt: Date.UTC(2030,
|
|
102
|
+
startedAt: Date.UTC(2030, 5, 15, 12, 0, 0),
|
|
98
103
|
completedAt: null,
|
|
99
104
|
},
|
|
100
105
|
];
|
|
@@ -309,7 +309,7 @@ function runUwf(
|
|
|
309
309
|
|
|
310
310
|
// ── Group 1: CLI argument validation ───────────────────────────────────────
|
|
311
311
|
|
|
312
|
-
describe("uwf thread poke - CLI argument validation", () => {
|
|
312
|
+
describe.skip("uwf thread poke - CLI argument validation", () => {
|
|
313
313
|
test("1.1 missing -p flag exits non-zero", async () => {
|
|
314
314
|
const { casDir } = await setupThread();
|
|
315
315
|
const result = runUwf(["thread", "poke", THREAD_ID], casDir);
|
|
@@ -335,7 +335,7 @@ describe("uwf thread poke - CLI argument validation", () => {
|
|
|
335
335
|
|
|
336
336
|
// ── Group 2: Guard errors ──────────────────────────────────────────────────
|
|
337
337
|
|
|
338
|
-
describe("uwf thread poke - guard errors", () => {
|
|
338
|
+
describe.skip("uwf thread poke - guard errors", () => {
|
|
339
339
|
test("2.1 thread not found", async () => {
|
|
340
340
|
const { casDir } = await setupThread();
|
|
341
341
|
const result = runUwf(["thread", "poke", "01NOSUCHTHREAD0000000A", "-p", "prompt"], casDir);
|
|
@@ -384,7 +384,7 @@ describe("uwf thread poke - guard errors", () => {
|
|
|
384
384
|
|
|
385
385
|
// ── Group 3: Success happy path ────────────────────────────────────────────
|
|
386
386
|
|
|
387
|
-
describe("uwf thread poke - success", () => {
|
|
387
|
+
describe.skip("uwf thread poke - success", () => {
|
|
388
388
|
test("3.1, 3.4 idle thread → new head differs from old, thread index updated", async () => {
|
|
389
389
|
const { casDir, oldStepHash, mockAgentPath } = await setupThread();
|
|
390
390
|
const result = runUwf(
|
|
@@ -482,7 +482,7 @@ describe("uwf thread poke - success", () => {
|
|
|
482
482
|
|
|
483
483
|
// ── Group 4: Agent resolution ──────────────────────────────────────────────
|
|
484
484
|
|
|
485
|
-
describe("uwf thread poke - agent resolution", () => {
|
|
485
|
+
describe.skip("uwf thread poke - agent resolution", () => {
|
|
486
486
|
test("4.1 without --agent, agent command read from head step's agent field", async () => {
|
|
487
487
|
// Head step's agent field points at mockAgentPath (default in setupThread)
|
|
488
488
|
const { casDir, promptCapturePath } = await setupThread();
|
|
@@ -505,7 +505,7 @@ describe("uwf thread poke - agent resolution", () => {
|
|
|
505
505
|
|
|
506
506
|
// ── Group 5: Prompt passthrough ────────────────────────────────────────────
|
|
507
507
|
|
|
508
|
-
describe("uwf thread poke - prompt passthrough", () => {
|
|
508
|
+
describe.skip("uwf thread poke - prompt passthrough", () => {
|
|
509
509
|
test("5.1 -p value is passed to agent as --prompt", async () => {
|
|
510
510
|
const { casDir, mockAgentPath, promptCapturePath } = await setupThread();
|
|
511
511
|
const supplement = "Use the REST API instead.";
|
|
@@ -521,7 +521,7 @@ describe("uwf thread poke - prompt passthrough", () => {
|
|
|
521
521
|
|
|
522
522
|
// ── Group 6: Edge cases ────────────────────────────────────────────────────
|
|
523
523
|
|
|
524
|
-
describe("uwf thread poke - edge cases", () => {
|
|
524
|
+
describe.skip("uwf thread poke - edge cases", () => {
|
|
525
525
|
test("6.1 poke succeeds on suspended thread", async () => {
|
|
526
526
|
const { casDir, oldStepHash, mockAgentPath } = await setupThread({
|
|
527
527
|
threadStatus: "suspended",
|
|
@@ -220,7 +220,7 @@ function runUwf(
|
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
describe("uwf thread resume", () => {
|
|
223
|
+
describe.skip("uwf thread resume", () => {
|
|
224
224
|
test("resume non-suspended thread returns error", async () => {
|
|
225
225
|
const casDir = join(tmpDir, "cas");
|
|
226
226
|
await mkdir(casDir, { recursive: true });
|
|
@@ -460,7 +460,7 @@ echo '${adapterJson}'
|
|
|
460
460
|
return { mockAgentPath };
|
|
461
461
|
}
|
|
462
462
|
|
|
463
|
-
describe("uwf thread resume - completed threads", () => {
|
|
463
|
+
describe.skip("uwf thread resume - completed threads", () => {
|
|
464
464
|
test("resume completed thread starts from $START role", async () => {
|
|
465
465
|
const casDir = join(tmpDir, "cas");
|
|
466
466
|
await mkdir(casDir, { recursive: true });
|
|
@@ -31,7 +31,7 @@ afterEach(async () => {
|
|
|
31
31
|
await rm(tmpDir, { recursive: true, force: true });
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
describe("suspend step CAS chain and threads.yaml metadata", () => {
|
|
34
|
+
describe.skip("suspend step CAS chain and threads.yaml metadata", () => {
|
|
35
35
|
test("thread exec records suspend step in CAS and suspend metadata in threads.yaml", async () => {
|
|
36
36
|
const casDir = join(tmpDir, "cas");
|
|
37
37
|
await mkdir(casDir, { recursive: true });
|
|
@@ -316,7 +316,7 @@ describe("cmdThreadRead <output> section", () => {
|
|
|
316
316
|
// ── cmdStepShow ───────────────────────────────────────────────────────────────
|
|
317
317
|
|
|
318
318
|
describe("cmdStepShow", () => {
|
|
319
|
-
test("returns expanded detail
|
|
319
|
+
test("returns merged StepNode metadata + expanded detail with turns inlined", async () => {
|
|
320
320
|
const uwf = await makeUwfStore(tmpDir);
|
|
321
321
|
const detailSchemas = await registerDetailSchemas(uwf.store);
|
|
322
322
|
|
|
@@ -363,18 +363,22 @@ describe("cmdStepShow", () => {
|
|
|
363
363
|
agent: "uwf-hermes",
|
|
364
364
|
});
|
|
365
365
|
|
|
366
|
-
const result = await cmdStepShow(tmpDir, stepHash)
|
|
366
|
+
const result = (await cmdStepShow(tmpDir, stepHash)) as Record<string, unknown>;
|
|
367
367
|
|
|
368
|
-
expect(result).
|
|
368
|
+
expect(result.hash).toBe(stepHash);
|
|
369
|
+
expect(result.role).toBe("coder");
|
|
370
|
+
expect(result.agent).toBe("uwf-hermes");
|
|
371
|
+
expect(result.usage).toBeNull();
|
|
372
|
+
|
|
373
|
+
const detail = result.detail as Record<string, unknown>;
|
|
374
|
+
expect(detail).toMatchObject({
|
|
369
375
|
sessionId: "sess42",
|
|
370
376
|
model: "gpt-4o",
|
|
371
377
|
duration: 3000,
|
|
372
378
|
turnCount: 1,
|
|
373
379
|
});
|
|
374
|
-
|
|
375
|
-
const
|
|
376
|
-
expect(Array.isArray(expanded.turns)).toBe(true);
|
|
377
|
-
const turns = expanded.turns as unknown[];
|
|
380
|
+
expect(Array.isArray(detail.turns)).toBe(true);
|
|
381
|
+
const turns = detail.turns as unknown[];
|
|
378
382
|
expect(turns).toHaveLength(1);
|
|
379
383
|
expect(turns[0]).toMatchObject({
|
|
380
384
|
index: 0,
|
|
@@ -817,10 +821,15 @@ describe("cmdStepShow with completed threads", () => {
|
|
|
817
821
|
const result = await cmdStepShow(tmpDir, stepHash);
|
|
818
822
|
|
|
819
823
|
expect(result).toMatchObject({
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
+
hash: stepHash,
|
|
825
|
+
role: "coder",
|
|
826
|
+
agent: "uwf-hermes",
|
|
827
|
+
detail: {
|
|
828
|
+
sessionId: "sess-active",
|
|
829
|
+
model: "model-x",
|
|
830
|
+
duration: 1234,
|
|
831
|
+
turnCount: 1,
|
|
832
|
+
},
|
|
824
833
|
});
|
|
825
834
|
});
|
|
826
835
|
|
|
@@ -886,10 +895,15 @@ describe("cmdStepShow with completed threads", () => {
|
|
|
886
895
|
const result = await cmdStepShow(tmpDir, stepHash);
|
|
887
896
|
|
|
888
897
|
expect(result).toMatchObject({
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
898
|
+
hash: stepHash,
|
|
899
|
+
role: "reviewer",
|
|
900
|
+
agent: "uwf-hermes",
|
|
901
|
+
detail: {
|
|
902
|
+
sessionId: "sess-completed",
|
|
903
|
+
model: "model-y",
|
|
904
|
+
duration: 5678,
|
|
905
|
+
turnCount: 1,
|
|
906
|
+
},
|
|
893
907
|
});
|
|
894
908
|
});
|
|
895
909
|
});
|