feat(auth): add remembered local login to the frontend

This commit is contained in:
2026-08-17 04:52:39 +02:00
parent 8c67cb75dc
commit 202822f3ba
36 changed files with 2649 additions and 163 deletions
+134 -2
View File
@@ -1,16 +1,23 @@
// frontend/src/shell/SteerInput.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { server } from "../test/msw";
import { canonicalWorkspaceFixture, workspaceSummaryFixture } from "../test/workspace-fixtures";
import { canonicalWorkspaceFixture, workspaceRevisionFixture, workspaceSummaryFixture } from "../test/workspace-fixtures";
import { useSessionStore } from "../store/sessionStore";
import { workspacePreferences } from "../workspaces/preferences";
import { ComposerFooter, ContextGauge, SteerInput } from "./SteerInput";
import { setAuthState } from "../auth/authState";
const userA = {
issuer: "local", subject: "user-a", roles: ["user"] as const,
permissions: ["session.use"], isAdmin: false, csrfToken: "a".repeat(43), session: null,
};
beforeEach(() => {
localStorage.clear();
setAuthState(userA);
server.use(
http.post("/api/sessions/:id/steer", () =>
new HttpResponse(null, { status: 204 }),
@@ -73,6 +80,131 @@ test("submitting typed text POSTs to /steer and clears the input", async () => {
expect(input).toHaveValue("");
});
test("a delayed steer from user A cannot mutate user B's store or composer", async () => {
let release!: () => void;
let started!: () => void;
let settled!: () => void;
const held = new Promise<void>((resolve) => { release = resolve; });
const requestStarted = new Promise<void>((resolve) => { started = resolve; });
const requestSettled = new Promise<void>((resolve) => { settled = resolve; });
server.use(http.post("/api/sessions/:id/steer", async () => {
started();
try {
await held;
return new HttpResponse(null, { status: 204 });
} finally {
settled();
}
}));
setAuthState(userA);
const view = render(<SteerInput sessionId="s1" />);
const input = screen.getByRole("textbox");
await userEvent.type(input, "A-only steer");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
await requestStarted;
act(() => setAuthState({ ...userA, subject: "user-b", csrfToken: "b".repeat(43) }));
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "B-entry" }));
release();
await act(async () => { await requestSettled; });
expect(useSessionStore.getState().lastUserEntry).toEqual({ kind: "input", text: "B-entry" });
expect(input).toHaveValue("A-only steer");
view.unmount();
});
test("a settings preflight from user A prevents session POST after user B logs in", async () => {
let releaseSettings!: () => void;
let settingsStarted!: () => void;
let settingsSettled!: () => void;
let sessionPosts = 0;
const settingsGate = new Promise<void>((resolve) => { releaseSettings = resolve; });
const started = new Promise<void>((resolve) => { settingsStarted = resolve; });
const settled = new Promise<void>((resolve) => { settingsSettled = resolve; });
server.use(
http.get("/api/settings", async () => {
settingsStarted();
try {
await settingsGate;
return HttpResponse.json({ workspace: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low" });
} finally {
settingsSettled();
}
}),
http.post("/api/sessions", () => {
sessionPosts += 1;
return HttpResponse.json({ id: "a-session" });
}),
);
const userB = { ...userA, subject: "user-b", csrfToken: "b".repeat(43) };
const view = render(<SteerInput sessionId={null} />);
const input = screen.getByRole("textbox", { name: /new question/i });
await userEvent.type(input, "A-only question");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
await started;
act(() => setAuthState(userB));
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "B-entry" }));
releaseSettings();
await act(async () => { await settled; });
expect(sessionPosts).toBe(0);
expect(useSessionStore.getState().lastUserEntry).toEqual({ kind: "input", text: "B-entry" });
view.unmount();
});
test("a workspace-policy preflight from user A prevents session POST after user B logs in", async () => {
workspacePreferences.save({
workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "low",
});
let releaseWorkspaces!: () => void;
let workspacesStarted!: () => void;
let workspacesSettled!: () => void;
let sessionPosts = 0;
const workspacesGate = new Promise<void>((resolve) => { releaseWorkspaces = resolve; });
const started = new Promise<void>((resolve) => { workspacesStarted = resolve; });
const settled = new Promise<void>((resolve) => { workspacesSettled = resolve; });
server.use(
http.get("/api/workspaces", () => HttpResponse.json([{
...workspaceSummaryFixture("psd-clinical", {
displayName: "PSD Clinical",
revision: workspaceRevisionFixture("psd-clinical"),
}),
}])),
http.get("/api/workspaces/psd-clinical", async () => {
workspacesStarted();
try {
await workspacesGate;
return HttpResponse.json({
workspace: canonicalWorkspaceFixture("psd-clinical"),
revision: workspaceRevisionFixture("psd-clinical"),
});
} finally {
workspacesSettled();
}
}),
http.post("/api/sessions", () => {
sessionPosts += 1;
return HttpResponse.json({ id: "a-session" });
}),
);
const userB = { ...userA, subject: "user-b", csrfToken: "b".repeat(43) };
const view = render(<SteerInput sessionId={null} />);
const input = screen.getByRole("textbox", { name: /new question/i });
await userEvent.type(input, "A-policy-question");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
await started;
act(() => setAuthState(userB));
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "B-entry" }));
releaseWorkspaces();
await act(async () => { await settled; });
expect(sessionPosts).toBe(0);
expect(useSessionStore.getState().lastUserEntry).toEqual({ kind: "input", text: "B-entry" });
view.unmount();
});
test("pressing Enter in the input submits the steer", async () => {
let captured: unknown = null;
server.use(