huskies: merge 949

This commit is contained in:
dave
2026-05-13 07:10:00 +00:00
parent d87722f6c8
commit 4a0fbcaa95
15 changed files with 1454 additions and 231 deletions
@@ -0,0 +1,196 @@
/**
* Frontend seam test: drive a real React component against a fixture derived
* from the actual RPC response (the canonical `CONTRACT_FIXTURES` shared with
* the Rust side via the snapshot file).
*
* The first test renders `SettingsPage` against the well-formed fixture and
* asserts the form populates with values from the RPC response — proving the
* backend ↔ frontend wire shape lines up end-to-end without hand-rolled
* fixtures.
*
* The second test feeds a *malformed* RPC response (a frame missing the
* required envelope `ok` field) and asserts the `rpc.ts` client surfaces a
* visible error in the rendered UI instead of leaving the page empty.
*/
import { afterEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { SettingsPage } from "./SettingsPage";
import { CONTRACT_FIXTURES } from "../api/rpcContract";
import snapshot from "../api/rpcContract.snapshot.json";
afterEach(() => {
vi.restoreAllMocks();
});
interface MockSocket {
url: string;
onopen: ((ev: Event) => void) | null;
onmessage: ((ev: { data: string }) => void) | null;
onerror: ((ev: Event) => void) | null;
onclose: ((ev: CloseEvent) => void) | null;
readyState: number;
send(data: string): void;
close(): void;
}
/**
* Install a `WebSocket` shim that hands each registered method a single
* canned frame. Callers register either a normal RPC result or a
* deliberately malformed frame body (returned verbatim — i.e. the body
* literally has no `ok` field, simulating a server bug).
*/
function installSeamWs(replies: {
[method: string]: { kind: "ok"; result: unknown } | { kind: "raw"; body: object };
}) {
const instances: MockSocket[] = [];
class SeamWs implements MockSocket {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
static readonly CLOSING = 2;
static readonly CLOSED = 3;
url: string;
onopen: ((ev: Event) => void) | null = null;
onmessage: ((ev: { data: string }) => void) | null = null;
onerror: ((ev: Event) => void) | null = null;
onclose: ((ev: CloseEvent) => void) | null = null;
readyState = 0;
constructor(url: string) {
this.url = url;
instances.push(this);
queueMicrotask(() => {
this.readyState = 1;
this.onopen?.(new Event("open"));
});
}
send(data: string) {
let frame: {
correlation_id?: string;
method?: string;
};
try {
frame = JSON.parse(data);
} catch {
return;
}
const { correlation_id, method } = frame;
if (!correlation_id || !method) return;
queueMicrotask(() => {
const reply = replies[method];
if (!reply) {
this.onmessage?.({
data: JSON.stringify({
kind: "rpc_response",
version: 1,
correlation_id,
ok: false,
error: `no fixture for ${method}`,
code: "NOT_FOUND",
}),
});
return;
}
if (reply.kind === "ok") {
this.onmessage?.({
data: JSON.stringify({
kind: "rpc_response",
version: 1,
correlation_id,
ok: true,
result: reply.result,
}),
});
return;
}
// raw: deliberately malformed envelope (no `ok` field)
this.onmessage?.({
data: JSON.stringify({
kind: "rpc_response",
version: 1,
correlation_id,
...reply.body,
}),
});
});
}
close() {
this.readyState = 3;
}
}
vi.stubGlobal("WebSocket", SeamWs);
return instances;
}
describe("SettingsPage seam test", () => {
it("renders ProjectSettings from the typed RPC contract fixture", async () => {
// Sanity: the in-source fixture mirrors the on-disk snapshot file. If
// this trips, the contract has drifted from the Rust side.
expect(CONTRACT_FIXTURES["settings.put_project"].result).toEqual(
snapshot["settings.put_project"].result,
);
const fixture = CONTRACT_FIXTURES["settings.put_project"].result;
installSeamWs({
"settings.get_project": { kind: "ok", result: fixture },
});
const onBack = vi.fn();
render(<SettingsPage onBack={onBack} />);
await waitFor(() => {
expect(screen.getByDisplayValue(String(fixture.max_retries))).toBeInTheDocument();
});
// Field driven directly by the RPC payload populates the form.
expect(
screen.getByDisplayValue(String(fixture.watcher_sweep_interval_secs)),
).toBeInTheDocument();
expect(
screen.getByDisplayValue(String(fixture.watcher_done_retention_secs)),
).toBeInTheDocument();
});
it("shows a visible error when the RPC response is malformed", async () => {
// `body` lacks the envelope `ok` field. The fixed `rpc.ts` client
// should reject loudly with a `MALFORMED` error instead of letting
// the page render empty.
installSeamWs({
"settings.get_project": {
kind: "raw",
body: { result: { not_actually_settings: true } },
},
});
const onBack = vi.fn();
render(<SettingsPage onBack={onBack} />);
await waitFor(() => {
expect(screen.getByText(/Malformed RPC response/i)).toBeInTheDocument();
});
// And critically — no empty form is rendered.
expect(screen.queryByText(/default qa/i)).not.toBeInTheDocument();
});
it("user can edit and the new value flows through settings.put_project RPC", async () => {
const fixture = CONTRACT_FIXTURES["settings.put_project"].result;
const updated = { ...fixture, max_retries: 9 };
installSeamWs({
"settings.get_project": { kind: "ok", result: fixture },
"settings.put_project": { kind: "ok", result: updated },
});
const onBack = vi.fn();
render(<SettingsPage onBack={onBack} />);
const maxRetriesInput = (await screen.findByDisplayValue(
String(fixture.max_retries),
)) as HTMLInputElement;
fireEvent.change(maxRetriesInput, { target: { value: "9" } });
fireEvent.click(screen.getByRole("button", { name: /save/i }));
await waitFor(() => {
expect(screen.getByDisplayValue("9")).toBeInTheDocument();
});
});
});