rename(phase 3): compose/DB/infra names + stray code refs capakraken → nexus
CI / Architecture Guardrails (pull_request) Successful in 2m59s
CI / Typecheck (pull_request) Successful in 6m41s
CI / Lint (pull_request) Successful in 4m18s
CI / Assistant Split Regression (pull_request) Successful in 5m6s
CI / Unit Tests (pull_request) Successful in 7m21s
CI / Build (pull_request) Successful in 5m21s
CI / Fresh-Linux Docker Deploy (pull_request) Failing after 38s
CI / E2E Tests (pull_request) Successful in 3m28s
CI / Release Images (pull_request) Has been skipped

- docker-compose.yml / .prod.yml / .ci.yml: project names, POSTGRES_DB/USER,
  pg_isready, DATABASE_URL, volume names (nexus_pgdata, nexus_prod_*)
- .github/workflows/ci.yml: POSTGRES_PASSWORD, pg_isready, psql credentials,
  GRANT statements, POSTGRES_PASSWORD=nexus_dev for Docker Deploy job
- scripts/db-target-guard.mjs: expectedDatabase default, NEXUS_EXPECTED_DB_NAME
- scripts/prisma-with-env.mjs, e2e/test-server.mjs: env-var rename
- packages/db/src/safe-destructive-env.ts + reset-dispo-import.ts: DB name set
- packages/db/src/destructive-db-guard.ts: PROTECTED_DATABASE_NAMES → "nexus"
- packages/db/src/destructive-db-guard.test.ts: all fixture DB names + comments
- .env.example, tooling/deploy/deploy.env.example: DATABASE_URL, image refs
- packages/api: Redis channel/key prefixes (rbac-invalidate, sse, ratelimit),
  logger service name, app-base-url log prefix
- E2E: DB container names, localStorage/sessionStorage keys, email domains
- scripts: architecture-guardrails filter, export/import-dev-seed defaults,
  harden-postgres defaults, start.sh pg_isready, worktree-hygiene fixture
- tooling/migrate/rename-to-nexus.sh: new maintenance-window cutover script

Only intentional capakraken survivor: anonymization.ts DEFAULT_ANONYMIZATION_SEED
(functional cryptographic constant — changing it would invalidate stored aliases).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 16:35:39 +02:00
parent b41c1d2501
commit 01f8974314
44 changed files with 401 additions and 186 deletions
@@ -12,7 +12,7 @@
* - Creates a temporary test user via tRPC (admin session) for isolation.
* - Cleans up the test user in afterAll.
* - Uses an empty storageState to ensure no cross-user localStorage bleed.
* - localStorage key is user-scoped: "capakraken_dashboard_v1_{userId}".
* - localStorage key is user-scoped: "nexus_dashboard_v1_{userId}".
*/
import { expect, test, type Browser, type Page } from "@playwright/test";
@@ -20,9 +20,16 @@ import { STORAGE_STATE } from "../../playwright.dev.config.js";
// ─── tRPC helpers ─────────────────────────────────────────────────────────────
type TrpcResult = { result?: { data?: unknown }; error?: { data?: { code?: string }; message?: string } };
type TrpcResult = {
result?: { data?: unknown };
error?: { data?: { code?: string }; message?: string };
};
async function trpcMutation(page: Page, procedure: string, input: unknown = null): Promise<TrpcResult> {
async function trpcMutation(
page: Page,
procedure: string,
input: unknown = null,
): Promise<TrpcResult> {
return page.evaluate(
async ({ procedure, input }) => {
const res = await fetch(`/api/trpc/${procedure}?batch=1`, {
@@ -38,7 +45,11 @@ async function trpcMutation(page: Page, procedure: string, input: unknown = null
);
}
async function trpcQuery(page: Page, procedure: string, input: unknown = null): Promise<TrpcResult> {
async function trpcQuery(
page: Page,
procedure: string,
input: unknown = null,
): Promise<TrpcResult> {
return page.evaluate(
async ({ procedure, input }) => {
const encodedInput = encodeURIComponent(JSON.stringify({ "0": { json: input } }));
@@ -128,7 +139,9 @@ test.describe("Dashboard — widget management", () => {
// Default layout should show at least the stat-cards widget
// (from createDefaultDashboardLayout in useDashboardLayout)
await expect(page.locator('[data-testid="widget-stat-cards"], .react-grid-item').first()).toBeVisible({
await expect(
page.locator('[data-testid="widget-stat-cards"], .react-grid-item').first(),
).toBeVisible({
timeout: 8000,
});
});
@@ -138,16 +151,21 @@ test.describe("Dashboard — widget management", () => {
await navigateToDashboard(page);
// Open modal
await page.getByRole("button", { name: /add widget/i }).first().click();
await page
.getByRole("button", { name: /add widget/i })
.first()
.click();
// Verify modal is open
await expect(page.getByRole("heading", { name: /add widget/i })).toBeVisible({ timeout: 5000 });
await expect(page.getByRole("heading", { name: /add widget/i })).toBeVisible({
timeout: 5000,
});
// Verify widget entries are visible in the modal
// The catalog has 11 widgets; check for at least 5 visible buttons inside the modal
const widgetButtons = page.locator(
'[role="dialog"] button, .fixed button[type="button"]',
).filter({ hasText: /./ });
const widgetButtons = page
.locator('[role="dialog"] button, .fixed button[type="button"]')
.filter({ hasText: /./ });
// Count items in the grid (the ×-close button is excluded by checking for icon content)
const modalContent = page.locator(".fixed.inset-0 .grid");
@@ -166,10 +184,16 @@ test.describe("Dashboard — widget management", () => {
const initialCount = await page.locator(".react-grid-item").count();
// Open modal and add "Resource Table" widget
await page.getByRole("button", { name: /add widget/i }).first().click();
await page
.getByRole("button", { name: /add widget/i })
.first()
.click();
await expect(page.locator(".fixed.inset-0")).toBeVisible({ timeout: 5000 });
await page.locator(".fixed.inset-0 button").filter({ hasText: /resource table/i }).click();
await page
.locator(".fixed.inset-0 button")
.filter({ hasText: /resource table/i })
.click();
// Modal should close after adding
await expect(page.locator(".fixed.inset-0")).not.toBeVisible({ timeout: 5000 });
@@ -184,9 +208,15 @@ test.describe("Dashboard — widget management", () => {
await navigateToDashboard(page);
// Add a recognizable widget
await page.getByRole("button", { name: /add widget/i }).first().click();
await page
.getByRole("button", { name: /add widget/i })
.first()
.click();
await expect(page.locator(".fixed.inset-0")).toBeVisible({ timeout: 5000 });
await page.locator(".fixed.inset-0 button").filter({ hasText: /project overview/i }).click();
await page
.locator(".fixed.inset-0 button")
.filter({ hasText: /project overview/i })
.click();
await expect(page.locator(".fixed.inset-0")).not.toBeVisible({ timeout: 5000 });
const countAfterAdd = await page.locator(".react-grid-item").count();
@@ -214,19 +244,23 @@ test.describe("Dashboard — widget management", () => {
// Read the admin's localStorage key to verify it is user-scoped
const adminUserId = await adminPage.evaluate(async () => {
const res = await fetch("/api/trpc/user.me?batch=1&input=" + encodeURIComponent(JSON.stringify({ "0": { json: null } })), {
credentials: "include",
});
const body = await res.json() as [{ result?: { data?: { json?: { id?: string } } } }];
const res = await fetch(
"/api/trpc/user.me?batch=1&input=" +
encodeURIComponent(JSON.stringify({ "0": { json: null } })),
{
credentials: "include",
},
);
const body = (await res.json()) as [{ result?: { data?: { json?: { id?: string } } } }];
return body[0]?.result?.data?.json?.id ?? null;
});
// Verify admin has a user-scoped storage key (not shared "capakraken_dashboard_v1")
// Verify admin has a user-scoped storage key (not shared "nexus_dashboard_v1")
if (adminUserId) {
const storageKey = await adminPage.evaluate((userId) => {
// Check both old (unscoped) and new (user-scoped) key formats
const oldKey = "capakraken_dashboard_v1";
const newKey = `capakraken_dashboard_v1_${userId}`;
const oldKey = "nexus_dashboard_v1";
const newKey = `nexus_dashboard_v1_${userId}`;
const oldValue = localStorage.getItem(oldKey);
const newValue = localStorage.getItem(newKey);
return { oldKey: oldValue !== null, newKey: newValue !== null };
@@ -244,8 +278,13 @@ test.describe("Dashboard — widget management", () => {
// Inject the admin's storage key to simulate same browser
await newUserPage.evaluate(
({ key, value }) => { localStorage.setItem(key, value ?? ""); },
{ key: `capakraken_dashboard_v1_${adminUserId}`, value: JSON.stringify({ version: 2, gridCols: 12, widgets: [] }) },
({ key, value }) => {
localStorage.setItem(key, value ?? "");
},
{
key: `nexus_dashboard_v1_${adminUserId}`,
value: JSON.stringify({ version: 2, gridCols: 12, widgets: [] }),
},
);
// Log in as test user
@@ -262,7 +301,10 @@ test.describe("Dashboard — widget management", () => {
const gridItems = await newUserPage.locator(".react-grid-item").count();
// Either show default layout (≥1 widget) OR the properly-scoped empty state with Add Widget CTA
// The key check: the test user's Add Widget button should still work
await newUserPage.getByRole("button", { name: /add widget/i }).first().click();
await newUserPage
.getByRole("button", { name: /add widget/i })
.first()
.click();
// Modal must show widgets to choose from
const modalContent = newUserPage.locator(".fixed.inset-0 .grid");