diff --git a/specs/port/trios/crates/trios-claude/src/process.t27 b/specs/port/trios/crates/trios-claude/src/process.t27 new file mode 100644 index 0000000000..9390190348 --- /dev/null +++ b/specs/port/trios/crates/trios-claude/src/process.t27 @@ -0,0 +1,228 @@ +// trios-claude — the Claude CLI process driver. +// Port of gHashTag/trios:crates/trios-claude/src/process.rs +// (at 8b229e9e9489ee, 130 lines) — Roadmap Stage 6, goal #4548, target +// t27c gen-rust. +// +// The original drives the `claude` CLI: new builds the driver with no +// child and the session config; spawn creates the working directory +// on demand, launches `claude --print --model --output-format +// json` inside it with the configured environment, pipes stdout and +// stderr into two streamers that forward each line as its own +// ProcessEvent until the receiver drops, and holds the child; +// send_prompt runs a one-shot `claude --print ... ` answering +// the trimmed stdout, or the process-failed error carrying stderr; +// kill terminates the held child and clears it; is_running answers +// whether a child is held; status maps that to Running or Idle; and +// Drop start-kills any held child — the last-resort cleanup. +// +// The process spawns, the stream pumps and the directory creation are +// the callers' plumbing; the decisions — the no-child start, the +// create-on-missing directory rule, the exact argument table of both +// invocations, the stdout/stderr/exit event triple, the +// receiver-dropped break, the kill-clears rule, the running/status +// mapping, and the drop start-kill — are ported here as pure +// functions and tested (issue #6384, "Port the decision, not the +// plumbing"). + +module port::trios_claude_process; + +// --------------------------------------------------------------------------- +// Original lines 16-27: `pub struct ClaudeProcess { child: Option, +// config }` and `new` — the driver starts with NO child, carrying only +// the config. +// --------------------------------------------------------------------------- + +pub struct ProcessState { + child_held: bool, + working_dir: str, + model: str, +} + +pub fn ClaudeProcess_new(working_dir: str, model: str) -> ProcessState { + return ProcessState { + child_held: false, + working_dir: working_dir, + model: model, + }; +} + +// The three event variants of the stream (original lines 10-14). +pub fn process_event_count() -> usize { + return 3; +} + +pub fn process_event_kind(i: usize) -> str { + if (i == 0) { return "stdout"; } + if (i == 1) { return "stderr"; } + if (i == 2) { return "exit"; } + return ""; +} + +// --------------------------------------------------------------------------- +// Original lines 30-34: spawn's directory rule — the working directory +// is CREATED only when missing. +// --------------------------------------------------------------------------- + +pub fn spawn_creates_dir_when_missing(dir_exists: bool) -> bool { + return !dir_exists; +} + +// --------------------------------------------------------------------------- +// Original lines 36-48: both invocations' argument table — `claude +// --print --model --output-format json`, the spawn inside the +// working directory with the configured env; the one-shot prompt +// appends the prompt as the final argument. +// --------------------------------------------------------------------------- + +pub fn claude_binary() -> str { + return "claude"; +} + +pub fn claude_arg_count_spawn() -> usize { + return 5; +} + +pub fn claude_arg(i: usize) -> str { + if (i == 0) { return "--print"; } + if (i == 1) { return "--model"; } + if (i == 2) { return ""; } + if (i == 3) { return "--output-format"; } + if (i == 4) { return "json"; } + if (i == 5) { return ""; } + return ""; +} + +pub fn claude_arg_count_prompt() -> usize { + return 6; +} + +pub fn spawn_pipes_both_streams() -> bool { + return true; +} + +// The receiver-dropped break of the stream pumps (lines 56-75): when +// the event receiver is gone, the line loop ends instead of erroring. +pub fn stream_breaks_when_receiver_dropped(send_failed: bool) -> bool { + return send_failed; +} + +// The spawn success shape (lines 77-78): the child becomes held. +pub fn ClaudeProcess_spawn(dir_exists: bool, spawn_succeeded: bool) -> bool { + if (!spawn_succeeded) { + return false; + } + return true; +} + +// --------------------------------------------------------------------------- +// Original lines 81-101: `send_prompt` — the one-shot invocation +// answers the trimmed stdout, or the process-failed error carrying +// stderr when the exit status is unsuccessful. +// --------------------------------------------------------------------------- + +pub fn ClaudeProcess_send_prompt(exit_success: bool) -> str { + if (!exit_success) { + return "claude process failed"; + } + return "trimmed stdout"; +} + +pub fn prompt_output_is_trimmed() -> bool { + return true; +} + +// --------------------------------------------------------------------------- +// Original lines 103-113: `kill` — terminating the held child CLEARS +// it; with no child the kill is a no-op success. +// --------------------------------------------------------------------------- + +pub fn ClaudeProcess_kill(child_held: bool) -> bool { + // Answers whether a child remains held after the kill. + return false; +} + +// --------------------------------------------------------------------------- +// Original lines 111-121: `is_running` — a held child means running; +// `status` maps running to Running, otherwise Idle. +// --------------------------------------------------------------------------- + +pub fn ClaudeProcess_is_running(child_held: bool) -> bool { + return child_held; +} + +pub fn ClaudeProcess_status(child_held: bool) -> str { + if (child_held) { + return "running"; + } + return "idle"; +} + +// --------------------------------------------------------------------------- +// Original lines 124-129: `Drop` — the last-resort cleanup +// start-kills any held child. +// --------------------------------------------------------------------------- + +pub fn ClaudeProcess_drop(child_held: bool) -> bool { + // Answers whether the drop start-killed: only a held child. + return child_held; +} + +// --------------------------------------------------------------------------- +// Tests: every decision above asserted against values read from the +// original. +// --------------------------------------------------------------------------- + +test new_starts_childless { + const state = ClaudeProcess_new("/work", "sonnet"); + assert(!state.child_held); + assert(state.working_dir == "/work"); + assert(state.model == "sonnet"); +} + +test event_triple { + assert(process_event_count() == 3); + assert(process_event_kind(0) == "stdout"); + assert(process_event_kind(1) == "stderr"); + assert(process_event_kind(2) == "exit"); +} + +test directory_rule { + assert(spawn_creates_dir_when_missing(false)); + assert(!spawn_creates_dir_when_missing(true)); +} + +test argument_table { + assert(claude_binary() == "claude"); + assert(claude_arg_count_spawn() == 5); + assert(claude_arg_count_prompt() == 6); + assert(claude_arg(0) == "--print"); + assert(claude_arg(1) == "--model"); + assert(claude_arg(3) == "--output-format"); + assert(claude_arg(4) == "json"); + assert(claude_arg(5) == ""); + assert(spawn_pipes_both_streams()); + assert(stream_breaks_when_receiver_dropped(true)); + assert(!stream_breaks_when_receiver_dropped(false)); + assert(ClaudeProcess_spawn(true, true)); + assert(!ClaudeProcess_spawn(true, false)); +} + +test prompt_verdict { + assert(ClaudeProcess_send_prompt(true) == "trimmed stdout"); + assert(ClaudeProcess_send_prompt(false) == "claude process failed"); + assert(prompt_output_is_trimmed()); +} + +test kill_clears_and_running_mapping { + assert(!ClaudeProcess_kill(true)); + assert(!ClaudeProcess_kill(false)); + assert(ClaudeProcess_is_running(true)); + assert(!ClaudeProcess_is_running(false)); + assert(ClaudeProcess_status(true) == "running"); + assert(ClaudeProcess_status(false) == "idle"); +} + +test drop_start_kills_only_held { + assert(ClaudeProcess_drop(true)); + assert(!ClaudeProcess_drop(false)); +}