fix: preserve capability call receipts
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user