supercode-cli 0.4.6

supercode — a lightweight, fully-customizable AI coding agent CLI in Rust. Any model via OpenRouter; natively continues Claude Code and Codex sessions.
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
//! CLI-level acceptance tests for UX-25 ("Memorable auto-generated session
//! names"):
//!
//! - dev/01: newly created sessions get a memorable word-based name (e.g.
//!   `<tag>-brave-otter`), collision-free within the store.
//! - dev/02: `resume`-style resolution finds a session by its generated
//!   memorable name; existing timestamp-named sessions still resume.
//!
//! Spawns the real, built `supercode` binary (mirroring `spinner_cli.rs`'s /
//! `token_counter_cli.rs`'s idiom) against a local, hand-framed HTTP/SSE stub
//! — a genuine end-to-end turn, not a reimplementation, so a session actually
//! gets persisted through `persist_session`/`SessionStore::save` exactly like
//! a real run. Legacy timestamp-named sessions are exercised by writing
//! directly through `SessionStore::save` (same idiom `json_breadth_cli.rs`'s
//! `sessions_list_json_reports_a_real_saved_session` already uses).

use std::io::{Read, Write};
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::time::Duration;

use supercode::store::SessionStore;
use supercode::ChatMessage;

fn bin() -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_supercode"))
}

fn fresh_home(tag: &str) -> PathBuf {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_nanos();
    let dir = std::env::temp_dir().join(format!(
        "supercode-ux25-names-{tag}-{}-{nanos}",
        std::process::id()
    ));
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

/// A one-shot local HTTP server that accepts one connection, drains the
/// request, then writes back a hand-framed `text/event-stream` response —
/// same shape `spinner_cli.rs`/`token_counter_cli.rs` already use.
fn spawn_sse_stub(reply: &'static str) -> (std::net::SocketAddr, std::thread::JoinHandle<()>) {
    let listener = TcpListener::bind("127.0.0.1:0").expect("bind stub listener");
    let addr = listener.local_addr().unwrap();
    let handle = std::thread::spawn(move || {
        let (mut sock, _) = listener.accept().expect("accept one connection");
        sock.set_read_timeout(Some(Duration::from_millis(500)))
            .expect("set read timeout");
        let mut buf = [0u8; 65536];
        loop {
            match sock.read(&mut buf) {
                Ok(0) => break,
                Ok(_) => continue,
                Err(e)
                    if e.kind() == std::io::ErrorKind::WouldBlock
                        || e.kind() == std::io::ErrorKind::TimedOut =>
                {
                    break
                }
                Err(e) => panic!("stub read failed: {e}"),
            }
        }
        let sse = format!(
            "data: {{\"choices\":[{{\"delta\":{{\"content\":\"{reply}\"}}}}]}}\n\ndata: [DONE]\n\n"
        );
        let resp = format!(
            "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
            sse.len(),
            sse
        );
        sock.write_all(resp.as_bytes())
            .expect("write stub response");
        sock.flush().ok();
    });
    (addr, handle)
}

fn run(home: &Path, base_url: &str, extra: &[&str]) -> Output {
    let mut args = vec!["--api-key", "x", "--base-url", base_url];
    args.extend_from_slice(extra);
    Command::new(bin())
        .env("SUPERCODE_HOME", home)
        .env_remove("OPENROUTER_API_KEY")
        .env_remove("OPENAI_API_KEY")
        .env_remove("ANTHROPIC_API_KEY")
        .env_remove("NO_COLOR")
        .args(&args)
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .expect("failed to spawn the supercode binary")
}

fn run_json(home: &Path, args: &[&str]) -> serde_json::Value {
    // `sessions list --json` etc. never touch the network, so a plainly
    // unreachable base url is fine (and never dialed).
    let out = run(home, "http://127.0.0.1:1", args);
    assert!(
        out.status.success(),
        "expected success for {args:?}: stdout={} stderr={}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    serde_json::from_slice(&out.stdout).unwrap_or_else(|e| {
        panic!(
            "stdout was not valid JSON: {e}\nstdout: {}",
            String::from_utf8_lossy(&out.stdout)
        )
    })
}

/// A memorable UX-25 name is `<16-hex-tag>-<alpha-word>-<alpha-word-plus-
/// optional-digits>` — validated structurally (no assumption about which
/// exact words the embedded list contains, since that's an implementation
/// detail).
fn assert_memorable_name_shape(name: &str) {
    let parts: Vec<&str> = name.split('-').collect();
    assert_eq!(
        parts.len(),
        3,
        "expected `<tag>-<adjective>-<noun>`, got `{name}`"
    );
    let (tag, adj, noun) = (parts[0], parts[1], parts[2]);
    assert_eq!(tag.len(), 16, "cwd tag should be 16 hex chars: `{tag}`");
    assert!(
        tag.chars().all(|c| c.is_ascii_hexdigit()),
        "cwd tag should be hex: `{tag}`"
    );
    assert!(
        !adj.is_empty() && adj.chars().all(|c| c.is_ascii_lowercase()),
        "adjective should be lowercase alpha: `{adj}`"
    );
    assert!(!noun.is_empty(), "noun segment empty in `{name}`");
    let alpha_prefix_len = noun.chars().take_while(|c| c.is_ascii_lowercase()).count();
    assert!(
        alpha_prefix_len > 0,
        "noun segment should start with lowercase letters: `{noun}`"
    );
    // Anything after the alpha prefix (if any — the collision discriminator)
    // must be plain digits.
    assert!(
        noun[alpha_prefix_len..].chars().all(|c| c.is_ascii_digit()),
        "trailing chars after the noun should be a numeric discriminator: `{noun}`"
    );
}

// ---- dev/01: newly created sessions get a memorable name -----------------

#[test]
fn run_without_an_explicit_name_creates_a_memorable_session() {
    let (addr, _server) = spawn_sse_stub("hello there");
    let home = fresh_home("mint");
    let proj = home.join("proj");
    std::fs::create_dir_all(&proj).unwrap();

    let out = run(
        &home,
        &format!("http://{addr}"),
        &["--cwd", proj.to_str().unwrap(), "run", "say hi"],
    );
    assert!(
        out.status.success(),
        "run failed: stdout={} stderr={}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(String::from_utf8_lossy(&out.stdout).contains("hello there"));

    let listed = run_json(&home, &["sessions", "list", "--json"]);
    let arr = listed.as_array().expect("array");
    assert_eq!(arr.len(), 1, "expected exactly one saved session: {arr:?}");
    let name = arr[0]["name"]
        .as_str()
        .expect("name is a string")
        .to_string();
    assert_memorable_name_shape(&name);

    // `short_id` (the display id) is the segment after the last `-` — the
    // noun (+ discriminator, if any) for a memorable name.
    let expected_short_id = name.rsplit('-').next().unwrap();
    assert_eq!(arr[0]["short_id"], expected_short_id);

    // The human table also shows the memorable name (not raw digits).
    let human = run(&home, "http://127.0.0.1:1", &["sessions", "list"]);
    assert!(human.status.success());
    let human_out = String::from_utf8_lossy(&human.stdout);
    assert!(
        human_out.contains(expected_short_id),
        "human `sessions list` should show the memorable short id: {human_out}"
    );
}

#[test]
fn two_sessions_created_back_to_back_get_distinct_memorable_names() {
    let (addr1, _s1) = spawn_sse_stub("first reply");
    let home = fresh_home("distinct");
    let proj = home.join("proj");
    std::fs::create_dir_all(&proj).unwrap();

    let out1 = run(
        &home,
        &format!("http://{addr1}"),
        &["--cwd", proj.to_str().unwrap(), "run", "one"],
    );
    assert!(out1.status.success());

    let (addr2, _s2) = spawn_sse_stub("second reply");
    let out2 = run(
        &home,
        &format!("http://{addr2}"),
        &["--cwd", proj.to_str().unwrap(), "run", "two"],
    );
    assert!(out2.status.success());

    let listed = run_json(&home, &["sessions", "list", "--json"]);
    let arr = listed.as_array().expect("array");
    assert_eq!(arr.len(), 2, "expected two saved sessions: {arr:?}");
    let names: Vec<&str> = arr.iter().map(|s| s["name"].as_str().unwrap()).collect();
    assert_ne!(names[0], names[1], "two sessions collided on name");
    for n in &names {
        assert_memorable_name_shape(n);
    }
}

// ---- dev/02: resolving/resuming by the generated memorable name ----------

#[test]
fn sessions_show_reductions_resolves_a_memorable_name_and_a_unique_prefix() {
    let (addr, _server) = spawn_sse_stub("hi back");
    let home = fresh_home("resolve");
    let proj = home.join("proj");
    std::fs::create_dir_all(&proj).unwrap();

    let out = run(
        &home,
        &format!("http://{addr}"),
        &["--cwd", proj.to_str().unwrap(), "run", "say hi"],
    );
    assert!(out.status.success());

    let listed = run_json(&home, &["sessions", "list", "--json"]);
    let name = listed[0]["name"].as_str().unwrap().to_string();

    // Exact memorable name resolves (offline, no API key needed —
    // `show-reductions` never contacts a model).
    let exact = run(
        &home,
        "http://127.0.0.1:1",
        &["sessions", "show-reductions", &name],
    );
    assert!(
        exact.status.success(),
        "resolving by exact memorable name failed: stderr={}",
        String::from_utf8_lossy(&exact.stderr)
    );

    // A unique prefix (just the cwd tag + adjective) also resolves.
    let prefix: String = name.rsplit_once('-').unwrap().0.to_string();
    let by_prefix = run(
        &home,
        "http://127.0.0.1:1",
        &["sessions", "show-reductions", &prefix],
    );
    assert!(
        by_prefix.status.success(),
        "resolving by unique prefix `{prefix}` failed: stderr={}",
        String::from_utf8_lossy(&by_prefix.stderr)
    );
}

#[test]
fn continue_flag_resumes_the_freshly_minted_memorable_session() {
    let (addr, _server) = spawn_sse_stub("hi back");
    let home = fresh_home("continue-new");
    let proj = home.join("proj");
    std::fs::create_dir_all(&proj).unwrap();

    let out = run(
        &home,
        &format!("http://{addr}"),
        &["--cwd", proj.to_str().unwrap(), "run", "say hi"],
    );
    assert!(out.status.success());

    // `-c` in the same directory: the connection-refused base url means the
    // turn itself fails, but session RESOLUTION (which happens first, purely
    // offline) is exactly what's under test here — same idiom as
    // `reduced_resume.rs`.
    let cont = run(
        &home,
        "http://127.0.0.1:1",
        &["--cwd", proj.to_str().unwrap(), "-c", "run", "again"],
    );
    let stderr = String::from_utf8_lossy(&cont.stderr);
    assert!(
        stderr.contains("Continuing session ("),
        "expected a `Continuing session (...)` banner, got: {stderr}"
    );
    assert!(
        !stderr.contains("no prior session found"),
        "the freshly minted memorable session should have been found: {stderr}"
    );
}

#[test]
fn existing_timestamp_named_sessions_still_resume_and_list() {
    // A pre-UX-25 session, written directly through the store exactly like
    // the real (old) `new_session_name` used to name one — no memorable
    // words anywhere in it.
    let home = fresh_home("legacy");
    let store = SessionStore::open(home.join("sessions")).unwrap();
    let legacy_tag = "00112233445566aa";
    let old_micros = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_micros()
        - 3_600_000_000; // ~1h ago
    let legacy_name = format!("{legacy_tag}-{old_micros:020}");
    let legacy_jsonl = r#"{"role":"system","content":"hi"}
{"role":"user","content":"hello"}
{"role":"assistant","content":"hi there"}"#;
    store
        .save(&legacy_name, "legacy session", legacy_jsonl)
        .unwrap();

    // Lists fine, with a sane age (not "?").
    let listed = run_json(&home, &["sessions", "list", "--json"]);
    let arr = listed.as_array().unwrap();
    assert_eq!(arr.len(), 1);
    assert_eq!(arr[0]["name"], legacy_name);
    assert_eq!(arr[0]["short_id"], format!("{old_micros:020}"));
    let age = arr[0]["age"].as_str().unwrap();
    assert_ne!(age, "?", "legacy session age should resolve, got `?`");

    // Resolves by exact name and by prefix, same as a memorable one would.
    let exact = run(
        &home,
        "http://127.0.0.1:1",
        &["sessions", "show-reductions", &legacy_name],
    );
    assert!(exact.status.success());
    let by_prefix = run(
        &home,
        "http://127.0.0.1:1",
        &["sessions", "show-reductions", legacy_tag],
    );
    assert!(by_prefix.status.success());

    // `--last` resumes it (fake cwd — `--last` is not directory-scoped).
    let cont = run(&home, "http://127.0.0.1:1", &["--last", "run", "again"]);
    let stderr = String::from_utf8_lossy(&cont.stderr);
    assert!(
        stderr.contains("Continuing session (3 messages)"),
        "expected the legacy session's 3 messages to load, got: {stderr}"
    );
}

#[test]
fn newest_first_ordering_mixes_legacy_and_memorable_sessions_correctly() {
    let (addr, _server) = spawn_sse_stub("hi back");
    let home = fresh_home("mixed-order");
    let proj = home.join("proj");
    std::fs::create_dir_all(&proj).unwrap();

    // An old legacy session (~2h ago) written directly.
    let store = SessionStore::open(home.join("sessions")).unwrap();
    let old_micros = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_micros()
        - 7_200_000_000; // ~2h ago
    let legacy_name = format!("ffeeddccbbaa9988-{old_micros:020}");
    store.save(&legacy_name, "old", "{}\n").unwrap();

    // A fresh memorable session, created just now via a real turn.
    let out = run(
        &home,
        &format!("http://{addr}"),
        &["--cwd", proj.to_str().unwrap(), "run", "say hi"],
    );
    assert!(out.status.success());

    let listed = run_json(&home, &["sessions", "list", "--json"]);
    let arr = listed.as_array().unwrap();
    assert_eq!(arr.len(), 2);
    // newest first: the just-created memorable session leads.
    assert_ne!(
        arr[0]["name"], legacy_name,
        "memorable session should sort newest-first"
    );
    assert_eq!(arr[1]["name"], legacy_name);
    let legacy_age = arr[1]["age"].as_str().unwrap();
    assert!(
        legacy_age.ends_with("h ago") || legacy_age.ends_with("m ago"),
        "legacy session should show a real age, got `{legacy_age}`"
    );
}

#[test]
fn no_reduced_saved_session_is_guarded_before_network_or_persistence() {
    let home = fresh_home("context-guard");
    let store = SessionStore::open(home.join("sessions")).unwrap();
    let messages = [
        ChatMessage::system("system"),
        ChatMessage::user("x".repeat(900_000)),
    ];
    let transcript = messages
        .iter()
        .map(|message| serde_json::to_string(message).unwrap())
        .collect::<Vec<_>>()
        .join("\n");
    store.save("oversized", "oversized", &transcript).unwrap();
    let before = store.load("oversized").unwrap();

    let out = run(
        &home,
        "http://127.0.0.1:1",
        &[
            "--model",
            "tiny-unknown/model",
            "--last",
            "--no-reduced",
            "run",
            "must be refused locally",
        ],
    );
    assert!(
        !out.status.success(),
        "oversized run unexpectedly succeeded"
    );
    let err = String::from_utf8_lossy(&out.stderr);
    assert!(
        err.contains("cannot reduce below context limit") && !err.contains("transport error"),
        "the local context guard must win before any network attempt: {err}"
    );
    assert_eq!(
        store.load("oversized").unwrap(),
        before,
        "a locally-refused prompt must not mutate the persisted transcript"
    );
}

#[test]
fn bare_guard_uses_the_agents_model_not_the_ignored_user_config_model() {
    let home = fresh_home("context-guard-bare-model");
    std::fs::write(
        home.join("config.toml"),
        "schema_version = 1\nmodel = \"google/gemini-2.5-pro\"\n",
    )
    .unwrap();
    let store = SessionStore::open(home.join("sessions")).unwrap();
    // About 550k content tokens: above --bare's actual 500k default Claude
    // window, but below the ignored configured Gemini model's 1,048,576.
    // A guard that independently reloads config will therefore dial the
    // unreachable endpoint; a guard derived from Agent::config refuses first.
    let messages = [
        ChatMessage::system("system"),
        ChatMessage::user("x".repeat(2_200_000)),
    ];
    let transcript = messages
        .iter()
        .map(|message| serde_json::to_string(message).unwrap())
        .collect::<Vec<_>>()
        .join("\n");
    store
        .save("bare-oversized", "bare oversized", &transcript)
        .unwrap();

    let out = run(
        &home,
        "http://127.0.0.1:1",
        &["--bare", "--last", "--no-reduced", "run", "refuse"],
    );
    assert!(
        !out.status.success(),
        "oversized run unexpectedly succeeded"
    );
    let err = String::from_utf8_lossy(&out.stderr);
    assert!(
        err.contains("model anthropic/claude-opus-4-8 limit 500000")
            && !err.contains("transport error"),
        "guard must use the model installed on the bare agent: {err}"
    );
}