fabro/lib/apps/fabro-cli/tests/it/cmd/system_events.rs
Bryan Helmkamp bc31b8d23a
Serve the run stream as the only run event API
Step 4 of the legacy executor deletion, third commit: the legacy event
API and every reader of it go, so that the next commits can delete the
event log, its reducer and the types beneath them.

The API:
- `GET /runs/{id}/events` pages the run stream only
  (`PaginatedRunStreamList` by `after`); the legacy `since_seq`,
  `before_seq` and `order` cursors, the `oneOf` envelope, the legacy
  `EventEnvelope`, `PaginatedEventList`, `RunEvent`, `EventSeq`,
  `AppendEventResponse` and `RunEventDetailResponse` schemas,
  `POST /runs/{id}/events`, `GET /runs/{id}/events/{seq}` and
  `GET /runs/{id}/stages/{stageId}/events` are deleted. `GET
  /runs/{id}/attach` and `GET /attach` frame `RunStreamItem`s only.
- The Rust and TypeScript clients regenerate; the removed models leave
  the TypeScript package.

The readers:
- `fabro-client` drops the legacy run event listing, tail and attach
  methods and `RunEventStream`; `list_run_stream_until` bounds a stream
  read.
- `fabro-tool`'s `fabro_run_events` lists, searches and details the run
  stream: `after` is the exclusive `stream_seq` cursor, `event_id` the
  item's id, filters match the item's name and `recorded_at`.
- `fabro-dump` writes the stream to `events.jsonl`; `fabro dump` reads
  it.
- The CLI's progress renderer keeps only what the run stream drives:
  the legacy event conversion, the sandbox and setup displays and their
  styles go. `fabro system events` prints stream items.
- The server's demo mode folds its agent fixture straight into the
  session projection and answers the attach stub with a stream item;
  the demo stage events endpoint is gone.
- The web app: every run is a Petri run. The legacy event hooks,
  renderer props, stage popover summary, run phases derivation and
  live-event payload handling are deleted or ported to `RunStreamItem`;
  toasts and board refreshes read the stream's platform records.
- Tests: the legacy API round trips and pagination tests are deleted;
  the CLI's MCP, attach and system event mocks serve stream pages; the
  CLI test helpers read stream items.

Still failing until the later commits: the CLI tests seeded through
`POST /runs/{id}/events`, the server tests over the legacy store, and
the legacy type tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 13:36:00 -04:00

123 lines
4 KiB
Rust

use fabro_test::{fabro_snapshot, test_context};
use httpmock::MockServer;
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["system", "events", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Stream run events from the server
Usage: fabro system events [OPTIONS]
Options:
--json Output as JSON [env: FABRO_JSON=]
--storage-dir <STORAGE_DIR> Local storage directory (default: ~/.fabro/storage) [env: FABRO_STORAGE_DIR=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--server <SERVER> Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--run-id <RUN_IDS> Filter by run ID (repeatable)
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-h, --help Print help
----- stderr -----
");
}
#[test]
fn system_events_renders_text_lines_from_sse_payloads() {
let context = test_context!();
let server = MockServer::start();
let run_id = crate::support::unique_run_id();
// One `RunStreamItem`: the platform record that ended the run.
let payload = serde_json::json!({
"run_id": run_id,
"stream_seq": 7,
"kind": "platform",
"id": "7",
"recorded_at": 1_775_390_400_000_u64,
"item": {
"seq": 7,
"recorded_at": 1_775_390_400_000_u64,
"record": {
"kind": "run.lifecycle",
"transition": "succeeded",
"status": { "kind": "succeeded", "reason": "completed" }
}
}
});
let attach_mock = server.mock(|when, then| {
when.method("GET")
.path("/api/v1/attach")
.query_param("run_id", run_id.as_str());
then.status(200)
.header("Content-Type", "text/event-stream")
.body(format!("data: {payload}\n\n"));
});
let output = context
.command()
.args([
"system",
"events",
"--server",
&format!("{}/api/v1", server.base_url()),
"--run-id",
&run_id,
])
.output()
.expect("command should run");
assert!(output.status.success(), "system events failed");
let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
assert_eq!(
stdout.trim(),
format!("2026-04-05T12:00:00+00:00 {} run.lifecycle", &run_id[..12])
);
attach_mock.assert();
}
#[test]
fn system_events_json_emits_raw_sse_payloads() {
let context = test_context!();
let server = MockServer::start();
let run_id = crate::support::unique_run_id();
let payload = serde_json::json!({
"payload": {
"ts": "2026-04-05T12:00:00Z",
"run_id": run_id,
"event": "run.completed",
}
});
let attach_mock = server.mock(|when, then| {
when.method("GET")
.path("/api/v1/attach")
.query_param("run_id", run_id.as_str());
then.status(200)
.header("Content-Type", "text/event-stream")
.body(format!("data: {payload}\n\n"));
});
let output = context
.command()
.args([
"--json",
"system",
"events",
"--server",
&format!("{}/api/v1", server.base_url()),
"--run-id",
&run_id,
])
.output()
.expect("command should run");
assert!(output.status.success(), "system events failed");
let stdout = String::from_utf8(output.stdout).expect("stdout should be UTF-8");
assert_eq!(stdout.trim(), payload.to_string());
attach_mock.assert();
}