agent-yes 1.258.3 → 1.259.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/dist/{SUPPORTED_CLIS-DD6bhkyI.js → SUPPORTED_CLIS-BBeIFAZD.js} +2 -2
- package/dist/SUPPORTED_CLIS-BRgPb6Pz.js +11 -0
- package/dist/{agentShare-CoI7IHeq.js → agentShare-90Y_SFqK.js} +4 -4
- package/dist/{callback-CbdkyTRw.js → callback-CLzRxjEn.js} +3 -3
- package/dist/{callback-DNI__ASS.js → callback-DYQ2jQi2.js} +4 -4
- package/dist/cli.js +11 -11
- package/dist/{cwdConflictWarn-DZTxTGXq.js → cwdConflictWarn-B_047KCF.js} +2 -2
- package/dist/{forkNested-CgJ1rTnT.js → forkNested-CBdhC852.js} +2 -2
- package/dist/globalPidIndex-CHA__9Xf.js +4 -0
- package/dist/{globalPidIndex-Dq4KY2IV.js → globalPidIndex-pwp7Vb7o.js} +8 -2
- package/dist/index.js +4 -4
- package/dist/{notifyDaemon-BQ6_UbJS.js → notifyDaemon-D5RtFwMq.js} +4 -4
- package/dist/pidStore-1PQ4ARnh.js +6 -0
- package/dist/{pidStore-Cmcc8Oej.js → pidStore-DfSm1jog.js} +7 -4
- package/dist/{remotes-Dy4Rg8Yp.js → remotes-Cn-wszbK.js} +2 -2
- package/dist/{remotes-CNe85Icx.js → remotes-lOhHSoSS.js} +1 -1
- package/dist/{rustBinary-orhBmTNo.js → rustBinary-BEscpLLP.js} +2 -2
- package/dist/{schedule-BQy6gXYk.js → schedule-Cw5nW_di.js} +6 -6
- package/dist/{serve-C2mQMjUR.js → serve-DctJ7tY-.js} +19 -19
- package/dist/{setup-EEF1vfhi.js → setup-BA9D1ExO.js} +10 -2
- package/dist/{spawnGate-D0E1uRP7.js → spawnGate-D-68B907.js} +2 -2
- package/dist/spawnGate-DbSHnfqA.js +5 -0
- package/dist/{subcommands-CiX7J7hA.js → subcommands-D_cPHm34.js} +93 -24
- package/dist/{subcommands-S7uULM_d.js → subcommands-Dg7KkEi6.js} +3 -3
- package/dist/systemPathLink-DTAhFXFx.js +91 -0
- package/dist/{terminal-i7IimD_R.js → terminal-CB5K4Jjh.js} +2 -2
- package/dist/{todoCli-B2kqgqPh.js → todoCli-DNOk38lq.js} +2 -2
- package/dist/{tray-CgkwxtG6.js → tray-wWMvGWeY.js} +1 -1
- package/dist/trayApp-B1OhYfj3.js +5 -0
- package/dist/{trayApp-4sZgxxP-.js → trayApp-Ck3kWM-7.js} +2 -2
- package/dist/{ts-D-E4xqpA.js → ts-Bd_7sgVu.js} +5 -4
- package/dist/{versionChecker-YDl8a9il.js → versionChecker-DRijpksb.js} +2 -2
- package/dist/{webrtcRemote-C3OlHYSt.js → webrtcRemote-BkZcA5yq.js} +1 -1
- package/dist/{widget-7j9RrYZR.js → widget-DiRKbtng.js} +3 -3
- package/dist/{ws-DjyQ20AD.js → ws-Bp8eOEoV.js} +2 -2
- package/dist/{ws-Clem1WQ-.js → ws-Cqn8mxsS.js} +4 -4
- package/package.json +1 -1
- package/ts/globalPidIndex.spec.ts +36 -0
- package/ts/globalPidIndex.ts +19 -1
- package/ts/hist.spec.ts +219 -0
- package/ts/hist.ts +136 -0
- package/ts/histStore.spec.ts +351 -0
- package/ts/histStore.ts +494 -0
- package/ts/index.ts +4 -0
- package/ts/messageLog.ts +2 -0
- package/ts/pidStore.spec.ts +26 -0
- package/ts/pidStore.ts +10 -0
- package/ts/setup.ts +17 -0
- package/ts/subcommands.spec.ts +180 -0
- package/ts/subcommands.ts +103 -3
- package/ts/systemPathLink.spec.ts +69 -0
- package/ts/systemPathLink.ts +134 -0
- package/dist/SUPPORTED_CLIS-D1BoydrZ.js +0 -11
- package/dist/globalPidIndex-CQX_0JSB.js +0 -4
- package/dist/pidStore-BbKqQOS8.js +0 -6
- package/dist/spawnGate-DqKBA07k.js +0 -5
- package/dist/trayApp-C35IkXnz.js +0 -5
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "fs/promises";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import {
|
|
6
|
+
discoverTranscripts,
|
|
7
|
+
encodeProjectSlug,
|
|
8
|
+
histPage,
|
|
9
|
+
projectRecord,
|
|
10
|
+
readCodexCwd,
|
|
11
|
+
readLinesBackward,
|
|
12
|
+
tailTranscript,
|
|
13
|
+
type TranscriptFile,
|
|
14
|
+
} from "./histStore.ts";
|
|
15
|
+
|
|
16
|
+
// histStore takes `home` explicitly rather than calling homedir(), so these
|
|
17
|
+
// tests point it at a tempdir instead of mocking the os module.
|
|
18
|
+
let home: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
home = await mkdtemp(path.join(tmpdir(), "agent-yes-hist-"));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await rm(home, { recursive: true, force: true }).catch(() => null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const CWD = "/code/snomiao/agent-yes/tree/main";
|
|
29
|
+
|
|
30
|
+
function claudeTurn(role: "user" | "assistant", text: string, ts: string, extra = {}) {
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
type: role,
|
|
33
|
+
timestamp: ts,
|
|
34
|
+
cwd: CWD,
|
|
35
|
+
message: { role, content: [{ type: "text", text }] },
|
|
36
|
+
...extra,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function codexEvent(kind: "user_message" | "agent_message", message: string, ts: string) {
|
|
41
|
+
return JSON.stringify({ timestamp: ts, type: "event_msg", payload: { type: kind, message } });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function writeClaudeSession(session: string, lines: string[], cwd = CWD) {
|
|
45
|
+
const dir = path.join(home, ".claude", "projects", encodeProjectSlug(cwd));
|
|
46
|
+
await mkdir(dir, { recursive: true });
|
|
47
|
+
const file = path.join(dir, `${session}.jsonl`);
|
|
48
|
+
await writeFile(file, lines.length ? lines.join("\n") + "\n" : "");
|
|
49
|
+
return file;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function writeCodexSession(session: string, lines: string[], cwd = CWD) {
|
|
53
|
+
const dir = path.join(home, ".codex", "sessions", "2026", "08", "03");
|
|
54
|
+
await mkdir(dir, { recursive: true });
|
|
55
|
+
const file = path.join(dir, `rollout-${session}.jsonl`);
|
|
56
|
+
const meta = JSON.stringify({
|
|
57
|
+
timestamp: "2026-08-03T00:00:00.000Z",
|
|
58
|
+
type: "session_meta",
|
|
59
|
+
payload: { session_id: session, cwd },
|
|
60
|
+
});
|
|
61
|
+
await writeFile(file, [meta, ...lines].join("\n") + "\n");
|
|
62
|
+
return file;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function asFile(p: string, source: "claude" | "codex"): TranscriptFile {
|
|
66
|
+
return { path: p, source, sessionId: path.basename(p, ".jsonl"), mtimeMs: 0, size: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("encodeProjectSlug", () => {
|
|
70
|
+
it("matches Claude's cwd-to-directory encoding", () => {
|
|
71
|
+
expect(encodeProjectSlug("/code/snomiao/agent-yes/tree/main")).toBe(
|
|
72
|
+
"-code-snomiao-agent-yes-tree-main",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("collapses dots, as Claude does for domain-shaped directories", () => {
|
|
77
|
+
expect(encodeProjectSlug("/code/snomiao/cv.snomiao.com/tree/main")).toBe(
|
|
78
|
+
"-code-snomiao-cv-snomiao-com-tree-main",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("readLinesBackward", () => {
|
|
84
|
+
it("yields lines newest-first with correct byte offsets", async () => {
|
|
85
|
+
const file = path.join(home, "a.jsonl");
|
|
86
|
+
await writeFile(file, "one\ntwo\nthree\n");
|
|
87
|
+
const got = [];
|
|
88
|
+
for await (const l of readLinesBackward(file)) got.push(l);
|
|
89
|
+
expect(got.map((g) => g.text)).toEqual(["three", "two", "one"]);
|
|
90
|
+
expect(got.map((g) => g.offset)).toEqual([8, 4, 0]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("reassembles lines that straddle chunk boundaries", async () => {
|
|
94
|
+
const file = path.join(home, "big.jsonl");
|
|
95
|
+
const lines = Array.from({ length: 200 }, (_, i) => `line-${i}-${"x".repeat(i % 37)}`);
|
|
96
|
+
await writeFile(file, lines.join("\n") + "\n");
|
|
97
|
+
const got = [];
|
|
98
|
+
// A chunk far smaller than the file forces the carry-over path repeatedly.
|
|
99
|
+
for await (const l of readLinesBackward(file, { chunkSize: 16 })) got.push(l.text);
|
|
100
|
+
expect(got).toEqual([...lines].reverse());
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("handles a file with no trailing newline", async () => {
|
|
104
|
+
const file = path.join(home, "b.jsonl");
|
|
105
|
+
await writeFile(file, "one\ntwo");
|
|
106
|
+
const got = [];
|
|
107
|
+
for await (const l of readLinesBackward(file)) got.push(l.text);
|
|
108
|
+
// "two" is unterminated, so it is treated as a partial write and skipped.
|
|
109
|
+
expect(got).toEqual(["one"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("drops a half-written trailing record from a live session", async () => {
|
|
113
|
+
const file = path.join(home, "live.jsonl");
|
|
114
|
+
await writeFile(file, `${claudeTurn("user", "done", "2026-08-01T00:00:00Z")}\n{"type":"assi`);
|
|
115
|
+
const got = [];
|
|
116
|
+
for await (const l of readLinesBackward(file)) got.push(l.text);
|
|
117
|
+
expect(got).toHaveLength(1);
|
|
118
|
+
expect(got[0]).toContain("done");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("reads only what the consumer asks for", async () => {
|
|
122
|
+
const file = path.join(home, "huge.jsonl");
|
|
123
|
+
// 5MB of records; taking one must not depend on reading them all.
|
|
124
|
+
const lines = Array.from({ length: 500 }, (_, i) => `${i}:${"y".repeat(10_000)}`);
|
|
125
|
+
await writeFile(file, lines.join("\n") + "\n");
|
|
126
|
+
const it = readLinesBackward(file, { chunkSize: 4096 })[Symbol.asyncIterator]();
|
|
127
|
+
const first = await it.next();
|
|
128
|
+
expect(first.value?.text.startsWith("499:")).toBe(true);
|
|
129
|
+
await it.return?.(undefined);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("returns nothing for an empty file", async () => {
|
|
133
|
+
const file = path.join(home, "empty.jsonl");
|
|
134
|
+
await writeFile(file, "");
|
|
135
|
+
const got = [];
|
|
136
|
+
for await (const l of readLinesBackward(file)) got.push(l);
|
|
137
|
+
expect(got).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("projectRecord", () => {
|
|
142
|
+
it("extracts Claude text turns", () => {
|
|
143
|
+
const r = projectRecord(claudeTurn("assistant", "hello", "2026-08-01T00:00:00Z"), "claude");
|
|
144
|
+
expect(r).toEqual({ ts: "2026-08-01T00:00:00Z", role: "assistant", text: "hello" });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("skips Claude sidechains unless asked", () => {
|
|
148
|
+
const line = claudeTurn("assistant", "sub", "2026-08-01T00:00:00Z", { isSidechain: true });
|
|
149
|
+
expect(projectRecord(line, "claude")).toBeNull();
|
|
150
|
+
expect(projectRecord(line, "claude", { includeSidechains: true })?.text).toBe("sub");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("drops tool plumbing by default and marks it when requested", () => {
|
|
154
|
+
const line = JSON.stringify({
|
|
155
|
+
type: "assistant",
|
|
156
|
+
timestamp: "2026-08-01T00:00:00Z",
|
|
157
|
+
message: { role: "assistant", content: [{ type: "tool_use", name: "Bash", input: {} }] },
|
|
158
|
+
});
|
|
159
|
+
expect(projectRecord(line, "claude")).toBeNull();
|
|
160
|
+
expect(projectRecord(line, "claude", { includeTools: true })?.text).toBe("[tool: Bash]");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("accepts Claude's string-shaped content", () => {
|
|
164
|
+
const line = JSON.stringify({
|
|
165
|
+
type: "user",
|
|
166
|
+
timestamp: "2026-08-01T00:00:00Z",
|
|
167
|
+
message: { role: "user", content: "plain" },
|
|
168
|
+
});
|
|
169
|
+
expect(projectRecord(line, "claude")?.text).toBe("plain");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("reads Codex event messages and ignores duplicate response_items", () => {
|
|
173
|
+
expect(projectRecord(codexEvent("agent_message", "hi", "2026-08-03T01:00:00Z"), "codex")).toEqual(
|
|
174
|
+
{ ts: "2026-08-03T01:00:00Z", role: "assistant", text: "hi" },
|
|
175
|
+
);
|
|
176
|
+
const dup = JSON.stringify({
|
|
177
|
+
timestamp: "2026-08-03T01:00:00Z",
|
|
178
|
+
type: "response_item",
|
|
179
|
+
payload: { type: "message", role: "assistant", content: [{ text: "hi" }] },
|
|
180
|
+
});
|
|
181
|
+
expect(projectRecord(dup, "codex")).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("ignores metadata and malformed lines rather than throwing", () => {
|
|
185
|
+
expect(projectRecord("{not json", "claude")).toBeNull();
|
|
186
|
+
expect(projectRecord(JSON.stringify({ type: "ai-title" }), "claude")).toBeNull();
|
|
187
|
+
expect(projectRecord(JSON.stringify({ type: "session_meta" }), "codex")).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("discoverTranscripts", () => {
|
|
192
|
+
it("finds both sources and filters Claude by cwd slug", async () => {
|
|
193
|
+
await writeClaudeSession("s1", [claudeTurn("user", "a", "2026-08-01T00:00:00Z")]);
|
|
194
|
+
await writeClaudeSession("s2", [claudeTurn("user", "b", "2026-08-01T00:00:00Z")], "/other/repo");
|
|
195
|
+
await writeCodexSession("c1", [codexEvent("user_message", "c", "2026-08-03T00:00:01Z")]);
|
|
196
|
+
|
|
197
|
+
const all = await discoverTranscripts({ home });
|
|
198
|
+
expect(all.map((f) => f.sessionId).sort()).toEqual(["rollout-c1", "s1", "s2"]);
|
|
199
|
+
|
|
200
|
+
const scoped = await discoverTranscripts({ home, cwd: CWD });
|
|
201
|
+
expect(scoped.map((f) => f.sessionId).sort()).toEqual(["rollout-c1", "s1"]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("filters Codex by the cwd inside session_meta", async () => {
|
|
205
|
+
await writeCodexSession("c1", [codexEvent("user_message", "c", "2026-08-03T00:00:01Z")]);
|
|
206
|
+
await writeCodexSession(
|
|
207
|
+
"c2",
|
|
208
|
+
[codexEvent("user_message", "d", "2026-08-03T00:00:02Z")],
|
|
209
|
+
"/elsewhere",
|
|
210
|
+
);
|
|
211
|
+
const scoped = await discoverTranscripts({ home, cwd: CWD });
|
|
212
|
+
expect(scoped.map((f) => f.sessionId)).toEqual(["rollout-c1"]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("ignores codex history.jsonl and empty files", async () => {
|
|
216
|
+
const dir = path.join(home, ".codex", "sessions");
|
|
217
|
+
await mkdir(dir, { recursive: true });
|
|
218
|
+
await writeFile(path.join(dir, "history.jsonl"), '{"x":1}\n');
|
|
219
|
+
await writeClaudeSession("empty", []);
|
|
220
|
+
expect(await discoverTranscripts({ home })).toEqual([]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("returns empty when no agent transcripts exist at all", async () => {
|
|
224
|
+
expect(await discoverTranscripts({ home })).toEqual([]);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("readCodexCwd", () => {
|
|
229
|
+
it("reads cwd without consuming the whole file", async () => {
|
|
230
|
+
const file = await writeCodexSession("c1", [
|
|
231
|
+
codexEvent("user_message", "x".repeat(200_000), "2026-08-03T00:00:01Z"),
|
|
232
|
+
]);
|
|
233
|
+
expect(await readCodexCwd(file)).toBe(CWD);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("tailTranscript", () => {
|
|
238
|
+
it("returns the newest N records oldest-first", async () => {
|
|
239
|
+
const file = await writeClaudeSession(
|
|
240
|
+
"s1",
|
|
241
|
+
Array.from({ length: 20 }, (_, i) =>
|
|
242
|
+
claudeTurn("user", `msg-${i}`, `2026-08-01T00:00:${String(i).padStart(2, "0")}Z`),
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
const got = await tailTranscript(asFile(file, "claude"), { limit: 3 });
|
|
246
|
+
expect(got.map((r) => r.text)).toEqual(["msg-17", "msg-18", "msg-19"]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("honours the before cursor and role filter", async () => {
|
|
250
|
+
const file = await writeClaudeSession("s1", [
|
|
251
|
+
claudeTurn("user", "q1", "2026-08-01T00:00:01Z"),
|
|
252
|
+
claudeTurn("assistant", "a1", "2026-08-01T00:00:02Z"),
|
|
253
|
+
claudeTurn("user", "q2", "2026-08-01T00:00:03Z"),
|
|
254
|
+
]);
|
|
255
|
+
const f = asFile(file, "claude");
|
|
256
|
+
expect(
|
|
257
|
+
(await tailTranscript(f, { limit: 10, before: "2026-08-01T00:00:03Z" })).map((r) => r.text),
|
|
258
|
+
).toEqual(["q1", "a1"]);
|
|
259
|
+
expect((await tailTranscript(f, { limit: 10, role: "user" })).map((r) => r.text)).toEqual([
|
|
260
|
+
"q1",
|
|
261
|
+
"q2",
|
|
262
|
+
]);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("histPage", () => {
|
|
267
|
+
it("merges sessions chronologically and caps to the limit", async () => {
|
|
268
|
+
await writeClaudeSession("s1", [
|
|
269
|
+
claudeTurn("user", "claude-early", "2026-08-01T00:00:01Z"),
|
|
270
|
+
claudeTurn("assistant", "claude-late", "2026-08-01T00:00:05Z"),
|
|
271
|
+
]);
|
|
272
|
+
await writeCodexSession("c1", [
|
|
273
|
+
codexEvent("user_message", "codex-mid", "2026-08-01T00:00:03Z"),
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
const page = await histPage({ home, cwd: CWD, limit: 2 });
|
|
277
|
+
expect(page.records.map((r) => r.text)).toEqual(["codex-mid", "claude-late"]);
|
|
278
|
+
expect(page.scanned).toBe(2);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("returns a cursor that pages backwards without overlap", async () => {
|
|
282
|
+
await writeClaudeSession(
|
|
283
|
+
"s1",
|
|
284
|
+
Array.from({ length: 10 }, (_, i) =>
|
|
285
|
+
claudeTurn("user", `m${i}`, `2026-08-01T00:00:${String(i).padStart(2, "0")}Z`),
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
const first = await histPage({ home, cwd: CWD, limit: 3 });
|
|
289
|
+
expect(first.records.map((r) => r.text)).toEqual(["m7", "m8", "m9"]);
|
|
290
|
+
expect(first.cursor).toBeTruthy();
|
|
291
|
+
|
|
292
|
+
const second = await histPage({ home, cwd: CWD, limit: 3, before: first.cursor! });
|
|
293
|
+
expect(second.records.map((r) => r.text)).toEqual(["m4", "m5", "m6"]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("pages through records that share one timestamp without losing any", async () => {
|
|
297
|
+
// Transcripts routinely record several turns inside the same millisecond.
|
|
298
|
+
// A bare-timestamp cursor drops every record tying with the page boundary.
|
|
299
|
+
const ts = "2026-08-01T00:00:00.000Z";
|
|
300
|
+
await writeClaudeSession(
|
|
301
|
+
"s1",
|
|
302
|
+
["a", "b", "c", "d"].map((t) => claudeTurn("user", t, ts)),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const first = await histPage({ home, cwd: CWD, limit: 2 });
|
|
306
|
+
expect(first.records.map((r) => r.text)).toEqual(["c", "d"]);
|
|
307
|
+
const second = await histPage({ home, cwd: CWD, limit: 2, before: first.cursor! });
|
|
308
|
+
expect(second.records.map((r) => r.text)).toEqual(["a", "b"]);
|
|
309
|
+
expect(second.cursor).toBeNull();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("walks the entire history in pages without gaps or repeats", async () => {
|
|
313
|
+
await writeClaudeSession(
|
|
314
|
+
"s1",
|
|
315
|
+
Array.from({ length: 7 }, (_, i) => claudeTurn("user", `x${i}`, "2026-08-01T00:00:00.000Z")),
|
|
316
|
+
);
|
|
317
|
+
await writeCodexSession(
|
|
318
|
+
"c1",
|
|
319
|
+
Array.from({ length: 5 }, (_, i) =>
|
|
320
|
+
codexEvent("user_message", `y${i}`, "2026-08-01T00:00:00.000Z"),
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const seen: string[] = [];
|
|
325
|
+
let before: string | undefined;
|
|
326
|
+
for (let guard = 0; guard < 20; guard++) {
|
|
327
|
+
const page = await histPage({ home, cwd: CWD, limit: 3, before });
|
|
328
|
+
seen.unshift(...page.records.map((r) => r.text));
|
|
329
|
+
if (!page.cursor) break;
|
|
330
|
+
before = page.cursor;
|
|
331
|
+
}
|
|
332
|
+
expect(seen).toHaveLength(12);
|
|
333
|
+
expect(new Set(seen).size).toBe(12);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("still accepts a hand-typed ISO timestamp for --before", async () => {
|
|
337
|
+
await writeClaudeSession("s1", [
|
|
338
|
+
claudeTurn("user", "old", "2026-08-01T00:00:01Z"),
|
|
339
|
+
claudeTurn("user", "new", "2026-08-01T00:00:09Z"),
|
|
340
|
+
]);
|
|
341
|
+
const page = await histPage({ home, cwd: CWD, limit: 5, before: "2026-08-01T00:00:05Z" });
|
|
342
|
+
expect(page.records.map((r) => r.text)).toEqual(["old"]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("reports no cursor once the history is exhausted", async () => {
|
|
346
|
+
await writeClaudeSession("s1", [claudeTurn("user", "only", "2026-08-01T00:00:01Z")]);
|
|
347
|
+
const page = await histPage({ home, cwd: CWD, limit: 6 });
|
|
348
|
+
expect(page.records).toHaveLength(1);
|
|
349
|
+
expect(page.cursor).toBeNull();
|
|
350
|
+
});
|
|
351
|
+
});
|