fix: close Task 5 contract graph review findings

This commit is contained in:
lda
2026-08-14 18:34:44 +07:00 Verified
parent 50a462f927
commit eae1a62fb2
8 changed files with 165 additions and 63 deletions
@@ -41,6 +41,10 @@ const options: ReadonlyArray<AuthoringPathOption> = [
availability: "conditional",
reason: "Available when the selected step runs in a viewer frame.",
}),
option("context.viewer_email", "Viewer email", "runtime_context", ["workflow_output"], {
availability: "conditional",
reason: "Available only in viewer frames.",
}),
option("output.report", "Final report", "workflow_output", ["workflow_output"]),
option("input.customer.name", "Customer name", "workflow_input", ["step_input"]),
];
@@ -133,6 +137,55 @@ describe("AuthoringPathPicker", () => {
expect(onChange).toHaveBeenCalledWith("input.title");
});
it("gives a conditional incompatible option one composed description node", () => {
render(
<AuthoringPathPicker
label="Source path"
onChange={vi.fn()}
options={options}
uses="step_input"
value=""
/>,
);
const optionButton = screen.getByRole("button", { name: /Viewer email/ });
const describedBy = optionButton.getAttribute("aria-describedby");
expect(optionButton).toBeDisabled();
expect(describedBy).not.toBeNull();
expect(describedBy?.trim().split(/\s+/)).toHaveLength(1);
expect(document.querySelectorAll(`#${describedBy}`).length).toBe(1);
expect(document.getElementById(describedBy ?? "")?.textContent).toContain(
"Available only in viewer frames.",
);
expect(document.getElementById(describedBy ?? "")?.textContent).toContain(
"Not available for this field.",
);
});
it("renders a supplied non-conditional reason before describing the option", () => {
render(
<AuthoringPathPicker
label="Source path"
onChange={vi.fn()}
options={[
option("input.reasoned", "Reasoned", "workflow_input", ["step_input"], {
reason: "Retained for repair context.",
}),
]}
uses="step_input"
value=""
/>,
);
const optionButton = screen.getByRole("button", { name: /Reasoned/ });
const describedBy = optionButton.getAttribute("aria-describedby");
expect(describedBy).not.toBeNull();
expect(document.getElementById(describedBy ?? "")?.textContent).toBe(
"Retained for repair context.",
);
});
it("keeps nested choices keyboard reachable and preserves normal choices in Advanced mode", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
@@ -80,14 +80,19 @@ export const AuthoringPathPicker = ({
<fieldset className="authoring-path-picker__group" key={group.origin}>
<legend>{group.label}</legend>
<div className="authoring-path-picker__list">
{groupOptions.map((option) => {
{groupOptions.map((option, optionIndex) => {
const compatible = option.uses.some((use) => requestedUses.has(use));
const reasonId = `${id}-${safeId(option.path)}-reason`;
const reasonId = `${id}-${safeId(group.origin)}-${optionIndex}-description`;
const availabilityReason = option.availability === "conditional"
? option.reason ?? "Conditionally available; verify this path at runtime."
: option.reason;
const description = [
availabilityReason,
...(compatible ? [] : ["Not available for this field."]),
].filter((part): part is string => part !== undefined).join(" ");
return (
<button
aria-describedby={
option.reason !== undefined || !compatible ? reasonId : undefined
}
aria-describedby={description === "" ? undefined : reasonId}
aria-pressed={option.path === value}
className="authoring-path-picker__option"
disabled={!compatible}
@@ -102,10 +107,7 @@ export const AuthoringPathPicker = ({
<code>{option.path}</code>
{option.description !== undefined && <span>{option.description}</span>}
{option.required && <small>Required</small>}
{option.availability === "conditional" && option.reason !== undefined && (
<small id={reasonId}>{option.reason}</small>
)}
{!compatible && <small id={reasonId}>Not available for this field.</small>}
{description !== "" && <small id={reasonId}>{description}</small>}
</button>
);
})}
@@ -125,6 +125,19 @@ describe("useAuthoringContract", () => {
expect(result.current.inventory?.selectedStepId).toBeNull();
});
it("surfaces an initial inspection failure instead of staying loading", async () => {
client.inspect.mockRejectedValue(new Error("initial inspection failed"));
const { result } = renderHook(() =>
useAuthoringContract({ workspaceId: "draft-report", revision: 7, selectedStepId: null }),
);
await waitFor(() => expect(result.current.phase).toBe("error"));
expect(result.current.inventory).toBeNull();
expect(result.current.message).toBe("initial inspection failed");
});
it("passes the executable step id and ignores a stale selection response", async () => {
const first = deferred<AuthoringContractInventory>();
const second = deferred<AuthoringContractInventory>();
@@ -42,12 +42,14 @@ type StoredInventory = {
type AuthoringContractState = {
readonly phase: AuthoringContractPhase;
readonly attempted: RequestIdentity | null;
readonly stored: StoredInventory | null;
readonly message: string | null;
};
const initialState: AuthoringContractState = {
phase: "disconnected",
attempted: null,
stored: null,
message: null,
};
@@ -98,6 +100,7 @@ export const useAuthoringContract = ({
const requested = request;
setState((current) => ({
phase: "loading",
attempted: requested,
stored:
current.stored !== null && sameRequest(current.stored.request, requested)
? current.stored
@@ -115,6 +118,7 @@ export const useAuthoringContract = ({
if (generation !== generationRef.current) return;
setState({
phase: "ready",
attempted: requested,
stored: { request: requested, inventory },
message: null,
});
@@ -134,6 +138,7 @@ export const useAuthoringContract = ({
generationRef.current++;
setState({
phase: client === null || connectedTarget === null ? "disconnected" : "idle",
attempted: null,
stored: null,
message: null,
});
@@ -150,20 +155,28 @@ export const useAuthoringContract = ({
request !== null && state.stored !== null && sameRequest(state.stored.request, request)
? state.stored.inventory
: null;
const phase =
const hasCurrentAttempt =
request !== null &&
(state.stored === null || !sameRequest(state.stored.request, request))
? "loading"
: request === null
? client === null || connectedTarget === null
? "disconnected"
: "idle"
: state.phase;
state.attempted !== null &&
sameRequest(state.attempted, request);
const phase = request === null
? client === null || connectedTarget === null
? "disconnected"
: "idle"
: hasCurrentAttempt && state.phase === "error"
? "error"
: state.stored !== null && sameRequest(state.stored.request, request)
? state.phase
: "loading";
return {
phase,
inventory: currentInventory,
message: currentInventory === null && phase === "loading" ? null : state.message,
message: hasCurrentAttempt && state.phase === "error"
? state.message
: currentInventory === null && phase === "loading"
? null
: state.message,
refresh,
};
};