import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { activateAgentViewerDirectory, agentViewerUrl, buildAgentViewerGit, buildAgentStartCommand, forwardedDefaultRootDir, forwardedServerTarget, normalizeAgentDirectory, isReusableAgentViewerServer, parseAgentStartArgs, probeAgentViewerPort, replaceForwardedPort, resolveAgentStartLaunch, resolveAgentStartCommand, resolveAgentViewerPort, selectAgentStartMode, stripDefaultRootDirArgs, } from "./start-agent-viewer.mjs"; test("parseAgentStartArgs consumes launcher mode and preserves server flags", async (t) => { const directory = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-directory-")); t.after(() => fs.rm(directory, { recursive: true, force: true })); assert.deepEqual( parseAgentStartArgs([ "--viewer-start-mode=dev", "--host", "127.0.0.1", "--dir", directory, "--port=4178", ]), { startMode: "dev", forwardedArgs: [ "--host", "127.0.0.1", "--dir", directory, "--port=4178", ], directory, portScanLimit: 64, jsonResult: false, } ); }); test("parseAgentStartArgs --json sets jsonResult and is not forwarded to the server", async (t) => { const directory = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-directory-")); t.after(() => fs.rm(directory, { recursive: true, force: true })); const result = parseAgentStartArgs(["--dir", directory, "--host", "127.0.0.1", "--json"]); assert.equal(result.jsonResult, true); assert.ok(!result.forwardedArgs.includes("--json"), "--json must not be forwarded to the server"); }); test("forwardedDefaultRootDir reads and strips default directory flags", () => { assert.equal(forwardedDefaultRootDir(["--host", "127.0.0.1", "--dir=/project/models"]), "/project/models"); assert.equal(forwardedDefaultRootDir(["--dir", "models", "--port", "4178"]), "models"); assert.deepEqual( stripDefaultRootDirArgs(["--host", "127.0.0.1", "--dir", "/project/models", "--port=4178"]), ["--host", "127.0.0.1", "--port=4178"] ); }); test("parseAgentStartArgs requires agent:start --dir to be an absolute existing directory", async (t) => { const directory = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-directory-")); t.after(() => fs.rm(directory, { recursive: true, force: true })); const filePath = path.join(directory, "not-a-dir"); await fs.writeFile(filePath, ""); assert.equal(normalizeAgentDirectory(directory), directory); assert.throws(() => parseAgentStartArgs(["--host", "127.0.0.1"]), /requires --dir/); assert.throws(() => parseAgentStartArgs(["--dir", "models"]), /absolute path/); assert.throws(() => parseAgentStartArgs(["--dir", path.join(directory, "missing")]), /directory not found/); assert.throws(() => parseAgentStartArgs(["--dir", filePath]), /is not a directory/); }); test("parseAgentStartArgs rejects invalid launcher modes", () => { assert.throws( () => parseAgentStartArgs(["--viewer-start-mode", "test"]), /must be one of/ ); }); test("parseAgentStartArgs rejects shutdown lifetime flags", async (t) => { const directory = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-directory-")); t.after(() => fs.rm(directory, { recursive: true, force: true })); assert.throws( () => parseAgentStartArgs(["--dir", directory, "--shutdown-after", "12h"]), /does not support --shutdown-after/ ); assert.throws( () => parseAgentStartArgs(["--dir", directory, "--shutdown-after=12h"]), /does not support --shutdown-after/ ); }); test("forwardedServerTarget reads host and port flags", () => { assert.deepEqual( forwardedServerTarget(["--host=0.0.0.0", "--port", "4200"]), { host: "0.0.0.0", port: 4200, } ); }); test("replaceForwardedPort updates or appends the selected port", () => { assert.deepEqual( replaceForwardedPort(["--host", "127.0.0.1", "--port=4178"], 4180), ["--host", "127.0.0.1", "--port=4180"] ); assert.deepEqual( replaceForwardedPort(["--host", "127.0.0.1"], 4181), ["--host", "127.0.0.1", "--port", "4181"] ); }); test("selectAgentStartMode uses dev mode for symlinked npm prefixes", async (t) => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-")); t.after(() => fs.rm(tmpDir, { recursive: true, force: true })); const realViewer = path.join(tmpDir, "viewer"); const linkedViewer = path.join(tmpDir, "skills", "cad-viewer", "scripts", "viewer"); await fs.mkdir(realViewer, { recursive: true }); await fs.mkdir(path.dirname(linkedViewer), { recursive: true }); await fs.symlink(realViewer, linkedViewer, "dir"); assert.equal(selectAgentStartMode({ npmConfigPrefix: linkedViewer }), "dev"); assert.equal(selectAgentStartMode({ npmPackageJson: path.join(linkedViewer, "package.json") }), "dev"); assert.equal(selectAgentStartMode({ npmConfigPrefix: realViewer }), "serve"); assert.equal(selectAgentStartMode({ requestedMode: "serve", npmConfigPrefix: linkedViewer }), "serve"); }); test("buildAgentViewerGit returns a worktree and branch value when git exists", async (t) => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-")); t.after(() => fs.rm(tmpDir, { recursive: true, force: true })); const packageRoot = path.join(tmpDir, "viewer"); await fs.mkdir(packageRoot, { recursive: true }); execFileSync("git", ["init"], { cwd: tmpDir, stdio: "ignore" }); execFileSync("git", ["checkout", "-b", "test-branch"], { cwd: tmpDir, stdio: "ignore" }); const git = buildAgentViewerGit({ env: {}, cwd: packageRoot }); assert.match(git, /#test-branch$/); assert.match(git, /\/\.git#test-branch$/); }); test("buildAgentViewerGit is empty outside git", async (t) => { const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-")); t.after(() => fs.rm(tmpDir, { recursive: true, force: true })); assert.equal(buildAgentViewerGit({ env: {}, cwd: tmpDir }), ""); }); test("buildAgentStartCommand prepares dev mode without a server lifetime", () => { const command = buildAgentStartCommand({ mode: "dev", packageRoot: "/project/viewer", forwardedArgs: ["--host", "127.0.0.1", "--dir", "/project/models", "--port", "4178"], env: {}, nodePath: "/node", git: "git-a", }); assert.equal(command.command, "/node"); assert.deepEqual(command.args, [ "/project/viewer/node_modules/vite/bin/vite.js", "dev", "--host", "127.0.0.1", "--port", "4178", ]); assert.equal(command.env.VIEWER_SERVER_LIFETIME_MS, undefined); assert.equal(command.env.VIEWER_DEFAULT_DIR, "/project/models"); assert.equal(command.env.VIEWER_GIT, "git-a"); assert.equal(command.env.VIEWER_AGENT_START_MODE, "dev"); }); test("resolveAgentStartCommand keeps server-only flags on the production server path", async (t) => { const directory = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-directory-")); t.after(() => fs.rm(directory, { recursive: true, force: true })); const command = resolveAgentStartCommand({ argv: ["--viewer-start-mode", "serve", "--dir", directory], env: {}, packageRoot: "/project/viewer", nodePath: "/node", }); assert.equal(command.mode, "serve"); assert.equal(command.env.VIEWER_AGENT_START_MODE, "serve"); assert.deepEqual(command.args, [ "/project/viewer/src/server/server.mjs", "--dir", directory, ]); }); test("isReusableAgentViewerServer uses git only when both sides report it", () => { assert.equal( isReusableAgentViewerServer({ app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], git: "git-a", }, "git-a"), true ); assert.equal( isReusableAgentViewerServer({ app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], git: "git-b", }, "git-a"), false ); assert.equal( isReusableAgentViewerServer({ app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], }, "git-a"), true ); assert.equal( isReusableAgentViewerServer({ app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, git: "git-a", }, "git-a"), false ); assert.equal( isReusableAgentViewerServer({ app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], git: "git-a", activeDirectories: [{ dir: "/project/models", rootPath: "/project/models", }], }, ""), true ); assert.equal( isReusableAgentViewerServer({ app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], git: "git-a", directoryRoot: "/project", activeDirectories: [{ dir: "/project/models", rootPath: "/project/models", }], }, "git-a"), true ); assert.equal( isReusableAgentViewerServer({ app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], git: "git-a", activeDirectories: [{ dir: "/project/skill", rootPath: "/project/skill", }], }, "git-a"), true ); }); test("isReusableAgentViewerServer ignores git for matching dist bundle versions", () => { const distServer = { app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], serverMode: "serve", viewerVersion: "0.3.2", git: "git-b", }; assert.equal( isReusableAgentViewerServer(distServer, { mode: "serve", viewerVersion: "0.3.2", git: "git-a", }), true ); assert.equal( isReusableAgentViewerServer(distServer, { mode: "serve", viewerVersion: "0.3.3", git: "git-a", }), false ); assert.equal( isReusableAgentViewerServer({ ...distServer, serverMode: "dev", }, { mode: "serve", viewerVersion: "0.3.2", git: "git-a", }), false ); assert.equal( isReusableAgentViewerServer({ ...distServer, serverMode: "serve", }, { mode: "dev", viewerVersion: "0.3.2", git: "git-a", }), false ); }); test("agentViewerUrl includes the selected absolute directory", async (t) => { const directory = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-directory-")); t.after(() => fs.rm(directory, { recursive: true, force: true })); const url = agentViewerUrl("http://127.0.0.1:4178", directory); assert.equal(new URL(url).searchParams.get("dir"), directory); }); test("activateAgentViewerDirectory posts the requested dir to the activation endpoint", async (t) => { const directory = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-directory-")); t.after(() => fs.rm(directory, { recursive: true, force: true })); const requests = []; const result = await activateAgentViewerDirectory({ baseUrl: "http://127.0.0.1:4178", directory, fetchImpl: async (url, options) => { requests.push({ url: String(url), method: options?.method || "GET" }); return { ok: true, json: async () => ({ ok: true, directory: { dir: directory, rootPath: directory, rootName: path.basename(directory), }, server: { app: "cad-viewer", serverFeatures: ["directory-activation"], }, }), }; }, }); assert.equal(result.directory, directory); assert.equal(new URL(result.viewerUrl).searchParams.get("dir"), directory); assert.deepEqual(result.activeDirectory, { dir: directory, rootPath: directory, rootName: path.basename(directory), }); assert.equal(requests[0].method, "POST"); assert.equal(new URL(requests[0].url).pathname, "/__cad/directory/activate"); assert.equal(new URL(requests[0].url).searchParams.get("dir"), directory); }); test("resolveAgentViewerPort reuses matching registry servers before free lower ports", async () => { const probes = []; const result = await resolveAgentViewerPort({ forwardedArgs: ["--host", "127.0.0.1", "--port", "4178"], git: "git-a", registryServers: [{ app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], git: "git-a", port: 5173, url: "http://127.0.0.1:5173", }], probePort: async ({ host, port }) => { probes.push(`${host}:${port}`); return { status: "viewer", port, baseUrl: `http://${host}:${port}`, serverInfo: { app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], git: "git-a", }, }; }, }); assert.equal(result.action, "reuse"); assert.equal(result.port, 5173); assert.deepEqual(probes, ["127.0.0.1:5173"]); }); test("resolveAgentViewerPort reuses matching-version dist servers across git branches", async () => { const probes = []; const result = await resolveAgentViewerPort({ forwardedArgs: ["--host", "127.0.0.1", "--port", "4178"], git: "git-a", mode: "serve", viewerVersion: "0.3.2", registryServers: [{ app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], serverMode: "serve", viewerVersion: "0.3.2", git: "git-b", port: 5173, url: "http://127.0.0.1:5173", }], probePort: async ({ host, port }) => { probes.push(`${host}:${port}`); return { status: "viewer", port, baseUrl: `http://${host}:${port}`, serverInfo: { app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], serverMode: "serve", viewerVersion: "0.3.2", git: "git-b", }, }; }, }); assert.equal(result.action, "reuse"); assert.equal(result.port, 5173); assert.deepEqual(probes, ["127.0.0.1:5173"]); }); test("resolveAgentViewerPort skips other viewers and starts on the first closed port", async () => { const probes = []; const result = await resolveAgentViewerPort({ forwardedArgs: ["--port", "4178"], git: "git-a", registryServers: [], portScanLimit: 3, probePort: async ({ host, port }) => { probes.push(port); if (port === 4178) { return { status: "viewer", port, baseUrl: `http://${host}:${port}`, serverInfo: { app: "cad-viewer", serverApiVersion: 2, dynamicRoot: true, serverFeatures: ["directory-activation"], git: "git-b", }, }; } return { status: "closed", port, baseUrl: `http://${host}:${port}`, }; }, }); assert.equal(result.action, "start"); assert.equal(result.port, 4179); assert.deepEqual(probes, [4178, 4179]); }); test("probeAgentViewerPort reports permission-blocked local probes", async () => { const error = new TypeError("fetch failed"); error.cause = { code: "EPERM" }; const result = await probeAgentViewerPort({ port: 4178, fetchImpl: async () => { throw error; }, }); assert.equal(result.status, "blocked"); assert.equal(result.port, 4178); }); test("resolveAgentStartLaunch starts the selected free port", async (t) => { const directory = await fs.mkdtemp(path.join(os.tmpdir(), "cad-viewer-agent-start-directory-")); t.after(() => fs.rm(directory, { recursive: true, force: true })); const result = await resolveAgentStartLaunch({ argv: ["--viewer-start-mode", "serve", "--host", "127.0.0.1", "--dir", directory, "--port", "4178"], env: {}, packageRoot: "/project/viewer", nodePath: "/node", registryServers: [], probePort: async ({ host, port }) => ({ status: port === 4178 ? "occupied" : "closed", port, baseUrl: `http://${host}:${port}`, }), }); assert.equal(result.action, "start"); assert.equal(result.port, 4179); assert.deepEqual(result.command.args, [ "/project/viewer/src/server/server.mjs", "--host", "127.0.0.1", "--dir", directory, "--port", "4179", ]); assert.equal(new URL(result.viewerUrl).searchParams.get("dir"), directory); });