chore(web): connected-flow smoke for the Tools, Resources and Prompts tabs
The web client had exactly one connected-flow smoke and it was the Apps tab. `smoke:web:browser` proves the bundle boots and paints but never connects; `smoke:web:app` / `smoke:web:elicit` connect but only drive the Apps surface. So the three highest-traffic tabs were unexercised through the prod bundle end to end — everything covering them (unit tests, Storybook, the integration project) mocks or bypasses the prod server, so a regression living only in the built bundle or in the prod Hono backend's wiring of a tab passed all of them. `scripts/smoke-web-tabs.mjs` drives all three against one server on one browser launch (~5s added wall-clock): Tools runs `list_items` and asserts its `structuredContent` section rendered (#1908), Resources reads `events` with the template list asserted separately — `resources/list` and `resources/templates/list` are different calls and either can fail alone — and Prompts fetches the argument-less `simple_prompt`. Every assertion is on a `data-*` attribute, never on copy, mirroring the `data-app-status` precedent: each screen now carries a readiness contract (`data-tool-count` / `data-call-status` and its Resources / Prompts equivalents), tabulated in the web README and pinned by screen unit tests so a rename fails loudly there rather than as an opaque timeout here. The deep-link connect step is extracted to `lib/deep-link-connect.mjs` rather than copied a third time. That matters because `parseDeepLink` ignores a link it cannot validate rather than reporting one, so a drifted copy fails as an opaque connect timeout in whichever smoke was not updated. `smoke:web:elicit`'s hand-rolled copy is retired onto it. Chromium-only and deliberately not in `ENGINE_SMOKES`: the engine tiers exist for the MCP Apps sandbox's genuinely divergent primitives, and these tabs are ordinary React and Mantine. Network, Protocol, Logs and Tasks remain uncovered — a stated gap, not an oversight. Closes #2148 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4 Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
833ad40a45
commit
f62410a9b3
8 changed files with 806 additions and 56 deletions
|
|
@ -85,3 +85,15 @@ describe("StructuredOutputPanel", () => {
|
|||
expect(screen.getByTestId("code-highlight")).toHaveTextContent("{}");
|
||||
});
|
||||
});
|
||||
|
||||
// The `data-testid` the headless tab smoke waits on (#2148). Asserting the
|
||||
// section by its heading copy would make the smoke fail on a rewording, so the
|
||||
// smoke keys off this attribute — pinned here so a rename fails loudly.
|
||||
describe("automation contract (#2148)", () => {
|
||||
it("exposes a stable data-testid on the section", () => {
|
||||
renderWithMantine(
|
||||
<StructuredOutputPanel structuredContent={structuredContent} />,
|
||||
);
|
||||
expect(screen.getByTestId("structured-output")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -72,7 +72,10 @@ export function StructuredOutputPanel({
|
|||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
return (
|
||||
<StructuredBox>
|
||||
// `data-testid` so the headless tab smoke (#2148) can assert this section
|
||||
// rendered without matching on its heading copy. A `data-*` attribute is
|
||||
// not part of a component's typed props, so it is passed at the call site.
|
||||
<StructuredBox data-testid="structured-output">
|
||||
<StructuredInner>
|
||||
<HeaderRow>
|
||||
<StructuredHeader>Structured Output</StructuredHeader>
|
||||
|
|
|
|||
|
|
@ -391,3 +391,34 @@ describe("PromptsScreen", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
// See the ToolsScreen counterpart: the `data-*` readiness contract the headless
|
||||
// tab smoke drives (#2148), pinned here so a rename fails loudly.
|
||||
describe("automation contract (#2148)", () => {
|
||||
it("reports the prompt count and an idle get status", () => {
|
||||
renderWithMantine(<PromptsScreen {...baseProps} />);
|
||||
const root = screen.getByTestId("prompts-screen");
|
||||
expect(root).toHaveAttribute(
|
||||
"data-prompt-count",
|
||||
String(promptsWithArgs.length),
|
||||
);
|
||||
expect(root).toHaveAttribute("data-get-status", "idle");
|
||||
});
|
||||
|
||||
it("tracks the get status through to a result", () => {
|
||||
renderWithMantine(
|
||||
<PromptsScreen
|
||||
{...baseProps}
|
||||
getPromptState={{
|
||||
status: "ok",
|
||||
promptName: promptsWithArgs[0].name,
|
||||
result: { messages: [] },
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("prompts-screen")).toHaveAttribute(
|
||||
"data-get-status",
|
||||
"ok",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -274,7 +274,13 @@ export function PromptsScreen({
|
|||
}
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
// `data-*` readiness contract for the headless tab smoke (#2148); see
|
||||
// clients/web/README.md#core-tab-automation-contract.
|
||||
<ScreenLayout
|
||||
data-testid="prompts-screen"
|
||||
data-prompt-count={prompts.length}
|
||||
data-get-status={getPromptState?.status ?? "idle"}
|
||||
>
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<PromptControls
|
||||
|
|
|
|||
|
|
@ -403,3 +403,36 @@ describe("ResourcesScreen", () => {
|
|||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// See the ToolsScreen counterpart: the `data-*` readiness contract the headless
|
||||
// tab smoke drives (#2148), pinned here so a rename fails loudly.
|
||||
describe("automation contract (#2148)", () => {
|
||||
it("reports the resource and template counts separately", () => {
|
||||
renderWithMantine(<ResourcesScreen {...baseProps} />);
|
||||
const root = screen.getByTestId("resources-screen");
|
||||
// Two attributes rather than one: `resources/list` and
|
||||
// `resources/templates/list` are separate calls and either can fail alone.
|
||||
expect(root).toHaveAttribute(
|
||||
"data-resource-count",
|
||||
String(resources.length),
|
||||
);
|
||||
expect(root).toHaveAttribute(
|
||||
"data-template-count",
|
||||
String(templates.length),
|
||||
);
|
||||
expect(root).toHaveAttribute("data-read-status", "idle");
|
||||
});
|
||||
|
||||
it("tracks the read status through to a result", () => {
|
||||
renderWithMantine(
|
||||
<ResourcesScreen
|
||||
{...baseProps}
|
||||
readState={{ status: "ok", uri: "file:///x", result: okResult }}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("resources-screen")).toHaveAttribute(
|
||||
"data-read-status",
|
||||
"ok",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -319,7 +319,14 @@ export function ResourcesScreen({
|
|||
}
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
// `data-*` readiness contract for the headless tab smoke (#2148); see
|
||||
// clients/web/README.md#core-tab-automation-contract.
|
||||
<ScreenLayout
|
||||
data-testid="resources-screen"
|
||||
data-resource-count={resources.length}
|
||||
data-template-count={templates.length}
|
||||
data-read-status={readState?.status ?? "idle"}
|
||||
>
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<ResourceControls
|
||||
|
|
|
|||
|
|
@ -445,3 +445,41 @@ describe("ToolsScreen", () => {
|
|||
expect(screen.getByText("invalid_header_tool")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// The `data-*` readiness contract the headless tab smoke drives (#2148). It is
|
||||
// a documented public contract (clients/web/README.md), and a smoke asserting
|
||||
// it fails as an opaque 45s timeout rather than a mismatch — so the attribute
|
||||
// names are pinned here, where a rename fails loudly instead.
|
||||
describe("automation contract (#2148)", () => {
|
||||
it("reports the tool count and an idle call status", () => {
|
||||
renderWithMantine(<ToolsScreen {...baseProps} />);
|
||||
const root = screen.getByTestId("tools-screen");
|
||||
expect(root).toHaveAttribute("data-tool-count", String(tools.length));
|
||||
// Absent call state reads `idle`, not empty: the smoke waits on a value.
|
||||
expect(root).toHaveAttribute("data-call-status", "idle");
|
||||
});
|
||||
|
||||
it("reports an empty list distinctly from a missing one", () => {
|
||||
renderWithMantine(<ToolsScreen {...baseProps} tools={[]} />);
|
||||
expect(screen.getByTestId("tools-screen")).toHaveAttribute(
|
||||
"data-tool-count",
|
||||
"0",
|
||||
);
|
||||
});
|
||||
|
||||
it("tracks the call status through to a result", () => {
|
||||
renderWithMantine(
|
||||
<ToolsScreen
|
||||
{...baseProps}
|
||||
callState={{
|
||||
status: "ok",
|
||||
result: { content: [{ type: "text", text: "done" }] },
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId("tools-screen")).toHaveAttribute(
|
||||
"data-call-status",
|
||||
"ok",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -198,7 +198,15 @@ export function ToolsScreen({
|
|||
};
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
// `data-*` readiness contract for the headless tab smoke (#2148); see
|
||||
// clients/web/README.md#core-tab-automation-contract. Attributes are not
|
||||
// part of a component's typed props, so they are passed here rather than
|
||||
// baked into the `ScreenLayout` constant.
|
||||
<ScreenLayout
|
||||
data-testid="tools-screen"
|
||||
data-tool-count={tools.length}
|
||||
data-call-status={callState?.status ?? "idle"}
|
||||
>
|
||||
<Sidebar>
|
||||
<SidebarCard>
|
||||
<ToolControls
|
||||
|
|
|
|||
Loading…
Reference in a new issue