fix: make console graph demo usable
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,7 @@ describe("WorkflowGraph", () => {
|
|||||||
expect(findNodeById(container, "start")).not.toBeNull();
|
expect(findNodeById(container, "start")).not.toBeNull();
|
||||||
expect(findNodeById(container, "review")).not.toBeNull();
|
expect(findNodeById(container, "review")).not.toBeNull();
|
||||||
expect(findNodeById(container, "end")).not.toBeNull();
|
expect(findNodeById(container, "end")).not.toBeNull();
|
||||||
|
expect(container.querySelectorAll(".react-flow__handle")).toHaveLength(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onNodeSelect when node is clicked", () => {
|
it("calls onNodeSelect when node is clicked", () => {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import {
|
|||||||
ReactFlow,
|
ReactFlow,
|
||||||
Background,
|
Background,
|
||||||
Controls,
|
Controls,
|
||||||
MiniMap,
|
Handle,
|
||||||
|
Position,
|
||||||
type Node,
|
type Node,
|
||||||
type Edge,
|
type Edge,
|
||||||
type NodeTypes,
|
type NodeTypes,
|
||||||
@@ -47,10 +48,12 @@ const CustomNode = ({ data, selected }: { data: WorkflowGraphNodeData; selected:
|
|||||||
className={`graph-node graph-node--${data.kind} ${selected ? "graph-node--selected" : ""} ${isActive ? "graph-node--active" : ""}`}
|
className={`graph-node graph-node--${data.kind} ${selected ? "graph-node--selected" : ""} ${isActive ? "graph-node--active" : ""}`}
|
||||||
style={{ borderColor: nodeColor(data) }}
|
style={{ borderColor: nodeColor(data) }}
|
||||||
>
|
>
|
||||||
|
<Handle type="target" position={Position.Top} />
|
||||||
<div className="graph-node__label">{data.label}</div>
|
<div className="graph-node__label">{data.label}</div>
|
||||||
{data.nodeRef && (
|
{data.nodeRef && (
|
||||||
<div className="graph-node__ref">{data.nodeRef}</div>
|
<div className="graph-node__ref">{data.nodeRef}</div>
|
||||||
)}
|
)}
|
||||||
|
<Handle type="source" position={Position.Bottom} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -113,7 +116,6 @@ export const WorkflowGraph = ({ model, activeNodeId = null, onNodeSelect }: Work
|
|||||||
>
|
>
|
||||||
<Background />
|
<Background />
|
||||||
<Controls />
|
<Controls />
|
||||||
<MiniMap />
|
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 () => {
|
it("ignores stale responses after target change", async () => {
|
||||||
let callCount = 0;
|
let callCount = 0;
|
||||||
mockCallOperation.mockImplementation(async () => {
|
mockCallOperation.mockImplementation(async () => {
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ export const useLifecycleExplorer = (
|
|||||||
): LifecycleExplorerController => {
|
): LifecycleExplorerController => {
|
||||||
const [state, dispatch] = useReducer(lifecycleReducer, initialLifecycleState);
|
const [state, dispatch] = useReducer(lifecycleReducer, initialLifecycleState);
|
||||||
const generationRef = useRef(0);
|
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 rawEvidenceRef = useRef<ReadonlyArray<EvidenceRecord>>([]);
|
||||||
const evidenceSeqRef = useRef(0);
|
const evidenceSeqRef = useRef(0);
|
||||||
|
|
||||||
@@ -81,6 +83,19 @@ export const useLifecycleExplorer = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} 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({
|
dispatch({
|
||||||
type: "pushError",
|
type: "pushError",
|
||||||
error: {
|
error: {
|
||||||
@@ -128,14 +143,14 @@ export const useLifecycleExplorer = (
|
|||||||
(artifactId: string | null) => {
|
(artifactId: string | null) => {
|
||||||
dispatch({ type: "selectArtifact", artifactId });
|
dispatch({ type: "selectArtifact", artifactId });
|
||||||
if (!artifactId || !target) return;
|
if (!artifactId || !target) return;
|
||||||
inspectGenerationRef.current++;
|
artifactGenerationRef.current++;
|
||||||
const generation = inspectGenerationRef.current;
|
const generation = artifactGenerationRef.current;
|
||||||
const [id, version] = artifactId.split("@");
|
const [id, version] = artifactId.split("@");
|
||||||
executeOperation(
|
executeOperation(
|
||||||
"workflow.artifacts.inspect",
|
"workflow.artifacts.inspect",
|
||||||
{ artifact_id: id, version: Number(version) },
|
{ artifact_id: id, version: Number(version) },
|
||||||
generation,
|
generation,
|
||||||
inspectGenerationRef,
|
artifactGenerationRef,
|
||||||
(interpreted) => {
|
(interpreted) => {
|
||||||
dispatch({ type: "setArtifactDetail", detail: decodeArtifactDetail(interpreted) });
|
dispatch({ type: "setArtifactDetail", detail: decodeArtifactDetail(interpreted) });
|
||||||
},
|
},
|
||||||
@@ -148,15 +163,15 @@ export const useLifecycleExplorer = (
|
|||||||
(deploymentId: string | null) => {
|
(deploymentId: string | null) => {
|
||||||
dispatch({ type: "selectDeployment", deploymentId });
|
dispatch({ type: "selectDeployment", deploymentId });
|
||||||
if (!deploymentId || !target) return;
|
if (!deploymentId || !target) return;
|
||||||
inspectGenerationRef.current++;
|
deploymentGenerationRef.current++;
|
||||||
const generation = inspectGenerationRef.current;
|
const generation = deploymentGenerationRef.current;
|
||||||
// Deployment selection fans out to inspect + validate. Both describe the
|
// Deployment selection fans out to inspect + validate. Both describe the
|
||||||
// same selected deployment, so they must share one generation token.
|
// same selected deployment, so they must share one generation token.
|
||||||
executeOperation(
|
executeOperation(
|
||||||
"workflow.deployments.inspect",
|
"workflow.deployments.inspect",
|
||||||
{ deployment_id: deploymentId },
|
{ deployment_id: deploymentId },
|
||||||
generation,
|
generation,
|
||||||
inspectGenerationRef,
|
deploymentGenerationRef,
|
||||||
(interpreted) => {
|
(interpreted) => {
|
||||||
dispatch({ type: "setDeploymentDetail", detail: decodeDeploymentDetail(interpreted) });
|
dispatch({ type: "setDeploymentDetail", detail: decodeDeploymentDetail(interpreted) });
|
||||||
},
|
},
|
||||||
@@ -165,7 +180,7 @@ export const useLifecycleExplorer = (
|
|||||||
"workflow.deployments.validate",
|
"workflow.deployments.validate",
|
||||||
{ deployment_id: deploymentId },
|
{ deployment_id: deploymentId },
|
||||||
generation,
|
generation,
|
||||||
inspectGenerationRef,
|
deploymentGenerationRef,
|
||||||
(interpreted) => {
|
(interpreted) => {
|
||||||
dispatch({ type: "setDeploymentValidation", validation: decodeDeploymentValidation(interpreted) });
|
dispatch({ type: "setDeploymentValidation", validation: decodeDeploymentValidation(interpreted) });
|
||||||
},
|
},
|
||||||
@@ -178,15 +193,27 @@ export const useLifecycleExplorer = (
|
|||||||
(runId: string | null) => {
|
(runId: string | null) => {
|
||||||
dispatch({ type: "selectRun", runId });
|
dispatch({ type: "selectRun", runId });
|
||||||
if (!runId || !target) return;
|
if (!runId || !target) return;
|
||||||
inspectGenerationRef.current++;
|
runGenerationRef.current++;
|
||||||
const generation = inspectGenerationRef.current;
|
const generation = runGenerationRef.current;
|
||||||
executeOperation(
|
executeOperation(
|
||||||
"workflow.runs.inspect",
|
"workflow.runs.inspect",
|
||||||
{ run_id: runId },
|
{ run_id: runId },
|
||||||
generation,
|
generation,
|
||||||
inspectGenerationRef,
|
runGenerationRef,
|
||||||
(interpreted) => {
|
(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(
|
const loadTrace = useCallback(
|
||||||
(start: number, limit: number) => {
|
(start: number, limit: number) => {
|
||||||
if (!state.selectedRunId || !target) return;
|
if (!state.selectedRunId || !target) return;
|
||||||
inspectGenerationRef.current++;
|
runGenerationRef.current++;
|
||||||
const generation = inspectGenerationRef.current;
|
const generation = runGenerationRef.current;
|
||||||
executeOperation(
|
executeOperation(
|
||||||
"workflow.runs.trace",
|
"workflow.runs.trace",
|
||||||
{ run_id: state.selectedRunId, trace_range: { start, limit } },
|
{ run_id: state.selectedRunId, trace_range: { start, limit } },
|
||||||
generation,
|
generation,
|
||||||
inspectGenerationRef,
|
runGenerationRef,
|
||||||
(interpreted) => {
|
(interpreted) => {
|
||||||
dispatch({ type: "setTrace", trace: decodeTracePage(interpreted) });
|
dispatch({ type: "setTrace", trace: decodeTracePage(interpreted) });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ tbody tr:hover {
|
|||||||
/* Layout grid */
|
/* Layout grid */
|
||||||
.app-layout {
|
.app-layout {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(22rem, 0.7fr);
|
grid-template-columns: minmax(24rem, 0.9fr) minmax(30rem, 1.1fr);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
@@ -260,6 +260,83 @@ tbody tr:hover {
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-content,
|
||||||
|
.workflow-graph {
|
||||||
|
height: 34rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
section[aria-label="Lifecycle Explorer"]:has(.graph-content) {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-layout:has(.graph-content) > section[aria-label="Source Inventory"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph .react-flow {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-node {
|
||||||
|
min-width: 13rem;
|
||||||
|
max-width: 16rem;
|
||||||
|
padding: 0.7rem 0.85rem;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--color-ink);
|
||||||
|
box-shadow: 0 3px 0 rgba(26, 26, 26, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-node__label {
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
line-height: 1;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-node__ref {
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
color: var(--color-slate);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph .react-flow__handle {
|
||||||
|
width: 0.55rem;
|
||||||
|
height: 0.55rem;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
background: var(--color-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph .react-flow__edge-path {
|
||||||
|
stroke: var(--color-slate);
|
||||||
|
stroke-width: 1.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph .react-flow__edge-text {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
fill: var(--color-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-graph .react-flow__controls {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Motion */
|
/* Motion */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from {
|
from {
|
||||||
|
|||||||
@@ -52,6 +52,22 @@ export type WorkflowSourcesListInterpreted = {
|
|||||||
readonly total: number;
|
readonly total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const interpretNextActions = (nextActions: {
|
||||||
|
readonly can_continue: boolean;
|
||||||
|
readonly can_save_now: boolean | null;
|
||||||
|
readonly recommended_next_tool: string | null;
|
||||||
|
readonly reason: string;
|
||||||
|
readonly patch_examples: ReadonlyArray<unknown>;
|
||||||
|
readonly warnings: ReadonlyArray<string>;
|
||||||
|
}) => ({
|
||||||
|
canContinue: nextActions.can_continue,
|
||||||
|
canSaveNow: nextActions.can_save_now,
|
||||||
|
recommendedNextTool: nextActions.recommended_next_tool,
|
||||||
|
reason: nextActions.reason,
|
||||||
|
patchExamples: nextActions.patch_examples,
|
||||||
|
warnings: nextActions.warnings,
|
||||||
|
});
|
||||||
|
|
||||||
const operationEntries: ReadonlyArray<OperationMeta> = [
|
const operationEntries: ReadonlyArray<OperationMeta> = [
|
||||||
{
|
{
|
||||||
method: "workflow.health",
|
method: "workflow.health",
|
||||||
@@ -236,7 +252,7 @@ const operationEntries: ReadonlyArray<OperationMeta> = [
|
|||||||
artifactVersion: decoded.artifact_version,
|
artifactVersion: decoded.artifact_version,
|
||||||
status: decoded.status,
|
status: decoded.status,
|
||||||
diagnostics: decoded.diagnostics,
|
diagnostics: decoded.diagnostics,
|
||||||
nextActions: decoded.next_actions,
|
nextActions: interpretNextActions(decoded.next_actions),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -304,7 +320,7 @@ const operationEntries: ReadonlyArray<OperationMeta> = [
|
|||||||
output: decoded.output,
|
output: decoded.output,
|
||||||
diagnostics: decoded.diagnostics,
|
diagnostics: decoded.diagnostics,
|
||||||
traceCount: decoded.trace_count,
|
traceCount: decoded.trace_count,
|
||||||
nextActions: decoded.next_actions,
|
nextActions: interpretNextActions(decoded.next_actions),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -333,7 +349,7 @@ const operationEntries: ReadonlyArray<OperationMeta> = [
|
|||||||
return {
|
return {
|
||||||
runId: r.run_id,
|
runId: r.run_id,
|
||||||
status: r.status,
|
status: r.status,
|
||||||
trace,
|
frames: trace,
|
||||||
traceStart: r.trace_start,
|
traceStart: r.trace_start,
|
||||||
traceLimit: r.trace_limit,
|
traceLimit: r.trace_limit,
|
||||||
traceTruncated: r.trace_truncated,
|
traceTruncated: r.trace_truncated,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const ArtifactNodeSchema = Schema.Struct({
|
|||||||
export const WorkflowArtifactsListResultSchema = Schema.Struct({
|
export const WorkflowArtifactsListResultSchema = Schema.Struct({
|
||||||
nodes: Schema.Array(ArtifactNodeSchema),
|
nodes: Schema.Array(ArtifactNodeSchema),
|
||||||
total: NonNegativeIntegerSchema,
|
total: NonNegativeIntegerSchema,
|
||||||
cursor: Schema.NullOr(Schema.String),
|
cursor: Schema.optional(Schema.NullOr(Schema.String)),
|
||||||
next_cursor: Schema.NullOr(Schema.String),
|
next_cursor: Schema.NullOr(Schema.String),
|
||||||
limit: Schema.optional(PositiveIntegerSchema),
|
limit: Schema.optional(PositiveIntegerSchema),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ const lifecycleCases = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
total: 1,
|
total: 1,
|
||||||
cursor: null,
|
|
||||||
next_cursor: null,
|
next_cursor: null,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
},
|
},
|
||||||
@@ -436,4 +435,41 @@ describe("lifecycle operations", () => {
|
|||||||
expect(exchange.interpreted).toBeDefined();
|
expect(exchange.interpreted).toBeDefined();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it("interprets run trace frames with the console lifecycle shape", async () => {
|
||||||
|
const traceCase = lifecycleCases.find(
|
||||||
|
(testCase) => testCase.operation === "workflow.runs.trace",
|
||||||
|
);
|
||||||
|
expect(traceCase).toBeDefined();
|
||||||
|
if (!traceCase) return;
|
||||||
|
|
||||||
|
const fetch: typeof globalThis.fetch = async (input, init) => {
|
||||||
|
const request = await requestBody(input, init);
|
||||||
|
return jsonResponse({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: request.id,
|
||||||
|
result: traceCase.result,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const exchange = await runOperation(
|
||||||
|
{ fetch },
|
||||||
|
traceCase.operation as "workflow.health" | "workflow.sources.list",
|
||||||
|
traceCase.params,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(exchange.interpreted).toMatchObject({
|
||||||
|
frames: [
|
||||||
|
{
|
||||||
|
nodeId: "review",
|
||||||
|
stepType: "interrupt",
|
||||||
|
outcome: "submitted",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
traceStart: 0,
|
||||||
|
traceLimit: 50,
|
||||||
|
traceTruncated: false,
|
||||||
|
});
|
||||||
|
expect(exchange.interpreted).not.toHaveProperty("trace");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user