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
//! CLI-level acceptance test for UX-22 (turn-finish notifications):
//!
//! - dev/01: after a turn longer than the threshold, `supercode` emits a
//!   desktop notification, gated by `--notify`/config.
//! - dev/02: off by default, and never breaks the run when the notifier
//!   (`notify-send`) is absent from `PATH`.
//! - Driver directive: never fires for `--output-format json`/piped runs;
//!   never blocks; stdout stays byte-clean.
//!
//! Spawns the real, built `supercode` binary against a real local HTTP/SSE
//! stub, mirroring `spinner_cli.rs`/`quiet_cli.rs`'s idiom. `notify-send`
//! itself is stubbed with a fake script placed on `PATH` that just logs its
//! argv — this box is headless (no dbus/notify-send guaranteed, verified: a
//! bare `which notify-send` fails here), so this proves the WIRING (gating
//! decision + spawn + exact title/body reaching the notifier), not that a
//! real desktop popup rendered on a real display. See `notify.rs`'s module
//! doc for the same caveat stated at the source.
//!
//! `should_notify`'s gating requires a real tty on stderr (the AC's
//! "non-interactive/piped" exclusion) — undetectable in a plain
//! `Command::spawn()` with `Stdio::piped()` (never a tty on any platform).
//! The "fires when enabled" / "doesn't fire when disabled" / "doesn't fire
//! when the notifier is absent" cases below therefore run the binary under
//! `script(1)`, which allocates a REAL pseudo-terminal for the child
//! regardless of this test process's own tty-ness — the standard way to
//! get an automatable tty in CI (`script` is util-linux on Linux and BSD's
//! implementation on macOS; [`run_under_pty`] handles both argument forms).
//! The machine-format (`--output-format json`) case doesn't need a pty: a
//! plain piped invocation already exercises the realistic "scripted
//! caller" shape the driver describes ("a scripted `run --output-format
//! json`"), and additionally proves the non-tty half of the gate.

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

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

fn fresh_dir(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-ux22-notify-{tag}-{}-{nanos}",
        std::process::id()
    ));
    std::fs::create_dir_all(&dir).unwrap();
    dir
}

/// A one-shot local HTTP server that accepts one connection, discards the
/// request, and immediately writes back a hand-framed SSE response — same
/// framing `OpenAiProvider` parses, matching `spinner_cli.rs`'s stub. No
/// artificial delay: with `--notify-threshold-secs 0` any turn duration
/// qualifies, so keeping this fast keeps the test fast.
fn spawn_sse_stub() -> (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");
        let mut buf = [0u8; 4096];
        let _ = sock.read(&mut buf);

        let sse = "data: {\"choices\":[{\"delta\":{\"content\":\"hello there\"}}]}\n\n\
                   data: [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)
}

/// Write an executable fake `notify-send` at `dir/notify-send` that appends
/// its argv (one arg per line, a blank line as a record separator) to
/// `log_file`. Real `notify-send`'s argv shape is
/// `notify-send [--app-name=NAME] TITLE BODY`, so `--app-name=supercode`
/// lands as argv[1], title as argv[2], body as argv[3] — the test greps the
/// log for the expected title/body substrings rather than depending on
/// exact positional parsing.
fn write_fake_notify_send(dir: &Path, log_file: &Path) {
    let script = format!(
        "#!/bin/sh\nfor a in \"$@\"; do printf '%s\\n' \"$a\" >> {log}; done\nprintf -- '---\\n' >> {log}\n",
        log = log_file.display()
    );
    let path = dir.join("notify-send");
    std::fs::write(&path, script).expect("write fake notify-send");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
    }
}

/// Run `supercode <args>` under `script(1)` so stderr is a REAL pty
/// (`should_notify`'s tty gate), with `path_prepend` (if any) prepended to
/// this test process's own inherited `PATH` for the child to search. HOME
/// points at a fresh `SUPERCODE_HOME` so no real user config/credentials
/// leak in. Returns (exit success, combined pty output — stdout+stderr are
/// necessarily interleaved through the one pty, same as a real terminal).
fn run_under_pty(home: &Path, args: &[&str], path_prepend: Option<&Path>) -> (bool, String) {
    let real_path = std::env::var("PATH").unwrap_or_default();
    let path = match path_prepend {
        Some(p) => format!("{}:{real_path}", p.display()),
        None => real_path,
    };

    // Build a tiny wrapper script so we never have to shell-quote the
    // binary path or its arguments through `script -c "..."`.
    let wrapper = home.join("run.sh");
    // `--bare` keeps this notification test independent of first-run
    // onboarding. In particular, BSD `script` keeps the pty input side open
    // after its own stdin reaches EOF, so a fresh config would otherwise
    // block in the unrelated onboarding prompt on macOS.
    let mut body = format!("#!/bin/sh\n{:?} --bare", bin());
    for a in args {
        body.push_str(&format!(" {a:?}"));
    }
    // Deliberately NOT `exec` (which would replace this shell's process
    // image, ending the pty session's foreground command — and this test
    // harness's own `script(1)` teardown — the instant `supercode` exits).
    // `supercode`'s own async, best-effort side effects (`notify::
    // fire_desktop`'s desktop notification, spawned as an independent OS
    // process supercode never waits on — see its doc comment) are children
    // of THIS pty session, not of `script(1)` itself; when `script -qec`
    // sees its wrapped command exit and closes the pty, the kernel sends a
    // SIGHUP to every remaining member of that session — including a
    // freshly-`spawn()`ed, not-yet-finished `notify-send` (real or, in
    // these tests, the fake logging one). A real user's terminal session
    // obviously outlives a single `run` invocation by a wide margin, so
    // this race never happens outside a harness that tears its pty down
    // the instant the wrapped command exits — capture `supercode`'s real
    // exit code, give any such detached child a moment to finish, THEN
    // exit with that code (not `sleep`'s, which would otherwise silently
    // shadow a real `supercode` failure).
    body.push_str("\nrc=$?\nsleep 0.5\nexit $rc\n");
    std::fs::write(&wrapper, body).expect("write wrapper");
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&wrapper, std::fs::Permissions::from_mode(0o755)).unwrap();
    }

    let mut command = Command::new("script");
    #[cfg(target_os = "macos")]
    command.args(["-qe", "/dev/null", &wrapper.display().to_string()]);
    #[cfg(not(target_os = "macos"))]
    command.args(["-qec", &wrapper.display().to_string(), "/dev/null"]);
    let out = command
        .env("SUPERCODE_HOME", home)
        .env("PATH", path)
        .env("OPENROUTER_API_KEY", "x")
        .env_remove("OPENAI_API_KEY")
        .env_remove("ANTHROPIC_API_KEY")
        .env_remove("SUPERCODE_QUIET")
        .env_remove("SUPERCODE_NOTIFY")
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .expect("failed to spawn script(1)");

    let combined = format!(
        "{}{}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    (out.status.success(), combined)
}

fn run_piped(home: &Path, base_url: &str, args: &[&str], path_prepend: Option<&Path>) -> Output {
    let real_path = std::env::var("PATH").unwrap_or_default();
    let path = match path_prepend {
        Some(p) => format!("{}:{real_path}", p.display()),
        None => real_path,
    };
    Command::new(bin())
        .args(args)
        .env("SUPERCODE_HOME", home)
        .env("OPENROUTER_API_KEY", "x")
        .env("PATH", path)
        .env_remove("OPENAI_API_KEY")
        .env_remove("ANTHROPIC_API_KEY")
        .env_remove("SUPERCODE_QUIET")
        .env_remove("SUPERCODE_NOTIFY")
        .args(["--base-url", base_url])
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("failed to spawn the supercode binary")
        .wait_with_output()
        .expect("child process failed")
}

fn read_log(path: &Path) -> String {
    std::fs::read_to_string(path).unwrap_or_default()
}

/// Poll for the fake notify-send's log to contain a COMPLETE record — not
/// merely become non-empty — up to `timeout`. Necessary (not just
/// belt-and-suspenders) because `notify::fire_desktop`'s reap is
/// deliberately on a DETACHED background thread the CLI process never
/// joins (see its doc comment) — the `notify-send` child's `spawn()` call
/// is synchronous (so it's guaranteed to have been LAUNCHED by the time
/// `supercode`/`script` returns), but nothing guarantees the fake script
/// has finished ALL of its own `printf >>` writes by that exact instant,
/// especially under concurrent `cargo test -j2` CPU contention.
///
/// A plain non-empty check is NOT enough: `write_fake_notify_send`'s script
/// appends one argv line at a time (`--app-name=supercode`, then the
/// title, then the body), each via its own `printf ... >>` open/write/close
/// — so the log can legitimately be non-empty (title line already landed)
/// while the body line — the one substring every assertion below actually
/// needs — hasn't been written yet. Reading at that instant isn't a real
/// bug in the fire-and-forget design under test, but it WAS a real bug in
/// this harness's synchronization: a test that returns as soon as the log
/// stops being empty can race ahead of the write it's about to assert on.
/// The script's `---` trailing line is written unconditionally, only after
/// every argv line has landed — waiting for it (rather than for mere
/// non-emptiness) is the same "wait for the actual thing you're about to
/// assert on" fix `notify_cli.rs`'s other polling helper already models.
fn wait_for_complete_log_record(path: &Path, timeout: Duration) -> String {
    let deadline = std::time::Instant::now() + timeout;
    loop {
        let contents = read_log(path);
        if contents.contains("---\n") || std::time::Instant::now() >= deadline {
            return contents;
        }
        std::thread::sleep(Duration::from_millis(20));
    }
}

// ---- dev/01: fires when enabled, threshold met, real tty, notifier present.

#[test]
fn fires_desktop_notification_when_enabled_over_threshold() {
    let (addr, server) = spawn_sse_stub();
    let home = fresh_dir("fires");
    let fake_bin_dir = fresh_dir("fires-fakebin");
    let log_file = fresh_dir("fires-log").join("notify.log");
    write_fake_notify_send(&fake_bin_dir, &log_file);

    let base_url = format!("http://{addr}");
    let (ok, transcript) = run_under_pty(
        &home,
        &[
            "--base-url",
            &base_url,
            "--notify",
            "--notify-threshold-secs",
            "0",
            "run",
            "say hi",
        ],
        Some(&fake_bin_dir),
    );
    assert!(ok, "run under pty failed: {transcript}");
    assert!(
        transcript.contains("hello there"),
        "expected the streamed reply in the pty transcript, got: {transcript}"
    );

    server.join().expect("stub server thread panicked");

    let log = wait_for_complete_log_record(&log_file, Duration::from_secs(5));
    assert!(
        !log.is_empty(),
        "fake notify-send should have been invoked; log is empty. transcript: {transcript}"
    );
    assert!(
        log.contains("supercode: turn finished"),
        "notification title missing from fake notify-send log: {log}"
    );
    assert!(
        log.contains("hello there"),
        "notification body should carry a safe preview of the reply: {log}"
    );
}

// ---- dev/02: off by default (no --notify) — must NOT fire, even with a
// real tty and a working fake notifier on PATH.

#[test]
fn does_not_fire_when_notify_is_not_enabled() {
    let (addr, server) = spawn_sse_stub();
    let home = fresh_dir("disabled");
    let fake_bin_dir = fresh_dir("disabled-fakebin");
    let log_file = fresh_dir("disabled-log").join("notify.log");
    write_fake_notify_send(&fake_bin_dir, &log_file);

    let base_url = format!("http://{addr}");
    // Deliberately NO --notify flag — AC dev/02's "off by default".
    let (ok, transcript) = run_under_pty(
        &home,
        &["--base-url", &base_url, "run", "say hi"],
        Some(&fake_bin_dir),
    );
    assert!(ok, "run under pty failed: {transcript}");
    server.join().expect("stub server thread panicked");

    let log = read_log(&log_file);
    assert!(
        log.is_empty(),
        "notify-send must not fire when --notify was never passed; log: {log}"
    );
}

// ---- dev/02: never breaks the run when the notifier is absent — same
// enabled+threshold-met conditions as the firing test, but PATH has no
// notify-send on it at all (this box's real, unmodified PATH — verified
// headless: `which notify-send` fails here).

#[test]
fn degrades_silently_when_notify_send_is_absent() {
    let (addr, server) = spawn_sse_stub();
    let home = fresh_dir("absent");

    let base_url = format!("http://{addr}");
    let (ok, transcript) = run_under_pty(
        &home,
        &[
            "--base-url",
            &base_url,
            "--notify",
            "--notify-threshold-secs",
            "0",
            "run",
            "say hi",
        ],
        None, // no fake bin dir prepended — real PATH, no notify-send.
    );
    assert!(
        ok,
        "run must succeed (exit 0) even when notify-send is absent from PATH: {transcript}"
    );
    assert!(
        transcript.contains("hello there"),
        "the actual reply must still be delivered normally: {transcript}"
    );

    server.join().expect("stub server thread panicked");
}

// ---- driver directive: never fire for a machine output format, and stdout
// stays exactly the JSON envelope. Piped (non-tty) on purpose — the
// realistic shape of a scripted `run --output-format json` caller, and it
// additionally proves the non-tty half of the gate; `notify.rs`'s own
// `never_fires_for_machine_output_format` unit test isolates the
// machine-format condition alone (with a tty) at the pure-function level.

#[test]
fn json_output_format_never_notifies_and_stdout_stays_clean() {
    let (addr, server) = spawn_sse_stub();
    let home = fresh_dir("json");
    let fake_bin_dir = fresh_dir("json-fakebin");
    let log_file = fresh_dir("json-log").join("notify.log");
    write_fake_notify_send(&fake_bin_dir, &log_file);

    let out = run_piped(
        &home,
        &format!("http://{addr}"),
        &[
            "--notify",
            "--notify-threshold-secs",
            "0",
            "run",
            "--output-format",
            "json",
            "say hi",
        ],
        Some(&fake_bin_dir),
    );

    assert!(
        out.status.success(),
        "run --output-format json failed: stdout={} stderr={}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );

    // stdout is EXACTLY the JSON envelope — parseable, and carries the
    // reply, with nothing else mixed in.
    let stdout_text = String::from_utf8_lossy(&out.stdout);
    let parsed: serde_json::Value =
        serde_json::from_str(stdout_text.trim()).expect("stdout must be a single JSON envelope");
    assert!(
        parsed
            .get("result")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .contains("hello there"),
        "stdout JSON envelope missing expected result: {stdout_text}"
    );

    server.join().expect("stub server thread panicked");

    // The fake notify-send IS on PATH (so a gating bug would be caught),
    // yet its log must stay untouched — no spawn was ever attempted.
    assert!(
        read_log(&log_file).is_empty(),
        "notify-send must never fire for --output-format json"
    );
}