mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Fix live parallel branch refresh and simplify branch rendering
Branches bypass the engine's stage.started/stage.completed lifecycle, so no SWR key invalidated the stages list while a fork ran. The new live branch rows stayed frozen at their first observed state until an incidental refetch. Map parallel.* events to the stages list, run events, and graph keys. Also: - Label branch rows with formatStageLabel so a re-entered branch renders as `review_glm@2`, matching the sidebar and waterfall. - Build branch rows in one pass and count live outcomes in one loop. - Name ParallelBranchId in the OpenAPI spec and reuse fabro_types:: ParallelBranchId, replacing two copies of an inline string format. - Hoist makeStage and textContent into lib/test-utils so widening Stage cannot leave per-file fixtures stale (tests are excluded from typecheck, so the two component-test copies had already gone stale). - Query stat tiles by data-stat instead of an exact Tailwind class. - Reuse append_scoped_stage_event's body via append_event_with_scope and add test_branch_event instead of poking envelope fields. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c812274db8
commit
690ddd2a96
16 changed files with 223 additions and 119 deletions
|
|
@ -9,6 +9,7 @@ import { StagePopover } from "./stage-popover";
|
|||
import { deriveStageSummary } from "./stage-popover-summary";
|
||||
import type { Stage } from "../lib/stage-sidebar";
|
||||
import { generatedAxios } from "../lib/api-client";
|
||||
import { makeStage as baseMakeStage } from "../lib/test-utils";
|
||||
|
||||
function makeEvent(overrides: Partial<EventEnvelope>): EventEnvelope {
|
||||
return {
|
||||
|
|
@ -22,20 +23,13 @@ function makeEvent(overrides: Partial<EventEnvelope>): EventEnvelope {
|
|||
}
|
||||
|
||||
function makeStage(overrides: Partial<Stage> = {}): Stage {
|
||||
return {
|
||||
id: "implement@1",
|
||||
name: "implement",
|
||||
handler: "agent",
|
||||
nodeId: "implement",
|
||||
visit: 1,
|
||||
graphVisit: null,
|
||||
resumedFromStageId: null,
|
||||
status: "succeeded",
|
||||
duration: "1m 30s",
|
||||
startedAt: "2026-05-24T11:58:30Z",
|
||||
providerUsed: { mode: "policy", model: "claude-opus-4-7", reasoning_effort: "high" },
|
||||
return baseMakeStage({
|
||||
status: "succeeded",
|
||||
duration: "1m 30s",
|
||||
startedAt: "2026-05-24T11:58:30Z",
|
||||
providerUsed: { mode: "policy", model: "claude-opus-4-7", reasoning_effort: "high" },
|
||||
...overrides,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
describe("deriveStageSummary", () => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ import type { EventEnvelope } from "@qltysh/fabro-api-client";
|
|||
import TestRenderer, { act } from "react-test-renderer";
|
||||
import { MemoryRouter } from "react-router";
|
||||
|
||||
import { makeEventEnvelope, setupReactTestEnv } from "../../lib/test-utils";
|
||||
import {
|
||||
makeEventEnvelope,
|
||||
makeStage as baseMakeStage,
|
||||
setupReactTestEnv,
|
||||
textContent,
|
||||
} from "../../lib/test-utils";
|
||||
import type { Stage } from "../stage-sidebar";
|
||||
import { ParallelChildren } from "./parallel-children";
|
||||
|
||||
|
|
@ -15,22 +20,14 @@ beforeEach(() => {
|
|||
afterEach(() => teardown());
|
||||
|
||||
function makeStage(overrides: Partial<Stage> = {}): Stage {
|
||||
return {
|
||||
return baseMakeStage({
|
||||
id: "stage@1",
|
||||
name: "stage",
|
||||
handler: "agent",
|
||||
status: StageState.RUNNING,
|
||||
duration: "--",
|
||||
nodeId: "stage",
|
||||
visit: 1,
|
||||
graphVisit: 1,
|
||||
resumedFromStageId: null,
|
||||
parallelGroupId: null,
|
||||
parallelBranchIndex: null,
|
||||
startedAt: "2026-04-09T12:00:00Z",
|
||||
providerUsed: null,
|
||||
...overrides,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const parallelStage = makeStage({
|
||||
|
|
@ -113,12 +110,6 @@ function renderParallel(
|
|||
return renderer;
|
||||
}
|
||||
|
||||
function textContent(node: TestRenderer.ReactTestInstance): string {
|
||||
return node.children
|
||||
.map((child) => typeof child === "string" ? child : textContent(child))
|
||||
.join("");
|
||||
}
|
||||
|
||||
function branchRowText(renderer: TestRenderer.ReactTestRenderer): string[] {
|
||||
return renderer.root.findAllByType("li").map(textContent);
|
||||
}
|
||||
|
|
@ -128,11 +119,7 @@ function hrefs(renderer: TestRenderer.ReactTestRenderer): string[] {
|
|||
}
|
||||
|
||||
function statValue(renderer: TestRenderer.ReactTestRenderer, label: string): string {
|
||||
const stat = renderer.root
|
||||
.findAllByProps({ className: "flex flex-col gap-0.5" })
|
||||
.find((item) => textContent(item).startsWith(label));
|
||||
if (!stat) throw new Error(`stat ${label} not found`);
|
||||
return textContent(stat.findAllByType("span")[1]);
|
||||
return textContent(renderer.root.findByProps({ "data-stat": label }));
|
||||
}
|
||||
|
||||
describe("ParallelChildren", () => {
|
||||
|
|
@ -171,6 +158,17 @@ describe("ParallelChildren", () => {
|
|||
expect(hrefs(renderer)).toEqual(["/runs/run-1/stages/review_glm@1"]);
|
||||
});
|
||||
|
||||
test("labels a re-entered branch with its visit, matching the sidebar", () => {
|
||||
const renderer = renderParallel(
|
||||
[startedEvent(1)],
|
||||
[branchStage("review_glm", 0, StageState.RUNNING, "fork@2", 2)],
|
||||
makeStage({ id: "fork@2", name: "fork", handler: "parallel", visit: 2 }),
|
||||
);
|
||||
|
||||
expect(branchRowText(renderer)).toEqual(["Runningreview_glm@2"]);
|
||||
expect(hrefs(renderer)).toEqual(["/runs/run-1/stages/review_glm@2"]);
|
||||
});
|
||||
|
||||
test("keeps duplicate branch targets in index order and only links recorded stages", () => {
|
||||
const renderer = renderParallel(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { StageState } from "@qltysh/fabro-api-client";
|
|||
import type { EventEnvelope } from "@qltysh/fabro-api-client";
|
||||
|
||||
import type { Stage } from "../stage-sidebar";
|
||||
import { stageStatusLabel, stageStatusTone } from "../../lib/stage-sidebar";
|
||||
import { formatStageLabel, stageStatusLabel, stageStatusTone } from "../../lib/stage-sidebar";
|
||||
import { formatDurationMs } from "../../lib/format";
|
||||
import { StageMetaBar } from "./meta-bar";
|
||||
import { parseParallelOverview } from "./helpers";
|
||||
|
|
@ -34,7 +34,9 @@ function StatItem({
|
|||
<span className="text-[10px] font-medium uppercase tracking-[0.16em] text-fg-muted">
|
||||
{label}
|
||||
</span>
|
||||
<span className={`font-mono text-xl tabular-nums ${toneClass}`}>{value}</span>
|
||||
<span data-stat={label} className={`font-mono text-xl tabular-nums ${toneClass}`}>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -108,20 +110,19 @@ export function ParallelChildren({
|
|||
}, [allStages, stage.id]);
|
||||
|
||||
const branchCount = overview.branchCount ?? stagesByBranchIndex.size;
|
||||
const slots = Array.from({ length: branchCount }, (_, index) => ({
|
||||
index,
|
||||
stage: stagesByBranchIndex.get(index) ?? null,
|
||||
result: overview.results[index] ?? null,
|
||||
}));
|
||||
const rows = slots.map<BranchRow>(({ index, stage: branchStage, result }) => {
|
||||
const rows = Array.from({ length: branchCount }, (_, index): BranchRow => {
|
||||
// A live branch stage is the freshest source; fall back to the completed
|
||||
// event's result for runs whose branches predate parallel identity.
|
||||
const branchStage = stagesByBranchIndex.get(index);
|
||||
if (branchStage) {
|
||||
return {
|
||||
branchIndex: index,
|
||||
id: branchStage.name,
|
||||
id: formatStageLabel(branchStage),
|
||||
status: branchStage.status,
|
||||
stageHref: `/runs/${runId}/stages/${branchStage.id}`,
|
||||
};
|
||||
}
|
||||
const result = overview.results[index];
|
||||
if (result) {
|
||||
return {
|
||||
branchIndex: index,
|
||||
|
|
@ -138,13 +139,12 @@ export function ParallelChildren({
|
|||
};
|
||||
});
|
||||
|
||||
const liveBranchStages = Array.from(stagesByBranchIndex.values());
|
||||
const liveSuccessCount = liveBranchStages
|
||||
.filter((branchStage) => branchStage.status === StageState.SUCCEEDED)
|
||||
.length;
|
||||
const liveFailureCount = liveBranchStages
|
||||
.filter((branchStage) => branchStage.status === StageState.FAILED)
|
||||
.length;
|
||||
let liveSuccessCount = 0;
|
||||
let liveFailureCount = 0;
|
||||
for (const branchStage of stagesByBranchIndex.values()) {
|
||||
if (branchStage.status === StageState.SUCCEEDED) liveSuccessCount += 1;
|
||||
else if (branchStage.status === StageState.FAILED) liveFailureCount += 1;
|
||||
}
|
||||
const successCount = overview.isComplete
|
||||
? overview.successCount ?? 0
|
||||
: liveSuccessCount;
|
||||
|
|
|
|||
|
|
@ -2,25 +2,9 @@ import { describe, expect, test } from "bun:test";
|
|||
import TestRenderer, { act } from "react-test-renderer";
|
||||
import { MemoryRouter } from "react-router";
|
||||
|
||||
import { makeStage } from "../lib/test-utils";
|
||||
import { StageSidebar, type Stage } from "./stage-sidebar";
|
||||
|
||||
function makeStage(overrides: Partial<Stage> = {}): Stage {
|
||||
return {
|
||||
id: "implement@1",
|
||||
name: "implement",
|
||||
handler: "agent",
|
||||
nodeId: "implement",
|
||||
visit: 1,
|
||||
graphVisit: null,
|
||||
resumedFromStageId: null,
|
||||
status: "running",
|
||||
duration: "--",
|
||||
startedAt: null,
|
||||
providerUsed: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderSidebar(stages: Stage[]): string {
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
let renderer!: TestRenderer.ReactTestRenderer;
|
||||
|
|
|
|||
|
|
@ -92,6 +92,36 @@ describe("queryKeysForRunEvent", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test("parallel branch lifecycle invalidates the stages list backing live branch rows", () => {
|
||||
// Branches bypass stage.started/stage.completed, so these events are the
|
||||
// only signal that a branch row's status changed.
|
||||
expect(queryKeysForRunEvent("run-1", "parallel.branch.started", "review_glm@1")).toEqual([
|
||||
queryKeys.runs.stages("run-1"),
|
||||
queryKeys.runs.events("run-1", 1000),
|
||||
queryKeys.runs.graph("run-1", "LR"),
|
||||
queryKeys.runs.graph("run-1", "TB"),
|
||||
queryKeys.runs.stageEvents("run-1", "review_glm@1"),
|
||||
]);
|
||||
expect(queryKeysForRunEvent("run-1", "parallel.branch.completed", "review_glm@1")).toEqual([
|
||||
queryKeys.runs.stages("run-1"),
|
||||
queryKeys.runs.events("run-1", 1000),
|
||||
queryKeys.runs.graph("run-1", "LR"),
|
||||
queryKeys.runs.graph("run-1", "TB"),
|
||||
queryKeys.runs.stageEvents("run-1", "review_glm@1"),
|
||||
]);
|
||||
});
|
||||
|
||||
test("fork lifecycle invalidates run-scoped resources without a stage id", () => {
|
||||
for (const event of ["parallel.started", "parallel.completed"]) {
|
||||
expect(queryKeysForRunEvent("run-1", event)).toEqual([
|
||||
queryKeys.runs.stages("run-1"),
|
||||
queryKeys.runs.events("run-1", 1000),
|
||||
queryKeys.runs.graph("run-1", "LR"),
|
||||
queryKeys.runs.graph("run-1", "TB"),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test("cancel requests invalidate the durable run summary", () => {
|
||||
expect(queryKeysForRunEvent("run-1", "run.cancel.requested")).toEqual([
|
||||
queryKeys.runs.detail("run-1"),
|
||||
|
|
|
|||
|
|
@ -88,6 +88,17 @@ export const STAGE_ACTIVITY_EVENT_TYPES = [
|
|||
] as const;
|
||||
export type StageActivityEventType = (typeof STAGE_ACTIVITY_EVENT_TYPES)[number];
|
||||
const STAGE_ACTIVITY_EVENTS = new Set<string>(STAGE_ACTIVITY_EVENT_TYPES);
|
||||
// Parallel branches bypass the engine's `stage.started` / `stage.completed`
|
||||
// lifecycle (the parallel handler dispatches each branch directly), so
|
||||
// `STAGE_EVENTS` never fires for them. Without this set the stages list never
|
||||
// refetches while a fork runs and branch rows stay frozen at their first
|
||||
// observed state.
|
||||
const PARALLEL_EVENTS = new Set([
|
||||
"parallel.started",
|
||||
"parallel.branch.started",
|
||||
"parallel.branch.completed",
|
||||
"parallel.completed",
|
||||
]);
|
||||
const INTERVIEW_EVENTS = new Set([
|
||||
"interview.started",
|
||||
"interview.completed",
|
||||
|
|
@ -179,6 +190,19 @@ export function queryKeysForRunEvent(
|
|||
return keys;
|
||||
}
|
||||
|
||||
if (PARALLEL_EVENTS.has(event)) {
|
||||
const keys: Key[] = [
|
||||
queryKeys.runs.stages(runId),
|
||||
queryKeys.runs.events(runId, 1000),
|
||||
queryKeys.runs.graph(runId, "LR"),
|
||||
queryKeys.runs.graph(runId, "TB"),
|
||||
];
|
||||
if (stageId) {
|
||||
keys.push(queryKeys.runs.stageEvents(runId, stageId));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
if (STEERING_EVENTS.has(event)) {
|
||||
const keys: Key[] = [queryKeys.runs.events(runId, 1000)];
|
||||
if (AGENT_CONTROL_STATE_EVENTS.has(event)) {
|
||||
|
|
|
|||
|
|
@ -3,23 +3,10 @@ import type { PaginatedRunStageList, StageHandler, StageState } from "@qltysh/fa
|
|||
|
||||
import type { Stage } from "../components/stage-sidebar";
|
||||
import { aggregateGraphNodeStatus, formatStageLabel, mapRunStagesToSidebarStages } from "./stage-sidebar";
|
||||
import { makeStage as baseMakeStage } from "./test-utils";
|
||||
|
||||
function makeStage(nodeId: string, visit: number, status: StageState): Stage {
|
||||
return {
|
||||
id: `${nodeId}@${visit}`,
|
||||
name: nodeId,
|
||||
handler: "agent",
|
||||
nodeId,
|
||||
visit,
|
||||
graphVisit: null,
|
||||
resumedFromStageId: null,
|
||||
parallelGroupId: null,
|
||||
parallelBranchIndex: null,
|
||||
status,
|
||||
duration: "--",
|
||||
startedAt: null,
|
||||
providerUsed: null,
|
||||
};
|
||||
return baseMakeStage({ id: `${nodeId}@${visit}`, name: nodeId, nodeId, visit, status });
|
||||
}
|
||||
|
||||
describe("mapRunStagesToSidebarStages", () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { createElement, type ReactNode } from "react";
|
|||
import type { EventEnvelope } from "@qltysh/fabro-api-client";
|
||||
import TestRenderer, { act } from "react-test-renderer";
|
||||
|
||||
import type { Stage } from "./stage-sidebar";
|
||||
|
||||
const IS_REACT_ACT_ENV = "IS_REACT_ACT_ENVIRONMENT" as const;
|
||||
|
||||
/**
|
||||
|
|
@ -55,6 +57,37 @@ export function makeEventEnvelope(
|
|||
} as EventEnvelope;
|
||||
}
|
||||
|
||||
/** 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,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderHook<T>(
|
||||
hook: () => T,
|
||||
options: { wrapper: React.ComponentType<{ children: ReactNode }> },
|
||||
|
|
|
|||
|
|
@ -9951,10 +9951,9 @@ components:
|
|||
Durable identity of one execution of a parallel node, formatted as
|
||||
"{node_id}@{visit}".
|
||||
parallel_branch_id:
|
||||
type: ["string", "null"]
|
||||
description: >
|
||||
Durable identity of one branch within a parallel execution,
|
||||
formatted as "{parallel_group_id}:{index}".
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/ParallelBranchId"
|
||||
- type: "null"
|
||||
session_id:
|
||||
type: ["string", "null"]
|
||||
parent_session_id:
|
||||
|
|
@ -10653,10 +10652,9 @@ components:
|
|||
$ref: "#/components/schemas/ParallelBranchResult"
|
||||
description: Ordered per-branch results produced by a parallel stage.
|
||||
parallel_branch_id:
|
||||
type: ["string", "null"]
|
||||
description: >
|
||||
Durable identity of this branch within a parallel execution,
|
||||
formatted as "{parallel_group_id}:{index}".
|
||||
oneOf:
|
||||
- $ref: "#/components/schemas/ParallelBranchId"
|
||||
- type: "null"
|
||||
output:
|
||||
type: ["string", "null"]
|
||||
output_bytes:
|
||||
|
|
@ -12575,6 +12573,13 @@ components:
|
|||
type: string
|
||||
example: verify@2
|
||||
|
||||
ParallelBranchId:
|
||||
description: >-
|
||||
Durable identity of one branch within a parallel execution, in
|
||||
`{parallel_group_id}:{index}` form.
|
||||
type: string
|
||||
example: review_fork@3:1
|
||||
|
||||
StageState:
|
||||
description: Lifecycle projection state of a workflow stage.
|
||||
type: string
|
||||
|
|
|
|||
|
|
@ -4905,7 +4905,16 @@ async fn append_scoped_stage_event(
|
|||
parallel_group_id: None,
|
||||
parallel_branch_id: None,
|
||||
};
|
||||
let stored = fabro_workflow::event::to_run_event_at(&run_id, event, Utc::now(), Some(&scope));
|
||||
append_event_with_scope(state, run_id, event, &scope).await;
|
||||
}
|
||||
|
||||
async fn append_event_with_scope(
|
||||
state: &Arc<AppState>,
|
||||
run_id: RunId,
|
||||
event: &workflow_event::Event,
|
||||
scope: &fabro_workflow::event::StageScope,
|
||||
) {
|
||||
let stored = fabro_workflow::event::to_run_event_at(&run_id, event, Utc::now(), Some(scope));
|
||||
let payload = fabro_workflow::event::build_redacted_event_payload(&stored, &run_id).unwrap();
|
||||
let run_store = state.stores.runs.open_run(&run_id).await.unwrap();
|
||||
run_store.append_event(&payload).await.unwrap();
|
||||
|
|
@ -5551,11 +5560,7 @@ async fn list_run_stages_exposes_parallel_branch_identity() {
|
|||
parallel_group_id,
|
||||
parallel_branch_id,
|
||||
);
|
||||
let stored =
|
||||
workflow_event::to_run_event_at(&run_id, &branch_event, Utc::now(), Some(&branch_scope));
|
||||
let payload = workflow_event::build_redacted_event_payload(&stored, &run_id).unwrap();
|
||||
let run_store = state.stores.runs.open_run(&run_id).await.unwrap();
|
||||
run_store.append_event(&payload).await.unwrap();
|
||||
append_event_with_scope(&state, run_id, &branch_event, &branch_scope).await;
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
|
|
|
|||
|
|
@ -1589,6 +1589,18 @@ mod tests {
|
|||
event
|
||||
}
|
||||
|
||||
fn test_branch_event(
|
||||
seq: u32,
|
||||
body: EventBody,
|
||||
stage_id: StageId,
|
||||
branch_id: ParallelBranchId,
|
||||
) -> EventEnvelope {
|
||||
let mut event = test_stage_event(seq, body, stage_id);
|
||||
event.event.parallel_group_id = Some(branch_id.group().clone());
|
||||
event.event.parallel_branch_id = Some(branch_id);
|
||||
event
|
||||
}
|
||||
|
||||
fn test_stage_event_at(
|
||||
seq: u32,
|
||||
ts: &str,
|
||||
|
|
@ -2286,7 +2298,7 @@ mod tests {
|
|||
let group_id = StageId::new("review_fork", 1);
|
||||
let branch_stage_id = StageId::new("review_glm", 1);
|
||||
let branch_id = ParallelBranchId::new(group_id.clone(), 0);
|
||||
let mut started = test_stage_event(
|
||||
let started = test_branch_event(
|
||||
3,
|
||||
EventBody::ParallelBranchStarted(ParallelBranchStartedProps {
|
||||
index: 0,
|
||||
|
|
@ -2294,9 +2306,8 @@ mod tests {
|
|||
resumed_from_stage_id: None,
|
||||
}),
|
||||
branch_stage_id.clone(),
|
||||
branch_id.clone(),
|
||||
);
|
||||
started.event.parallel_group_id = Some(group_id.clone());
|
||||
started.event.parallel_branch_id = Some(branch_id.clone());
|
||||
|
||||
state.apply_event(&started).unwrap();
|
||||
|
||||
|
|
@ -2309,8 +2320,7 @@ mod tests {
|
|||
Some(&branch_id)
|
||||
);
|
||||
|
||||
let replacement_branch_id = ParallelBranchId::new(group_id.clone(), 1);
|
||||
let mut reobserved = test_stage_event(
|
||||
let reobserved = test_branch_event(
|
||||
4,
|
||||
EventBody::ParallelBranchStarted(ParallelBranchStartedProps {
|
||||
index: 1,
|
||||
|
|
@ -2318,9 +2328,8 @@ mod tests {
|
|||
resumed_from_stage_id: Some(StageId::new("review_glm", 2)),
|
||||
}),
|
||||
branch_stage_id.clone(),
|
||||
ParallelBranchId::new(group_id, 1),
|
||||
);
|
||||
reobserved.event.parallel_group_id = Some(group_id);
|
||||
reobserved.event.parallel_branch_id = Some(replacement_branch_id);
|
||||
|
||||
state.apply_event(&reobserved).unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -345,6 +345,7 @@ fn main() {
|
|||
("Conclusion", "fabro_types::Conclusion", &[]),
|
||||
("StageOutcome", "fabro_types::StageOutcome", &[]),
|
||||
("StageId", "fabro_types::StageId", &[]),
|
||||
("ParallelBranchId", "fabro_types::ParallelBranchId", &[]),
|
||||
("StageHandler", "fabro_types::StageHandler", &[]),
|
||||
("StageState", "fabro_types::StageState", &[]),
|
||||
("AgentControlState", "fabro_types::AgentControlState", &[]),
|
||||
|
|
|
|||
|
|
@ -52,19 +52,19 @@ pub mod types {
|
|||
McpServerReplace as ReplaceMcpServerRequest, McpServerStatus, McpServerView as McpServer,
|
||||
McpTransportView, Message, PairId, PairMessageId, PairMessageRecord, PairMessageRequest,
|
||||
PairRecord, PairStartRequest, PairStatus, PairTarget, PairTranscriptEntry,
|
||||
PairTranscriptResponse, ParallelBranchResult, PendingInterviewRecord, PermissionLevel,
|
||||
PreRunPushOutcome, Principal, PullRequest, PullRequestDetails, PullRequestDetailsStatus,
|
||||
PullRequestDetailsUnavailableReason, PullRequestLink, PullRequestMeta, PullRequestResponse,
|
||||
QuestionType, ReasoningOutput, RepositoryRef, Role, Run, RunApproval, RunApprovalState,
|
||||
RunClientProvenance, RunEvent, RunEventDetailContentKind, RunEventDetailResponse,
|
||||
RunFailure, RunPairStatusResponse, RunProjection, RunProvenance, RunRunnableSource,
|
||||
RunSandbox, RunSandboxFailure, RunSandboxInstance, RunSandboxKind, RunSandboxPlan,
|
||||
RunSandboxRuntime, RunServerProvenance, RunSize, SandboxDetails, SandboxInfo,
|
||||
SandboxListMeta, SandboxListResponse, SandboxNetwork, SandboxNetworkPolicy,
|
||||
SandboxNetworkPolicyMode, SandboxProviderKind, SandboxProviderLookupError,
|
||||
SandboxResources, SandboxService, SandboxServiceListResponse, SandboxState,
|
||||
SandboxTimestamps, SecretMetadata, SecretType, ServerSettings, SessionDetail, SessionId,
|
||||
SessionMessage, SessionRecord, SessionStatus, SessionSummary, SessionTurn,
|
||||
PairTranscriptResponse, ParallelBranchId, ParallelBranchResult, PendingInterviewRecord,
|
||||
PermissionLevel, PreRunPushOutcome, Principal, PullRequest, PullRequestDetails,
|
||||
PullRequestDetailsStatus, PullRequestDetailsUnavailableReason, PullRequestLink,
|
||||
PullRequestMeta, PullRequestResponse, QuestionType, ReasoningOutput, RepositoryRef, Role,
|
||||
Run, RunApproval, RunApprovalState, RunClientProvenance, RunEvent,
|
||||
RunEventDetailContentKind, RunEventDetailResponse, RunFailure, RunPairStatusResponse,
|
||||
RunProjection, RunProvenance, RunRunnableSource, RunSandbox, RunSandboxFailure,
|
||||
RunSandboxInstance, RunSandboxKind, RunSandboxPlan, RunSandboxRuntime, RunServerProvenance,
|
||||
RunSize, SandboxDetails, SandboxInfo, SandboxListMeta, SandboxListResponse, SandboxNetwork,
|
||||
SandboxNetworkPolicy, SandboxNetworkPolicyMode, SandboxProviderKind,
|
||||
SandboxProviderLookupError, SandboxResources, SandboxService, SandboxServiceListResponse,
|
||||
SandboxState, SandboxTimestamps, SecretMetadata, SecretType, ServerSettings, SessionDetail,
|
||||
SessionId, SessionMessage, SessionRecord, SessionStatus, SessionSummary, SessionTurn,
|
||||
SkillsProjection, StageCompletion, StageContextWindow, StageContextWindowBreakdownItem,
|
||||
StageContextWindowCategory, StageContextWindowCountMethod, StageContextWindowProjection,
|
||||
StageContextWindowStaleness, StageContextWindowUnavailableReason,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
use std::any::{TypeId, type_name};
|
||||
|
||||
use fabro_api::types::ParallelBranchId as ApiParallelBranchId;
|
||||
use fabro_types::{ParallelBranchId, StageId};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn parallel_branch_id_reuses_canonical_type() {
|
||||
assert_same_type::<ApiParallelBranchId, ParallelBranchId>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parallel_branch_id_round_trips_openapi_representation() {
|
||||
let branch_id = ParallelBranchId::new(StageId::new("review_fork", 3), 1);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(&branch_id).unwrap(),
|
||||
json!("review_fork@3:1")
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<ApiParallelBranchId>(json!("review_fork@3:1")).unwrap(),
|
||||
branch_id
|
||||
);
|
||||
}
|
||||
|
||||
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>()
|
||||
);
|
||||
}
|
||||
|
|
@ -37,7 +37,7 @@ export interface RunEvent {
|
|||
*/
|
||||
'parallel_group_id'?: string | null;
|
||||
/**
|
||||
* Durable identity of one branch within a parallel execution, formatted as \"{parallel_group_id}:{index}\".
|
||||
* Durable identity of one branch within a parallel execution, in `{parallel_group_id}:{index}` form.
|
||||
*/
|
||||
'parallel_branch_id'?: string | null;
|
||||
'session_id'?: string | null;
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export interface StageProjection {
|
|||
*/
|
||||
'parallel_results'?: Array<ParallelBranchResult> | null;
|
||||
/**
|
||||
* Durable identity of this branch within a parallel execution, formatted as \"{parallel_group_id}:{index}\".
|
||||
* Durable identity of one branch within a parallel execution, in `{parallel_group_id}:{index}` form.
|
||||
*/
|
||||
'parallel_branch_id'?: string | null;
|
||||
'output'?: string | null;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue