cd78f72f33
Complete rename of all technical identifiers across the codebase: Package names (11 packages): - @planarchy/* → @capakraken/* in all package.json, tsconfig, imports Import statements: 277 files, 548 occurrences replaced Database & Docker: - PostgreSQL user/db: planarchy → capakraken - Docker volumes: planarchy_pgdata → capakraken_pgdata - Connection strings updated in docker-compose, .env, CI CI/CD: - GitHub Actions workflow: all filter commands updated - Test database credentials updated Infrastructure: - Redis channel: planarchy:sse → capakraken:sse - Logger service name: planarchy-api → capakraken-api - Anonymization seed updated - Start/stop/restart scripts updated Test data: - Seed emails: @planarchy.dev → @capakraken.dev - E2E test credentials: all 11 spec files updated - Email defaults: @planarchy.app → @capakraken.app - localStorage keys: planarchy_* → capakraken_* Documentation: 30+ .md files updated Verification: - pnpm install: workspace resolution works - TypeScript: only pre-existing TS2589 (no new errors) - Engine: 310/310 tests pass - Staffing: 37/37 tests pass Co-Authored-By: claude-flow <ruv@ruv.net>
306 lines
9.5 KiB
TypeScript
306 lines
9.5 KiB
TypeScript
import { SystemRole } from "@capakraken/shared";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("@capakraken/application", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("@capakraken/application")>();
|
|
return {
|
|
...actual,
|
|
getDashboardOverview: vi.fn(),
|
|
getDashboardPeakTimes: vi.fn(),
|
|
getDashboardDemand: vi.fn(),
|
|
getDashboardTopValueResources: vi.fn(),
|
|
getDashboardChargeabilityOverview: vi.fn(),
|
|
};
|
|
});
|
|
|
|
vi.mock("../lib/cache.js", () => ({
|
|
cacheGet: vi.fn().mockResolvedValue(null),
|
|
cacheSet: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
vi.mock("../lib/anonymization.js", () => ({
|
|
anonymizeResources: vi.fn((resources: unknown[]) => resources),
|
|
getAnonymizationDirectory: vi.fn().mockResolvedValue(null),
|
|
}));
|
|
|
|
import {
|
|
getDashboardOverview,
|
|
getDashboardPeakTimes,
|
|
getDashboardDemand,
|
|
getDashboardTopValueResources,
|
|
getDashboardChargeabilityOverview,
|
|
} from "@capakraken/application";
|
|
import { dashboardRouter } from "../router/dashboard.js";
|
|
import { createCallerFactory } from "../trpc.js";
|
|
|
|
const createCaller = createCallerFactory(dashboardRouter);
|
|
|
|
function createProtectedCaller(db: Record<string, unknown>) {
|
|
return createCaller({
|
|
session: {
|
|
user: { email: "user@example.com", name: "User", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: {
|
|
id: "user_1",
|
|
systemRole: SystemRole.USER,
|
|
permissionOverrides: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
function createControllerCaller(db: Record<string, unknown>) {
|
|
return createCaller({
|
|
session: {
|
|
user: { email: "controller@example.com", name: "Controller", image: null },
|
|
expires: "2099-01-01T00:00:00.000Z",
|
|
},
|
|
db: db as never,
|
|
dbUser: {
|
|
id: "user_2",
|
|
systemRole: SystemRole.CONTROLLER,
|
|
permissionOverrides: null,
|
|
},
|
|
});
|
|
}
|
|
|
|
function createUnauthenticatedCaller(db: Record<string, unknown>) {
|
|
return createCaller({
|
|
session: null,
|
|
db: db as never,
|
|
dbUser: null,
|
|
});
|
|
}
|
|
|
|
describe("dashboard router", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
// ─── getOverview ──────────────────────────────────────────────────────────
|
|
|
|
describe("getOverview", () => {
|
|
it("returns expected shape with resource and project counts", async () => {
|
|
const overview = {
|
|
totalResources: 42,
|
|
activeResources: 38,
|
|
totalProjects: 15,
|
|
activeProjects: 10,
|
|
draftProjects: 3,
|
|
completedProjects: 2,
|
|
totalBudgetCents: 5_000_000_00,
|
|
avgWinProbability: 78,
|
|
};
|
|
|
|
vi.mocked(getDashboardOverview).mockResolvedValue(overview);
|
|
|
|
const caller = createProtectedCaller({});
|
|
const result = await caller.getOverview();
|
|
|
|
expect(result).toMatchObject({
|
|
totalResources: 42,
|
|
activeResources: 38,
|
|
totalProjects: 15,
|
|
activeProjects: 10,
|
|
});
|
|
expect(getDashboardOverview).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("rejects unauthenticated users", async () => {
|
|
const caller = createUnauthenticatedCaller({});
|
|
await expect(caller.getOverview()).rejects.toThrow("Authentication required");
|
|
});
|
|
});
|
|
|
|
// ─── getPeakTimes ─────────────────────────────────────────────────────────
|
|
|
|
describe("getPeakTimes", () => {
|
|
it("returns array of time periods", async () => {
|
|
const peakData = [
|
|
{ period: "2026-03", totalHours: 1200, entries: 15 },
|
|
{ period: "2026-04", totalHours: 1400, entries: 18 },
|
|
];
|
|
|
|
vi.mocked(getDashboardPeakTimes).mockResolvedValue(peakData);
|
|
|
|
const caller = createProtectedCaller({});
|
|
const result = await caller.getPeakTimes({
|
|
startDate: "2026-03-01T00:00:00.000Z",
|
|
endDate: "2026-06-30T00:00:00.000Z",
|
|
granularity: "month",
|
|
groupBy: "project",
|
|
});
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0]).toHaveProperty("period", "2026-03");
|
|
expect(getDashboardPeakTimes).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
granularity: "month",
|
|
groupBy: "project",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("passes week granularity to application layer", async () => {
|
|
vi.mocked(getDashboardPeakTimes).mockResolvedValue([]);
|
|
|
|
const caller = createProtectedCaller({});
|
|
await caller.getPeakTimes({
|
|
startDate: "2026-03-01T00:00:00.000Z",
|
|
endDate: "2026-03-31T00:00:00.000Z",
|
|
granularity: "week",
|
|
groupBy: "chapter",
|
|
});
|
|
|
|
expect(getDashboardPeakTimes).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
granularity: "week",
|
|
groupBy: "chapter",
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── getDemand ────────────────────────────────────────────────────────────
|
|
|
|
describe("getDemand", () => {
|
|
it("returns demand entries grouped by project", async () => {
|
|
const demandData = [
|
|
{ groupKey: "Project Alpha", totalHours: 500, headcount: 3 },
|
|
{ groupKey: "Project Beta", totalHours: 300, headcount: 2 },
|
|
];
|
|
|
|
vi.mocked(getDashboardDemand).mockResolvedValue(demandData);
|
|
|
|
const caller = createProtectedCaller({});
|
|
const result = await caller.getDemand({
|
|
startDate: "2026-01-01T00:00:00.000Z",
|
|
endDate: "2026-12-31T00:00:00.000Z",
|
|
groupBy: "project",
|
|
});
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(getDashboardDemand).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ groupBy: "project" }),
|
|
);
|
|
});
|
|
|
|
it("supports grouping by chapter", async () => {
|
|
vi.mocked(getDashboardDemand).mockResolvedValue([]);
|
|
|
|
const caller = createProtectedCaller({});
|
|
await caller.getDemand({
|
|
startDate: "2026-06-01T00:00:00.000Z",
|
|
endDate: "2026-06-30T00:00:00.000Z",
|
|
groupBy: "chapter",
|
|
});
|
|
|
|
expect(getDashboardDemand).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ groupBy: "chapter" }),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── getTopValueResources ─────────────────────────────────────────────────
|
|
|
|
describe("getTopValueResources", () => {
|
|
it("returns sorted resources with default limit", async () => {
|
|
const resources = [
|
|
{ id: "res_1", displayName: "Alice", valueScore: 95 },
|
|
{ id: "res_2", displayName: "Bob", valueScore: 88 },
|
|
];
|
|
|
|
vi.mocked(getDashboardTopValueResources).mockResolvedValue(resources);
|
|
|
|
const caller = createProtectedCaller({});
|
|
const result = await caller.getTopValueResources({ limit: 10 });
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(getDashboardTopValueResources).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ limit: 10 }),
|
|
);
|
|
});
|
|
|
|
it("respects custom limit", async () => {
|
|
vi.mocked(getDashboardTopValueResources).mockResolvedValue([]);
|
|
|
|
const caller = createProtectedCaller({});
|
|
await caller.getTopValueResources({ limit: 5 });
|
|
|
|
expect(getDashboardTopValueResources).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ limit: 5 }),
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── getChargeabilityOverview ─────────────────────────────────────────────
|
|
|
|
describe("getChargeabilityOverview", () => {
|
|
it("returns chargeability data with top and watchlist arrays", async () => {
|
|
const overview = {
|
|
avgChargeability: 72,
|
|
top: [{ id: "res_1", displayName: "Alice", chargeability: 95 }],
|
|
watchlist: [{ id: "res_3", displayName: "Carol", chargeability: 30 }],
|
|
};
|
|
|
|
vi.mocked(getDashboardChargeabilityOverview).mockResolvedValue(overview);
|
|
|
|
const caller = createControllerCaller({});
|
|
const result = await caller.getChargeabilityOverview({
|
|
includeProposed: false,
|
|
topN: 10,
|
|
watchlistThreshold: 15,
|
|
});
|
|
|
|
expect(result).toHaveProperty("top");
|
|
expect(result).toHaveProperty("watchlist");
|
|
expect(result.top).toHaveLength(1);
|
|
expect(result.watchlist).toHaveLength(1);
|
|
});
|
|
|
|
it("passes includeProposed flag to application layer", async () => {
|
|
vi.mocked(getDashboardChargeabilityOverview).mockResolvedValue({
|
|
avgChargeability: 60,
|
|
top: [],
|
|
watchlist: [],
|
|
});
|
|
|
|
const caller = createControllerCaller({});
|
|
await caller.getChargeabilityOverview({
|
|
includeProposed: true,
|
|
topN: 5,
|
|
watchlistThreshold: 20,
|
|
});
|
|
|
|
expect(getDashboardChargeabilityOverview).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
includeProposed: true,
|
|
topN: 5,
|
|
watchlistThreshold: 20,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("requires controller role — blocks USER", async () => {
|
|
const caller = createProtectedCaller({});
|
|
await expect(
|
|
caller.getChargeabilityOverview({
|
|
includeProposed: false,
|
|
topN: 10,
|
|
watchlistThreshold: 15,
|
|
}),
|
|
).rejects.toThrow(
|
|
expect.objectContaining({ code: "FORBIDDEN" }),
|
|
);
|
|
});
|
|
});
|
|
});
|