import { describe, expect, it } from "vitest"; import { createStackConfig, formatServiceLogs, getDefaultTemporalCandidates, getServiceCommandMatchers, isServiceHealthy, isServiceProcessMatch, resolveTemporalCli, selectServicesForLogs, } from "../../scripts/dev-stack/stack.mjs"; type StackService = ReturnType["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(), }); 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); }); });