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
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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user