fix: preserve capability call receipts

This commit is contained in:
lda
2026-08-11 22:01:50 +07:00 Verified
parent fe49d1a11c
commit a7bc3d5d24
3 changed files with 285 additions and 137 deletions
@@ -65,8 +65,9 @@ const capabilityEvidence = (
qualifiedName: string,
durationMs: number,
evidenceTarget = target,
payload: Record<string, unknown> = {},
payload: unknown = {},
deploymentId: string | null = null,
method = "workflow.capabilities.call",
): EvidenceRecord => ({
id,
target: evidenceTarget,
@@ -74,9 +75,14 @@ const capabilityEvidence = (
label: "Call capability",
equivalentCli: "wf capability call",
request: {
qualified_name: qualifiedName,
payload,
...(deploymentId === null ? {} : { deployment_id: deploymentId }),
jsonrpc: "2.0",
method,
params: {
qualified_name: qualifiedName,
payload,
...(deploymentId === null ? {} : { deployment_id: deploymentId }),
},
id,
},
response: {},
durationMs,
@@ -142,7 +148,7 @@ const submitNodeCall = async (query = "README.md"): Promise<void> => {
const user = userEvent.setup();
await user.click(screen.getByRole("tab", { name: "Try capability" }));
await user.type(screen.getByRole("textbox", { name: "Query" }), query);
await user.click(screen.getByRole("button", { name: "Call capability" }));
await user.click(screen.getByRole("button", { name: "Call capability now" }));
};
beforeEach(() => {
@@ -219,7 +225,7 @@ describe("CapabilityPlayground", () => {
expect(screen.getByRole("textbox", { name: "Query" })).toBeInTheDocument();
expect(screen.queryByRole("group", { name: "Value source" })).not.toBeInTheDocument();
expect(screen.getByRole("checkbox", { name: /I understand/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Call capability" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Call capability now" })).toBeDisabled();
});
it("gates the call behind acknowledgement and never calls for render, tab selection, or edits", async () => {
@@ -248,7 +254,7 @@ describe("CapabilityPlayground", () => {
await user.click(screen.getByRole("tab", { name: "Try capability" }));
await user.type(screen.getByRole("textbox", { name: "Query" }), "README.md");
await user.click(screen.getByRole("button", { name: "Call capability" }));
await user.click(screen.getByRole("button", { name: "Call capability now" }));
expect(call).toHaveBeenCalledWith({ query: "README.md" });
});
@@ -278,7 +284,7 @@ describe("CapabilityPlayground", () => {
await user.click(screen.getByRole("tab", { name: "Try capability" }));
await user.type(screen.getByRole("textbox", { name: "Query" }), "README.md");
await user.click(screen.getByRole("button", { name: "Call capability" }));
await user.click(screen.getByRole("button", { name: "Call capability now" }));
await user.clear(screen.getByRole("textbox", { name: "Query" }));
await user.type(screen.getByRole("textbox", { name: "Query" }), "changed.md");
await user.clear(screen.getByRole("textbox", { name: "Wrapper deployment ID" }));
@@ -342,7 +348,7 @@ describe("CapabilityPlayground", () => {
renderPlayground();
await user.click(screen.getByRole("tab", { name: "Try capability" }));
await user.click(screen.getByRole("button", { name: "Call capability" }));
await user.click(screen.getByRole("button", { name: "Call capability now" }));
expect(call).not.toHaveBeenCalled();
expect(screen.getAllByRole("alert").at(-1)).toHaveTextContent(
@@ -358,7 +364,7 @@ describe("CapabilityPlayground", () => {
await user.click(screen.getByRole("tab", { name: "Try capability" }));
await user.type(screen.getByRole("textbox", { name: "Query" }), "README.md");
await user.click(screen.getByRole("button", { name: "Call capability" }));
await user.click(screen.getByRole("button", { name: "Call capability now" }));
activeController = controller({
phase: "result",
@@ -435,6 +441,54 @@ describe("CapabilityPlayground", () => {
expect(screen.getByText("Document source was checked.")).toBeInTheDocument();
expect(screen.getByText("A direct capability call creates no workflow run or trace.")).toBeInTheDocument();
expect(screen.getByLabelText("Capability output")).toHaveTextContent("README.md");
expect(screen.getByRole("status")).toHaveTextContent(
"Capability call completed. Outcome: ok.",
);
expect(screen.getByRole("status")).not.toContainElement(
screen.getByLabelText("Capability output"),
);
});
it("preserves the submitted snapshot and completed receipt across Contract and Try tabs", async () => {
const user = userEvent.setup();
let activeController = controller({ acknowledged: true });
mockedUseCapabilityPlayground.mockImplementation(() => activeController);
const view = renderPlayground();
await submitNodeCall();
activeController = controller({
phase: "result",
acknowledged: true,
result: callResult(),
});
mockedUseCapabilityPlayground.mockReturnValue(activeController);
mockedUseConsoleWorkspace.mockReturnValue(
workspaceWithEvidence([
capabilityEvidence("call-tab-state", nodeCapability.name, 26, target, {
query: "README.md",
}),
]),
);
view.rerender(
<CapabilityPlayground
capability={nodeCapability}
executor={writeExecutor}
target={target}
/>,
);
expect(screen.getByLabelText("Submitted payload")).toHaveTextContent("README.md");
await user.click(screen.getByRole("tab", { name: "Contract" }));
expect(screen.getAllByRole("status")).toHaveLength(1);
expect(screen.getByRole("status")).toHaveTextContent(
"Capability call completed. Outcome: ok.",
);
await user.click(screen.getByRole("tab", { name: "Try capability" }));
expect(screen.getByLabelText("Submitted payload")).toHaveTextContent("README.md");
expect(screen.getAllByRole("status")).toHaveLength(1);
expect(screen.getByText("26 ms")).toBeInTheDocument();
expect(screen.getByRole("checkbox", { name: /I understand/i })).toBeChecked();
});
it("labels runtime_error as completed without claiming a workflow run or trace", async () => {
@@ -481,7 +535,7 @@ describe("CapabilityPlayground", () => {
});
await user.click(screen.getByRole("tab", { name: "Try capability" }));
await user.click(screen.getByRole("button", { name: "Call capability" }));
await user.click(screen.getByRole("button", { name: "Call capability now" }));
expect(call).not.toHaveBeenCalled();
expect(screen.getByRole("alert")).toHaveTextContent(
@@ -505,7 +559,7 @@ describe("CapabilityPlayground", () => {
await userEvent.click(screen.getByRole("tab", { name: "Try capability" }));
await userEvent.type(screen.getByRole("textbox", { name: "Query" }), "README.md");
await userEvent.click(screen.getByRole("button", { name: "Call capability" }));
await userEvent.click(screen.getByRole("button", { name: "Call capability now" }));
firstState = controller({
phase: "result",
acknowledged: true,
@@ -572,7 +626,92 @@ describe("CapabilityPlayground", () => {
expect(screen.queryByText("99 ms")).not.toBeInTheDocument();
});
it("matches the exact submitted payload among same-target calls", async () => {
it.each([
{
label: "redacted",
payload: { query: "[redacted]" },
durationMs: 33,
},
{
label: "truncated",
payload: "[truncated: evidence limit]",
durationMs: 34,
},
])("attributes realistic evidence with a $label payload projection", async ({
payload,
durationMs,
}) => {
let activeController = controller({ acknowledged: true });
mockedUseCapabilityPlayground.mockImplementation(() => activeController);
const view = renderPlayground();
await submitNodeCall();
activeController = controller({
phase: "result",
acknowledged: true,
result: callResult(),
});
mockedUseCapabilityPlayground.mockReturnValue(activeController);
mockedUseConsoleWorkspace.mockReturnValue(
workspaceWithEvidence([
capabilityEvidence(
`call-${durationMs}`,
nodeCapability.name,
durationMs,
target,
payload,
),
]),
);
view.rerender(
<CapabilityPlayground
capability={nodeCapability}
executor={writeExecutor}
target={target}
/>,
);
expect(screen.getByText(`${durationMs} ms`)).toBeInTheDocument();
});
it("ignores an evidence record whose JSON-RPC method does not match", async () => {
let activeController = controller({ acknowledged: true });
mockedUseCapabilityPlayground.mockImplementation(() => activeController);
const view = renderPlayground();
await submitNodeCall();
activeController = controller({
phase: "result",
acknowledged: true,
result: callResult(),
});
mockedUseCapabilityPlayground.mockReturnValue(activeController);
mockedUseConsoleWorkspace.mockReturnValue(
workspaceWithEvidence([
capabilityEvidence(
"call-wrong-method",
nodeCapability.name,
88,
target,
{ query: "README.md" },
null,
"workflow.capabilities.list",
),
]),
);
view.rerender(
<CapabilityPlayground
capability={nodeCapability}
executor={writeExecutor}
target={target}
/>,
);
expect(screen.queryByText("88 ms")).not.toBeInTheDocument();
expect(screen.getByText("Call evidence was not retained for this connection.")).toBeInTheDocument();
});
it("fails closed when same-identity calls differ only by sanitized payload", async () => {
let activeController = controller({ acknowledged: true });
mockedUseCapabilityPlayground.mockImplementation(() => activeController);
const view = renderPlayground();
@@ -602,8 +741,9 @@ describe("CapabilityPlayground", () => {
/>,
);
expect(screen.getByText("24 ms")).toBeInTheDocument();
expect(screen.queryByText("24 ms")).not.toBeInTheDocument();
expect(screen.queryByText("99 ms")).not.toBeInTheDocument();
expect(screen.getByText("Call evidence was not retained for this connection.")).toBeInTheDocument();
});
it("matches the normalized submitted deployment", async () => {
@@ -616,7 +756,7 @@ describe("CapabilityPlayground", () => {
const user = userEvent.setup();
await user.click(screen.getByRole("tab", { name: "Try capability" }));
await user.type(screen.getByRole("textbox", { name: "Query" }), "README.md");
await user.click(screen.getByRole("button", { name: "Call capability" }));
await user.click(screen.getByRole("button", { name: "Call capability now" }));
activeController = controller({
phase: "result",
@@ -738,7 +878,7 @@ describe("CapabilityPlayground", () => {
const user = userEvent.setup();
await user.clear(screen.getByRole("textbox", { name: "Query" }));
await user.type(screen.getByRole("textbox", { name: "Query" }), "second.md");
await user.click(screen.getByRole("button", { name: "Call capability" }));
await user.click(screen.getByRole("button", { name: "Call capability now" }));
activeController = controller({
phase: "result",
@@ -49,7 +49,6 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
type CapabilityRequestProjection = {
readonly deploymentId: string | null;
readonly payload: Record<string, unknown>;
readonly qualifiedName: string;
};
@@ -62,43 +61,29 @@ const normalizeDeploymentId = (value: unknown): string | null => {
const capabilityRequestProjection = (
request: unknown,
): CapabilityRequestProjection | null => {
if (!isRecord(request) || typeof request.qualified_name !== "string") {
if (
!isRecord(request) ||
request.jsonrpc !== "2.0" ||
request.method !== "workflow.capabilities.call" ||
!isRecord(request.params)
) {
return null;
}
if (!isRecord(request.payload)) return null;
const params = request.params;
if (typeof params.qualified_name !== "string") return null;
if (
request.deployment_id !== undefined &&
request.deployment_id !== null &&
typeof request.deployment_id !== "string"
params.deployment_id !== undefined &&
params.deployment_id !== null &&
typeof params.deployment_id !== "string"
) {
return null;
}
return {
deploymentId: normalizeDeploymentId(request.deployment_id),
payload: request.payload,
qualifiedName: request.qualified_name,
deploymentId: normalizeDeploymentId(params.deployment_id),
qualifiedName: params.qualified_name,
};
};
const jsonDeepEqual = (left: unknown, right: unknown): boolean => {
if (Object.is(left, right)) return true;
if (left === null || right === null) return false;
if (typeof left !== typeof right) return false;
if (Array.isArray(left) || Array.isArray(right)) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
return false;
}
return left.every((value, index) => jsonDeepEqual(value, right[index]));
}
if (!isRecord(left) || !isRecord(right)) return false;
const leftKeys = Object.keys(left).sort();
const rightKeys = Object.keys(right).sort();
if (leftKeys.length !== rightKeys.length) return false;
return leftKeys.every(
(key, index) => key === rightKeys[index] && jsonDeepEqual(left[key], right[key]),
);
};
const submittedCallEvidence = (
evidence: ReadonlyArray<EvidenceRecord>,
submittedCall: SubmittedCall,
@@ -116,12 +101,11 @@ const submittedCallEvidence = (
return (
request !== null &&
request.qualifiedName === submittedCall.qualifiedName &&
request.deploymentId === submittedCall.deploymentId &&
jsonDeepEqual(request.payload, submittedCall.payload)
request.deploymentId === submittedCall.deploymentId
);
});
// Concurrent identical calls are indistinguishable here; fail closed rather
// than presenting one call's duration as proof for another call.
// Sanitized payloads cannot provide identity. Any concurrent same-identity
// call is therefore ambiguous and must fail closed.
return matches.length === 1 ? matches[0] ?? null : null;
};
@@ -146,14 +130,17 @@ const SchemaBlock = ({
const ContractView = ({
capability,
hidden,
onAddToDraft,
}: {
readonly capability: CapabilityDetail;
readonly hidden: boolean;
readonly onAddToDraft: (() => void) | undefined;
}) => (
<section
aria-labelledby={TAB_IDS.contract}
className="capability-playground__panel"
hidden={hidden}
id={PANEL_IDS.contract}
role="tabpanel"
tabIndex={0}
@@ -324,10 +311,12 @@ const ResultReceipt = ({
const TryView = ({
capability,
hidden,
target,
executor,
}: {
readonly capability: CapabilityDetail;
readonly hidden: boolean;
readonly target: string | null;
readonly executor: ConsoleWriteExecutor | null;
}) => {
@@ -373,91 +362,103 @@ const TryView = ({
};
return (
<section
aria-labelledby={TAB_IDS.try}
className="capability-playground__panel capability-playground__panel--try"
id={PANEL_IDS.try}
role="tabpanel"
tabIndex={0}
>
<div className="capability-playground__try-heading">
<div>
<h3 id="capability-playground-try-heading">Try capability</h3>
<p>
Use literal values from this form. Nothing runs until you acknowledge the immediate call.
</p>
<>
<section
aria-labelledby={TAB_IDS.try}
className="capability-playground__panel capability-playground__panel--try"
hidden={hidden}
id={PANEL_IDS.try}
role="tabpanel"
tabIndex={0}
>
<div className="capability-playground__try-heading">
<div>
<h3 id="capability-playground-try-heading">Try capability</h3>
<p>
Use literal values from this form. Nothing runs until you acknowledge the immediate call.
</p>
</div>
<Play aria-hidden="true" size={20} strokeWidth={1.8} />
</div>
<Play aria-hidden="true" size={20} strokeWidth={1.8} />
</div>
<div className="capability-playground__warning" role="note">
<AlertCircle aria-hidden="true" size={18} strokeWidth={1.8} />
<p>This calls the capability immediately against the connected workflow server.</p>
</div>
{!operationAvailable ? (
<p className="capability-playground__disabled" role="status">
Capability calls are unavailable until a workflow server is connected.
</p>
) : (
<>
{capability.kind === "wrapper_artifact" && (
<div className="capability-playground__deployment">
<label htmlFor="capability-wrapper-deployment">Wrapper deployment ID</label>
<input
id="capability-wrapper-deployment"
onChange={(event) => controller.setDeploymentId(event.target.value)}
type="text"
value={controller.deploymentId}
/>
<small>Optional. Leave blank to use the server default.</small>
</div>
)}
<label className="capability-playground__acknowledgement">
<input
checked={controller.acknowledged}
onChange={(event) => controller.setAcknowledged(event.target.checked)}
type="checkbox"
/>
<span>I understand this executes the capability now.</span>
</label>
<fieldset
className="capability-playground__form-fieldset"
disabled={!controller.acknowledged || controller.phase === "calling"}
>
<legend className="visually-hidden">Literal capability inputs</legend>
<SchemaForm
schema={capability.inputSchema}
onValueChange={() => setLocalError(null)}
onSubmit={handleSubmit}
showSourceControls={false}
submitLabel={controller.phase === "calling" ? "Calling capability..." : "Call capability"}
/>
</fieldset>
</>
)}
{localError && (
<p className="capability-playground__local-error" role="alert">
{localError}
</p>
)}
{controller.phase === "error" && controller.message && (
<p className="capability-playground__local-error" role="alert">
{controller.message}
</p>
)}
{controller.phase === "result" && controller.result && (
currentSubmittedCall ? (
<ResultReceipt
evidence={evidence}
result={controller.result}
submittedCall={currentSubmittedCall}
/>
) : (
<p className="capability-playground__muted" role="status">
Result receipt unavailable; call again to capture the submitted request.
<div className="capability-playground__warning" role="note">
<AlertCircle aria-hidden="true" size={18} strokeWidth={1.8} />
<p>This calls the capability immediately against the connected workflow server.</p>
</div>
{!operationAvailable ? (
<p className="capability-playground__disabled" role="status">
Capability calls are unavailable until a workflow server is connected.
</p>
)
) : (
<>
{capability.kind === "wrapper_artifact" && (
<div className="capability-playground__deployment">
<label htmlFor="capability-wrapper-deployment">Wrapper deployment ID</label>
<input
id="capability-wrapper-deployment"
onChange={(event) => controller.setDeploymentId(event.target.value)}
type="text"
value={controller.deploymentId}
/>
<small>Optional. Leave blank to use the server default.</small>
</div>
)}
<label className="capability-playground__acknowledgement">
<input
checked={controller.acknowledged}
onChange={(event) => controller.setAcknowledged(event.target.checked)}
type="checkbox"
/>
<span>I understand this executes the capability now.</span>
</label>
<fieldset
className="capability-playground__form-fieldset"
disabled={!controller.acknowledged || controller.phase === "calling"}
>
<legend className="visually-hidden">Literal capability inputs</legend>
<SchemaForm
schema={capability.inputSchema}
onValueChange={() => setLocalError(null)}
onSubmit={handleSubmit}
showSourceControls={false}
submitLabel={
controller.phase === "calling"
? "Calling capability..."
: "Call capability now"
}
/>
</fieldset>
</>
)}
{localError && (
<p className="capability-playground__local-error" role="alert">
{localError}
</p>
)}
{controller.phase === "error" && controller.message && (
<p className="capability-playground__local-error" role="alert">
{controller.message}
</p>
)}
{controller.phase === "result" && controller.result && (
currentSubmittedCall ? (
<ResultReceipt
evidence={evidence}
result={controller.result}
submittedCall={currentSubmittedCall}
/>
) : (
<p className="capability-playground__muted">
Result receipt unavailable; call again to capture the submitted request.
</p>
)
)}
</section>
{controller.phase === "result" && controller.result && (
<p className="visually-hidden" role="status">
Capability call completed. Outcome: {controller.result.outcome}.
</p>
)}
</section>
</>
);
};
@@ -527,11 +528,17 @@ export const CapabilityPlayground = ({
);
})}
</div>
{activeTab === "contract" ? (
<ContractView capability={capability} onAddToDraft={onAddToDraft} />
) : (
<TryView capability={capability} executor={executor} target={target} />
)}
<ContractView
capability={capability}
hidden={activeTab !== "contract"}
onAddToDraft={onAddToDraft}
/>
<TryView
capability={capability}
executor={executor}
hidden={activeTab !== "try"}
target={target}
/>
</section>
);
};
@@ -1,4 +1,4 @@
import { cleanup, render, screen } from "@testing-library/react";
import { cleanup, render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -191,7 +191,8 @@ describe("DiscoverRoute", () => {
expect(screen.getByRole("heading", { name: "Input schema" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Output schema" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Wrapper hints" })).toBeInTheDocument();
expect(screen.getAllByText(/"names"/)).toHaveLength(2);
const contractPanel = screen.getByRole("tabpanel", { name: "Contract" });
expect(within(contractPanel).getAllByText(/"names"/)).toHaveLength(2);
expect(screen.getByRole("button", { name: "Add to draft" })).toBeInTheDocument();
});