dpc-tau-cli 0.1.0

A minimal Unix-first coding agent.
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
use std::io::Write as _;
#[cfg(unix)]
use std::os::unix::fs as path_unix_fs;
use std::sync::{Arc, Barrier};
use std::{fs as path_std_fs, thread};

use fs2::FileExt;

use super::*;

fn record(timestamp_us: u64, agent_id: &str, session_id: &str, report: &str) -> PapercutRecord {
    PapercutRecord::new(
        tau_proto::AgentId::parse(agent_id).expect("valid test agent"),
        tau_proto::SessionId::parse(session_id).expect("valid test session"),
        tau_proto::UnixMicros::new(timestamp_us),
        report.to_owned(),
    )
}

fn store(tempdir: &tempfile::TempDir) -> PapercutStore {
    PapercutStore::new(tempdir.path())
}

fn write_records(store: &PapercutStore, records: &[PapercutRecord]) {
    path_std_fs::create_dir_all(&store.root).expect("create reporter root");
    let contents = records
        .iter()
        .map(|record| serde_json::to_string(record).expect("serialize record"))
        .collect::<Vec<_>>()
        .join("\n");
    path_std_fs::write(store.file(), format!("{contents}\n")).expect("write records");
}

fn write_raw_records(store: &PapercutStore, contents: &[u8]) {
    path_std_fs::create_dir_all(&store.root).expect("create reporter root");
    path_std_fs::write(store.file(), contents).expect("write raw records");
}

fn append_report(store: &PapercutStore, record: &PapercutRecord) {
    let root = open_existing_directory_no_follow(&store.root).expect("open reporter root");
    root.lock_exclusive().expect("lock reporter root");
    let mut file = path_std_fs::OpenOptions::new()
        .append(true)
        .create(true)
        .open(store.file())
        .expect("open papercut file");
    serde_json::to_writer(&mut file, record).expect("serialize appended record");
    file.write_all(b"\n").expect("append newline");
    file.sync_all().expect("sync appended record");
    root.unlock().expect("unlock reporter root");
}

/// Ensures ordinary output orders canonical records by timestamp and escapes
/// report controls so one model report cannot forge terminal output lines.
#[test]
fn plain_list_is_timestamp_ordered_and_line_safe() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let store = store(&tempdir);
    write_records(
        &store,
        &[
            record(2_000_000, "agent-a", "session-a", "second\nline"),
            record(1_000_000, "agent-b", "session-b", "first"),
        ],
    );

    let output = format_plain(&store.list().expect("list records")).expect("format plain list");

    assert_eq!(
        output,
        "1970-01-01T00:00:01Z agent-b [session-b] first\n\
1970-01-01T00:00:02Z agent-a [session-a] second\\nline\n"
    );
}

/// Ensures Markdown retains report text as copyable literal content and selects
/// a longer fence when a report itself contains a normal triple-backtick fence.
#[test]
fn markdown_list_uses_safe_fences_for_same_records() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let store = store(&tempdir);
    write_records(
        &store,
        &[record(
            1_000_000,
            "agent-a",
            "session-a",
            "```rust\nlet answer = 42;\n```",
        )],
    );

    let output =
        format_markdown(&store.list().expect("list records")).expect("format Markdown list");

    assert_eq!(
        output,
        "# Papercuts\n\n\
## 1970-01-01T00:00:01Z\n\n\
- Agent: `agent-a`\n\
- Session: `session-a`\n\n\
````text\n\
```rust\n\
let answer = 42;\n\
```\n\
````\n\n"
    );
}

/// Ensures both renderers report an absent canonical file as an empty history
/// without creating a reporter directory that no agent has used.
#[test]
fn empty_storage_formats_as_an_empty_history() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let store = store(&tempdir);

    let records = store.list().expect("list absent records");

    assert!(records.is_empty());
    assert_eq!(
        format_plain(&records).expect("format plain"),
        "no papercut reports\n"
    );
    assert_eq!(
        format_markdown(&records).expect("format Markdown"),
        "# Papercuts\n\nNo papercut reports.\n"
    );
    assert_eq!(
        store.clear().expect("clear absent records"),
        PapercutClearResult {
            count: 0,
            archive: None,
        }
    );
    assert!(!store.root.exists());
}

/// Ensures clear removes every record from the active listing while preserving
/// the exact canonical bytes in the surfaced archive.
#[test]
fn clear_archives_listed_records() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let store = store(&tempdir);
    write_records(
        &store,
        &[
            record(1_000_000, "agent-a", "session-a", "first"),
            record(2_000_000, "agent-b", "session-b", "second"),
        ],
    );
    let original = path_std_fs::read(store.file()).expect("read original records");

    let result = store.clear().expect("clear records");

    assert_eq!(result.count, 2);
    assert!(store.list().expect("list cleared records").is_empty());
    assert!(!store.file().exists());
    let archive = result.archive.expect("archive path");
    assert_eq!(
        path_std_fs::read(&archive).expect("read archived records"),
        original
    );
    assert_eq!(
        format_clear_result(&PapercutClearResult {
            count: 2,
            archive: Some(archive.clone()),
        }),
        format!(
            "cleared 2 papercut report(s); archived at {}\n",
            archive.display()
        )
    );
}

/// Ensures an already-cleared history remains a successful no-op, so scripts
/// can safely repeat cleanup without overwriting the existing archive.
#[test]
fn repeated_clear_is_a_successful_no_op_without_overwriting_archive() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let store = store(&tempdir);
    write_records(
        &store,
        &[record(1_000_000, "agent-a", "session-a", "first")],
    );

    let first = store.clear().expect("first clear");
    let archive = first.archive.expect("first archive");
    let archived = path_std_fs::read(&archive).expect("read first archive");
    assert_eq!(first.count, 1);
    assert_eq!(
        store.clear().expect("second clear"),
        PapercutClearResult {
            count: 0,
            archive: None,
        }
    );
    assert_eq!(
        path_std_fs::read(&archive).expect("reread first archive"),
        archived
    );
    assert!(store.list().expect("list cleared records").is_empty());
}

/// Ensures separate non-empty clears preserve distinct snapshots when a fresh
/// active file is appended between them.
#[test]
fn repeated_non_empty_clears_create_distinct_archives() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let store = store(&tempdir);
    let first_record = record(1_000_000, "agent-a", "session-a", "first");
    let second_record = record(2_000_000, "agent-b", "session-b", "second");
    write_records(&store, std::slice::from_ref(&first_record));

    let first = store.clear().expect("first clear");
    append_report(&store, &second_record);
    let second = store.clear().expect("second clear");

    let first_archive = first.archive.expect("first archive");
    let second_archive = second.archive.expect("second archive");
    assert_ne!(first_archive, second_archive);
    assert_eq!(
        store
            .read_records_from(&first_archive)
            .expect("read first archive"),
        vec![first_record]
    );
    assert_eq!(
        store
            .read_records_from(&second_archive)
            .expect("read second archive"),
        vec![second_record]
    );
}

/// Ensures an existing archive candidate is never overwritten and a later
/// clear advances to a distinct preserved path.
#[test]
fn clear_never_overwrites_an_existing_archive() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let store = store(&tempdir);
    path_std_fs::create_dir_all(&store.root).expect("create reporter root");
    let existing = store
        .root
        .join(format!("{PAPERCUT_ARCHIVE_PREFIX}{:016}.jsonl", 1));
    path_std_fs::write(&existing, b"historical bytes\n").expect("write existing archive");
    write_records(&store, &[record(1_000_000, "agent-a", "session-a", "new")]);

    let result = store.clear().expect("clear records");

    assert_eq!(
        path_std_fs::read(&existing).expect("read existing archive"),
        b"historical bytes\n"
    );
    assert_eq!(
        result.archive,
        Some(
            store
                .root
                .join(format!("{PAPERCUT_ARCHIVE_PREFIX}{:016}.jsonl", 2))
        )
    );
}

/// Ensures archive selection treats directories and dangling symlinks as
/// occupied entries instead of replacing them during clear.
#[test]
#[cfg(unix)]
fn clear_skips_non_regular_and_dangling_archive_entries() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let store = store(&tempdir);
    path_std_fs::create_dir_all(&store.root).expect("create reporter root");
    let directory = store
        .root
        .join(format!("{PAPERCUT_ARCHIVE_PREFIX}{:016}.jsonl", 1));
    path_std_fs::create_dir(&directory).expect("create occupied archive directory");
    let dangling = store
        .root
        .join(format!("{PAPERCUT_ARCHIVE_PREFIX}{:016}.jsonl", 2));
    path_unix_fs::symlink(store.root.join("missing-target"), &dangling)
        .expect("create dangling archive symlink");
    write_records(&store, &[record(1_000_000, "agent-a", "session-a", "new")]);

    let result = store.clear().expect("clear records");

    assert!(directory.is_dir());
    assert!(path_std_fs::symlink_metadata(&dangling).is_ok());
    assert_eq!(
        result.archive,
        Some(
            store
                .root
                .join(format!("{PAPERCUT_ARCHIVE_PREFIX}{:016}.jsonl", 3))
        )
    );
}

/// Ensures malformed, unsupported, invalid-identity, and unrenderable-timestamp
/// input fails closed and clear leaves each rejected canonical file untouched.
#[test]
fn rejected_records_are_never_cleared() {
    let cases = [
        b"{not json}\n".as_slice(),
        br#"{"schema":2,"agent_id":"agent-a","session_id":"session-a","timestamp_us":1,"report":"future"}"#,
        br#"{"schema":1,"agent_id":"agent\ninjected","session_id":"session-a","timestamp_us":1,"report":"bad"}"#,
        br#"{"schema":1,"agent_id":"agent-a","session_id":"session-a","timestamp_us":18446744073709551615,"report":"bad"}"#,
    ];
    for contents in cases {
        let tempdir = tempfile::tempdir().expect("tempdir");
        let store = store(&tempdir);
        write_raw_records(&store, contents);

        assert!(store.list().is_err(), "list must reject {contents:?}");
        assert!(store.clear().is_err(), "clear must reject {contents:?}");
        assert_eq!(
            path_std_fs::read(store.file()).expect("read rejected record"),
            contents,
            "clear must preserve rejected input"
        );
    }
}

/// Ensures storage inspection rejects oversized, symlinked, and non-regular
/// records rather than following or deleting data outside the reporter
/// contract.
#[test]
#[cfg(unix)]
fn unsafe_or_oversized_record_files_fail_closed() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let store = store(&tempdir);
    path_std_fs::create_dir_all(&store.root).expect("create reporter root");
    path_std_fs::File::create(store.file())
        .expect("create oversized record")
        .set_len(tau_harness::EXTENSION_DATA_MAX_FILE_BYTES + 1)
        .expect("extend oversized record");
    assert!(store.list().is_err());
    assert!(store.clear().is_err());
    assert!(store.file().exists());

    path_std_fs::remove_file(store.file()).expect("remove oversized record");
    path_std_fs::create_dir(store.file()).expect("create record directory");
    assert!(store.list().is_err());
    assert!(store.clear().is_err());
    path_std_fs::remove_dir(store.file()).expect("remove record directory");

    let target = tempdir.path().join("outside.jsonl");
    path_std_fs::write(&target, b"outside\n").expect("write symlink target");
    path_unix_fs::symlink(&target, store.file()).expect("create records symlink");
    assert!(store.list().is_err());
    assert!(store.clear().is_err());
    assert_eq!(
        path_std_fs::read(target).expect("read symlink target"),
        b"outside\n"
    );
}

/// Ensures a reporter that waits behind clear's shared directory lock appends
/// to the newly empty file, preserving reports accepted after the clear
/// boundary.
#[test]
fn clear_preserves_reports_appended_after_its_lock_boundary() {
    let tempdir = tempfile::tempdir().expect("tempdir");
    let store = store(&tempdir);
    write_records(
        &store,
        &[record(
            1_000_000,
            "agent-before",
            "session-before",
            "before",
        )],
    );
    let midpoint = Arc::new(Barrier::new(2));
    let clear_store = store.clone().with_clear_midpoint(Arc::clone(&midpoint));
    let clear_thread = thread::spawn(move || clear_store.clear().expect("clear records"));
    midpoint.wait();
    let reporter_store = PapercutStore::new(tempdir.path());
    let reporter_thread = thread::spawn(move || {
        append_report(
            &reporter_store,
            &record(2_000_000, "agent-after", "session-after", "after"),
        );
    });

    midpoint.wait();
    let clear_result = clear_thread.join().expect("clear thread");
    assert_eq!(clear_result.count, 1);
    reporter_thread.join().expect("reporter thread");

    let archive = clear_result.archive.expect("archive path");
    assert_eq!(
        PapercutStore::new(tempdir.path())
            .read_records_from(&archive)
            .expect("read archive"),
        vec![record(
            1_000_000,
            "agent-before",
            "session-before",
            "before"
        )]
    );
    assert_eq!(
        store.list().expect("list post-boundary record"),
        vec![record(2_000_000, "agent-after", "session-after", "after")]
    );
}