mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-04 02:33:56 +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>
203 lines
5.7 KiB
TypeScript
203 lines
5.7 KiB
TypeScript
import { createElement, type ReactNode } from "react";
|
|
import type { RunStreamItem } from "@qltysh/fabro-api-client";
|
|
import TestRenderer, { act } from "react-test-renderer";
|
|
|
|
import type { Stage } from "./stage-sidebar";
|
|
import { makeUsage } from "./test-fixtures";
|
|
import type { UnknownRecord } from "./unknown";
|
|
|
|
const IS_REACT_ACT_ENV = "IS_REACT_ACT_ENVIRONMENT" as const;
|
|
|
|
/**
|
|
* Per-test setup for code that uses react-test-renderer:
|
|
* - Sets IS_REACT_ACT_ENVIRONMENT (required by act()).
|
|
* - Silences react-test-renderer's deprecation warning.
|
|
*
|
|
* Returns a teardown function; pair with beforeEach/afterEach so the global
|
|
* state is scoped to the test rather than leaking process-wide.
|
|
*/
|
|
export function setupReactTestEnv(): () => void {
|
|
type Globals = { [IS_REACT_ACT_ENV]?: boolean };
|
|
const globals = globalThis as Globals;
|
|
const hadEnv = IS_REACT_ACT_ENV in globals;
|
|
const previousEnv = globals[IS_REACT_ACT_ENV];
|
|
globals[IS_REACT_ACT_ENV] = true;
|
|
|
|
const originalConsoleError = console.error;
|
|
console.error = ((...args: unknown[]) => {
|
|
if (
|
|
typeof args[0] === "string" &&
|
|
args[0].startsWith("react-test-renderer is deprecated")
|
|
) {
|
|
return;
|
|
}
|
|
originalConsoleError(...args);
|
|
}) as typeof console.error;
|
|
|
|
return () => {
|
|
console.error = originalConsoleError;
|
|
if (hadEnv) {
|
|
globals[IS_REACT_ACT_ENV] = previousEnv;
|
|
} else {
|
|
delete globals[IS_REACT_ACT_ENV];
|
|
}
|
|
};
|
|
}
|
|
|
|
const STREAM_EPOCH_MS = Date.parse("2026-04-09T12:00:00Z");
|
|
|
|
/** The stage a Petri item's subject names: a node, its visit and its firing. */
|
|
export interface StreamStage {
|
|
name: string;
|
|
visit?: number;
|
|
firing?: number;
|
|
/** The node's `meta.kind`; `agent` unless given. */
|
|
kind?: string;
|
|
}
|
|
|
|
/**
|
|
* A platform record item of a run's stream, as `GET /runs/{id}/events`
|
|
* serves it: `record.kind` names the fact. Recorded one second per `seq`
|
|
* after the stream epoch unless `recorded_at` is overridden.
|
|
*/
|
|
export function makePlatformItem(
|
|
seq: number,
|
|
record: UnknownRecord & { kind: string },
|
|
overrides: Partial<RunStreamItem> = {},
|
|
): RunStreamItem {
|
|
const recordedAt = overrides.recorded_at ?? STREAM_EPOCH_MS + seq * 1000;
|
|
return {
|
|
run_id: "run-1",
|
|
stream_seq: seq,
|
|
kind: "platform",
|
|
id: String(seq),
|
|
recorded_at: recordedAt,
|
|
item: { seq, recorded_at: recordedAt, record },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A Petri event item of a run's stream: the recorded `body` (named by its
|
|
* `event`) under the stage `subject`, with Petri's `derived` view beside it.
|
|
*/
|
|
export function makePetriItem(
|
|
seq: number,
|
|
body: UnknownRecord & { event: string },
|
|
{
|
|
stage,
|
|
derived,
|
|
...overrides
|
|
}: { stage?: StreamStage; derived?: UnknownRecord } & Partial<RunStreamItem> = {},
|
|
): RunStreamItem {
|
|
const recordedAt = overrides.recorded_at ?? STREAM_EPOCH_MS + seq * 1000;
|
|
const subject = stage
|
|
? {
|
|
node: { id: 0, name: stage.name, kind: "attractor/stage", meta: { kind: stage.kind ?? "agent" } },
|
|
firing: stage.firing ?? stage.visit ?? 1,
|
|
visit: stage.visit ?? 1,
|
|
attempt: 1,
|
|
}
|
|
: undefined;
|
|
return {
|
|
run_id: "run-1",
|
|
stream_seq: seq,
|
|
kind: "petri",
|
|
id: `execution 0/${seq}/0`,
|
|
recorded_at: recordedAt,
|
|
item: {
|
|
id: { log: "execution", execution: 0, seq, index: 0 },
|
|
origin: "external",
|
|
context: { invocation: 0, execution: 0 },
|
|
...(subject ? { subject } : {}),
|
|
recorded_at: recordedAt,
|
|
record: { seq, origin: "external", recorded_at: recordedAt, body },
|
|
...(derived ? { derived } : {}),
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A stage's `step.progress.recorded` carrying one Pebble `CodingAgentEvent`
|
|
* envelope: the `variant` (`AssistantMessage`, `ToolCallStarted`, ...) with
|
|
* its `payload`, stamped `ts`.
|
|
*/
|
|
export function makePebbleItem(
|
|
seq: number,
|
|
ts: string,
|
|
stage: StreamStage,
|
|
variant: string,
|
|
payload: UnknownRecord,
|
|
): RunStreamItem {
|
|
return makePetriItem(
|
|
seq,
|
|
{
|
|
event: "step.progress.recorded",
|
|
firing: stage.firing ?? stage.visit ?? 1,
|
|
ev: {
|
|
custom: {
|
|
kind: "pebble",
|
|
node: stage.name,
|
|
event: {
|
|
seq,
|
|
stream_id: "ses_1",
|
|
session_id: "ses_1",
|
|
timestamp: ts,
|
|
event: { [variant]: payload },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{ stage, recorded_at: Date.parse(ts) },
|
|
);
|
|
}
|
|
|
|
/** Flatten a rendered subtree to its visible text. */
|
|
export function textContent(node: TestRenderer.ReactTestInstance): string {
|
|
return node.children
|
|
.map((child) => (typeof child === "string" ? child : textContent(child)))
|
|
.join("");
|
|
}
|
|
|
|
/**
|
|
* Build a sidebar `Stage` fixture; override any field via `overrides`. Kept
|
|
* here so widening `Stage` updates every fixture at once — test files are
|
|
* excluded from typecheck, so a per-file copy silently goes stale instead.
|
|
*/
|
|
export function makeStage(overrides: Partial<Stage> = {}): Stage {
|
|
return {
|
|
id: "implement@1",
|
|
name: "implement",
|
|
handler: "agent",
|
|
nodeId: "implement",
|
|
visit: 1,
|
|
graphVisit: null,
|
|
resumedFromStageId: null,
|
|
parallelGroupId: null,
|
|
parallelBranchIndex: null,
|
|
status: "running",
|
|
duration: "--",
|
|
startedAt: null,
|
|
providerUsed: null,
|
|
usage: makeUsage(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function renderHook<T>(
|
|
hook: () => T,
|
|
options: { wrapper: React.ComponentType<{ children: ReactNode }> },
|
|
): { result: { current: T } } {
|
|
const result = { current: undefined as unknown as T };
|
|
function HookHost() {
|
|
result.current = hook();
|
|
return null;
|
|
}
|
|
act(() => {
|
|
TestRenderer.create(
|
|
createElement(options.wrapper, null, createElement(HookHost)),
|
|
);
|
|
});
|
|
return { result };
|
|
}
|