forked from bai/curriculum-project-hub
120 lines
4.2 KiB
TypeScript
120 lines
4.2 KiB
TypeScript
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> = {}): 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");
|
|
});
|
|
});
|