import { describe, expect, it } from "vitest"; import { DeploymentPreflightError, validateDeploymentPreflight, type DeploymentPreflightInput, } from "../../src/deployment/preflight.js"; const VALID_ENV = { NODE_ENV: "production", DATABASE_URL: "postgresql://hub:secret@127.0.0.1:5432/hub", HUB_SILO_ORGANIZATION_ID: "org_test", HUB_SYSTEMD_UNIT: "cph-hub-test.service", CPH_BIN: "/srv/curriculum-project-hub/current/bin/cph", HOST: "127.0.0.1", PORT: "8788", HUB_PROJECT_WORKSPACE_ROOT: "/var/lib/cph-hub/workspaces", HUB_PUBLIC_BASE_URL: "https://hub.example.com", HUB_SESSION_SECRET: "a-production-session-secret-with-32-bytes", HUB_AGENT_MAX_CONCURRENT_RUNS: "1", HUB_AGENT_MAX_RUN_SECONDS: "900", HUB_HTTP_BODY_LIMIT_BYTES: "1048576", HUB_MAX_FILES_PER_MESSAGE: "8", HUB_MAX_FILE_BYTES: "26214400", HUB_HTTP_REQUESTS_PER_MINUTE: "120", HUB_FEISHU_EVENTS_PER_MINUTE: "120", } as const; function input(overrides: Partial = {}): DeploymentPreflightInput { return { baseDir: "/srv/curriculum-project-hub", hubDir: "/srv/curriculum-project-hub/current/hub", serviceHome: "/var/lib/cph-hub/home", stateDir: "/var/lib/cph-hub/state", cacheDir: "/var/cache/cph-hub", workspaceRoot: "/var/lib/cph-hub/workspaces", expectedServiceUnit: "cph-hub-test.service", env: VALID_ENV, ...overrides, }; } describe("validateDeploymentPreflight", () => { it("accepts a complete production configuration with persistent storage outside the deployment tree", () => { expect(validateDeploymentPreflight(input())).toEqual({ bind: { host: "127.0.0.1", port: 8788 }, databaseUrl: "postgresql://hub:secret@127.0.0.1:5432/hub", cphBin: "/srv/curriculum-project-hub/current/bin/cph", }); }); it.each([ ["inside the live Hub", "/srv/curriculum-project-hub/current/hub/data/workspaces"], ["inside future releases", "/srv/curriculum-project-hub/releases/workspaces"], ["an ancestor of the deployment root", "/srv"], ])("rejects a workspace %s", (_description, workspaceRoot) => { expect(() => validateDeploymentPreflight( input({ workspaceRoot, env: { ...VALID_ENV, HUB_PROJECT_WORKSPACE_ROOT: workspaceRoot }, }), ), ).toThrow("must not overlap deployment root"); }); it("rejects a runtime workspace that differs from the provisioned path", () => { expect(() => validateDeploymentPreflight( input({ env: { ...VALID_ENV, HUB_PROJECT_WORKSPACE_ROOT: "/var/lib/cph-hub/other" } }), ), ).toThrow("HUB_PROJECT_WORKSPACE_ROOT must equal provisioned workspace root"); }); it("rejects a relative runtime workspace even if it resolves to the provisioned path", () => { expect(() => validateDeploymentPreflight( input({ env: { ...VALID_ENV, HUB_PROJECT_WORKSPACE_ROOT: "./workspaces" } }), ), ).toThrow("HUB_PROJECT_WORKSPACE_ROOT must be an absolute normalized path"); }); it("reports all invalid required production values in one failed preflight", () => { expect.assertions(2); try { validateDeploymentPreflight( input({ env: { ...VALID_ENV, HUB_SILO_ORGANIZATION_ID: "", HUB_PUBLIC_BASE_URL: "http://hub.example.com", HUB_SESSION_SECRET: "short", }, }), ); } catch (error) { expect(error).toBeInstanceOf(DeploymentPreflightError); expect((error as Error).message).toMatchInlineSnapshot(` "deployment preflight failed: - HUB_SILO_ORGANIZATION_ID is required - HUB_PUBLIC_BASE_URL must use https - HUB_SESSION_SECRET must contain at least 32 characters" `); } }); it("rejects relative operational paths", () => { expect(() => validateDeploymentPreflight(input({ cacheDir: "./cache" }))).toThrow( "cache directory must be an absolute normalized path", ); }); it("rejects legacy process-global provider settings instead of silently ignoring them", () => { expect(() => validateDeploymentPreflight( input({ env: { ...VALID_ENV, ANTHROPIC_AUTH_TOKEN: "global-secret" } }), ), ).toThrow("ANTHROPIC_AUTH_TOKEN must not be set"); }); });