fix: final review pass — email collision, orphaned-run loop, null confidence guards

- changeEmail validates and refuses an address another account owns (users.email is UNIQUE; a collision was a 500 inside the settings form).
- /app/[id] no longer redirects a 'running' row to itself; it renders what exists and says the run was interrupted.
- ScoreBar / OptionRow / VerdictBlock survive null numbers from the model instead of throwing and losing the block (seen live: '[openui] runtime/render-error: Component ScoreBar render failed').
- drop the ad-hoc render-debug scripts now that Playwright covers the browser.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ahmed mohammed
2026-09-22 09:10:29 +03:00
co-authored by Claude Opus 4.7
parent 07dd3d6bf8
commit 5160a00db5
12 changed files with 101 additions and 80 deletions
+38
View File
@@ -0,0 +1,38 @@
import { test, expect } from "@playwright/test";
import { DatabaseSync } from "node:sqlite";
import { randomUUID } from "node:crypto";
test.use({ storageState: ".auth/user.json" });
const DB = ".playwright-data/qai.db";
// The app opens this file too (WAL), so writes here are just another connection.
function withDb<T>(fn: (d: DatabaseSync) => T): T {
const d = new DatabaseSync(DB);
try { return fn(d); } finally { d.close(); }
}
test("a row left running renders what exists instead of redirecting forever", async ({ page }) => {
const id = withDb((d) => {
const userId = d.prepare("SELECT id FROM users ORDER BY rowid DESC LIMIT 1").get() as { id: string };
const aid = randomUUID();
// Backdated so it never becomes the rail's first row: history.spec.ts depends on that row being a real run.
d.prepare("INSERT INTO analyses (id, user_id, title, xml, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?)")
.run(aid, userId.id, "Orphaned run", "<ShowPlanXML/>", "running", Date.now() - 3_600_000, Date.now() - 3_600_000);
return aid;
});
await page.goto(`/app/${id}`);
await expect(page.getByText(/This run was interrupted before it finished/)).toBeVisible({ timeout: 15_000 });
expect(page.url()).toContain(`/app/${id}`); // the old redirect sent this to itself
await expect(page.getByRole("button", { name: "Analyse" })).toHaveCount(0);
});
test("an email another account already has is refused inline", async ({ page }) => {
withDb((d) => d.prepare("INSERT INTO users (id, email, pass, created_at) VALUES (?,?,?,?)")
.run(randomUUID(), "taken@qai.test", "salt:hash", Date.now()));
await page.goto("/settings");
await page.fill("#email", "taken@qai.test");
await page.getByRole("button", { name: "Save" }).click();
await expect(page.locator("form p[role=alert]")).toContainText("That email already has an account", { timeout: 15_000 });
const still = withDb((d) => (d.prepare("SELECT email FROM users WHERE email = ?").get("taken@qai.test") as { email: string }).email);
expect(still).toBe("taken@qai.test");
});