Files
2026-03-27 23:57:47 +08:00

236 lines
7.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
createStackConfig,
formatServiceLogs,
getDefaultTemporalCandidates,
getServiceCommandMatchers,
isServiceHealthy,
isServiceProcessMatch,
resolveTemporalCli,
selectServicesForLogs,
} from "../../scripts/dev-stack/stack.mjs";
type StackService = ReturnType<typeof createStackConfig>["services"][number];
describe("createStackConfig", () => {
it("derives frontend and sibling backend paths from the workspace root", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
expect(config.frontendRoot).toBe("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
expect(config.backendRoot).toBe("/Volumes/DockCase/codes/auto-virtual-tryon");
expect(config.runtimeRoot).toBe(
"/Volumes/DockCase/codes/auto-virtual-tryon-frontend/.dev-stack",
);
expect(config.temporalDatabaseFile).toBe(
"/Volumes/DockCase/codes/auto-virtual-tryon-frontend/.dev-stack/temporal-cli-dev.db",
);
});
it("builds the expected service commands", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
expect(config.services).toEqual([
{
key: "temporal",
cwd: "/Volumes/DockCase/codes/auto-virtual-tryon-frontend/.dev-stack",
port: 7233,
command: [
"__TEMPORAL_CLI__",
"server",
"start-dev",
"--ip",
"127.0.0.1",
"--port",
"7233",
"--headless",
"--db-filename",
"/Volumes/DockCase/codes/auto-virtual-tryon-frontend/.dev-stack/temporal-cli-dev.db",
],
},
{
key: "backend-api",
cwd: "/Volumes/DockCase/codes/auto-virtual-tryon",
port: 8000,
command: [
"/Volumes/DockCase/codes/auto-virtual-tryon/.venv/bin/python",
"-m",
"uvicorn",
"app.main:app",
"--host",
"127.0.0.1",
"--port",
"8000",
],
},
{
key: "backend-worker",
cwd: "/Volumes/DockCase/codes/auto-virtual-tryon",
port: null,
command: [
"/Volumes/DockCase/codes/auto-virtual-tryon/.venv/bin/python",
"-m",
"app.workers.runner",
],
},
{
key: "frontend",
cwd: "/Volumes/DockCase/codes/auto-virtual-tryon-frontend",
port: 3000,
command: [
"/Volumes/DockCase/codes/auto-virtual-tryon-frontend/node_modules/.bin/next",
"dev",
"--hostname",
"127.0.0.1",
"--port",
"3000",
],
},
]);
});
});
describe("resolveTemporalCli", () => {
it("prefers a CLI already available on PATH", () => {
const resolved = resolveTemporalCli({
pathLookupResult: "/usr/local/bin/temporal",
candidatePaths: ["/tmp/temporal-sdk-python-1.24.0"],
existingPaths: new Set(["/usr/local/bin/temporal", "/tmp/temporal-sdk-python-1.24.0"]),
});
expect(resolved).toBe("/usr/local/bin/temporal");
});
it("falls back to a downloaded SDK binary when PATH does not contain temporal", () => {
const resolved = resolveTemporalCli({
pathLookupResult: null,
candidatePaths: ["/tmp/temporal-sdk-python-1.24.0", "/tmp/temporal-cli"],
existingPaths: new Set(["/tmp/temporal-sdk-python-1.24.0"]),
});
expect(resolved).toBe("/tmp/temporal-sdk-python-1.24.0");
});
it("returns null when no temporal binary is available", () => {
const resolved = resolveTemporalCli({
pathLookupResult: null,
candidatePaths: ["/tmp/temporal-sdk-python-1.24.0"],
existingPaths: new Set<string>(),
});
expect(resolved).toBeNull();
});
});
describe("getDefaultTemporalCandidates", () => {
it("includes the known SDK cache binary names under the temp directory", () => {
expect(getDefaultTemporalCandidates("/var/folders/example/T")).toEqual([
"/var/folders/example/T/temporal-sdk-python-1.24.0",
"/var/folders/example/T/temporal-sdk-python-1.25.0",
"/var/folders/example/T/temporal-sdk-python-1.26.0",
"/var/folders/example/T/temporal-cli",
]);
});
});
describe("selectServicesForLogs", () => {
it("returns every service when no filter is provided", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
expect(
selectServicesForLogs(config.services, null).map((service: { key: string }) => service.key),
).toEqual(["temporal", "backend-api", "backend-worker", "frontend"]);
});
it("filters to a single service key", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
expect(
selectServicesForLogs(config.services, "frontend").map((service: { key: string }) => service.key),
).toEqual(["frontend"]);
});
it("throws a helpful error for an unknown service", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
expect(() => selectServicesForLogs(config.services, "foo")).toThrow(
"Unknown service \"foo\". Expected one of: temporal, backend-api, backend-worker, frontend",
);
});
});
describe("formatServiceLogs", () => {
it("formats sections with stable headers and preserves line order", () => {
const output = formatServiceLogs([
{
key: "frontend",
logFilePath: "/tmp/frontend.log",
lines: ["> dev", "ready"],
},
{
key: "backend-api",
logFilePath: "/tmp/backend-api.log",
lines: [],
},
]);
expect(output).toBe(
[
"--- frontend (/tmp/frontend.log) ---",
"> dev",
"ready",
"",
"--- backend-api (/tmp/backend-api.log) ---",
"(log file is empty)",
].join("\n"),
);
});
});
describe("service process matching", () => {
it("does not treat an unrelated process with a reused pid as the frontend service", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
const frontend = config.services.find((service: StackService) => service.key === "frontend");
expect(frontend).toBeDefined();
expect(
isServiceProcessMatch(frontend!, "node /tmp/unrelated-script.js --watch"),
).toBe(false);
});
it("builds stable command matchers for backend api and worker processes", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
const backendApi = config.services.find((service: StackService) => service.key === "backend-api");
const backendWorker = config.services.find((service: StackService) => service.key === "backend-worker");
expect(getServiceCommandMatchers(backendApi!)).toEqual(["uvicorn", "app.main:app"]);
expect(getServiceCommandMatchers(backendWorker!)).toEqual(["app.workers.runner"]);
});
it("requires the expected port to be open for port-bound services", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
const backendApi = config.services.find((service: StackService) => service.key === "backend-api");
expect(
isServiceHealthy(backendApi!, {
pid: 123,
processCommand: "/path/to/python -m uvicorn app.main:app --host 127.0.0.1 --port 8000",
portOpen: false,
}),
).toBe(false);
});
it("allows worker services without ports when the process command matches", () => {
const config = createStackConfig("/Volumes/DockCase/codes/auto-virtual-tryon-frontend");
const backendWorker = config.services.find((service: StackService) => service.key === "backend-worker");
expect(
isServiceHealthy(backendWorker!, {
pid: 123,
processCommand: "/path/to/python -m app.workers.runner",
portOpen: false,
}),
).toBe(true);
});
});