@remnic/core 9.3.641 → 9.3.643

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.
@@ -0,0 +1,1748 @@
1
+ import assert from "node:assert/strict";
2
+ import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { collectLocalSessionSummaries, compileRedactionRules, runLocalSessionSummaryCliCommand } from "./index.js";
8
+
9
+ async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
10
+ const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-session-summary-"));
11
+ try {
12
+ return await fn(dir);
13
+ } finally {
14
+ await rm(dir, { recursive: true, force: true });
15
+ }
16
+ }
17
+
18
+ test("collectLocalSessionSummaries produces metadata-only drafts without local paths or raw session keys", async () => {
19
+ await withTempDir(async (dir) => {
20
+ const transcriptPath = path.join(dir, "session.jsonl");
21
+ await writeFile(
22
+ transcriptPath,
23
+ [
24
+ JSON.stringify({
25
+ sessionKey: "local-session-secret",
26
+ role: "user",
27
+ timestamp: "2026-01-02T03:04:05.000Z",
28
+ content:
29
+ "Please inspect /Users/alex/src/private and email alex@example.com about http://internal.example.test",
30
+ }),
31
+ JSON.stringify({
32
+ sessionKey: "local-session-secret",
33
+ role: "assistant",
34
+ timestamp: "2026-01-02T03:04:06.000Z",
35
+ content: "Use API_KEY=super-secret and connect to 192.168.1.10.",
36
+ }),
37
+ ].join("\n"),
38
+ "utf-8"
39
+ );
40
+
41
+ const report = await collectLocalSessionSummaries({
42
+ inputDir: dir,
43
+ source: "generic-jsonl",
44
+ now: new Date("2026-01-02T04:00:00.000Z"),
45
+ });
46
+
47
+ assert.equal(report.filesScanned, 1);
48
+ assert.equal(report.filesParsed, 1);
49
+ assert.equal(report.turnsParsed, 2);
50
+ assert.equal(report.sessionsSummarized, 1);
51
+ const draft = report.drafts[0];
52
+ assert.equal(draft.turnCount, 2);
53
+ assert.equal(draft.roles.user, 1);
54
+ assert.equal(draft.roles.assistant, 1);
55
+ assert.equal(draft.metadata.storesRawTranscript, false);
56
+ assert.equal(draft.metadata.storesLocalPaths, false);
57
+ assert.equal(draft.metadata.storesSourceSessionKey, false);
58
+ assert.equal(draft.sourceFileRefs.length, 1);
59
+ assert.match(draft.sourceFileRefs[0].hash, /^[a-f0-9]{24}$/);
60
+
61
+ const serialized = JSON.stringify(draft);
62
+ assert.equal(serialized.includes(transcriptPath), false);
63
+ assert.equal(serialized.includes("local-session-secret"), false);
64
+ assert.equal(serialized.includes("/Users/alex"), false);
65
+ assert.equal(serialized.includes("alex@example.com"), false);
66
+ assert.equal(serialized.includes("192.168.1.10"), false);
67
+ assert.equal(serialized.includes("super-secret"), false);
68
+ });
69
+ });
70
+
71
+ test("includeRedactedExcerpts emits sanitized excerpts only", async () => {
72
+ await withTempDir(async (dir) => {
73
+ await writeFile(
74
+ path.join(dir, "session.jsonl"),
75
+ JSON.stringify({
76
+ session_key: "thread-1",
77
+ sender: "human",
78
+ created_at: "2026-02-03T00:00:00.000Z",
79
+ text: "Read /home/person/work and send results to person@example.com",
80
+ }),
81
+ "utf-8"
82
+ );
83
+
84
+ const report = await collectLocalSessionSummaries({
85
+ inputDir: dir,
86
+ includeRedactedExcerpts: true,
87
+ });
88
+ const draft = report.drafts[0];
89
+ assert.equal(draft.excerpts?.length, 1);
90
+ assert.equal(draft.excerpts?.[0].text, "Read [REDACTED_PATH] and send results to [REDACTED_EMAIL]");
91
+ assert.deepEqual(draft.redaction.ruleNames, ["absolute-posix-path", "email"]);
92
+ });
93
+ });
94
+
95
+ test("includeRedactedExcerpts forces default redaction when defaults are disabled", async () => {
96
+ await withTempDir(async (dir) => {
97
+ await writeFile(
98
+ path.join(dir, "session.jsonl"),
99
+ JSON.stringify({
100
+ session_key: "thread-1",
101
+ sender: "human",
102
+ created_at: "2026-02-03T00:00:00.000Z",
103
+ text: "Read /home/person/work and send results to person@example.com",
104
+ }),
105
+ "utf-8"
106
+ );
107
+
108
+ const report = await collectLocalSessionSummaries({
109
+ inputDir: dir,
110
+ includeRedactedExcerpts: true,
111
+ redactionConfig: { disableDefaults: true },
112
+ });
113
+ const draft = report.drafts[0];
114
+ assert.equal(draft.excerpts?.[0].text, "Read [REDACTED_PATH] and send results to [REDACTED_EMAIL]");
115
+ assert.equal(draft.redaction.enabled, true);
116
+ assert.equal(
117
+ report.warnings.some((warning) => warning.code === "session-summaries.default_redaction_forced_for_excerpts"),
118
+ true
119
+ );
120
+ });
121
+ });
122
+
123
+ test("summary includes all normalized role counts", async () => {
124
+ await withTempDir(async (dir) => {
125
+ await writeFile(
126
+ path.join(dir, "session.jsonl"),
127
+ [
128
+ JSON.stringify({
129
+ sessionKey: "roles-session",
130
+ role: "system",
131
+ timestamp: "2026-02-03T00:00:00.000Z",
132
+ content: "System instruction.",
133
+ }),
134
+ JSON.stringify({
135
+ sessionKey: "roles-session",
136
+ role: "other",
137
+ timestamp: "2026-02-03T00:00:01.000Z",
138
+ content: "Other event.",
139
+ }),
140
+ ].join("\n"),
141
+ "utf-8"
142
+ );
143
+
144
+ const report = await collectLocalSessionSummaries({ inputDir: dir });
145
+
146
+ assert.match(report.drafts[0].summary, /\(0 user, 0 assistant, 0 tool, 1 system, 1 other\)/);
147
+ });
148
+ });
149
+
150
+ test("parsing falls back to later non-empty content fields", async () => {
151
+ await withTempDir(async (dir) => {
152
+ await writeFile(
153
+ path.join(dir, "session.jsonl"),
154
+ JSON.stringify({
155
+ sessionKey: "fallback-session",
156
+ role: "user",
157
+ timestamp: "2026-02-03T00:00:00.000Z",
158
+ content: "",
159
+ text: "Use this fallback text.",
160
+ }),
161
+ "utf-8"
162
+ );
163
+
164
+ const report = await collectLocalSessionSummaries({
165
+ inputDir: dir,
166
+ includeRedactedExcerpts: true,
167
+ });
168
+
169
+ assert.equal(report.turnsParsed, 1);
170
+ assert.equal(report.drafts[0].excerpts?.[0].text, "Use this fallback text.");
171
+ });
172
+ });
173
+
174
+ test("parsing reads top-level parts and input content fields", async () => {
175
+ await withTempDir(async (dir) => {
176
+ await writeFile(
177
+ path.join(dir, "session.jsonl"),
178
+ [
179
+ JSON.stringify({
180
+ sessionKey: "parts-session",
181
+ role: "user",
182
+ timestamp: "2026-02-03T00:00:00.000Z",
183
+ parts: [{ text: "Use parts text." }],
184
+ }),
185
+ JSON.stringify({
186
+ sessionKey: "parts-session",
187
+ role: "assistant",
188
+ timestamp: "2026-02-03T00:00:01.000Z",
189
+ input: "Use input text.",
190
+ }),
191
+ ].join("\n"),
192
+ "utf-8"
193
+ );
194
+
195
+ const report = await collectLocalSessionSummaries({
196
+ inputDir: dir,
197
+ includeRedactedExcerpts: true,
198
+ });
199
+
200
+ assert.equal(report.turnsParsed, 2);
201
+ assert.deepEqual(
202
+ report.drafts[0].excerpts?.map((excerpt) => excerpt.text),
203
+ ["Use parts text.", "Use input text."]
204
+ );
205
+ });
206
+ });
207
+
208
+ test("codex-jsonl parses Codex rollout payload rows", async () => {
209
+ await withTempDir(async (dir) => {
210
+ await writeFile(
211
+ path.join(dir, "rollout.jsonl"),
212
+ [
213
+ JSON.stringify({
214
+ type: "event_msg",
215
+ sessionKey: "codex-session",
216
+ timestamp: "2026-02-03T00:00:00.000Z",
217
+ payload: { message: "User message from payload." },
218
+ }),
219
+ JSON.stringify({
220
+ type: "response_item",
221
+ sessionKey: "codex-session",
222
+ timestamp: "2026-02-03T00:00:01.000Z",
223
+ payload: { content: [{ type: "output_text", text: "Assistant response from payload." }] },
224
+ }),
225
+ JSON.stringify({
226
+ type: "event_msg",
227
+ sessionKey: "codex-session",
228
+ timestamp: "2026-02-03T00:00:02.000Z",
229
+ payload: { type: "agent_message", message: "Agent event message from payload." },
230
+ }),
231
+ ].join("\n"),
232
+ "utf-8"
233
+ );
234
+
235
+ const report = await collectLocalSessionSummaries({
236
+ inputDir: dir,
237
+ source: "codex-jsonl",
238
+ includeRedactedExcerpts: true,
239
+ });
240
+
241
+ assert.equal(report.turnsParsed, 3);
242
+ assert.equal(report.drafts[0].roles.user, 1);
243
+ assert.equal(report.drafts[0].roles.assistant, 2);
244
+ assert.deepEqual(
245
+ report.drafts[0].excerpts?.map((excerpt) => excerpt.text),
246
+ ["User message from payload.", "Assistant response from payload.", "Agent event message from payload."]
247
+ );
248
+ });
249
+ });
250
+
251
+ test("codex-jsonl skips compacted rollout summary rows", async () => {
252
+ await withTempDir(async (dir) => {
253
+ await writeFile(
254
+ path.join(dir, "rollout.jsonl"),
255
+ [
256
+ JSON.stringify({
257
+ type: "event_msg",
258
+ sessionKey: "codex-session",
259
+ timestamp: "2026-02-03T00:00:00.000Z",
260
+ payload: { message: "Original user request." },
261
+ }),
262
+ JSON.stringify({
263
+ type: "compacted",
264
+ sessionKey: "codex-session",
265
+ timestamp: "2026-02-03T00:00:01.000Z",
266
+ payload: { message: "Synthetic compaction summary should never become an excerpt." },
267
+ }),
268
+ JSON.stringify({
269
+ type: "response_item",
270
+ sessionKey: "codex-session",
271
+ timestamp: "2026-02-03T00:00:02.000Z",
272
+ payload: { content: [{ type: "output_text", text: "Assistant response." }] },
273
+ }),
274
+ ].join("\n"),
275
+ "utf-8"
276
+ );
277
+
278
+ const report = await collectLocalSessionSummaries({
279
+ inputDir: dir,
280
+ source: "codex-jsonl",
281
+ includeRedactedExcerpts: true,
282
+ });
283
+
284
+ assert.equal(report.turnsParsed, 2);
285
+ assert.equal(report.drafts[0].turnCount, 2);
286
+ assert.equal(report.drafts[0].roles.other, 0);
287
+ assert.deepEqual(
288
+ report.drafts[0].excerpts?.map((excerpt) => excerpt.text),
289
+ ["Original user request.", "Assistant response."]
290
+ );
291
+ assert.equal(JSON.stringify(report.drafts[0]).includes("Synthetic compaction summary"), false);
292
+ });
293
+ });
294
+
295
+ test("codex-jsonl deduplicates mirrored agent event and response item rows", async () => {
296
+ await withTempDir(async (dir) => {
297
+ await writeFile(
298
+ path.join(dir, "rollout.jsonl"),
299
+ [
300
+ JSON.stringify({
301
+ type: "event_msg",
302
+ sessionKey: "codex-session",
303
+ timestamp: "2026-02-03T00:00:00.000Z",
304
+ payload: { message: "User request." },
305
+ }),
306
+ JSON.stringify({
307
+ type: "event_msg",
308
+ id: "assistant-turn-1",
309
+ sessionKey: "codex-session",
310
+ timestamp: "2026-02-03T00:00:01.000Z",
311
+ payload: { type: "agent_message", message: "Mirrored assistant response." },
312
+ }),
313
+ JSON.stringify({
314
+ type: "response_item",
315
+ id: "assistant-turn-1",
316
+ sessionKey: "codex-session",
317
+ timestamp: "2026-02-03T00:00:02.000Z",
318
+ payload: { content: [{ type: "output_text", text: "Mirrored assistant response." }] },
319
+ }),
320
+ ].join("\n"),
321
+ "utf-8"
322
+ );
323
+
324
+ const report = await collectLocalSessionSummaries({
325
+ inputDir: dir,
326
+ source: "codex-jsonl",
327
+ includeRedactedExcerpts: true,
328
+ });
329
+
330
+ assert.equal(report.turnsParsed, 2);
331
+ assert.equal(report.drafts[0].turnCount, 2);
332
+ assert.equal(report.drafts[0].roles.user, 1);
333
+ assert.equal(report.drafts[0].roles.assistant, 1);
334
+ assert.deepEqual(
335
+ report.drafts[0].excerpts?.map((excerpt) => excerpt.text),
336
+ ["User request.", "Mirrored assistant response."]
337
+ );
338
+ });
339
+ });
340
+
341
+ test("codex-jsonl keeps same-text cross-surface rows with different signals", async () => {
342
+ await withTempDir(async (dir) => {
343
+ await writeFile(
344
+ path.join(dir, "rollout.jsonl"),
345
+ [
346
+ JSON.stringify({
347
+ type: "event_msg",
348
+ sessionKey: "codex-session",
349
+ timestamp: "2026-02-03T00:00:00.000Z",
350
+ payload: { message: "User request." },
351
+ }),
352
+ JSON.stringify({
353
+ type: "event_msg",
354
+ id: "assistant-event-1",
355
+ sessionKey: "codex-session",
356
+ timestamp: "2026-02-03T00:00:01.000Z",
357
+ payload: { type: "agent_message", message: "OK" },
358
+ }),
359
+ JSON.stringify({
360
+ type: "response_item",
361
+ id: "assistant-response-1",
362
+ sessionKey: "codex-session",
363
+ timestamp: "2026-02-03T00:00:02.000Z",
364
+ payload: { content: [{ type: "output_text", text: "OK" }] },
365
+ }),
366
+ ].join("\n"),
367
+ "utf-8"
368
+ );
369
+
370
+ const report = await collectLocalSessionSummaries({
371
+ inputDir: dir,
372
+ source: "codex-jsonl",
373
+ includeRedactedExcerpts: true,
374
+ });
375
+
376
+ assert.equal(report.turnsParsed, 3);
377
+ assert.equal(report.drafts[0].turnCount, 3);
378
+ assert.equal(report.drafts[0].roles.assistant, 2);
379
+ assert.deepEqual(
380
+ report.drafts[0].excerpts?.map((excerpt) => excerpt.text),
381
+ ["User request.", "OK", "OK"]
382
+ );
383
+ });
384
+ });
385
+
386
+ test("codex-jsonl deduplicates same-surface mirrored assistant rows", async () => {
387
+ await withTempDir(async (dir) => {
388
+ await writeFile(
389
+ path.join(dir, "rollout.jsonl"),
390
+ [
391
+ JSON.stringify({
392
+ type: "event_msg",
393
+ sessionKey: "codex-session",
394
+ timestamp: "2026-02-03T00:00:00.000Z",
395
+ payload: { message: "User request." },
396
+ }),
397
+ JSON.stringify({
398
+ type: "response_item",
399
+ id: "assistant-1",
400
+ sessionKey: "codex-session",
401
+ timestamp: "2026-02-03T00:00:01.000Z",
402
+ payload: { content: [{ type: "output_text", text: "Repeated assistant response." }] },
403
+ }),
404
+ JSON.stringify({
405
+ type: "response_item",
406
+ id: "assistant-1",
407
+ sessionKey: "codex-session",
408
+ timestamp: "2026-02-03T00:00:02.000Z",
409
+ payload: { content: [{ type: "output_text", text: "Repeated assistant response." }] },
410
+ }),
411
+ ].join("\n"),
412
+ "utf-8"
413
+ );
414
+
415
+ const report = await collectLocalSessionSummaries({
416
+ inputDir: dir,
417
+ source: "codex-jsonl",
418
+ includeRedactedExcerpts: true,
419
+ });
420
+
421
+ assert.equal(report.turnsParsed, 2);
422
+ assert.equal(report.drafts[0].turnCount, 2);
423
+ assert.equal(report.drafts[0].roles.assistant, 1);
424
+ assert.deepEqual(
425
+ report.drafts[0].excerpts?.map((excerpt) => excerpt.text),
426
+ ["User request.", "Repeated assistant response."]
427
+ );
428
+ });
429
+ });
430
+
431
+ test("codex-jsonl keeps distinct same-text response item rows with different ids", async () => {
432
+ await withTempDir(async (dir) => {
433
+ await writeFile(
434
+ path.join(dir, "rollout.jsonl"),
435
+ [
436
+ JSON.stringify({
437
+ type: "event_msg",
438
+ sessionKey: "codex-session",
439
+ timestamp: "2026-02-03T00:00:00.000Z",
440
+ payload: { message: "User request." },
441
+ }),
442
+ JSON.stringify({
443
+ type: "response_item",
444
+ id: "assistant-1",
445
+ sessionKey: "codex-session",
446
+ timestamp: "2026-02-03T00:00:01.000Z",
447
+ payload: { content: [{ type: "output_text", text: "OK" }] },
448
+ }),
449
+ JSON.stringify({
450
+ type: "response_item",
451
+ id: "assistant-2",
452
+ sessionKey: "codex-session",
453
+ timestamp: "2026-02-03T00:00:02.000Z",
454
+ payload: { content: [{ type: "output_text", text: "OK" }] },
455
+ }),
456
+ ].join("\n"),
457
+ "utf-8"
458
+ );
459
+
460
+ const report = await collectLocalSessionSummaries({
461
+ inputDir: dir,
462
+ source: "codex-jsonl",
463
+ includeRedactedExcerpts: true,
464
+ });
465
+
466
+ assert.equal(report.turnsParsed, 3);
467
+ assert.equal(report.drafts[0].roles.assistant, 2);
468
+ assert.deepEqual(
469
+ report.drafts[0].excerpts?.map((excerpt) => excerpt.text),
470
+ ["User request.", "OK", "OK"]
471
+ );
472
+ });
473
+ });
474
+
475
+ test("codex-jsonl inherits legacy session_meta payload ids", async () => {
476
+ await withTempDir(async (dir) => {
477
+ const transcriptPath = path.join(dir, "rollout.jsonl");
478
+ const writeTranscript = async (message: string): Promise<void> => {
479
+ await writeFile(
480
+ transcriptPath,
481
+ [
482
+ JSON.stringify({
483
+ type: "session_meta",
484
+ payload: { id: "legacy-codex-session" },
485
+ }),
486
+ JSON.stringify({
487
+ type: "event_msg",
488
+ timestamp: "2026-02-03T00:00:00.000Z",
489
+ payload: { message },
490
+ }),
491
+ ].join("\n"),
492
+ "utf-8"
493
+ );
494
+ };
495
+
496
+ await writeTranscript("Original message.");
497
+ const firstReport = await collectLocalSessionSummaries({
498
+ inputDir: dir,
499
+ source: "codex-jsonl",
500
+ });
501
+
502
+ await writeTranscript("Changed message.");
503
+ const secondReport = await collectLocalSessionSummaries({
504
+ inputDir: dir,
505
+ source: "codex-jsonl",
506
+ });
507
+
508
+ assert.equal(firstReport.drafts[0].sourceSessionRef, secondReport.drafts[0].sourceSessionRef);
509
+ assert.equal(firstReport.drafts[0].draftId, secondReport.drafts[0].draftId);
510
+ assert.equal(firstReport.drafts[0].turnCount, 1);
511
+ });
512
+ });
513
+
514
+ test("payload.message metadata contributes session, role, and timestamp", async () => {
515
+ await withTempDir(async (dir) => {
516
+ const transcriptPath = path.join(dir, "nested.jsonl");
517
+ await writeFile(
518
+ transcriptPath,
519
+ JSON.stringify({
520
+ type: "event_msg",
521
+ payload: {
522
+ message: {
523
+ sessionId: "nested-payload-session",
524
+ role: "assistant",
525
+ timestamp: "2026-02-03T00:00:00.000Z",
526
+ content: "Nested assistant content.",
527
+ },
528
+ },
529
+ }),
530
+ "utf-8"
531
+ );
532
+ const firstReport = await collectLocalSessionSummaries({ inputDir: dir });
533
+
534
+ await writeFile(
535
+ transcriptPath,
536
+ JSON.stringify({
537
+ type: "event_msg",
538
+ payload: {
539
+ message: {
540
+ sessionId: "nested-payload-session",
541
+ role: "assistant",
542
+ timestamp: "2026-02-03T00:00:00.000Z",
543
+ content: "Changed nested assistant content.",
544
+ },
545
+ },
546
+ }),
547
+ "utf-8"
548
+ );
549
+ const secondReport = await collectLocalSessionSummaries({ inputDir: dir });
550
+
551
+ assert.equal(firstReport.drafts[0].sourceSessionRef, secondReport.drafts[0].sourceSessionRef);
552
+ assert.equal(firstReport.drafts[0].draftId, secondReport.drafts[0].draftId);
553
+ assert.equal(firstReport.drafts[0].roles.assistant, 1);
554
+ assert.equal(firstReport.drafts[0].firstTimestamp, "2026-02-03T00:00:00.000Z");
555
+ });
556
+ });
557
+
558
+ test("top-level message metadata contributes stable session ids", async () => {
559
+ await withTempDir(async (dir) => {
560
+ const transcriptPath = path.join(dir, "nested-message.jsonl");
561
+ await writeFile(
562
+ transcriptPath,
563
+ JSON.stringify({
564
+ message: {
565
+ sessionId: "top-level-message-session",
566
+ role: "assistant",
567
+ timestamp: "2026-02-03T00:00:00.000Z",
568
+ content: "Original nested content.",
569
+ },
570
+ }),
571
+ "utf-8"
572
+ );
573
+ const firstReport = await collectLocalSessionSummaries({ inputDir: dir });
574
+
575
+ await writeFile(
576
+ transcriptPath,
577
+ JSON.stringify({
578
+ message: {
579
+ sessionId: "top-level-message-session",
580
+ role: "assistant",
581
+ timestamp: "2026-02-03T00:00:00.000Z",
582
+ content: "Changed nested content.",
583
+ },
584
+ }),
585
+ "utf-8"
586
+ );
587
+ const secondReport = await collectLocalSessionSummaries({ inputDir: dir });
588
+
589
+ assert.equal(firstReport.drafts[0].sourceSessionRef, secondReport.drafts[0].sourceSessionRef);
590
+ assert.equal(firstReport.drafts[0].draftId, secondReport.drafts[0].draftId);
591
+ assert.equal(firstReport.drafts[0].roles.assistant, 1);
592
+ });
593
+ });
594
+
595
+ test("nested transcript id metadata contributes stable session ids", async () => {
596
+ await withTempDir(async (dir) => {
597
+ const transcriptPath = path.join(dir, "nested-transcript.jsonl");
598
+ await writeFile(
599
+ transcriptPath,
600
+ JSON.stringify({
601
+ message: {
602
+ transcriptId: "top-level-message-transcript",
603
+ role: "assistant",
604
+ timestamp: "2026-02-03T00:00:00.000Z",
605
+ content: "Original nested transcript content.",
606
+ },
607
+ }),
608
+ "utf-8"
609
+ );
610
+ const firstReport = await collectLocalSessionSummaries({ inputDir: dir });
611
+
612
+ await writeFile(
613
+ transcriptPath,
614
+ JSON.stringify({
615
+ message: {
616
+ transcriptId: "top-level-message-transcript",
617
+ role: "assistant",
618
+ timestamp: "2026-02-03T00:00:00.000Z",
619
+ content: "Changed nested transcript content.",
620
+ },
621
+ }),
622
+ "utf-8"
623
+ );
624
+ const secondReport = await collectLocalSessionSummaries({ inputDir: dir });
625
+
626
+ assert.equal(firstReport.drafts[0].sourceSessionRef, secondReport.drafts[0].sourceSessionRef);
627
+ assert.equal(firstReport.drafts[0].draftId, secondReport.drafts[0].draftId);
628
+ assert.equal(firstReport.drafts[0].roles.assistant, 1);
629
+ });
630
+ });
631
+
632
+ test("claude-jsonl ignores tool_result blocks instead of storing them as user excerpts", async () => {
633
+ await withTempDir(async (dir) => {
634
+ await writeFile(
635
+ path.join(dir, "claude.jsonl"),
636
+ [
637
+ JSON.stringify({
638
+ sessionId: "claude-session",
639
+ message: {
640
+ role: "user",
641
+ content: [{ type: "tool_result", content: "local command output" }],
642
+ },
643
+ }),
644
+ JSON.stringify({
645
+ sessionId: "claude-session",
646
+ message: {
647
+ role: "user",
648
+ content: [{ type: "text", text: "Actual user message." }],
649
+ },
650
+ }),
651
+ ].join("\n"),
652
+ "utf-8"
653
+ );
654
+
655
+ const report = await collectLocalSessionSummaries({
656
+ inputDir: dir,
657
+ source: "claude-jsonl",
658
+ includeRedactedExcerpts: true,
659
+ });
660
+
661
+ assert.equal(report.turnsParsed, 1);
662
+ assert.equal(report.drafts[0].roles.user, 1);
663
+ assert.deepEqual(
664
+ report.drafts[0].excerpts?.map((excerpt) => excerpt.text),
665
+ ["Actual user message."]
666
+ );
667
+ });
668
+ });
669
+
670
+ test("sessionId fields produce stable session refs across content changes", async () => {
671
+ await withTempDir(async (dir) => {
672
+ const transcriptPath = path.join(dir, "session.jsonl");
673
+ await writeFile(
674
+ transcriptPath,
675
+ JSON.stringify({
676
+ sessionId: "stable-session",
677
+ role: "user",
678
+ timestamp: "2026-02-03T00:00:00.000Z",
679
+ content: "Original content.",
680
+ }),
681
+ "utf-8"
682
+ );
683
+ const firstReport = await collectLocalSessionSummaries({ inputDir: dir });
684
+
685
+ await writeFile(
686
+ transcriptPath,
687
+ JSON.stringify({
688
+ sessionId: "stable-session",
689
+ role: "user",
690
+ timestamp: "2026-02-03T00:00:00.000Z",
691
+ content: "Changed content.",
692
+ }),
693
+ "utf-8"
694
+ );
695
+ const secondReport = await collectLocalSessionSummaries({ inputDir: dir });
696
+
697
+ assert.equal(firstReport.drafts[0].sourceSessionRef, secondReport.drafts[0].sourceSessionRef);
698
+ assert.equal(firstReport.drafts[0].draftId, secondReport.drafts[0].draftId);
699
+ });
700
+ });
701
+
702
+ test("numeric sessionId fields produce stable session refs", async () => {
703
+ await withTempDir(async (dir) => {
704
+ await writeFile(
705
+ path.join(dir, "session.jsonl"),
706
+ [
707
+ JSON.stringify({
708
+ sessionId: 12345,
709
+ role: "user",
710
+ timestamp: "2026-02-03T00:00:00.000Z",
711
+ content: "One",
712
+ }),
713
+ JSON.stringify({
714
+ sessionId: 12345,
715
+ role: "assistant",
716
+ timestamp: "2026-02-03T00:00:01.000Z",
717
+ content: "Two",
718
+ }),
719
+ ].join("\n"),
720
+ "utf-8"
721
+ );
722
+
723
+ const report = await collectLocalSessionSummaries({ inputDir: dir });
724
+
725
+ assert.equal(report.sessionsSummarized, 1);
726
+ assert.equal(report.drafts[0].turnCount, 2);
727
+ });
728
+ });
729
+
730
+ test("JSONL rows inherit the previous stable session id in file order", async () => {
731
+ await withTempDir(async (dir) => {
732
+ await writeFile(
733
+ path.join(dir, "session.jsonl"),
734
+ [
735
+ JSON.stringify({
736
+ sessionId: "jsonl-session",
737
+ role: "user",
738
+ timestamp: "2026-02-03T00:00:00.000Z",
739
+ content: "One",
740
+ }),
741
+ JSON.stringify({
742
+ role: "assistant",
743
+ timestamp: "2026-02-03T00:00:01.000Z",
744
+ content: "Two",
745
+ }),
746
+ ].join("\n"),
747
+ "utf-8"
748
+ );
749
+
750
+ const report = await collectLocalSessionSummaries({ inputDir: dir });
751
+
752
+ assert.equal(report.sessionsSummarized, 1);
753
+ assert.equal(report.drafts[0].turnCount, 2);
754
+ });
755
+ });
756
+
757
+ test("JSONL rows inherit stable session id from metadata-only rows", async () => {
758
+ await withTempDir(async (dir) => {
759
+ const transcriptPath = path.join(dir, "session.jsonl");
760
+ await writeFile(
761
+ transcriptPath,
762
+ [
763
+ JSON.stringify({ sessionId: "metadata-session", created_at: "2026-02-03T00:00:00.000Z" }),
764
+ JSON.stringify({
765
+ role: "user",
766
+ timestamp: "2026-02-03T00:00:01.000Z",
767
+ content: "Original content.",
768
+ }),
769
+ ].join("\n"),
770
+ "utf-8"
771
+ );
772
+ const firstReport = await collectLocalSessionSummaries({ inputDir: dir });
773
+
774
+ await writeFile(
775
+ transcriptPath,
776
+ [
777
+ JSON.stringify({ sessionId: "metadata-session", created_at: "2026-02-03T00:00:00.000Z" }),
778
+ JSON.stringify({
779
+ role: "user",
780
+ timestamp: "2026-02-03T00:00:01.000Z",
781
+ content: "Changed content.",
782
+ }),
783
+ ].join("\n"),
784
+ "utf-8"
785
+ );
786
+ const secondReport = await collectLocalSessionSummaries({ inputDir: dir });
787
+
788
+ assert.equal(firstReport.drafts[0].sourceSessionRef, secondReport.drafts[0].sourceSessionRef);
789
+ assert.equal(firstReport.drafts[0].draftId, secondReport.drafts[0].draftId);
790
+ });
791
+ });
792
+
793
+ test("JSON envelope sessionId fields are inherited by child rows", async () => {
794
+ await withTempDir(async (dir) => {
795
+ const transcriptPath = path.join(dir, "session.json");
796
+ await writeFile(
797
+ transcriptPath,
798
+ JSON.stringify({
799
+ sessionId: "stable-envelope-session",
800
+ messages: [
801
+ {
802
+ role: "user",
803
+ timestamp: "2026-02-03T00:00:00.000Z",
804
+ content: "Original content.",
805
+ },
806
+ ],
807
+ }),
808
+ "utf-8"
809
+ );
810
+ const firstReport = await collectLocalSessionSummaries({ inputDir: dir });
811
+
812
+ await writeFile(
813
+ transcriptPath,
814
+ JSON.stringify({
815
+ sessionId: "stable-envelope-session",
816
+ messages: [
817
+ {
818
+ role: "user",
819
+ timestamp: "2026-02-03T00:00:00.000Z",
820
+ content: "Changed content.",
821
+ },
822
+ ],
823
+ }),
824
+ "utf-8"
825
+ );
826
+ const secondReport = await collectLocalSessionSummaries({ inputDir: dir });
827
+
828
+ assert.equal(firstReport.drafts[0].sourceSessionRef, secondReport.drafts[0].sourceSessionRef);
829
+ assert.equal(firstReport.drafts[0].draftId, secondReport.drafts[0].draftId);
830
+ });
831
+ });
832
+
833
+ test("JSON envelope sessionId fields override unusable child session ids", async () => {
834
+ await withTempDir(async (dir) => {
835
+ const transcriptPath = path.join(dir, "session.json");
836
+ await writeFile(
837
+ transcriptPath,
838
+ JSON.stringify({
839
+ sessionId: "stable-envelope-session",
840
+ messages: [
841
+ {
842
+ sessionId: "",
843
+ role: "user",
844
+ timestamp: "2026-02-03T00:00:00.000Z",
845
+ content: "Original content.",
846
+ },
847
+ {
848
+ sessionId: null,
849
+ role: "assistant",
850
+ timestamp: "2026-02-03T00:00:01.000Z",
851
+ content: "Original reply.",
852
+ },
853
+ ],
854
+ }),
855
+ "utf-8"
856
+ );
857
+ const firstReport = await collectLocalSessionSummaries({ inputDir: dir });
858
+
859
+ await writeFile(
860
+ transcriptPath,
861
+ JSON.stringify({
862
+ sessionId: "stable-envelope-session",
863
+ messages: [
864
+ {
865
+ sessionId: "",
866
+ role: "user",
867
+ timestamp: "2026-02-03T00:00:00.000Z",
868
+ content: "Changed content.",
869
+ },
870
+ {
871
+ sessionId: null,
872
+ role: "assistant",
873
+ timestamp: "2026-02-03T00:00:01.000Z",
874
+ content: "Changed reply.",
875
+ },
876
+ ],
877
+ }),
878
+ "utf-8"
879
+ );
880
+ const secondReport = await collectLocalSessionSummaries({ inputDir: dir });
881
+
882
+ assert.equal(firstReport.sessionsSummarized, 1);
883
+ assert.equal(firstReport.drafts[0].turnCount, 2);
884
+ assert.equal(firstReport.drafts[0].sourceSessionRef, secondReport.drafts[0].sourceSessionRef);
885
+ assert.equal(firstReport.drafts[0].draftId, secondReport.drafts[0].draftId);
886
+ });
887
+ });
888
+
889
+ test("JSON envelope parsing skips empty row arrays in favor of populated arrays", async () => {
890
+ await withTempDir(async (dir) => {
891
+ await writeFile(
892
+ path.join(dir, "session.json"),
893
+ JSON.stringify({
894
+ sessionId: "envelope-session",
895
+ turns: [],
896
+ messages: [
897
+ {
898
+ role: "user",
899
+ timestamp: "2026-02-03T00:00:00.000Z",
900
+ content: "Message content survives empty turns.",
901
+ },
902
+ ],
903
+ }),
904
+ "utf-8"
905
+ );
906
+
907
+ const report = await collectLocalSessionSummaries({
908
+ inputDir: dir,
909
+ includeRedactedExcerpts: true,
910
+ });
911
+
912
+ assert.equal(report.turnsParsed, 1);
913
+ assert.equal(report.drafts[0].sourceSessionRef, "9a04ff52f050d0b4");
914
+ assert.equal(report.drafts[0].excerpts?.[0].text, "Message content survives empty turns.");
915
+ });
916
+ });
917
+
918
+ test("inputDir supports tilde expansion", async () => {
919
+ const dir = await mkdtemp(path.join(os.homedir(), ".remnic-session-summary-"));
920
+ try {
921
+ await writeFile(
922
+ path.join(dir, "session.jsonl"),
923
+ JSON.stringify({
924
+ sessionKey: "home-session",
925
+ role: "assistant",
926
+ timestamp: "2026-02-03T00:00:00.000Z",
927
+ content: "Home-relative input worked.",
928
+ }),
929
+ "utf-8"
930
+ );
931
+
932
+ const report = await collectLocalSessionSummaries({
933
+ inputDir: `~/${path.basename(dir)}`,
934
+ });
935
+
936
+ assert.equal(report.filesParsed, 1);
937
+ assert.equal(report.sessionsSummarized, 1);
938
+ } finally {
939
+ await rm(dir, { recursive: true, force: true });
940
+ }
941
+ });
942
+
943
+ test("inputDir rejects symlinked roots", async () => {
944
+ await withTempDir(async (dir) => {
945
+ const target = path.join(dir, "target");
946
+ const link = path.join(dir, "link");
947
+ await mkdir(target);
948
+ await writeFile(
949
+ path.join(target, "session.jsonl"),
950
+ JSON.stringify({
951
+ sessionKey: "s",
952
+ role: "user",
953
+ timestamp: "2026-02-03T00:00:00.000Z",
954
+ content: "Should not be scanned through symlink.",
955
+ }),
956
+ "utf-8"
957
+ );
958
+ await symlink(target, link, "dir");
959
+
960
+ await assert.rejects(() => collectLocalSessionSummaries({ inputDir: link }), /symbolic link/i);
961
+ });
962
+ });
963
+
964
+ test("inputDir skips nested symlinks while scanning", async () => {
965
+ await withTempDir(async (dir) => {
966
+ const inputDir = path.join(dir, "input");
967
+ const outsideDir = path.join(dir, "outside");
968
+ await mkdir(inputDir);
969
+ await mkdir(outsideDir);
970
+ await writeFile(
971
+ path.join(outsideDir, "session.jsonl"),
972
+ JSON.stringify({
973
+ sessionKey: "outside",
974
+ role: "user",
975
+ timestamp: "2026-02-03T00:00:00.000Z",
976
+ content: "Should not be read through a nested symlink.",
977
+ }),
978
+ "utf-8"
979
+ );
980
+ await symlink(outsideDir, path.join(inputDir, "linked-outside"), "dir");
981
+
982
+ const report = await collectLocalSessionSummaries({ inputDir });
983
+
984
+ assert.equal(report.filesScanned, 0);
985
+ assert.equal(report.turnsParsed, 0);
986
+ });
987
+ });
988
+
989
+ test("maxFiles reports truncation instead of dropping files silently", async () => {
990
+ await withTempDir(async (dir) => {
991
+ for (const name of ["a", "b"]) {
992
+ await writeFile(
993
+ path.join(dir, `${name}.jsonl`),
994
+ JSON.stringify({
995
+ sessionKey: name,
996
+ role: "user",
997
+ timestamp: "2026-02-03T00:00:00.000Z",
998
+ content: name,
999
+ }),
1000
+ "utf-8"
1001
+ );
1002
+ }
1003
+
1004
+ const report = await collectLocalSessionSummaries({
1005
+ inputDir: dir,
1006
+ maxFiles: 1,
1007
+ });
1008
+
1009
+ assert.equal(report.filesScanned, 1);
1010
+ assert.equal(
1011
+ report.warnings.some((warning) => warning.code === "session-summaries.max_files_truncated"),
1012
+ true
1013
+ );
1014
+ });
1015
+ });
1016
+
1017
+ test("maxFiles stops walking after enough transcript candidates are found", async () => {
1018
+ await withTempDir(async (dir) => {
1019
+ for (const name of ["a", "b"]) {
1020
+ await writeFile(
1021
+ path.join(dir, `${name}.jsonl`),
1022
+ JSON.stringify({
1023
+ sessionKey: name,
1024
+ role: "user",
1025
+ timestamp: "2026-02-03T00:00:00.000Z",
1026
+ content: name,
1027
+ }),
1028
+ "utf-8"
1029
+ );
1030
+ }
1031
+
1032
+ const neverReadDir = path.join(dir, "z-never-read");
1033
+ await mkdir(neverReadDir);
1034
+ await chmod(neverReadDir, 0o000);
1035
+ try {
1036
+ const report = await collectLocalSessionSummaries({
1037
+ inputDir: dir,
1038
+ maxFiles: 1,
1039
+ });
1040
+
1041
+ assert.equal(report.filesScanned, 1);
1042
+ assert.equal(
1043
+ report.warnings.some((warning) => warning.code === "session-summaries.max_files_truncated"),
1044
+ true
1045
+ );
1046
+ } finally {
1047
+ await chmod(neverReadDir, 0o700);
1048
+ }
1049
+ });
1050
+ });
1051
+
1052
+ test("maxFiles uses deterministic sorted file order", async () => {
1053
+ await withTempDir(async (dir) => {
1054
+ await writeFile(
1055
+ path.join(dir, "b.jsonl"),
1056
+ JSON.stringify({
1057
+ sessionKey: "b",
1058
+ role: "user",
1059
+ timestamp: "2026-02-03T00:00:00.000Z",
1060
+ content: "B",
1061
+ }),
1062
+ "utf-8"
1063
+ );
1064
+ await writeFile(
1065
+ path.join(dir, "a.jsonl"),
1066
+ JSON.stringify({
1067
+ sessionKey: "a",
1068
+ role: "user",
1069
+ timestamp: "2026-02-03T00:00:00.000Z",
1070
+ content: "A",
1071
+ }),
1072
+ "utf-8"
1073
+ );
1074
+
1075
+ const report = await collectLocalSessionSummaries({
1076
+ inputDir: dir,
1077
+ includeRedactedExcerpts: true,
1078
+ maxFiles: 1,
1079
+ });
1080
+
1081
+ assert.equal(report.drafts[0].sourceSessionRef, "ca978112ca1bbdca");
1082
+ assert.equal(report.drafts[0].excerpts?.[0].text, "A");
1083
+ });
1084
+ });
1085
+
1086
+ test("maxSessions reports truncation instead of dropping sessions silently", async () => {
1087
+ await withTempDir(async (dir) => {
1088
+ await writeFile(
1089
+ path.join(dir, "sessions.jsonl"),
1090
+ [
1091
+ JSON.stringify({
1092
+ sessionKey: "session-a",
1093
+ role: "user",
1094
+ timestamp: "2026-02-03T00:00:00.000Z",
1095
+ content: "A",
1096
+ }),
1097
+ JSON.stringify({
1098
+ sessionKey: "session-b",
1099
+ role: "user",
1100
+ timestamp: "2026-02-03T00:01:00.000Z",
1101
+ content: "B",
1102
+ }),
1103
+ ].join("\n"),
1104
+ "utf-8"
1105
+ );
1106
+
1107
+ const report = await collectLocalSessionSummaries({
1108
+ inputDir: dir,
1109
+ maxSessions: 1,
1110
+ });
1111
+
1112
+ assert.equal(report.sessionsSummarized, 1);
1113
+ assert.equal(
1114
+ report.warnings.some((warning) => warning.code === "session-summaries.max_sessions_truncated"),
1115
+ true
1116
+ );
1117
+ });
1118
+ });
1119
+
1120
+ test("session ordering handles large sessions without spreading timestamp arrays", async () => {
1121
+ await withTempDir(async (dir) => {
1122
+ await writeFile(
1123
+ path.join(dir, "a-small.jsonl"),
1124
+ JSON.stringify({
1125
+ sessionKey: "small-session",
1126
+ role: "user",
1127
+ timestamp: "2026-02-03T00:00:00.000Z",
1128
+ content: "Small session wins the first slot.",
1129
+ }),
1130
+ "utf-8"
1131
+ );
1132
+
1133
+ const largeTurnCount = 130_000;
1134
+ const largeRows = Array.from({ length: largeTurnCount }, () =>
1135
+ JSON.stringify({
1136
+ sessionKey: "large-session",
1137
+ role: "assistant",
1138
+ timestamp: "2026-02-04T00:00:00.000Z",
1139
+ content: "Large later session.",
1140
+ })
1141
+ ).join("\n");
1142
+ await writeFile(path.join(dir, "b-large.jsonl"), largeRows, "utf-8");
1143
+
1144
+ const report = await collectLocalSessionSummaries({
1145
+ inputDir: dir,
1146
+ maxSessions: 1,
1147
+ });
1148
+
1149
+ assert.equal(report.turnsParsed, largeTurnCount + 1);
1150
+ assert.equal(report.sessionsSummarized, 1);
1151
+ assert.equal(report.drafts[0].turnCount, 1);
1152
+ assert.equal(
1153
+ report.warnings.some((warning) => warning.code === "session-summaries.max_sessions_truncated"),
1154
+ true
1155
+ );
1156
+ });
1157
+ });
1158
+
1159
+ test("duplicate transcript files are skipped by content hash", async () => {
1160
+ await withTempDir(async (dir) => {
1161
+ const content = JSON.stringify({
1162
+ sessionKey: "duplicate-session",
1163
+ role: "user",
1164
+ timestamp: "2026-02-03T00:00:00.000Z",
1165
+ content: "Count this once.",
1166
+ });
1167
+ await writeFile(path.join(dir, "a.jsonl"), content, "utf-8");
1168
+ await writeFile(path.join(dir, "b.jsonl"), content, "utf-8");
1169
+
1170
+ const report = await collectLocalSessionSummaries({ inputDir: dir });
1171
+
1172
+ assert.equal(report.filesScanned, 2);
1173
+ assert.equal(report.filesParsed, 1);
1174
+ assert.equal(report.turnsParsed, 1);
1175
+ assert.equal(report.drafts[0].turnCount, 1);
1176
+ assert.equal(
1177
+ report.warnings.some((warning) => warning.code === "session-summaries.duplicate_file_skipped"),
1178
+ true
1179
+ );
1180
+ });
1181
+ });
1182
+
1183
+ test("runLocalSessionSummaryCliCommand writes opt-in Remnic draft JSONL", async () => {
1184
+ await withTempDir(async (dir) => {
1185
+ const inputDir = path.join(dir, "transcripts");
1186
+ const memoryDir = path.join(dir, "memory");
1187
+ await mkdir(inputDir, { recursive: true });
1188
+ await writeFile(
1189
+ path.join(inputDir, "session.json"),
1190
+ JSON.stringify({
1191
+ messages: [
1192
+ {
1193
+ conversation_id: "conversation-1",
1194
+ role: "user",
1195
+ timestamp: "2026-03-04T05:00:00.000Z",
1196
+ content: "Remember project status without raw transcript storage.",
1197
+ },
1198
+ ],
1199
+ }),
1200
+ "utf-8"
1201
+ );
1202
+
1203
+ const report = await runLocalSessionSummaryCliCommand({
1204
+ inputDir,
1205
+ memoryDir,
1206
+ write: true,
1207
+ now: new Date("2026-03-04T06:00:00.000Z"),
1208
+ });
1209
+
1210
+ assert.equal(report.wroteFiles.length, 1);
1211
+ assert.match(report.wroteFiles[0], /state\/session-summary-drafts\/session-summaries-/);
1212
+ const written = await readFile(report.wroteFiles[0], "utf-8");
1213
+ assert.equal(written.trim().split("\n").length, 1);
1214
+ assert.equal(written.includes("conversation-1"), false);
1215
+ assert.equal(written.includes(inputDir), false);
1216
+ });
1217
+ });
1218
+
1219
+ test("custom redaction config adds user-defined rules", async () => {
1220
+ await withTempDir(async (dir) => {
1221
+ await writeFile(
1222
+ path.join(dir, "session.jsonl"),
1223
+ JSON.stringify({
1224
+ sessionKey: "s",
1225
+ role: "user",
1226
+ timestamp: "2026-04-05T00:00:00.000Z",
1227
+ content: "Internal ticket ABC-123 should be hidden.",
1228
+ }),
1229
+ "utf-8"
1230
+ );
1231
+
1232
+ const report = await collectLocalSessionSummaries({
1233
+ inputDir: dir,
1234
+ includeRedactedExcerpts: true,
1235
+ redactionConfig: {
1236
+ rules: [
1237
+ {
1238
+ name: "ticket",
1239
+ pattern: "\\bABC-\\d+\\b",
1240
+ replacement: "[REDACTED_TICKET]",
1241
+ },
1242
+ ],
1243
+ },
1244
+ });
1245
+
1246
+ assert.equal(report.drafts[0].excerpts?.[0].text, "Internal ticket [REDACTED_TICKET] should be hidden.");
1247
+ assert.equal(report.drafts[0].redaction.ruleNames.includes("ticket"), true);
1248
+ });
1249
+ });
1250
+
1251
+ test("custom redaction flags remain global", async () => {
1252
+ await withTempDir(async (dir) => {
1253
+ await writeFile(
1254
+ path.join(dir, "session.jsonl"),
1255
+ JSON.stringify({
1256
+ sessionKey: "s",
1257
+ role: "user",
1258
+ timestamp: "2026-04-05T00:00:00.000Z",
1259
+ content: "Hide ABC and abc.",
1260
+ }),
1261
+ "utf-8"
1262
+ );
1263
+
1264
+ const report = await collectLocalSessionSummaries({
1265
+ inputDir: dir,
1266
+ includeRedactedExcerpts: true,
1267
+ redactionConfig: {
1268
+ rules: [
1269
+ {
1270
+ name: "letters",
1271
+ pattern: "abc",
1272
+ flags: "i",
1273
+ replacement: "[REDACTED_LETTERS]",
1274
+ },
1275
+ ],
1276
+ },
1277
+ });
1278
+
1279
+ assert.equal(report.drafts[0].excerpts?.[0].text, "Hide [REDACTED_LETTERS] and [REDACTED_LETTERS].");
1280
+ assert.equal(report.drafts[0].redaction.applied, 2);
1281
+ });
1282
+ });
1283
+
1284
+ test("redaction covers quoted secret values with spaces", async () => {
1285
+ await withTempDir(async (dir) => {
1286
+ await writeFile(
1287
+ path.join(dir, "session.jsonl"),
1288
+ JSON.stringify({
1289
+ sessionKey: "s",
1290
+ role: "user",
1291
+ timestamp: "2026-04-05T00:00:00.000Z",
1292
+ content: 'Use password="correct horse battery" for the test account.',
1293
+ }),
1294
+ "utf-8"
1295
+ );
1296
+
1297
+ const report = await collectLocalSessionSummaries({
1298
+ inputDir: dir,
1299
+ includeRedactedExcerpts: true,
1300
+ });
1301
+
1302
+ assert.equal(report.drafts[0].excerpts?.[0].text, "Use [REDACTED_SECRET] for the test account.");
1303
+ });
1304
+ });
1305
+
1306
+ test("redaction covers quoted JSON secret keys", async () => {
1307
+ await withTempDir(async (dir) => {
1308
+ const passwordKey = `${"password"}`;
1309
+ const authKey = `${"Authorization"}`;
1310
+ const authScheme = `${"Bearer"}`;
1311
+ await writeFile(
1312
+ path.join(dir, "session.jsonl"),
1313
+ JSON.stringify({
1314
+ sessionKey: "s",
1315
+ role: "user",
1316
+ timestamp: "2026-04-05T00:00:00.000Z",
1317
+ content: `Use {"${passwordKey}": "correct horse battery", "${authKey}": "${authScheme} sample-json-value"} before retrying.`,
1318
+ }),
1319
+ "utf-8"
1320
+ );
1321
+
1322
+ const report = await collectLocalSessionSummaries({
1323
+ inputDir: dir,
1324
+ includeRedactedExcerpts: true,
1325
+ });
1326
+
1327
+ assert.equal(
1328
+ report.drafts[0].excerpts?.[0].text,
1329
+ "Use {[REDACTED_SECRET], [REDACTED_SECRET]} before retrying."
1330
+ );
1331
+ });
1332
+ });
1333
+
1334
+ test("redaction covers prefixed environment secret keys", async () => {
1335
+ await withTempDir(async (dir) => {
1336
+ await writeFile(
1337
+ path.join(dir, "session.jsonl"),
1338
+ JSON.stringify({
1339
+ sessionKey: "s",
1340
+ role: "user",
1341
+ timestamp: "2026-04-05T00:00:00.000Z",
1342
+ content: 'Use ANTHROPIC_API_KEY="correct horse" and ACCESS_TOKEN=abc123 before running tests.',
1343
+ }),
1344
+ "utf-8"
1345
+ );
1346
+
1347
+ const report = await collectLocalSessionSummaries({
1348
+ inputDir: dir,
1349
+ includeRedactedExcerpts: true,
1350
+ });
1351
+
1352
+ assert.equal(
1353
+ report.drafts[0].excerpts?.[0].text,
1354
+ "Use [REDACTED_SECRET] and [REDACTED_SECRET] before running tests."
1355
+ );
1356
+ });
1357
+ });
1358
+
1359
+ test("redaction covers common access and private key assignments", async () => {
1360
+ await withTempDir(async (dir) => {
1361
+ await writeFile(
1362
+ path.join(dir, "session.jsonl"),
1363
+ JSON.stringify({
1364
+ sessionKey: "s",
1365
+ role: "user",
1366
+ timestamp: "2026-04-05T00:00:00.000Z",
1367
+ content:
1368
+ 'Use AWS_SECRET_ACCESS_KEY=aws-secret, ACCESS_KEY=plain-access, and PRIVATE_KEY="private material" for tests.',
1369
+ }),
1370
+ "utf-8"
1371
+ );
1372
+
1373
+ const report = await collectLocalSessionSummaries({
1374
+ inputDir: dir,
1375
+ includeRedactedExcerpts: true,
1376
+ });
1377
+
1378
+ assert.equal(
1379
+ report.drafts[0].excerpts?.[0].text,
1380
+ "Use [REDACTED_SECRET], [REDACTED_SECRET], and [REDACTED_SECRET] for tests."
1381
+ );
1382
+ });
1383
+ });
1384
+
1385
+ test("redaction covers bearer Authorization header values", async () => {
1386
+ await withTempDir(async (dir) => {
1387
+ const authHeader = `${"Authorization"}: ${"Bearer"} sample-bearer-value`;
1388
+ await writeFile(
1389
+ path.join(dir, "session.jsonl"),
1390
+ JSON.stringify({
1391
+ sessionKey: "s",
1392
+ role: "user",
1393
+ timestamp: "2026-04-05T00:00:00.000Z",
1394
+ content: `Run curl -H "${authHeader}" before retrying.`,
1395
+ }),
1396
+ "utf-8"
1397
+ );
1398
+
1399
+ const report = await collectLocalSessionSummaries({
1400
+ inputDir: dir,
1401
+ includeRedactedExcerpts: true,
1402
+ });
1403
+
1404
+ assert.equal(report.drafts[0].excerpts?.[0].text, 'Run curl -H "[REDACTED_SECRET]" before retrying.');
1405
+ });
1406
+ });
1407
+
1408
+ test("redaction covers Basic Authorization header values", async () => {
1409
+ await withTempDir(async (dir) => {
1410
+ const authHeader = `${"Authorization"}: ${"Basic"} sample-basic-value`;
1411
+ await writeFile(
1412
+ path.join(dir, "session.jsonl"),
1413
+ JSON.stringify({
1414
+ sessionKey: "s",
1415
+ role: "user",
1416
+ timestamp: "2026-04-05T00:00:00.000Z",
1417
+ content: `Run curl -H "${authHeader}" before retrying.`,
1418
+ }),
1419
+ "utf-8"
1420
+ );
1421
+
1422
+ const report = await collectLocalSessionSummaries({
1423
+ inputDir: dir,
1424
+ includeRedactedExcerpts: true,
1425
+ });
1426
+
1427
+ assert.equal(report.drafts[0].excerpts?.[0].text, 'Run curl -H "[REDACTED_SECRET]" before retrying.');
1428
+ });
1429
+ });
1430
+
1431
+ test("custom redaction rules must be an array", async () => {
1432
+ assert.throws(
1433
+ () => compileRedactionRules({ rules: { name: "bad", pattern: "bad" } } as never),
1434
+ /redaction rules must be an array/
1435
+ );
1436
+ });
1437
+
1438
+ test("redaction covers common absolute POSIX paths", async () => {
1439
+ await withTempDir(async (dir) => {
1440
+ await writeFile(
1441
+ path.join(dir, "session.jsonl"),
1442
+ JSON.stringify({
1443
+ sessionKey: "s",
1444
+ role: "user",
1445
+ timestamp: "2026-04-05T00:00:00.000Z",
1446
+ content:
1447
+ "Inspect /workspace/remnic/secrets, /root/.ssh/config, /home/alex/.env.local, /Users/alex/My Documents/secret.txt, /Users/alex/secret file.txt, /Users/alex/Project (Client)/secret.txt, /secrets, and /tmp/customer-notes.pdf.",
1448
+ }),
1449
+ "utf-8"
1450
+ );
1451
+
1452
+ const report = await collectLocalSessionSummaries({
1453
+ inputDir: dir,
1454
+ includeRedactedExcerpts: true,
1455
+ });
1456
+
1457
+ assert.equal(
1458
+ report.drafts[0].excerpts?.[0].text,
1459
+ "Inspect [REDACTED_PATH], [REDACTED_PATH], [REDACTED_PATH], [REDACTED_PATH], [REDACTED_PATH], [REDACTED_PATH], [REDACTED_PATH], and [REDACTED_PATH]."
1460
+ );
1461
+ });
1462
+ });
1463
+
1464
+ test("redaction covers POSIX paths with common non-alphanumeric characters", async () => {
1465
+ await withTempDir(async (dir) => {
1466
+ await writeFile(
1467
+ path.join(dir, "session.jsonl"),
1468
+ JSON.stringify({
1469
+ sessionKey: "s",
1470
+ role: "user",
1471
+ timestamp: "2026-04-05T00:00:00.000Z",
1472
+ content:
1473
+ "Inspect /Users/alex/client+secret/file.txt, /Users/alex/team@client/config.json, and /Users/alex/caf\u00e9/notes.md.",
1474
+ }),
1475
+ "utf-8"
1476
+ );
1477
+
1478
+ const report = await collectLocalSessionSummaries({
1479
+ inputDir: dir,
1480
+ includeRedactedExcerpts: true,
1481
+ });
1482
+
1483
+ assert.equal(
1484
+ report.drafts[0].excerpts?.[0].text,
1485
+ "Inspect [REDACTED_PATH], [REDACTED_PATH], and [REDACTED_PATH]."
1486
+ );
1487
+ });
1488
+ });
1489
+
1490
+ test("redaction covers home-relative POSIX paths with spaced filenames", async () => {
1491
+ await withTempDir(async (dir) => {
1492
+ await writeFile(
1493
+ path.join(dir, "session.jsonl"),
1494
+ JSON.stringify({
1495
+ sessionKey: "s",
1496
+ role: "user",
1497
+ timestamp: "2026-04-05T00:00:00.000Z",
1498
+ content: "Inspect ~/My Documents/secret file.txt before replying.",
1499
+ }),
1500
+ "utf-8"
1501
+ );
1502
+
1503
+ const report = await collectLocalSessionSummaries({
1504
+ inputDir: dir,
1505
+ includeRedactedExcerpts: true,
1506
+ });
1507
+
1508
+ assert.equal(report.drafts[0].excerpts?.[0].text, "Inspect [REDACTED_PATH] before replying.");
1509
+ });
1510
+ });
1511
+
1512
+ test("redaction covers extensionless POSIX paths with spaced names", async () => {
1513
+ await withTempDir(async (dir) => {
1514
+ await writeFile(
1515
+ path.join(dir, "session.jsonl"),
1516
+ JSON.stringify({
1517
+ sessionKey: "s",
1518
+ role: "user",
1519
+ timestamp: "2026-04-05T00:00:00.000Z",
1520
+ content: "Open /Users/alex/Secret Project before replying and ~/My Documents/Client Folder after.",
1521
+ }),
1522
+ "utf-8"
1523
+ );
1524
+
1525
+ const report = await collectLocalSessionSummaries({
1526
+ inputDir: dir,
1527
+ includeRedactedExcerpts: true,
1528
+ });
1529
+
1530
+ assert.equal(
1531
+ report.drafts[0].excerpts?.[0].text,
1532
+ "Open [REDACTED_PATH] before replying and [REDACTED_PATH] after."
1533
+ );
1534
+ });
1535
+ });
1536
+
1537
+ test("redaction covers POSIX paths with connector words in spaced names", async () => {
1538
+ await withTempDir(async (dir) => {
1539
+ await writeFile(
1540
+ path.join(dir, "session.jsonl"),
1541
+ JSON.stringify({
1542
+ sessionKey: "s",
1543
+ role: "user",
1544
+ timestamp: "2026-04-05T00:00:00.000Z",
1545
+ content:
1546
+ 'Open /Users/alex/Research and Development before replying and "/Users/alex/Research and Development" after.',
1547
+ }),
1548
+ "utf-8"
1549
+ );
1550
+
1551
+ const report = await collectLocalSessionSummaries({
1552
+ inputDir: dir,
1553
+ includeRedactedExcerpts: true,
1554
+ });
1555
+
1556
+ assert.equal(
1557
+ report.drafts[0].excerpts?.[0].text,
1558
+ 'Open [REDACTED_PATH] before replying and "[REDACTED_PATH]" after.'
1559
+ );
1560
+ });
1561
+ });
1562
+
1563
+ test("redaction covers single-segment POSIX paths with spaced names", async () => {
1564
+ await withTempDir(async (dir) => {
1565
+ await writeFile(
1566
+ path.join(dir, "session.jsonl"),
1567
+ JSON.stringify({
1568
+ sessionKey: "s",
1569
+ role: "user",
1570
+ timestamp: "2026-04-05T00:00:00.000Z",
1571
+ content: "Inspect /Secret Project before replying and /Secret Project.txt after.",
1572
+ }),
1573
+ "utf-8"
1574
+ );
1575
+
1576
+ const report = await collectLocalSessionSummaries({
1577
+ inputDir: dir,
1578
+ includeRedactedExcerpts: true,
1579
+ });
1580
+
1581
+ assert.equal(
1582
+ report.drafts[0].excerpts?.[0].text,
1583
+ "Inspect [REDACTED_PATH] before replying and [REDACTED_PATH] after."
1584
+ );
1585
+ });
1586
+ });
1587
+
1588
+ test("redaction covers quoted extensionless POSIX paths with spaced names", async () => {
1589
+ await withTempDir(async (dir) => {
1590
+ await writeFile(
1591
+ path.join(dir, "session.jsonl"),
1592
+ JSON.stringify({
1593
+ sessionKey: "s",
1594
+ role: "user",
1595
+ timestamp: "2026-04-05T00:00:00.000Z",
1596
+ content: 'Inspect "/Users/alex/Secret Project" and "~/Client Folder" before replying.',
1597
+ }),
1598
+ "utf-8"
1599
+ );
1600
+
1601
+ const report = await collectLocalSessionSummaries({
1602
+ inputDir: dir,
1603
+ includeRedactedExcerpts: true,
1604
+ });
1605
+
1606
+ assert.equal(
1607
+ report.drafts[0].excerpts?.[0].text,
1608
+ 'Inspect "[REDACTED_PATH]" and "[REDACTED_PATH]" before replying.'
1609
+ );
1610
+ });
1611
+ });
1612
+
1613
+ test("redaction covers UNC Windows paths", async () => {
1614
+ await withTempDir(async (dir) => {
1615
+ await writeFile(
1616
+ path.join(dir, "session.jsonl"),
1617
+ JSON.stringify({
1618
+ sessionKey: "s",
1619
+ role: "user",
1620
+ timestamp: "2026-04-05T00:00:00.000Z",
1621
+ content:
1622
+ "Inspect C:\\Users\\alex\\secret file.txt, C:\\Users\\alex\\Secret Project before replying, \\\\server\\share\\client\\secret.txt, and \\\\server\\share\\client\\Secret Project before replying.",
1623
+ }),
1624
+ "utf-8"
1625
+ );
1626
+
1627
+ const report = await collectLocalSessionSummaries({
1628
+ inputDir: dir,
1629
+ includeRedactedExcerpts: true,
1630
+ });
1631
+
1632
+ assert.equal(
1633
+ report.drafts[0].excerpts?.[0].text,
1634
+ "Inspect [REDACTED_PATH], [REDACTED_PATH], [REDACTED_PATH], and [REDACTED_PATH]"
1635
+ );
1636
+ });
1637
+ });
1638
+
1639
+ test("redaction covers quoted extensionless Windows paths with spaced names", async () => {
1640
+ await withTempDir(async (dir) => {
1641
+ await writeFile(
1642
+ path.join(dir, "session.jsonl"),
1643
+ JSON.stringify({
1644
+ sessionKey: "s",
1645
+ role: "user",
1646
+ timestamp: "2026-04-05T00:00:00.000Z",
1647
+ content:
1648
+ 'Inspect "C:\\Users\\alex\\Secret Project" and "\\\\server\\share\\client\\Secret Project" before replying.',
1649
+ }),
1650
+ "utf-8"
1651
+ );
1652
+
1653
+ const report = await collectLocalSessionSummaries({
1654
+ inputDir: dir,
1655
+ includeRedactedExcerpts: true,
1656
+ });
1657
+
1658
+ assert.equal(
1659
+ report.drafts[0].excerpts?.[0].text,
1660
+ 'Inspect "[REDACTED_PATH]" and "[REDACTED_PATH]" before replying.'
1661
+ );
1662
+ });
1663
+ });
1664
+
1665
+ test("redaction covers UNC share roots", async () => {
1666
+ await withTempDir(async (dir) => {
1667
+ await writeFile(
1668
+ path.join(dir, "session.jsonl"),
1669
+ JSON.stringify({
1670
+ sessionKey: "s",
1671
+ role: "user",
1672
+ timestamp: "2026-04-05T00:00:00.000Z",
1673
+ content: 'Inspect \\\\server\\share and "\\\\server\\quoted-share" before replying.',
1674
+ }),
1675
+ "utf-8"
1676
+ );
1677
+
1678
+ const report = await collectLocalSessionSummaries({
1679
+ inputDir: dir,
1680
+ includeRedactedExcerpts: true,
1681
+ });
1682
+
1683
+ assert.equal(
1684
+ report.drafts[0].excerpts?.[0].text,
1685
+ 'Inspect [REDACTED_PATH] and "[REDACTED_PATH]" before replying.'
1686
+ );
1687
+ });
1688
+ });
1689
+
1690
+ test("redaction config file must be a JSON object", async () => {
1691
+ await withTempDir(async (dir) => {
1692
+ const inputDir = path.join(dir, "transcripts");
1693
+ const configPath = path.join(dir, "redaction.json");
1694
+ await mkdir(inputDir);
1695
+ await writeFile(
1696
+ path.join(inputDir, "session.jsonl"),
1697
+ JSON.stringify({
1698
+ sessionKey: "s",
1699
+ role: "user",
1700
+ timestamp: "2026-04-05T00:00:00.000Z",
1701
+ content: "hello",
1702
+ }),
1703
+ "utf-8"
1704
+ );
1705
+ await writeFile(configPath, "[]", "utf-8");
1706
+
1707
+ await assert.rejects(
1708
+ () =>
1709
+ runLocalSessionSummaryCliCommand({
1710
+ inputDir,
1711
+ redactionConfigPath: configPath,
1712
+ }),
1713
+ /redaction config must be a JSON object/
1714
+ );
1715
+ });
1716
+ });
1717
+
1718
+ test("strict mode rejects malformed JSONL", async () => {
1719
+ await withTempDir(async (dir) => {
1720
+ await writeFile(path.join(dir, "bad.jsonl"), "{not json}\n", "utf-8");
1721
+ await assert.rejects(
1722
+ () => collectLocalSessionSummaries({ inputDir: dir, strict: true }),
1723
+ /invalid JSONL line 1 in fileRef [a-f0-9]+/i
1724
+ );
1725
+ });
1726
+ });
1727
+
1728
+ test("malformed JSON warnings do not include transcript snippets", async () => {
1729
+ await withTempDir(async (dir) => {
1730
+ await writeFile(path.join(dir, "bad.json"), '{"content":"PRIVATE_VALUE_SHOULD_NOT_APPEAR",', "utf-8");
1731
+
1732
+ const report = await collectLocalSessionSummaries({ inputDir: dir });
1733
+
1734
+ assert.equal(report.warnings.length, 1);
1735
+ assert.match(report.warnings[0].message, /invalid generic-jsonl JSON in fileRef [a-f0-9]+; skipping file/i);
1736
+ assert.equal(report.warnings[0].message.includes("PRIVATE_VALUE_SHOULD_NOT_APPEAR"), false);
1737
+ });
1738
+ });
1739
+
1740
+ test("strict mode rejects malformed JSON with fileRef context", async () => {
1741
+ await withTempDir(async (dir) => {
1742
+ await writeFile(path.join(dir, "bad.json"), "{not json}\n", "utf-8");
1743
+ await assert.rejects(
1744
+ () => collectLocalSessionSummaries({ inputDir: dir, strict: true }),
1745
+ /invalid generic-jsonl JSON in fileRef [a-f0-9]+/i
1746
+ );
1747
+ });
1748
+ });