Files
coreci-chat/apps/control-plane/tests/shell-lib.test.ts
T
coreci-chat-po 754222083c
ci / test-pglite (PGlite, default) (push) Successful in 3m53s
ci / test-postgres (Postgres 16, G-022 RLS) (push) Successful in 2m21s
ci / test-pglite (PGlite, default) (pull_request) Successful in 3m44s
ci / test-postgres (Postgres 16, G-022 RLS) (pull_request) Successful in 2m3s
feat(app): minimal root-layout shell - theme + env-config session providers
M3 slice 1 (coreci-chat#16): wrap the root layout's children with a theme
provider (light/dark/system, persisted, no external dep) and a session
wrapper over @coreci/auth's session lane (identity via GET /api/me, client
config from NEXT_PUBLIC_API_BASE_URL, default same-origin). Additive only:
no redirects or gates, SSR pages keep server-side session resolution, and
no boot-time DDL (deploy owns DDL, app boots schema-check only). Pure
logic extracted to app/shell/_lib.ts + unit tests, repo Wave-J convention.
2026-11-03 18:57:24 +00:00

95 lines
3.2 KiB
TypeScript

/**
* Root-shell pure logic (coreci-chat#16, M3 slice 1).
*/
import { describe, expect, it } from "vitest";
import {
classifyMe,
normalizeTheme,
resolveApiBase,
resolveTheme,
themeClass,
THEME_STORAGE_KEY,
} from "../app/shell/_lib.js";
describe("normalizeTheme", () => {
it("accepts the three theme names", () => {
expect(normalizeTheme("light")).toBe("light");
expect(normalizeTheme("dark")).toBe("dark");
expect(normalizeTheme("system")).toBe("system");
});
it("falls back for stored garbage and absence", () => {
expect(normalizeTheme("blue")).toBe("system");
expect(normalizeTheme(null)).toBe("system");
expect(normalizeTheme(undefined, "dark")).toBe("dark");
});
it("uses the env default as fallback (the shipped env default)", () => {
expect(normalizeTheme(null, "light")).toBe("light");
});
});
describe("resolveTheme + themeClass", () => {
it("resolves system to the media query result", () => {
expect(resolveTheme("system", false)).toBe("light");
expect(resolveTheme("system", true)).toBe("dark");
});
it("resolves explicit preferences over the media query", () => {
expect(resolveTheme("dark", false)).toBe("dark");
expect(resolveTheme("light", true)).toBe("light");
});
it("produces the html class name", () => {
expect(themeClass("dark")).toBe("theme-dark");
expect(themeClass("light")).toBe("theme-light");
});
it("exposes the storage key shape", () => {
expect(THEME_STORAGE_KEY).toBe("coreci.theme");
});
});
describe("resolveApiBase (env-based session client config)", () => {
it("defaults to same-origin when the env var is absent", () => {
expect(resolveApiBase(undefined)).toBe("");
expect(resolveApiBase(null)).toBe("");
expect(resolveApiBase("")).toBe("");
expect(resolveApiBase(" ")).toBe("");
});
it("accepts same-origin-relative and absolute http(s) bases", () => {
expect(resolveApiBase("/gw")).toBe("/gw");
expect(resolveApiBase("https://chat.coreci.dev/")).toBe("https://chat.coreci.dev");
expect(resolveApiBase(" http://localhost:3000/ ")).toBe("http://localhost:3000");
});
it("falls back to same-origin for unsupported forms", () => {
expect(resolveApiBase("//evil.example")).toBe("");
expect(resolveApiBase("ftp://x")).toBe("");
expect(resolveApiBase("javascript:alert(1)")).toBe("");
});
});
describe("classifyMe (/api/me responses)", () => {
it("classifies a valid 200 payload as authenticated with the user", () => {
const out = classifyMe(200, { user: { id: "u1", tenantId: "t1", role: "operator" } });
expect(out.status).toBe("authenticated");
expect(out.user).toEqual({ id: "u1", tenantId: "t1", role: "operator" });
});
it("treats a malformed 200 payload as an error, not a crash", () => {
expect(classifyMe(200, { user: { id: 7 } }).status).toBe("error");
expect(classifyMe(200, null).status).toBe("error");
});
it("treats 401/403 as guest — the shell never redirects", () => {
expect(classifyMe(401, null)).toEqual({ status: "guest", user: null });
expect(classifyMe(403, null)).toEqual({ status: "guest", user: null });
});
it("treats other statuses as an error state", () => {
expect(classifyMe(500, null)).toEqual({ status: "error", user: null });
});
});