mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-28 06:24:48 +00:00
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>
163 lines
5.2 KiB
Rust
163 lines
5.2 KiB
Rust
use std::any::{TypeId, type_name};
|
|
|
|
use fabro_api::types::{
|
|
PairId as ApiPairId, PairMessageId as ApiPairMessageId,
|
|
PairMessageRecord as ApiPairMessageRecord, PairMessageRequest as ApiPairMessageRequest,
|
|
PairRecord as ApiPairRecord, PairStartRequest as ApiPairStartRequest,
|
|
PairStatus as ApiPairStatus, PairTarget as ApiPairTarget,
|
|
PairTranscriptEntry as ApiPairTranscriptEntry,
|
|
PairTranscriptResponse as ApiPairTranscriptResponse,
|
|
RunPairStatusResponse as ApiRunPairStatusResponse,
|
|
};
|
|
use fabro_types::{
|
|
PairId, PairMessageId, PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest,
|
|
PairStatus, PairTarget, PairTranscriptEntry, PairTranscriptResponse, RunPairStatusResponse,
|
|
fixtures,
|
|
};
|
|
use serde_json::{Value, json};
|
|
|
|
#[test]
|
|
fn pair_api_reuses_canonical_types() {
|
|
assert_same_type::<ApiPairId, PairId>();
|
|
assert_same_type::<ApiPairMessageId, PairMessageId>();
|
|
assert_same_type::<ApiPairStatus, PairStatus>();
|
|
assert_same_type::<ApiPairTarget, PairTarget>();
|
|
assert_same_type::<ApiPairRecord, PairRecord>();
|
|
assert_same_type::<ApiRunPairStatusResponse, RunPairStatusResponse>();
|
|
assert_same_type::<ApiPairStartRequest, PairStartRequest>();
|
|
assert_same_type::<ApiPairMessageRequest, PairMessageRequest>();
|
|
assert_same_type::<ApiPairMessageRecord, PairMessageRecord>();
|
|
assert_same_type::<ApiPairTranscriptEntry, PairTranscriptEntry>();
|
|
assert_same_type::<ApiPairTranscriptResponse, PairTranscriptResponse>();
|
|
}
|
|
|
|
#[test]
|
|
fn pair_target_omits_internal_fields() {
|
|
let target = PairTarget {
|
|
stage_id: "code@1".parse().unwrap(),
|
|
node_label: "Code".to_string(),
|
|
};
|
|
let serialized = serde_json::to_value(&target).unwrap();
|
|
assert_eq!(
|
|
serialized,
|
|
json!({
|
|
"stage_id": "code@1",
|
|
"node_label": "Code"
|
|
})
|
|
);
|
|
assert!(serialized.get("agent_session_id").is_none());
|
|
assert!(serialized.get("session_id").is_none());
|
|
assert!(serialized.get("node_id").is_none());
|
|
assert!(serialized.get("visit").is_none());
|
|
assert!(serialized.get("provider").is_none());
|
|
assert!(serialized.get("model").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn pair_start_request_only_carries_stage_id() {
|
|
let request = PairStartRequest {
|
|
stage_id: "code@1".parse().unwrap(),
|
|
};
|
|
let serialized = serde_json::to_value(&request).unwrap();
|
|
assert_eq!(
|
|
serialized,
|
|
json!({
|
|
"stage_id": "code@1"
|
|
})
|
|
);
|
|
assert!(serialized.get("agent_session_id").is_none());
|
|
assert!(serialized.get("target").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn pair_record_round_trips_json() {
|
|
assert_round_trip::<PairRecord>(json!({
|
|
"pair_id": "01HZX6M29F1CD5YYMHT1F5D7WQ",
|
|
"run_id": fixtures::RUN_1,
|
|
"status": "active",
|
|
"started_at": "2026-05-18T12:00:01Z",
|
|
"ended_at": null,
|
|
"failure_reason": null,
|
|
"target": pair_target_json()
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn pair_message_record_round_trips_json() {
|
|
assert_round_trip::<PairMessageRecord>(json!({
|
|
"message_id": "01HZX6M4D7Y1QW0Q0P6V8Z4DR5",
|
|
"client_message_id": "client-1",
|
|
"pair_id": "01HZX6M29F1CD5YYMHT1F5D7WQ",
|
|
"run_id": fixtures::RUN_1,
|
|
"stage_id": "code@1",
|
|
"text": "Can you inspect the failing test?",
|
|
"accepted_at": "2026-05-18T12:01:00Z"
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn pair_status_response_round_trips_json() {
|
|
assert_round_trip::<RunPairStatusResponse>(json!({
|
|
"run_id": fixtures::RUN_1,
|
|
"current_pair": null,
|
|
"targets": [pair_target_json()]
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn pair_transcript_response_round_trips_json() {
|
|
assert_round_trip::<PairTranscriptResponse>(json!({
|
|
"data": [
|
|
{
|
|
"kind": "user_message",
|
|
"seq": 42,
|
|
"event_id": "evt_1",
|
|
"ts": "2026-05-18T12:01:00Z",
|
|
"pair_id": "01HZX6M29F1CD5YYMHT1F5D7WQ",
|
|
"target": pair_target_json(),
|
|
"message_id": "01HZX6M4D7Y1QW0Q0P6V8Z4DR5",
|
|
"client_message_id": "client-1",
|
|
"text": "Can you inspect the failing test?"
|
|
},
|
|
{
|
|
"kind": "system_message",
|
|
"seq": 43,
|
|
"event_id": "evt_2",
|
|
"ts": "2026-05-18T12:01:01Z",
|
|
"pair_id": "01HZX6M29F1CD5YYMHT1F5D7WQ",
|
|
"target": pair_target_json(),
|
|
"system_message_kind": "human_joined",
|
|
"text": "A human has joined this workflow run for live pairing. Wait for their next message before continuing."
|
|
}
|
|
],
|
|
"meta": {
|
|
"next_since_seq": 44,
|
|
"has_more": false
|
|
}
|
|
}));
|
|
}
|
|
|
|
fn pair_target_json() -> Value {
|
|
json!({
|
|
"stage_id": "code@1",
|
|
"node_label": "Code"
|
|
})
|
|
}
|
|
|
|
fn assert_round_trip<T>(value: Value)
|
|
where
|
|
T: serde::de::DeserializeOwned + serde::Serialize,
|
|
{
|
|
let parsed: T = serde_json::from_value(value.clone()).unwrap();
|
|
assert_eq!(serde_json::to_value(parsed).unwrap(), value);
|
|
}
|
|
|
|
fn assert_same_type<T: 'static, U: 'static>() {
|
|
assert_eq!(
|
|
TypeId::of::<T>(),
|
|
TypeId::of::<U>(),
|
|
"{} should be the same type as {}",
|
|
type_name::<T>(),
|
|
type_name::<U>()
|
|
);
|
|
}
|