// ===================================================================== // cli_dispatch.sx — acceptance for the `dist` CLI entrypoint (P3.1). // // Drives the BUILT `build/dist` binary through `process.run` (the binary, // not `sx run src/dist.sx` — only a real executable sees its own argv; // under `sx run` the process argv is the interpreter's). Asserts the // std.cli exit-code contract and the `--json` stdout-purity contract: // // 1. no args → human help/usage on STDERR + EX_USAGE (64). // 2. unknown command → human error on STDERR + EX_USAGE (64). // 3. `release promote --json` → STDOUT is a SINGLE valid JSON object // (parses via std.json with no trailing junk); the human acknowledgement // is on STDERR, never stdout. (`release promote` is still a stub; the // real `ci publish` json output is exercised by publish_happy.sx.) // 4. `--help` → lists the `ci` / `release` groups, exits 0. // 5. `ci publish --json` with NO required flags → EX_USAGE (64), error on // stderr (the --manifest / --local-store contract). // // `make test` depends on `build`, so `build/dist` exists before this runs; // the relative path resolves from the repo root (the `make test` cwd). // ===================================================================== #import "modules/std.sx"; proc :: #import "modules/process.sx"; json :: #import "modules/std/json.sx"; // True iff `needle` occurs in `hay`. Plain scan — the captured streams are // small, and the test only needs presence, not position. contains :: (hay: string, needle: string) -> bool { if needle.len == 0 { return true; } if needle.len > hay.len { return false; } i := 0; while i + needle.len <= hay.len { j := 0; ok := true; while j < needle.len { if hay[i + j] != needle[j] { ok = false; break; } j += 1; } if ok { return true; } i += 1; } return false; } main :: () -> s32 { gpa := GPA.init(); // ── 1. No command → readable usage on stderr, EX_USAGE (64) ─────── // `2>&1 1>/dev/null` routes the command's stderr into the captured // pipe and discards its stdout, so `r.stdout` here IS the stderr text. if r := proc.run("build/dist 2>&1 1>/dev/null") { proc.assert(r.exit_code == 64, "no-args must exit EX_USAGE (64)"); proc.assert(r.stdout.len > 0, "no-args must print human help/usage to stderr"); proc.assert(contains(r.stdout, "Usage"), "no-args help must carry a usage line"); } else { proc.assert(false, "spawn build/dist (no args) failed"); } // ── 2. Unknown command → readable error on stderr, EX_USAGE (64) ── if r := proc.run("build/dist bogus 2>&1 1>/dev/null") { proc.assert(r.exit_code == 64, "unknown command must exit EX_USAGE (64)"); proc.assert(contains(r.stdout, "unknown"), "unknown command must name the failure on stderr"); } else { proc.assert(false, "spawn build/dist bogus failed"); } // ── 3a. `--json` stdout purity: a single valid JSON object, nothing // else. `2>/dev/null` drops the human note so the pipe carries // ONLY stdout; std.json.parse rejects trailing junk. ───────── if r := proc.run("build/dist release promote --json 2>/dev/null") { proc.assert(r.exit_code == 0, "stub --json command must succeed (EX_OK)"); v, e := json.parse(r.stdout, xx gpa); proc.assert(!e, "stdout in --json mode must be a single valid JSON object (parse failed / trailing junk)"); if !e { o := v.object; proc.assert(o.len == 3, "stub json object carries command/status/stub"); proc.assert(o.items[0].key == "command" and o.items[0].val.str == "release promote", "stub json names the dispatched command"); proc.assert(o.items[1].key == "status" and o.items[1].val.str == "ok", "stub json reports status ok"); } } else { proc.assert(false, "spawn build/dist release promote --json failed"); } // ── 3b. `--json` mode keeps human text on STDERR (not stdout) ────── if r := proc.run("build/dist release promote --json 2>&1 1>/dev/null") { proc.assert(r.stdout.len > 0, "--json mode must still emit human text to stderr"); } else { proc.assert(false, "spawn build/dist release promote --json (stderr) failed"); } // ── 4. `--help` lists the ci / release groups, exits 0 ──────────── if r := proc.run("build/dist --help 2>/dev/null") { proc.assert(r.exit_code == 0, "--help exits 0"); proc.assert(contains(r.stdout, "ci"), "--help lists the ci group"); proc.assert(contains(r.stdout, "release"), "--help lists the release group"); } else { proc.assert(false, "spawn build/dist --help failed"); } // ── 5. `ci publish` requires --manifest / --local-store ─────────── // Missing a required flag is a usage error: EX_USAGE (64), human // diagnostic on stderr (`2>&1 1>/dev/null` captures the stderr text). if r := proc.run("build/dist ci publish --json 2>&1 1>/dev/null") { proc.assert(r.exit_code == 64, "ci publish without required flags must exit EX_USAGE (64)"); proc.assert(contains(r.stdout, "missing required flag"), "missing-flag error names the failure on stderr"); } else { proc.assert(false, "spawn build/dist ci publish (no flags) failed"); } print("cli_dispatch: ok\n"); return 0; }