fix: make console graph demo usable

This commit is contained in:
lda
2026-07-02 23:14:06 +07:00 Verified
parent 3cdcf6ff3b
commit 86ccbe35cf
9 changed files with 1560 additions and 23 deletions
@@ -161,6 +161,119 @@ describe("useLifecycleExplorer", () => {
});
});
it("keeps deployment validation when a run selection happens before validation returns", async () => {
let releaseValidation: (() => void) | null = null;
const validationGate = new Promise<void>((resolve) => {
releaseValidation = resolve;
});
mockCallOperation.mockImplementation(async (operation: string) => {
if (operation === "workflow.deployments.validate") {
await validationGate;
return {
ok: true,
operation,
interpreted: {
deploymentId: "report.default",
artifactId: "report",
artifactVersion: 1,
status: "runnable",
diagnostics: [],
nextActions: {
canContinue: true,
canSaveNow: null,
recommendedNextTool: null,
reason: "deployment is runnable",
patchExamples: [],
warnings: [],
},
},
exchange: { request: {}, response: {} },
equivalentCli: "uv run wf deploy validate report.default",
durationMs: 5,
};
}
if (operation === "workflow.deployments.inspect") {
return {
ok: true,
operation,
interpreted: {
id: "report.default",
artifactId: "report",
artifactVersion: 1,
bindings: [],
driftPolicy: "block",
},
exchange: { request: {}, response: {} },
equivalentCli: "uv run wf deploy inspect report.default",
durationMs: 5,
};
}
if (operation === "workflow.runs.inspect") {
return {
ok: true,
operation,
interpreted: {
runId: "run_1",
deploymentId: "report.default",
artifactId: "report",
artifactVersion: 1,
status: "completed",
resumeReadiness: "not_applicable",
interrupt: null,
outcome: "ok",
error: null,
output: {},
diagnostics: [],
traceCount: 0,
nextActions: {
canContinue: false,
canSaveNow: null,
recommendedNextTool: null,
reason: "done",
patchExamples: [],
warnings: [],
},
},
exchange: { request: {}, response: {} },
equivalentCli: "uv run wf run inspect run_1",
durationMs: 5,
};
}
return {
ok: true,
operation,
interpreted:
operation === "workflow.deployments.list"
? { items: [] }
: { items: [], total: 0, nextCursor: null },
exchange: { request: {}, response: {} },
equivalentCli: "",
durationMs: 5,
};
});
const recordEvidence = vi.fn();
const { result } = renderHook(() =>
useLifecycleExplorer("http://127.0.0.1:8000/rpc", recordEvidence),
);
await act(async () => {
result.current.selectDeployment("report.default");
result.current.selectRun("run_1");
});
await act(async () => {
releaseValidation?.();
await validationGate;
});
await waitFor(() => {
expect(result.current.state.runDetail?.runId).toBe("run_1");
expect(result.current.state.deploymentValidation?.status).toBe("runnable");
});
});
it("ignores stale responses after target change", async () => {
let callCount = 0;
mockCallOperation.mockImplementation(async () => {
@@ -38,7 +38,9 @@ export const useLifecycleExplorer = (
): LifecycleExplorerController => {
const [state, dispatch] = useReducer(lifecycleReducer, initialLifecycleState);
const generationRef = useRef(0);
const inspectGenerationRef = useRef(0);
const artifactGenerationRef = useRef(0);
const deploymentGenerationRef = useRef(0);
const runGenerationRef = useRef(0);
const rawEvidenceRef = useRef<ReadonlyArray<EvidenceRecord>>([]);
const evidenceSeqRef = useRef(0);
@@ -81,6 +83,19 @@ export const useLifecycleExplorer = (
});
}
} else {
const seq = evidenceSeqRef.current++;
const record: EvidenceRecord = {
id: `${operation}-${seq}`,
operation,
label: `${operation} failed`,
equivalentCli: "unavailable: operation failed before CLI metadata",
request: result.exchange.request,
response: result.exchange.response,
durationMs: 0,
};
recordEvidence(record);
rawEvidenceRef.current = [...rawEvidenceRef.current, record];
dispatch({ type: "setRawEvidence", evidence: rawEvidenceRef.current });
dispatch({
type: "pushError",
error: {
@@ -128,14 +143,14 @@ export const useLifecycleExplorer = (
(artifactId: string | null) => {
dispatch({ type: "selectArtifact", artifactId });
if (!artifactId || !target) return;
inspectGenerationRef.current++;
const generation = inspectGenerationRef.current;
artifactGenerationRef.current++;
const generation = artifactGenerationRef.current;
const [id, version] = artifactId.split("@");
executeOperation(
"workflow.artifacts.inspect",
{ artifact_id: id, version: Number(version) },
generation,
inspectGenerationRef,
artifactGenerationRef,
(interpreted) => {
dispatch({ type: "setArtifactDetail", detail: decodeArtifactDetail(interpreted) });
},
@@ -148,15 +163,15 @@ export const useLifecycleExplorer = (
(deploymentId: string | null) => {
dispatch({ type: "selectDeployment", deploymentId });
if (!deploymentId || !target) return;
inspectGenerationRef.current++;
const generation = inspectGenerationRef.current;
deploymentGenerationRef.current++;
const generation = deploymentGenerationRef.current;
// Deployment selection fans out to inspect + validate. Both describe the
// same selected deployment, so they must share one generation token.
executeOperation(
"workflow.deployments.inspect",
{ deployment_id: deploymentId },
generation,
inspectGenerationRef,
deploymentGenerationRef,
(interpreted) => {
dispatch({ type: "setDeploymentDetail", detail: decodeDeploymentDetail(interpreted) });
},
@@ -165,7 +180,7 @@ export const useLifecycleExplorer = (
"workflow.deployments.validate",
{ deployment_id: deploymentId },
generation,
inspectGenerationRef,
deploymentGenerationRef,
(interpreted) => {
dispatch({ type: "setDeploymentValidation", validation: decodeDeploymentValidation(interpreted) });
},
@@ -178,15 +193,27 @@ export const useLifecycleExplorer = (
(runId: string | null) => {
dispatch({ type: "selectRun", runId });
if (!runId || !target) return;
inspectGenerationRef.current++;
const generation = inspectGenerationRef.current;
runGenerationRef.current++;
const generation = runGenerationRef.current;
executeOperation(
"workflow.runs.inspect",
{ run_id: runId },
generation,
inspectGenerationRef,
runGenerationRef,
(interpreted) => {
dispatch({ type: "setRunDetail", detail: decodeRunDetail(interpreted) });
const detail = decodeRunDetail(interpreted);
dispatch({ type: "setRunDetail", detail });
if (detail.traceCount > 0) {
executeOperation(
"workflow.runs.trace",
{ run_id: runId, trace_range: { start: 0, limit: 50 } },
generation,
runGenerationRef,
(traceInterpreted) => {
dispatch({ type: "setTrace", trace: decodeTracePage(traceInterpreted) });
},
);
}
},
);
},
@@ -243,13 +270,13 @@ export const useLifecycleExplorer = (
const loadTrace = useCallback(
(start: number, limit: number) => {
if (!state.selectedRunId || !target) return;
inspectGenerationRef.current++;
const generation = inspectGenerationRef.current;
runGenerationRef.current++;
const generation = runGenerationRef.current;
executeOperation(
"workflow.runs.trace",
{ run_id: state.selectedRunId, trace_range: { start, limit } },
generation,
inspectGenerationRef,
runGenerationRef,
(interpreted) => {
dispatch({ type: "setTrace", trace: decodeTracePage(interpreted) });
},