549 lines
16 KiB
JavaScript
549 lines
16 KiB
JavaScript
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);
|
|
});
|