supercode-harness 0.4.10

The optional native Supercode agent and tool harness
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
//! Acceptance tests for TR-2 (T15 — content-hash duplicate-output dedup):
//! `ReductionKind::DuplicateOutput`.
//!
//! Each test is labeled with the TR-2.md acceptance-criterion id it proves.
//! Exercises the real pipeline paths (`project`/`project_messages`,
//! `invert`, `rehydrate::expand_reduction`) rather than hand-rolled
//! simulations, following the idiom of `reduce_projection.rs`/`reduce_loop.rs`.

use supercode_harness::reduce::rehydrate::{expand_reduction, sidecar_search};
use supercode_harness::reduce::{
    invert, project, project_messages, stub, ReductionKind, ReductionLog, ReductionPolicy,
};
use supercode_harness::session::Session;
use supercode_harness::{ChatMessage, FunctionCall, Role, ToolCall};

/// Wraps `msgs` in a bare `Session` (mirrors `reduce_projection.rs::session_of`)
/// so it can serve as the `sidecar_session` argument `invert` needs — the
/// sidecar is just the unreduced message list.
fn session_of(msgs: Vec<ChatMessage>) -> Session {
    let mut session = Session::from_claude_code_str("").unwrap();
    session.messages = msgs;
    session
}

/// A deterministic all-ASCII filler string of exactly `len` bytes.
fn filler(len: usize) -> String {
    (0..len).map(|i| (b'a' + (i % 26) as u8) as char).collect()
}

/// A small transcript: a user turn, then `n` (assistant tool-call, tool
/// result) turns, each tool result carrying `contents[i]` verbatim. Mirrors
/// the shape real sessions have (assistant call immediately followed by its
/// tool result) without needing full `ToolCall` plumbing, since dedup only
/// ever looks at `Role::Tool` messages.
fn transcript(contents: &[&str]) -> Vec<ChatMessage> {
    let mut msgs = vec![ChatMessage::user("please investigate")];
    for (i, c) in contents.iter().enumerate() {
        msgs.push(ChatMessage::assistant(format!("running check {i}")));
        msgs.push(ChatMessage::tool_result(
            format!("call_{i}"),
            "bash",
            c.to_string(),
        ));
    }
    msgs
}

fn no_protection_policy() -> ReductionPolicy {
    ReductionPolicy {
        protect_last_n_tool_results: 0,
        ..ReductionPolicy::default()
    }
}

fn wire_bytes(msgs: &[ChatMessage]) -> usize {
    msgs.iter()
        .map(|m| serde_json::to_string(m).map(|s| s.len()).unwrap_or(0))
        .sum()
}

// ---- dev/01: three byte-identical outputs -> one full + two stubs, invertible ----

#[test]
fn three_identical_outputs_dedup_to_one_full_and_two_stubs_and_invert_restores_all() {
    let body = "git status\n".repeat(40); // well over the 256B default floor
    let msgs = transcript(&[&body, &body, &body]);
    let session = session_of(msgs.clone());

    let (view, log) = project(&session, &no_protection_policy(), &ReductionLog::default());

    let dups: Vec<_> = log
        .reductions
        .iter()
        .filter(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. }))
        .collect();
    assert_eq!(
        dups.len(),
        2,
        "three identical outputs must yield exactly two DuplicateOutput reductions: {:?}",
        log.reductions
    );

    // Tool-result indices in `msgs`: 2, 4, 6 (see `transcript`).
    let tool_idxs = [2usize, 4, 6];

    // The first (canonical) instance stays full.
    assert_eq!(
        view[tool_idxs[0]].content.as_deref(),
        Some(body.as_str()),
        "the canonical (first) instance must remain full"
    );

    // The two later instances are stubs naming the canonical address and
    // carrying the `duplicate` stub kind.
    for &idx in &tool_idxs[1..] {
        let content = view[idx].content.as_deref().unwrap();
        assert!(
            content.starts_with("[sc-reduced duplicate"),
            "message {idx} must carry a `duplicate` stub: {content}"
        );
        let (kind, _, summary) = stub::parse(content).expect("stub must parse");
        assert_eq!(kind, stub::Kind::Duplicate);
        assert!(
            summary.contains(&format!("msg #{}", tool_idxs[0])),
            "stub must name the canonical address: {summary}"
        );
    }
    for r in &dups {
        match &r.kind {
            ReductionKind::DuplicateOutput {
                canonical,
                original_bytes,
            } => {
                assert_eq!(canonical.index, tool_idxs[0]);
                assert_eq!(canonical.role, Role::Tool);
                assert_eq!(*original_bytes, body.len());
            }
            _ => unreachable!(),
        }
    }

    // Total projected bytes strictly less than input.
    let full_bytes = wire_bytes(&session.messages);
    let reduced_bytes = wire_bytes(&view);
    assert!(
        reduced_bytes < full_bytes,
        "reduced {reduced_bytes} must be < full {full_bytes}"
    );

    // invert() restores all three byte-exact (roundtrip suite, A6).
    let inverted = invert(&view, &log, &session).unwrap();
    assert_eq!(inverted.len(), session.messages.len());
    for &idx in &tool_idxs {
        assert_eq!(
            inverted[idx].content.as_deref(),
            Some(body.as_str()),
            "invert must restore message {idx} byte-exact"
        );
    }
}

// ---- dev/02: near-duplicates (1-byte difference) are NEVER deduped ----

#[test]
fn near_duplicate_one_byte_different_is_never_deduped() {
    let a = filler(500);
    let mut b = a.clone();
    // Flip exactly one byte (stays valid ASCII/UTF-8).
    unsafe {
        let bytes = b.as_bytes_mut();
        bytes[250] = b'Z';
    }
    assert_ne!(a, b);
    assert_eq!(a.len(), b.len());

    let msgs = transcript(&[&a, &b]);
    let session = session_of(msgs.clone());
    let (view, log) = project(&session, &no_protection_policy(), &ReductionLog::default());

    assert!(
        !log.reductions
            .iter()
            .any(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. })),
        "a 1-byte difference must never be deduped: {:?}",
        log.reductions
    );
    // Both tool results survive with their full, distinct original content.
    assert_eq!(view[2].content.as_deref(), Some(a.as_str()));
    assert_eq!(view[4].content.as_deref(), Some(b.as_str()));
}

// ---- dev/03: below the savings-floor threshold, never deduped (property over sizes) ----

#[test]
fn outputs_below_the_savings_floor_are_never_deduped_across_a_range_of_sizes() {
    let threshold = ReductionPolicy::default().duplicate_output_min_bytes;
    // Boundary-focused sweep: comfortably below, at, and comfortably above
    // the floor, plus a couple of arbitrary in-between sizes -- the
    // "property test over sizes" the AC calls for.
    for size in [
        0usize,
        1,
        threshold / 2,
        threshold - 1,
        threshold,
        threshold + 1,
        threshold * 3,
    ] {
        let body = filler(size);
        let msgs = transcript(&[&body, &body]);
        let session = session_of(msgs);
        let (_, log) = project(&session, &no_protection_policy(), &ReductionLog::default());

        let deduped = log
            .reductions
            .iter()
            .any(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. }));

        if size < threshold {
            assert!(
                !deduped,
                "size {size} is below the {threshold}B floor and must never be deduped"
            );
        } else {
            assert!(
                deduped,
                "size {size} is at/above the {threshold}B floor and must be deduped"
            );
        }
    }
}

// ---- ordering: TR-2 runs BEFORE A7, so a duplicate is deduped, never independently truncated ----

#[test]
fn dedup_runs_before_a7_truncation_so_duplicates_are_never_independently_truncated() {
    // Three identical, LARGE (50 KB) outputs -- comfortably over both the
    // default A7 trigger (8,192B) and the TR-2 savings floor. If A7 ran
    // first (or TR-2's own candidate-claiming didn't exclude A7's later
    // scan), every occurrence would independently become
    // `ToolOutputTruncated` with its own unique id baked into the kept
    // prefix's placeholder -- exactly what "two identical 200KB outputs get
    // truncated differently and never match" (TR-2.md) warns against.
    let body = filler(50_000);
    let msgs = transcript(&[&body, &body, &body]);
    let session = session_of(msgs.clone());

    // Default trigger triggers on ALL three (50KB > 8,192B); no protection,
    // so nothing shields any of them from becoming *some* new reduction.
    let (view, log) = project(&session, &no_protection_policy(), &ReductionLog::default());

    assert_eq!(
        log.reductions.len(),
        3,
        "canonical (truncated by A7) + two duplicates: {:?}",
        log.reductions
    );

    let tool_idxs = [2usize, 4, 6];

    // The canonical (first, oldest) instance is independently oversized and
    // so is truncated by A7 -- that's expected and correct; TR-2 only ever
    // claims LATER occurrences.
    let canonical_kind = &log
        .reductions
        .iter()
        .find(|r| r.ptr.addr.index == tool_idxs[0])
        .expect("canonical must have its own reduction")
        .kind;
    assert!(
        matches!(canonical_kind, ReductionKind::ToolOutputTruncated { .. }),
        "canonical must be independently truncated by A7: {canonical_kind:?}"
    );

    // The two LATER, byte-identical occurrences must be `DuplicateOutput`,
    // never `ToolOutputTruncated` -- proof that TR-2's pass claimed them
    // before A7's own candidate scan ran.
    for &idx in &tool_idxs[1..] {
        let r = log
            .reductions
            .iter()
            .find(|r| r.ptr.addr.index == idx)
            .unwrap_or_else(|| panic!("message {idx} must have a reduction: {:?}", log.reductions));
        assert!(
            matches!(r.kind, ReductionKind::DuplicateOutput { .. }),
            "message {idx} must be deduped (TR-2), not independently truncated (A7): {:?}",
            r.kind
        );
    }

    // Round-trips byte-exact regardless (A6).
    let inverted = invert(&view, &log, &session).unwrap();
    for &idx in &tool_idxs {
        assert_eq!(inverted[idx].content.as_deref(), Some(body.as_str()));
    }
}

// ---- dev/04: canonical later truncated (A7) -- the duplicate still resolves from the sidecar ----

#[test]
fn duplicate_still_resolves_after_the_canonical_is_later_truncated_by_a7() {
    let body = filler(2_000); // below the default 8,192B A7 trigger for now
                              // Six tool results: [dup-body, dup-body, filler, filler, filler, filler]
                              // -- the two identical ones sit oldest, well outside any protected tail
                              // once more turns accumulate.
    let others = "ok";
    let msgs = transcript(&[&body, &body, others, others, others, others]);
    let session = session_of(msgs.clone());

    // Turn 1: dedup fires (both instances are below the trigger, so A7 never
    // touches them yet); protect_last_n stays at its default (3), which
    // still leaves indices 0/1 (the identical pair) outside the protected
    // tail since there are 6 tool results total.
    let (_, log1) = project(
        &session,
        &ReductionPolicy::default(),
        &ReductionLog::default(),
    );
    let dup = log1
        .reductions
        .iter()
        .find(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. }))
        .expect("turn 1 must produce a DuplicateOutput reduction");
    let dup_id = dup.id.clone();
    assert!(
        !log1
            .reductions
            .iter()
            .any(|r| matches!(r.kind, ReductionKind::ToolOutputTruncated { .. })),
        "nothing should be independently truncated yet: {:?}",
        log1.reductions
    );

    // Turn 2: re-project the SAME messages with a much lower A7 trigger, so
    // the (still full, non-duplicate) canonical instance now crosses it and
    // gets truncated -- the "canonical-then-reduced" case (dev/04).
    let policy2 = ReductionPolicy {
        tool_output_trigger_bytes: 100,
        tool_output_keep_bytes: 32,
        protect_last_n_tool_results: 0,
        ..ReductionPolicy::default()
    };
    let (view2, log2) = project(&session, &policy2, &log1);

    let canonical_reduction = log2
        .reductions
        .iter()
        .find(
            |r| r.ptr.addr.index == 2, /* first tool result's msgs index */
        )
        .expect("canonical must now have its own reduction");
    assert!(
        matches!(
            canonical_reduction.kind,
            ReductionKind::ToolOutputTruncated { .. }
        ),
        "canonical must now be A7-truncated: {:?}",
        canonical_reduction.kind
    );

    // The original DuplicateOutput reduction survives verbatim (prefix
    // stability) -- same id, same placeholder.
    let dup2 = log2
        .reductions
        .iter()
        .find(|r| r.id == dup_id)
        .expect("the duplicate reduction must still be present");
    assert_eq!(dup2, dup, "the duplicate reduction must reproduce verbatim");

    // `expand_reduction` (TR-1) on the duplicate's id still returns the FULL
    // original bytes, resolved from `minted_view` (here, the unreduced
    // `session.messages` slice the reductions were minted against) --
    // completely independent of the canonical's now-truncated state.
    let outcome = expand_reduction(&log2, &session.messages, None, &dup_id, None).unwrap();
    assert_eq!(
        outcome.content, body,
        "expand_reduction must return the full original bytes"
    );
    assert_eq!(outcome.total_bytes, body.len());

    // invert() also still restores the duplicate's message byte-exact.
    let inverted = invert(&view2, &log2, &session).unwrap();
    assert_eq!(inverted[4].content.as_deref(), Some(body.as_str()));
}

// ---- dev/04 (A10 variant): canonical later cleared -- the duplicate still resolves ----

#[test]
fn duplicate_still_resolves_after_the_canonical_is_later_cleared_by_a10() {
    let body = filler(500);

    // Layout: the canonical sits early (msg #2); the duplicate sits near
    // the very end (last message), separated by 10 filler (user, assistant)
    // turns with no other tool results at all. A10's "keep the newest few
    // turns" window (below) is sized so the clear range swallows the
    // canonical but never reaches the duplicate.
    let mut msgs = vec![ChatMessage::user("investigate")];
    msgs.push(ChatMessage::assistant("run 1"));
    msgs.push(ChatMessage::tool_result("c0", "bash", body.clone())); // idx 2: canonical
    for i in 0..10 {
        msgs.push(ChatMessage::user(format!("follow-up {i}")));
        msgs.push(ChatMessage::assistant(format!("reply {i}")));
    }
    msgs.push(ChatMessage::assistant("run 2"));
    let dup_idx = msgs.len();
    msgs.push(ChatMessage::tool_result("c1", "bash", body.clone())); // idx 24: duplicate
    assert_eq!(
        dup_idx, 24,
        "test layout assumption -- update the math below if this changes"
    );

    let session = session_of(msgs.clone());

    let (_, log1) = project(&session, &no_protection_policy(), &ReductionLog::default());
    let dup = log1
        .reductions
        .iter()
        .find(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. }))
        .expect("turn 1 must dedup the identical pair");
    let dup_id = dup.id.clone();
    match dup.kind {
        ReductionKind::DuplicateOutput { canonical, .. } => assert_eq!(canonical.index, 2),
        _ => unreachable!(),
    }
    assert_eq!(dup.ptr.addr.index, dup_idx);

    // Turn 2: enable A10 turn-clearing. `keep_recent = (6/2).max(2) = 3`, so
    // `cut = 25 - 3 = 22` -- the surviving tail is messages [22, 23, 24],
    // which includes the duplicate (24) but not the canonical (2).
    let policy2 = ReductionPolicy {
        clear_turns_older_than: Some(6),
        protect_last_n_tool_results: 0,
        ..ReductionPolicy::default()
    };
    let (view2, log2) = project(&session, &policy2, &log1);

    let cleared = log2
        .reductions
        .iter()
        .find(|r| matches!(r.kind, ReductionKind::TurnsCleared { .. }))
        .expect("turn 2 must have established a TurnsCleared range");
    match cleared.kind {
        ReductionKind::TurnsCleared { first, last, .. } => {
            assert_eq!(first, 0);
            assert!(
                last >= 2 && last < dup_idx,
                "the cleared range must cover the canonical (2) but not the duplicate ({dup_idx}): last={last}"
            );
        }
        _ => unreachable!(),
    }

    // The DUPLICATE's own reduction (addressed outside the cleared range)
    // must survive completely untouched.
    let dup2 = log2
        .reductions
        .iter()
        .find(|r| r.id == dup_id)
        .expect("the duplicate reduction must survive A10 clearing the canonical");
    assert_eq!(dup2, dup);

    // `expand_reduction` still recovers the duplicate's full original bytes
    // from `session.messages` (the sidecar-equivalent, untouched slice) --
    // independent of the canonical having just been swallowed whole.
    let outcome = expand_reduction(&log2, &session.messages, None, &dup_id, None).unwrap();
    assert_eq!(outcome.content, body);

    // Search visibility flips with the canonical's state (fix pass, A10
    // variant): before the clear the canonical is fully visible, so the
    // shared bytes are not reported at all; after it, the duplicate's
    // content is genuinely hidden and reports under its own id.
    let needle = &body[..24]; // a distinctive prefix of the shared content
    let pre = sidecar_search(&log1, &session.messages, None, needle).unwrap();
    assert_eq!(
        pre.total_matches, 0,
        "canonical fully visible -> nothing reported: {pre:?}"
    );
    let post = sidecar_search(&log2, &session.messages, None, needle).unwrap();
    assert!(
        post.matches.iter().any(|m| m.reduction_id == dup_id),
        "canonical cleared -> the duplicate must be searchable: {post:?}"
    );

    // invert() restores the duplicate's message byte-exact too.
    let inverted = invert(&view2, &log2, &session).unwrap();
    assert_eq!(inverted[dup_idx].content.as_deref(), Some(body.as_str()));
}

// ---- determinism / prefix stability, mirroring reduce_projection.rs's A5 tests ----

#[test]
fn dedup_is_deterministic_and_prefix_stable() {
    let body = filler(1000);
    let msgs = transcript(&[&body, &body, &body]);
    let session = session_of(msgs);
    let policy = no_protection_policy();

    let (view1, log1) = project(&session, &policy, &ReductionLog::default());
    let (view2, log2) = project(&session, &policy, &ReductionLog::default());
    assert_eq!(log1, log2, "project() must be deterministic for TR-2 too");
    assert_eq!(
        view1.iter().map(|m| m.content.clone()).collect::<Vec<_>>(),
        view2.iter().map(|m| m.content.clone()).collect::<Vec<_>>()
    );

    // Append two synthetic turns and re-project with the prior log: the
    // existing DuplicateOutput reductions must reproduce byte-for-byte, and
    // no new reduction should appear (nothing new triggers).
    let mut grown = session.clone();
    grown.messages.push(ChatMessage::user("one more thing"));
    grown.messages.push(ChatMessage::assistant("sure"));
    let (_, log3) = project(&grown, &policy, &log1);
    assert_eq!(
        log3.reductions.len(),
        log1.reductions.len(),
        "re-projecting with a stable prior log must not invent new reductions"
    );
    for r in &log1.reductions {
        let r3 = log3
            .reductions
            .iter()
            .find(|x| x.id == r.id)
            .expect("prior reduction id must survive re-projection");
        assert_eq!(r3, r, "reduction {} must reproduce verbatim", r.id);
    }
}

// ---- project_messages() (the lower-level entry point Agent::run_loop uses) works identically ----

#[test]
fn dedup_works_through_project_messages_directly() {
    let body = filler(300);
    let msgs = transcript(&[&body, &body]);
    let (view, log) = project_messages(&msgs, &no_protection_policy(), &ReductionLog::default());
    assert_eq!(log.reductions.len(), 1);
    assert!(matches!(
        log.reductions[0].kind,
        ReductionKind::DuplicateOutput { .. }
    ));
    assert_eq!(view[2].content.as_deref(), Some(body.as_str()));
    assert!(view[4]
        .content
        .as_deref()
        .unwrap()
        .starts_with("[sc-reduced duplicate"));
}

// ---- fix pass: negative-savings guard (mint-time, structural) ----

/// Like [`transcript`], but with a caller-chosen tool name on every result.
fn transcript_named(contents: &[&str], name: &str) -> Vec<ChatMessage> {
    let mut msgs = vec![ChatMessage::user("please investigate")];
    for (i, c) in contents.iter().enumerate() {
        msgs.push(ChatMessage::assistant(format!("running check {i}")));
        msgs.push(ChatMessage::tool_result(
            format!("call_{i}"),
            name,
            c.to_string(),
        ));
    }
    msgs
}

#[test]
fn long_tool_name_near_the_floor_never_mints_a_negative_savings_stub() {
    // 300 bytes: over the 256B savings floor, so the floor alone does NOT
    // protect this pair -- but a 400-byte tool name pushes the stub's own
    // length past the content it would replace. Without the mint-time
    // `placeholder.len() < original_bytes` guard, this WOULD mint a
    // negative-savings stub (mutation check: comment the guard out and this
    // test fails).
    let body = filler(300);
    let long_name = "x".repeat(400);
    let msgs = transcript_named(&[&body, &body], &long_name);
    let (view, log) = project_messages(&msgs, &no_protection_policy(), &ReductionLog::default());
    assert!(
        !log.reductions
            .iter()
            .any(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. })),
        "a stub longer than the content it replaces must never be minted: {:?}",
        log.reductions
    );
    // Both instances survive with their full original content.
    assert_eq!(view[2].content.as_deref(), Some(body.as_str()));
    assert_eq!(view[4].content.as_deref(), Some(body.as_str()));

    // Control: the identical pair with a short tool name IS minted -- the
    // guard (not the floor, not some other filter) is what discriminated.
    let msgs_short = transcript_named(&[&body, &body], "bash");
    let (_, log_short) = project_messages(
        &msgs_short,
        &no_protection_policy(),
        &ReductionLog::default(),
    );
    let dup = log_short
        .reductions
        .iter()
        .find(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. }))
        .expect("the same pair with a short name must be minted");
    assert!(
        dup.placeholder.len() < body.len(),
        "every minted duplicate stub must be strictly smaller than what it replaces"
    );
}

// ---- fix pass: sidecar_search must not report content the model can already see ----

#[test]
fn search_skips_duplicates_whose_canonical_is_fully_visible() {
    // The canonical instance is untouched by any reduction: the shared
    // bytes are fully visible in the live view, so a search for them must
    // report NOTHING (TR-1 dev/04's "the same string visible in the live
    // view is not double-reported").
    let body = format!("{}UNIQUE-NEEDLE-42{}", filler(200), filler(200));
    let msgs = transcript(&[&body, &body, &body]);
    let (_, log) = project_messages(&msgs, &no_protection_policy(), &ReductionLog::default());
    let dup_ids: Vec<String> = log
        .reductions
        .iter()
        .filter(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. }))
        .map(|r| r.id.clone())
        .collect();
    assert_eq!(dup_ids.len(), 2, "{:?}", log.reductions);

    let hits = sidecar_search(&log, &msgs, None, "UNIQUE-NEEDLE-42").unwrap();
    assert_eq!(
        hits.total_matches, 0,
        "content visible in the view (via the unreduced canonical) must not \
         be reported: {hits:?}"
    );
    assert_eq!(hits.unresolvable, 0, "skipped-as-visible is not an error");

    // expand_reduction keeps working unconditionally on a duplicate's id --
    // only SEARCH reporting changes.
    for id in &dup_ids {
        let outcome = expand_reduction(&log, &msgs, None, id, None).unwrap();
        assert_eq!(outcome.content, body);
    }
}

#[test]
fn search_reports_duplicates_once_the_canonical_is_itself_reduced() {
    // Needle at byte ~200: beyond the kept prefix (32B) the canonical's A7
    // truncation leaves visible, so once the canonical is truncated the
    // needle is genuinely hidden EVERYWHERE -- and the duplicate must now be
    // searchable.
    let body = format!("{}HIDDEN-NEEDLE-77{}", filler(200), filler(1800));
    let msgs = transcript(&[&body, &body, "ok-a", "ok-b", "ok-c", "ok-d"]);
    let session = session_of(msgs.clone());

    // Turn 1: dedup only (2,016B is under the default 8,192B A7 trigger).
    let (_, log1) = project(
        &session,
        &ReductionPolicy::default(),
        &ReductionLog::default(),
    );
    let dup_id = log1
        .reductions
        .iter()
        .find(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. }))
        .expect("turn 1 must dedup the identical pair")
        .id
        .clone();

    // Pre-truncation: the canonical is fully visible, so nothing is
    // reported (control for the assertion below).
    let hits1 = sidecar_search(&log1, &session.messages, None, "HIDDEN-NEEDLE-77").unwrap();
    assert_eq!(hits1.total_matches, 0, "{hits1:?}");

    // Turn 2: a low A7 trigger truncates the canonical (kept prefix 32B --
    // the needle at ~200 is in the hidden tail).
    let policy2 = ReductionPolicy {
        tool_output_trigger_bytes: 100,
        tool_output_keep_bytes: 32,
        protect_last_n_tool_results: 0,
        ..ReductionPolicy::default()
    };
    let (_, log2) = project(&session, &policy2, &log1);
    assert!(log2
        .reductions
        .iter()
        .any(|r| r.ptr.addr.index == 2
            && matches!(r.kind, ReductionKind::ToolOutputTruncated { .. })));

    // Post-truncation: the duplicate's content is genuinely hidden and MUST
    // now be reported under its own reduction id.
    let hits2 = sidecar_search(&log2, &session.messages, None, "HIDDEN-NEEDLE-77").unwrap();
    assert!(
        hits2.matches.iter().any(|m| m.reduction_id == dup_id),
        "the duplicate must be searchable once its canonical is reduced: {hits2:?}"
    );

    // expand_reduction returns the full bytes in both states.
    let out1 = expand_reduction(&log1, &session.messages, None, &dup_id, None).unwrap();
    assert_eq!(out1.content, body);
    let out2 = expand_reduction(&log2, &session.messages, None, &dup_id, None).unwrap();
    assert_eq!(out2.content, body);
}

// ---- fix pass: hostile tool names must never break the stub grammar ----

#[test]
fn hostile_tool_name_never_panics_and_mints_a_parseable_stub() {
    // A tool name containing `]` and a newline arrives verbatim from
    // untrusted imported JSONL. Without sanitization, stub::format's
    // debug_assert panics in debug builds (this very test run), and release
    // builds mint a grammar-breaking stub stub::parse rejects.
    let body = filler(400);
    let msgs = transcript_named(&[&body, &body], "weird]name\nx");
    let (view, log) = project_messages(&msgs, &no_protection_policy(), &ReductionLog::default());

    let dup = log
        .reductions
        .iter()
        .find(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. }))
        .expect("the hostile-named pair must still dedup");
    let (kind, id, summary) =
        stub::parse(&dup.placeholder).expect("the minted stub must satisfy the grammar");
    assert_eq!(kind, stub::Kind::Duplicate);
    assert_eq!(id, dup.id);
    assert!(
        summary.contains("weird_name_x"),
        "hostile characters must be visibly replaced, not dropped: {summary}"
    );

    // Round-trips byte-exact.
    let session = session_of(msgs.clone());
    let inverted = invert(&view, &log, &session).unwrap();
    assert_eq!(inverted[4].content.as_deref(), Some(body.as_str()));
    assert_eq!(inverted[4].name.as_deref(), Some("weird]name\nx"));

    // Same class in A7 (pre-existing instance, fixed in passing): a single
    // oversized output under the hostile name truncates to a stub that
    // still parses.
    let big = filler(20_000);
    let msgs_a7 = transcript_named(&[&big], "weird]name\nx");
    let (_, log_a7) = project_messages(&msgs_a7, &no_protection_policy(), &ReductionLog::default());
    let trunc = log_a7
        .reductions
        .iter()
        .find(|r| matches!(r.kind, ReductionKind::ToolOutputTruncated { .. }))
        .expect("the oversized output must truncate");
    assert!(
        stub::parse(&trunc.placeholder).is_some(),
        "A7's stub must satisfy the grammar too: {}",
        trunc.placeholder
    );
}

// ---- fix pass (FIX #1): TR-2's read exclusion narrowed to SAME-PATH re-reads only ----
//
// Adversarial review on the TR-3 merge found the merged `read_indices`
// exclusion (reduce.rs's TR-2 pass) over-broad: it excluded EVERY detected
// read from TR-2 dedup, not just same-path re-reads that the A8/TR-3
// read-family pass actually owns. That silently disabled TR-2 for two
// content-identical reads of DIFFERENT paths -- a case A8/TR-3's path-keyed
// matching never claims and never will. These two tests prove the
// narrowing in both directions: cross-path identical reads DO dedup (TR-2
// still owns that address space); same-path identical reads do NOT dedup
// (A8 owns that address space exclusively).

/// Build an assistant message issuing a single `read_file` tool call for
/// `path`, with call id `id` (mirrors `reduce_projection.rs::read_call`,
/// duplicated here since integration test binaries share no code).
fn read_call(id: &str, path: &str) -> ChatMessage {
    ChatMessage {
        role: Role::Assistant,
        content: None,
        content_parts: None,
        tool_calls: Some(vec![ToolCall {
            id: id.to_string(),
            kind: "function".to_string(),
            function: FunctionCall {
                name: "read_file".to_string(),
                arguments: serde_json::json!({ "path": path }).to_string(),
            },
        }]),
        tool_call_id: None,
        name: None,
        metadata: Default::default(),
    }
}

#[test]
fn tr2_still_dedups_identical_content_reads_of_different_paths() {
    // Two `read_file` results on DIFFERENT paths, byte-identical content,
    // both above the dedup floor. Neither is a same-path re-read (each is
    // the FIRST read of its own path), so the A8/TR-3 read-family pass
    // never claims either index -- TR-2 must still dedup them as it would
    // any other pair of identical tool outputs.
    let body = filler(500); // well over the 256B default floor
    let msgs = vec![
        ChatMessage::user("read two files"),
        read_call("c1", "src/a.rs"),
        ChatMessage::tool_result("c1", "read_file", body.clone()),
        ChatMessage::user("read the other one"),
        read_call("c2", "src/b.rs"),
        ChatMessage::tool_result("c2", "read_file", body.clone()),
    ];
    let idx_a = 2;
    let idx_b = 5;
    let session = session_of(msgs.clone());

    let (view, log) = project(&session, &no_protection_policy(), &ReductionLog::default());

    let dups: Vec<_> = log
        .reductions
        .iter()
        .filter(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. }))
        .collect();
    assert_eq!(
        dups.len(),
        1,
        "cross-path identical reads must still dedup under TR-2: {:?}",
        log.reductions
    );
    assert_eq!(
        dups[0].ptr.addr.index, idx_b,
        "the later (path b) read becomes the DuplicateOutput"
    );
    match dups[0].kind {
        ReductionKind::DuplicateOutput { canonical, .. } => {
            assert_eq!(
                canonical.index, idx_a,
                "the earlier (path a) read is canonical"
            )
        }
        _ => unreachable!(),
    }

    assert_eq!(
        view[idx_a].content.as_deref(),
        Some(body.as_str()),
        "the canonical (first) read stays full"
    );
    assert!(
        view[idx_b]
            .content
            .as_deref()
            .unwrap()
            .starts_with("[sc-reduced duplicate"),
        "the second (different-path) read must carry a `duplicate` stub: {:?}",
        view[idx_b].content
    );

    // invert() restores both byte-exact.
    let inverted = invert(&view, &log, &session).unwrap();
    assert_eq!(inverted[idx_a].content.as_deref(), Some(body.as_str()));
    assert_eq!(
        inverted[idx_b].content.as_deref(),
        Some(body.as_str()),
        "invert must restore the cross-path duplicate byte-exact"
    );
}

#[test]
fn tr2_never_dedups_identical_content_reads_of_the_same_path() {
    // Two `read_file` results on the SAME path, byte-identical content, both
    // above the dedup floor. The second is a same-path re-read -- the
    // A8/TR-3 read-family pass's exclusive address space -- so TR-2 must
    // never touch it, even though the bytes are identical and would
    // otherwise qualify.
    let body = filler(500);
    let msgs = vec![
        ChatMessage::user("read the file twice"),
        read_call("c1", "src/a.rs"),
        ChatMessage::tool_result("c1", "read_file", body.clone()),
        ChatMessage::user("read it again"),
        read_call("c2", "src/a.rs"),
        ChatMessage::tool_result("c2", "read_file", body.clone()),
    ];
    let session = session_of(msgs.clone());

    let (_, log) = project(&session, &no_protection_policy(), &ReductionLog::default());

    assert!(
        !log.reductions
            .iter()
            .any(|r| matches!(r.kind, ReductionKind::DuplicateOutput { .. })),
        "same-path identical re-reads belong to A8/TR-3, never TR-2: {:?}",
        log.reductions
    );
    // A8 is disabled by default (`elide_stale_reads: false`, no freshness
    // probe run) and TR-3's diff pass has nothing to show for a zero-hunk
    // diff, so the unchanged re-read is left entirely untouched -- the
    // point of this test is solely that it is NOT a `DuplicateOutput`.
    assert!(
        log.reductions.is_empty(),
        "no reduction at all is expected here: {:?}",
        log.reductions
    );
}