236 lines
7.8 KiB
TypeScript
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);
|
|
});
|
|
});
|