@tracemarketplace/cli 0.0.13 → 0.0.17
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/api-client.d.ts +9 -2
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +80 -15
- package/dist/api-client.js.map +1 -1
- package/dist/api-client.test.d.ts +2 -0
- package/dist/api-client.test.d.ts.map +1 -0
- package/dist/api-client.test.js +34 -0
- package/dist/api-client.test.js.map +1 -0
- package/dist/cli.js +48 -18
- package/dist/cli.js.map +1 -1
- package/dist/commands/auto-submit.d.ts +2 -1
- package/dist/commands/auto-submit.d.ts.map +1 -1
- package/dist/commands/auto-submit.js +43 -56
- package/dist/commands/auto-submit.js.map +1 -1
- package/dist/commands/daemon.d.ts +8 -1
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +184 -63
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/history.d.ts +3 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +8 -4
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/login.d.ts +5 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +25 -9
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/register.d.ts +1 -0
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +4 -39
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/remove-daemon.d.ts +6 -0
- package/dist/commands/remove-daemon.d.ts.map +1 -0
- package/dist/commands/remove-daemon.js +66 -0
- package/dist/commands/remove-daemon.js.map +1 -0
- package/dist/commands/remove-hook.d.ts +6 -0
- package/dist/commands/remove-hook.d.ts.map +1 -0
- package/dist/commands/remove-hook.js +174 -0
- package/dist/commands/remove-hook.js.map +1 -0
- package/dist/commands/setup-hook.d.ts +2 -0
- package/dist/commands/setup-hook.d.ts.map +1 -1
- package/dist/commands/setup-hook.js +85 -41
- package/dist/commands/setup-hook.js.map +1 -1
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +8 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.d.ts.map +1 -1
- package/dist/commands/submit.js +138 -83
- package/dist/commands/submit.js.map +1 -1
- package/dist/commands/whoami.d.ts +3 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +8 -4
- package/dist/commands/whoami.js.map +1 -1
- package/dist/config.d.ts +38 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +175 -17
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -0
- package/dist/flush.d.ts +49 -0
- package/dist/flush.d.ts.map +1 -0
- package/dist/flush.js +405 -0
- package/dist/flush.js.map +1 -0
- package/dist/flush.test.d.ts +2 -0
- package/dist/flush.test.d.ts.map +1 -0
- package/dist/flush.test.js +330 -0
- package/dist/flush.test.js.map +1 -0
- package/dist/submitter.d.ts.map +1 -1
- package/dist/submitter.js +5 -2
- package/dist/submitter.js.map +1 -1
- package/package.json +8 -7
- package/src/api-client.test.ts +47 -0
- package/src/api-client.ts +100 -16
- package/src/cli.ts +55 -19
- package/src/commands/auto-submit.ts +80 -40
- package/src/commands/daemon.ts +243 -60
- package/src/commands/history.ts +9 -4
- package/src/commands/login.ts +37 -9
- package/src/commands/remove-daemon.ts +75 -0
- package/src/commands/remove-hook.ts +194 -0
- package/src/commands/setup-hook.ts +93 -43
- package/src/commands/status.ts +8 -4
- package/src/commands/submit.ts +191 -83
- package/src/commands/whoami.ts +8 -4
- package/src/config.ts +241 -21
- package/src/constants.ts +18 -0
- package/src/flush.test.ts +395 -0
- package/src/flush.ts +591 -0
- package/vitest.config.ts +8 -0
- package/src/commands/register.ts +0 -52
- package/src/submitter.ts +0 -110
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { NormalizedTrace, Turn } from "@tracemarketplace/shared";
|
|
3
|
+
import type { SessionSource } from "./flush.js";
|
|
4
|
+
import type { SessionUploadState } from "./config.js";
|
|
5
|
+
import {
|
|
6
|
+
collectIdleSessionSources,
|
|
7
|
+
createFreshSessionState,
|
|
8
|
+
migrateLegacySessionState,
|
|
9
|
+
planSessionUploads,
|
|
10
|
+
verifyUnconfirmedChunks,
|
|
11
|
+
} from "./flush.js";
|
|
12
|
+
import { migrateSessionUploadState } from "./config.js";
|
|
13
|
+
|
|
14
|
+
function makeTurn(
|
|
15
|
+
turnId: string,
|
|
16
|
+
role: "user" | "assistant",
|
|
17
|
+
timestamp: string,
|
|
18
|
+
outputTokens = 0
|
|
19
|
+
): Turn {
|
|
20
|
+
return {
|
|
21
|
+
turn_id: turnId,
|
|
22
|
+
parent_turn_id: null,
|
|
23
|
+
role,
|
|
24
|
+
actor: role === "user" ? "human" : "assistant",
|
|
25
|
+
timestamp,
|
|
26
|
+
model: "test-model",
|
|
27
|
+
usage: {
|
|
28
|
+
input_tokens: 0,
|
|
29
|
+
output_tokens: outputTokens,
|
|
30
|
+
cache_read_input_tokens: null,
|
|
31
|
+
cache_creation_input_tokens: null,
|
|
32
|
+
reasoning_tokens: null,
|
|
33
|
+
},
|
|
34
|
+
source_metadata: {},
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: `${turnId}:${role}`,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeTrace(
|
|
45
|
+
sessionId: string,
|
|
46
|
+
turns: Turn[],
|
|
47
|
+
endedAt = turns[turns.length - 1]?.timestamp ?? "2026-03-21T00:00:00.000Z"
|
|
48
|
+
): NormalizedTrace {
|
|
49
|
+
const outputTokens = turns.reduce((sum, turn) => sum + (turn.usage?.output_tokens ?? 0), 0);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
trace_id: `trace-${sessionId}`,
|
|
53
|
+
schema_version: "1.0",
|
|
54
|
+
source_tool: "codex_cli",
|
|
55
|
+
source_session_id: sessionId,
|
|
56
|
+
source_version: null,
|
|
57
|
+
submitted_by: "tester@example.com",
|
|
58
|
+
submitted_at: "2026-03-21T00:00:00.000Z",
|
|
59
|
+
extracted_at: endedAt,
|
|
60
|
+
git_branch: null,
|
|
61
|
+
cwd_hash: null,
|
|
62
|
+
working_language: null,
|
|
63
|
+
started_at: turns[0]?.timestamp ?? endedAt,
|
|
64
|
+
ended_at: endedAt,
|
|
65
|
+
turns,
|
|
66
|
+
turn_count: turns.length,
|
|
67
|
+
tool_call_count: 0,
|
|
68
|
+
has_tool_calls: false,
|
|
69
|
+
has_thinking_blocks: false,
|
|
70
|
+
has_file_changes: false,
|
|
71
|
+
has_shell_commands: false,
|
|
72
|
+
total_input_tokens: 0,
|
|
73
|
+
total_output_tokens: outputTokens,
|
|
74
|
+
total_cache_read_tokens: null,
|
|
75
|
+
content_fidelity: "full",
|
|
76
|
+
env_state: null,
|
|
77
|
+
score: null,
|
|
78
|
+
raw_r2_key: "",
|
|
79
|
+
normalized_r2_key: "",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function makeSource(tool: SessionSource["tool"], locator: string): SessionSource {
|
|
84
|
+
return { tool, locator, label: locator };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function makeSessionState(overrides: Partial<SessionUploadState> & Pick<SessionUploadState, "sourceTool" | "sourceSessionId" | "locator">): SessionUploadState {
|
|
88
|
+
return {
|
|
89
|
+
nextChunkIndex: 0,
|
|
90
|
+
openChunkStartTurn: 0,
|
|
91
|
+
lastSeenTurnCount: 0,
|
|
92
|
+
lastActivityAt: null,
|
|
93
|
+
lastFlushedTurnId: null,
|
|
94
|
+
confirmedChunkIndex: 0,
|
|
95
|
+
confirmedOpenChunkStartTurn: 0,
|
|
96
|
+
unconfirmedSince: null,
|
|
97
|
+
...overrides,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe("planSessionUploads", () => {
|
|
102
|
+
it("flushes a sealed 100k chunk and keeps the tail pending", () => {
|
|
103
|
+
const trace = makeTrace("session-100k", [
|
|
104
|
+
makeTurn("u1", "user", "2026-03-21T00:00:00.000Z"),
|
|
105
|
+
makeTurn("a1", "assistant", "2026-03-21T00:01:00.000Z", 100_200),
|
|
106
|
+
makeTurn("u2", "user", "2026-03-21T00:02:00.000Z"),
|
|
107
|
+
makeTurn("a2", "assistant", "2026-03-21T00:03:00.000Z", 25),
|
|
108
|
+
]);
|
|
109
|
+
const cursor = createFreshSessionState(makeSource("codex_cli", "/tmp/session.jsonl"), trace);
|
|
110
|
+
|
|
111
|
+
const plan = planSessionUploads(trace, cursor, new Date("2026-03-21T00:04:00.000Z"));
|
|
112
|
+
|
|
113
|
+
expect(plan.uploads).toHaveLength(1);
|
|
114
|
+
expect(plan.uploads[0]?.trace.chunk_index).toBe(0);
|
|
115
|
+
expect(plan.uploads[0]?.trace.chunk_start_turn).toBe(0);
|
|
116
|
+
expect(plan.uploads[0]?.trace.chunk_complete).toBe(true);
|
|
117
|
+
expect(plan.uploads[0]?.trace.chunk_close_reason).toBe("100k_tokens");
|
|
118
|
+
expect(plan.pending).toBe(true);
|
|
119
|
+
expect(plan.uploads[0]?.nextState.openChunkStartTurn).toBe(2);
|
|
120
|
+
expect(plan.uploads[0]?.nextState.nextChunkIndex).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("flushes an under-100k tail after two days of inactivity", () => {
|
|
124
|
+
const trace = makeTrace("session-idle", [
|
|
125
|
+
makeTurn("u1", "user", "2026-03-21T00:00:00.000Z"),
|
|
126
|
+
makeTurn("a1", "assistant", "2026-03-21T00:05:00.000Z", 40),
|
|
127
|
+
], "2026-03-21T00:05:00.000Z");
|
|
128
|
+
const cursor = createFreshSessionState(makeSource("codex_cli", "/tmp/session.jsonl"), trace);
|
|
129
|
+
|
|
130
|
+
const plan = planSessionUploads(trace, cursor, new Date("2026-03-23T00:06:00.000Z"));
|
|
131
|
+
|
|
132
|
+
expect(plan.uploads).toHaveLength(1);
|
|
133
|
+
expect(plan.uploads[0]?.trace.chunk_index).toBe(0);
|
|
134
|
+
expect(plan.uploads[0]?.trace.chunk_close_reason).toBe("idle_2d");
|
|
135
|
+
expect(plan.pending).toBe(false);
|
|
136
|
+
expect(plan.uploads[0]?.nextState.openChunkStartTurn).toBe(trace.turn_count);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("creates a later chunk when a session resumes after an idle-finalized chunk", () => {
|
|
140
|
+
const initialTrace = makeTrace("session-resume", [
|
|
141
|
+
makeTurn("u1", "user", "2026-03-21T00:00:00.000Z"),
|
|
142
|
+
makeTurn("a1", "assistant", "2026-03-21T00:05:00.000Z", 20),
|
|
143
|
+
], "2026-03-21T00:05:00.000Z");
|
|
144
|
+
const source = makeSource("codex_cli", "/tmp/session.jsonl");
|
|
145
|
+
const initialCursor = createFreshSessionState(source, initialTrace);
|
|
146
|
+
const initialPlan = planSessionUploads(initialTrace, initialCursor, new Date("2026-03-23T00:06:00.000Z"));
|
|
147
|
+
const finalizedCursor = initialPlan.uploads[0]?.nextState;
|
|
148
|
+
|
|
149
|
+
if (!finalizedCursor) {
|
|
150
|
+
throw new Error("expected initial chunk upload");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const resumedTrace = makeTrace("session-resume", [
|
|
154
|
+
...initialTrace.turns,
|
|
155
|
+
makeTurn("u2", "user", "2026-03-24T00:00:00.000Z"),
|
|
156
|
+
makeTurn("a2", "assistant", "2026-03-24T00:10:00.000Z", 15),
|
|
157
|
+
], "2026-03-24T00:10:00.000Z");
|
|
158
|
+
|
|
159
|
+
const resumedPlan = planSessionUploads(resumedTrace, finalizedCursor, new Date("2026-03-26T00:11:00.000Z"));
|
|
160
|
+
|
|
161
|
+
expect(resumedPlan.uploads).toHaveLength(1);
|
|
162
|
+
expect(resumedPlan.uploads[0]?.trace.chunk_index).toBe(1);
|
|
163
|
+
expect(resumedPlan.uploads[0]?.trace.chunk_start_turn).toBe(2);
|
|
164
|
+
expect(resumedPlan.uploads[0]?.trace.chunk_close_reason).toBe("idle_2d");
|
|
165
|
+
expect(resumedPlan.pending).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("does not re-upload a legacy session on first migration", () => {
|
|
169
|
+
const trace = makeTrace("legacy-session", [
|
|
170
|
+
makeTurn("u1", "user", "2026-03-21T00:00:00.000Z"),
|
|
171
|
+
makeTurn("a1", "assistant", "2026-03-21T00:05:00.000Z", 55),
|
|
172
|
+
]);
|
|
173
|
+
const migratedCursor = migrateLegacySessionState(
|
|
174
|
+
makeSource("codex_cli", "/tmp/legacy.jsonl"),
|
|
175
|
+
trace,
|
|
176
|
+
0
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const plan = planSessionUploads(trace, migratedCursor, new Date("2026-03-23T00:06:00.000Z"));
|
|
180
|
+
|
|
181
|
+
expect(plan.uploads).toHaveLength(0);
|
|
182
|
+
expect(plan.pending).toBe(false);
|
|
183
|
+
expect(plan.observedState.nextChunkIndex).toBe(1);
|
|
184
|
+
expect(plan.observedState.openChunkStartTurn).toBe(trace.turn_count);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("migrateSessionUploadState", () => {
|
|
189
|
+
it("fills in missing confirmation fields from next/openChunk values", () => {
|
|
190
|
+
const legacy = {
|
|
191
|
+
sourceTool: "codex_cli" as const,
|
|
192
|
+
sourceSessionId: "s1",
|
|
193
|
+
locator: "/tmp/s1.jsonl",
|
|
194
|
+
nextChunkIndex: 3,
|
|
195
|
+
openChunkStartTurn: 10,
|
|
196
|
+
lastSeenTurnCount: 10,
|
|
197
|
+
lastActivityAt: "2026-03-21T00:00:00.000Z",
|
|
198
|
+
lastFlushedTurnId: "a3",
|
|
199
|
+
// missing: confirmedChunkIndex, confirmedOpenChunkStartTurn, unconfirmedSince
|
|
200
|
+
} as any;
|
|
201
|
+
|
|
202
|
+
const migrated = migrateSessionUploadState(legacy);
|
|
203
|
+
|
|
204
|
+
expect(migrated.confirmedChunkIndex).toBe(3);
|
|
205
|
+
expect(migrated.confirmedOpenChunkStartTurn).toBe(10);
|
|
206
|
+
expect(migrated.unconfirmedSince).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("does not overwrite existing confirmation fields", () => {
|
|
210
|
+
const state = makeSessionState({
|
|
211
|
+
sourceTool: "codex_cli",
|
|
212
|
+
sourceSessionId: "s2",
|
|
213
|
+
locator: "/tmp/s2.jsonl",
|
|
214
|
+
nextChunkIndex: 5,
|
|
215
|
+
openChunkStartTurn: 20,
|
|
216
|
+
confirmedChunkIndex: 3,
|
|
217
|
+
confirmedOpenChunkStartTurn: 12,
|
|
218
|
+
unconfirmedSince: "2026-03-21T01:00:00.000Z",
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const migrated = migrateSessionUploadState(state);
|
|
222
|
+
|
|
223
|
+
expect(migrated.confirmedChunkIndex).toBe(3);
|
|
224
|
+
expect(migrated.confirmedOpenChunkStartTurn).toBe(12);
|
|
225
|
+
expect(migrated.unconfirmedSince).toBe("2026-03-21T01:00:00.000Z");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("verifyUnconfirmedChunks", () => {
|
|
230
|
+
function makeSubmitState(sessions: Record<string, ReturnType<typeof makeSessionState>>) {
|
|
231
|
+
return { version: 2 as const, chunks: {}, sessions };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function makeMockClient(responses: Record<string, { exists: boolean }>) {
|
|
235
|
+
return {
|
|
236
|
+
async get(path: string) {
|
|
237
|
+
const url = new URL(`http://x${path}`);
|
|
238
|
+
const tool = url.searchParams.get("source_tool")!;
|
|
239
|
+
const id = url.searchParams.get("source_session_id")!;
|
|
240
|
+
const idx = url.searchParams.get("chunk_index")!;
|
|
241
|
+
const key = `${tool}:${id}:${idx}`;
|
|
242
|
+
const r = responses[key];
|
|
243
|
+
if (!r) throw new Error(`Unexpected exists check: ${key}`);
|
|
244
|
+
return r;
|
|
245
|
+
},
|
|
246
|
+
} as any;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
it("advances confirmedChunkIndex when chunks are confirmed", async () => {
|
|
250
|
+
const state = makeSubmitState({
|
|
251
|
+
"codex_cli:sess1": makeSessionState({
|
|
252
|
+
sourceTool: "codex_cli",
|
|
253
|
+
sourceSessionId: "sess1",
|
|
254
|
+
locator: "/tmp/sess1.jsonl",
|
|
255
|
+
nextChunkIndex: 3,
|
|
256
|
+
openChunkStartTurn: 12,
|
|
257
|
+
confirmedChunkIndex: 1,
|
|
258
|
+
confirmedOpenChunkStartTurn: 4,
|
|
259
|
+
unconfirmedSince: "2026-03-21T00:00:00.000Z",
|
|
260
|
+
}),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const client = makeMockClient({
|
|
264
|
+
"codex_cli:sess1:1": { exists: true },
|
|
265
|
+
"codex_cli:sess1:2": { exists: true },
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await verifyUnconfirmedChunks(state, client, new Date("2026-03-21T01:00:00.000Z"));
|
|
269
|
+
|
|
270
|
+
const s = state.sessions["codex_cli:sess1"]!;
|
|
271
|
+
expect(s.confirmedChunkIndex).toBe(3);
|
|
272
|
+
// all confirmed → unconfirmedSince cleared
|
|
273
|
+
expect(s.unconfirmedSince).toBeNull();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("stops advancing at first missing chunk", async () => {
|
|
277
|
+
const state = makeSubmitState({
|
|
278
|
+
"codex_cli:sess2": makeSessionState({
|
|
279
|
+
sourceTool: "codex_cli",
|
|
280
|
+
sourceSessionId: "sess2",
|
|
281
|
+
locator: "/tmp/sess2.jsonl",
|
|
282
|
+
nextChunkIndex: 3,
|
|
283
|
+
openChunkStartTurn: 12,
|
|
284
|
+
confirmedChunkIndex: 1,
|
|
285
|
+
confirmedOpenChunkStartTurn: 4,
|
|
286
|
+
unconfirmedSince: "2026-03-21T00:00:00.000Z",
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const client = makeMockClient({
|
|
291
|
+
"codex_cli:sess2:1": { exists: true },
|
|
292
|
+
"codex_cli:sess2:2": { exists: false },
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await verifyUnconfirmedChunks(state, client, new Date("2026-03-21T01:00:00.000Z"));
|
|
296
|
+
|
|
297
|
+
const s = state.sessions["codex_cli:sess2"]!;
|
|
298
|
+
expect(s.confirmedChunkIndex).toBe(2);
|
|
299
|
+
// still one unconfirmed chunk → unconfirmedSince preserved
|
|
300
|
+
expect(s.unconfirmedSince).toBe("2026-03-21T00:00:00.000Z");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("resets to confirmed state after 2hr timeout", async () => {
|
|
304
|
+
const state = makeSubmitState({
|
|
305
|
+
"codex_cli:sess3": makeSessionState({
|
|
306
|
+
sourceTool: "codex_cli",
|
|
307
|
+
sourceSessionId: "sess3",
|
|
308
|
+
locator: "/tmp/sess3.jsonl",
|
|
309
|
+
nextChunkIndex: 3,
|
|
310
|
+
openChunkStartTurn: 12,
|
|
311
|
+
confirmedChunkIndex: 1,
|
|
312
|
+
confirmedOpenChunkStartTurn: 4,
|
|
313
|
+
unconfirmedSince: "2026-03-21T00:00:00.000Z",
|
|
314
|
+
}),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const client = makeMockClient({}); // should not be called
|
|
318
|
+
|
|
319
|
+
// now = 2h01m after unconfirmedSince
|
|
320
|
+
await verifyUnconfirmedChunks(state, client, new Date("2026-03-21T02:01:00.000Z"));
|
|
321
|
+
|
|
322
|
+
const s = state.sessions["codex_cli:sess3"]!;
|
|
323
|
+
// reset back to confirmed baseline so chunks get re-submitted
|
|
324
|
+
expect(s.nextChunkIndex).toBe(1);
|
|
325
|
+
expect(s.openChunkStartTurn).toBe(4);
|
|
326
|
+
expect(s.unconfirmedSince).toBeNull();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("skips sessions that are already fully confirmed", async () => {
|
|
330
|
+
const state = makeSubmitState({
|
|
331
|
+
"codex_cli:sess4": makeSessionState({
|
|
332
|
+
sourceTool: "codex_cli",
|
|
333
|
+
sourceSessionId: "sess4",
|
|
334
|
+
locator: "/tmp/sess4.jsonl",
|
|
335
|
+
nextChunkIndex: 2,
|
|
336
|
+
openChunkStartTurn: 8,
|
|
337
|
+
confirmedChunkIndex: 2,
|
|
338
|
+
confirmedOpenChunkStartTurn: 8,
|
|
339
|
+
unconfirmedSince: null,
|
|
340
|
+
}),
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
let callCount = 0;
|
|
344
|
+
const client = { async get() { callCount++; return { exists: true }; } } as any;
|
|
345
|
+
|
|
346
|
+
await verifyUnconfirmedChunks(state, client, new Date("2026-03-21T01:00:00.000Z"));
|
|
347
|
+
|
|
348
|
+
expect(callCount).toBe(0);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("collectIdleSessionSources", () => {
|
|
353
|
+
it("returns only tracked sessions with an open tail older than two days", () => {
|
|
354
|
+
const sources = collectIdleSessionSources({
|
|
355
|
+
"codex_cli:idle": makeSessionState({
|
|
356
|
+
sourceTool: "codex_cli",
|
|
357
|
+
sourceSessionId: "idle",
|
|
358
|
+
locator: "/tmp/idle.jsonl",
|
|
359
|
+
nextChunkIndex: 1,
|
|
360
|
+
openChunkStartTurn: 2,
|
|
361
|
+
lastSeenTurnCount: 4,
|
|
362
|
+
lastActivityAt: "2026-03-21T00:00:00.000Z",
|
|
363
|
+
lastFlushedTurnId: "a1",
|
|
364
|
+
}),
|
|
365
|
+
"codex_cli:closed": makeSessionState({
|
|
366
|
+
sourceTool: "codex_cli",
|
|
367
|
+
sourceSessionId: "closed",
|
|
368
|
+
locator: "/tmp/closed.jsonl",
|
|
369
|
+
nextChunkIndex: 1,
|
|
370
|
+
openChunkStartTurn: 4,
|
|
371
|
+
lastSeenTurnCount: 4,
|
|
372
|
+
lastActivityAt: "2026-03-21T00:00:00.000Z",
|
|
373
|
+
lastFlushedTurnId: "a2",
|
|
374
|
+
}),
|
|
375
|
+
"codex_cli:fresh": makeSessionState({
|
|
376
|
+
sourceTool: "codex_cli",
|
|
377
|
+
sourceSessionId: "fresh",
|
|
378
|
+
locator: "/tmp/fresh.jsonl",
|
|
379
|
+
nextChunkIndex: 0,
|
|
380
|
+
openChunkStartTurn: 0,
|
|
381
|
+
lastSeenTurnCount: 2,
|
|
382
|
+
lastActivityAt: "2026-03-22T23:59:59.000Z",
|
|
383
|
+
lastFlushedTurnId: null,
|
|
384
|
+
}),
|
|
385
|
+
}, new Date("2026-03-23T00:01:00.000Z"));
|
|
386
|
+
|
|
387
|
+
expect(sources).toEqual([
|
|
388
|
+
{
|
|
389
|
+
tool: "codex_cli",
|
|
390
|
+
locator: "/tmp/idle.jsonl",
|
|
391
|
+
label: "codex_cli:idle",
|
|
392
|
+
},
|
|
393
|
+
]);
|
|
394
|
+
});
|
|
395
|
+
});
|