feat: bootstrap auto virtual tryon admin frontend

This commit is contained in:
afei A
2026-03-27 23:38:50 +08:00
commit 98c6b741d6
119 changed files with 19046 additions and 0 deletions

28
tests/scripts/dev-stack-stack-mjs.d.ts vendored Normal file
View File

@@ -0,0 +1,28 @@
declare module "../../scripts/dev-stack/stack.mjs" {
export type StackServiceConfig = {
key: string;
cwd: string;
port: number | null;
command: string[];
};
export type StackConfig = {
frontendRoot: string;
backendRoot: string;
runtimeRoot: string;
pidRoot: string;
logRoot: string;
temporalDatabaseFile: string;
services: StackServiceConfig[];
};
export function getDefaultTemporalCandidates(tempDirectory: string): string[];
export function resolveTemporalCli(options: {
pathLookupResult: string | null;
candidatePaths: string[];
existingPaths: Set<string>;
}): string | null;
export function createStackConfig(frontendRoot: string, backendRoot?: string): StackConfig;
}

View File

@@ -0,0 +1,183 @@
import { describe, expect, it } from "vitest";
import {
createStackConfig,
formatServiceLogs,
getDefaultTemporalCandidates,
resolveTemporalCli,
selectServicesForLogs,
} from "../../scripts/dev-stack/stack.mjs";
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"),
);
});
});