import assert from "node:assert/strict";
import test from "node:test";
import {
describePortOwner,
parsePort,
selectDeploymentPort,
verifySelectedPorts
} from "./ports.mjs";
test("parsePort accepts valid integers", () => {
assert.equal(parsePort("3000", "Web"), 3000);
assert.equal(parsePort(8787, "API"), 8787);
});
test("parsePort rejects invalid values", () => {
assert.throws(() => parsePort("0", "Web"), /Web/);
assert.throws(() => parsePort("70000", "API"), /API/);
assert.throws(() => parsePort("abc", "Web"), /Web/);
});
test("loops until the requested alternative is available", async () => {
const answers = ["2", "3001", "3002"];
const selected = await selectDeploymentPort({
label: "Web",
defaultPort: 3000,
reserved: new Set([8787]),
managedPorts: new Set(),
ask: async () => answers.shift(),
probe: async (port) => ({ available: port === 3002, owner: port === 3001 ? "node pid=42" : null })
});
assert.equal(selected, 3002);
});
test("accepts available default without choosing alternative", async () => {
const selected = await selectDeploymentPort({
label: "Web",
defaultPort: 3000,
reserved: new Set(),
managedPorts: new Set(),
ask: async () => "1",
probe: async () => ({ available: true, owner: null }),
print: () => {}
});
assert.equal(selected, 3000);
});
test("rejects duplicate reserved ports", async () => {
await assert.rejects(
selectDeploymentPort({
label: "Web",
defaultPort: 3000,
reserved: new Set([3000]),
managedPorts: new Set(),
nonInteractive: true,
ask: async () => assert.fail("must not prompt"),
probe: async () => ({ available: true, owner: null })
}),
/already selected|reserved|冲突|duplicate/i
);
});
test("non-interactive mode fails on an unknown listener", async () => {
await assert.rejects(
selectDeploymentPort({
label: "API",
defaultPort: 8787,
reserved: new Set(),
managedPorts: new Set(),
nonInteractive: true,
ask: async () => assert.fail("must not prompt"),
probe: async () => ({ available: false, owner: "python pid=99" })
}),
/API port 8787 is already in use/
);
});
test("quitting from occupied-port prompt exits safely", async () => {
await assert.rejects(
selectDeploymentPort({
label: "Web",
defaultPort: 3000,
reserved: new Set(),
managedPorts: new Set(),
ask: async () => "q",
probe: async () => ({ available: false, owner: "node pid=42" }),
print: () => {}
}),
/cancelled|退出|abort/i
);
});
test("managed ports may be selected during update preflight", async () => {
const selected = await selectDeploymentPort({
label: "Web",
defaultPort: 3000,
reserved: new Set(),
managedPorts: new Set([3000]),
nonInteractive: true,
ask: async () => assert.fail("must not prompt"),
probe: async () => ({ available: false, owner: "datafoundry pid=1" })
});
assert.equal(selected, 3000);
});
test("verifySelectedPorts fails when a port becomes occupied", async () => {
await assert.rejects(
verifySelectedPorts(
[{ label: "Web", port: 3000 }],
{
managedPorts: new Set(),
retries: 0,
probe: async () => ({ available: false, owner: "nginx pid=7" })
}
),
/Web port 3000 is already in use/
);
});
test("verifySelectedPorts skips ports still held by the managed stack", async () => {
let probed = false;
await verifySelectedPorts(
[
{ label: "Web", port: 3000 },
{ label: "API", port: 8787 }
],
{
managedPorts: new Set([3000]),
probe: async (port) => {
if (port === 3000) {
probed = true;
return { available: false, owner: "datafoundry pid=1" };
}
return { available: true, owner: null };
}
}
);
assert.equal(probed, false);
});
test("verifySelectedPorts retries briefly for transient TIME_WAIT occupancy", async () => {
let attempts = 0;
await verifySelectedPorts(
[{ label: "Web", port: 3000 }],
{
managedPorts: new Set(),
retries: 3,
retryDelayMs: 1,
probe: async () => {
attempts += 1;
if (attempts < 3) return { available: false, owner: "unknown process" };
return { available: true, owner: null };
}
}
);
assert.equal(attempts, 3);
});
test("describePortOwner matches exact port and not :30001 for :3000", async () => {
const owner = await describePortOwner(3000, {
runSs: async () =>
[
"State Recv-Q Send-Q Local Address:Port Peer Address:Port Process",
'LISTEN 0 511 0.0.0.0:30001 0.0.0.0:* users:(("other",pid=9,fd=3))',
'LISTEN 0 511 0.0.0.0:3000 0.0.0.0:* users:(("node",pid=42,fd=3))'
].join("\n")
});
assert.equal(owner, "node pid=42");
const noMatch = await describePortOwner(3000, {
runSs: async () =>
'LISTEN 0 511 0.0.0.0:30001 0.0.0.0:* users:(("other",pid=9,fd=3))\n'
});
assert.equal(noMatch, "unknown process");
});