Replace legacy scheme commands with run-id based analysis and timeseries queries. Update sensor placement and SCADA routes, and refresh CLI help, tests, and usage guidance. BREAKING CHANGE: legacy scheme, risk, and valve-close CLI command paths are removed.
499 lines
21 KiB
JavaScript
499 lines
21 KiB
JavaScript
import { strict as assert } from "node:assert";
|
|
import { spawn } from "node:child_process";
|
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { createServer } from "node:http";
|
|
import { tmpdir } from "node:os";
|
|
import { test } from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join, resolve } from "node:path";
|
|
|
|
const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../cli/tjwater-cli");
|
|
|
|
const visibleCommandPaths = [
|
|
"analysis age",
|
|
"analysis burst",
|
|
"analysis burst-detection detect",
|
|
"analysis burst-location locate",
|
|
"analysis contaminant",
|
|
"analysis flushing",
|
|
"analysis leakage identify",
|
|
"analysis runs get",
|
|
"analysis runs list",
|
|
"analysis runs results",
|
|
"analysis sensor-placement get",
|
|
"analysis sensor-placement list",
|
|
"analysis sensor-placement run",
|
|
"analysis valve isolation",
|
|
"component option get",
|
|
"component option schema",
|
|
"data scada get",
|
|
"data scada list",
|
|
"data scada schema",
|
|
"data pipeline-health",
|
|
"data timeseries analysis link-field",
|
|
"data timeseries analysis node-field",
|
|
"data timeseries analysis values",
|
|
"data timeseries composite",
|
|
"data timeseries realtime links",
|
|
"data timeseries realtime nodes",
|
|
"data timeseries realtime simulation-by-id-time",
|
|
"data timeseries realtime simulation-by-time-property",
|
|
"data timeseries scada query",
|
|
"network get-all-pipes-properties",
|
|
"network get-all-pumps-properties",
|
|
"network get-all-reservoirs-properties",
|
|
"network get-all-tanks-properties",
|
|
"network get-all-valves-properties",
|
|
"network get-junction-properties",
|
|
"network get-pipe-properties",
|
|
"network get-pump-properties",
|
|
"network get-reservoir-properties",
|
|
"network get-tank-properties",
|
|
"network get-valve-properties",
|
|
"simulation run",
|
|
];
|
|
|
|
const hiddenCommandPaths = [];
|
|
|
|
function runCommand(command, args, input, options = {}) {
|
|
return new Promise((resolveRun, reject) => {
|
|
const child = spawn(command, args, {
|
|
cwd: options.cwd,
|
|
stdio: ["pipe", "pipe", "pipe"],
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk.toString("utf8");
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk.toString("utf8");
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (exitCode) => resolveRun({ exitCode, stdout, stderr }));
|
|
if (input !== undefined) child.stdin.end(JSON.stringify(input));
|
|
else child.stdin.end();
|
|
});
|
|
}
|
|
|
|
function runCli(args, input) {
|
|
return runCommand(cliPath, args, input);
|
|
}
|
|
|
|
function parseJsonResult(result) {
|
|
return JSON.parse(result.stdout);
|
|
}
|
|
|
|
async function startJsonServer(responseData) {
|
|
const seen = [];
|
|
const server = createServer(async (req, res) => {
|
|
const chunks = [];
|
|
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
seen.push({
|
|
body: text ? JSON.parse(text) : null,
|
|
headers: req.headers,
|
|
method: req.method,
|
|
url: req.url,
|
|
});
|
|
res.setHeader("content-type", "application/json");
|
|
res.end(
|
|
JSON.stringify(
|
|
typeof responseData === "function" ? responseData(req) : responseData,
|
|
),
|
|
);
|
|
});
|
|
|
|
await new Promise((resolveListen, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", resolveListen);
|
|
});
|
|
const address = server.address();
|
|
return {
|
|
seen,
|
|
url: `http://127.0.0.1:${address.port}`,
|
|
close: () => new Promise((resolveClose) => server.close(resolveClose)),
|
|
};
|
|
}
|
|
|
|
function normalizeSeenRequest(request) {
|
|
const url = new URL(request.url, "http://127.0.0.1");
|
|
const query = {};
|
|
for (const key of [...new Set(url.searchParams.keys())].sort()) {
|
|
const values = url.searchParams.getAll(key);
|
|
query[key] = values.length === 1 ? values[0] : values;
|
|
}
|
|
return {
|
|
body: request.body,
|
|
headers: {
|
|
authorization: request.headers.authorization,
|
|
"x-project-id": request.headers["x-project-id"],
|
|
},
|
|
method: request.method,
|
|
path: url.pathname,
|
|
query,
|
|
};
|
|
}
|
|
|
|
function defaultContractResponse(req) {
|
|
const url = new URL(req.url, "http://127.0.0.1");
|
|
if (["/api/v1/pipes", "/api/v1/reservoirs", "/api/v1/tanks", "/api/v1/pumps", "/api/v1/valves", "/api/v1/scada-devices"].includes(url.pathname)) {
|
|
return {
|
|
items: [],
|
|
limit: Number(url.searchParams.get("limit")),
|
|
offset: Number(url.searchParams.get("offset")),
|
|
total: 0,
|
|
};
|
|
}
|
|
return { accepted: true };
|
|
}
|
|
|
|
async function runAgainstServer(name, runner, args, auth, responseData = defaultContractResponse) {
|
|
const server = await startJsonServer(responseData);
|
|
try {
|
|
const result = await runner(["--auth-stdin", ...args], { ...auth, server: server.url });
|
|
if (!result.stdout.trim()) {
|
|
throw new Error(`${name}: CLI produced empty stdout; exit=${result.exitCode}; stderr=${result.stderr}`);
|
|
}
|
|
return {
|
|
exitCode: result.exitCode,
|
|
payload: parseJsonResult(result),
|
|
requests: server.seen.map(normalizeSeenRequest),
|
|
stderr: result.stderr,
|
|
};
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
}
|
|
|
|
test("emits structured JSON help compatible with tjwater-cli/v1", async () => {
|
|
const result = await runCli(["help", "simulation", "run"]);
|
|
|
|
assert.equal(result.exitCode, 0, result.stderr);
|
|
const payload = JSON.parse(result.stdout);
|
|
assert.equal(payload.schema_version, "tjwater-cli/v1");
|
|
assert.equal(payload.command, "simulation run");
|
|
assert.equal(payload.usage, "tjwater-cli simulation run --start-time <START_TIME> --duration <DURATION>");
|
|
});
|
|
|
|
test("discovers every visible command and keeps internal commands hidden", async () => {
|
|
const rootResult = await runCli(["help"]);
|
|
assert.equal(rootResult.exitCode, 0, rootResult.stderr);
|
|
assert.deepEqual(
|
|
parseJsonResult(rootResult).commands.map(({ command }) => command),
|
|
["analysis", "component", "data", "network", "simulation"],
|
|
);
|
|
|
|
for (const command of visibleCommandPaths) {
|
|
const result = await runCli(["help", ...command.split(" ")]);
|
|
assert.equal(result.exitCode, 0, `${command}: ${result.stderr}`);
|
|
const payload = parseJsonResult(result);
|
|
assert.equal(payload.ok, true, command);
|
|
assert.equal(payload.command, command, command);
|
|
assert.equal(payload.schema_version, "tjwater-cli/v1", command);
|
|
assert.ok(payload.usage, `${command}: missing usage`);
|
|
assert.ok(payload.examples.length > 0, `${command}: missing examples`);
|
|
}
|
|
|
|
for (const command of hiddenCommandPaths) {
|
|
const result = await runCli(["help", ...command.split(" ")]);
|
|
assert.equal(result.exitCode, 0, `${command}: ${result.stderr}`);
|
|
const payload = parseJsonResult(result);
|
|
assert.equal(payload.ok, false, command);
|
|
assert.equal(payload.error.code, "COMMAND_NOT_FOUND", command);
|
|
}
|
|
});
|
|
|
|
test("sends auth headers and simulation body through the backend API contract", async () => {
|
|
const server = await startJsonServer({ accepted: true });
|
|
try {
|
|
const result = await runCli(
|
|
[
|
|
"--auth-stdin",
|
|
"simulation",
|
|
"run",
|
|
"--start-time",
|
|
"2025-01-02T03:00:00+08:00",
|
|
"--duration",
|
|
"60",
|
|
],
|
|
{
|
|
server: server.url,
|
|
access_token: "token-1",
|
|
project_id: "project-1",
|
|
headers: { "x-extra": "extra" },
|
|
},
|
|
);
|
|
|
|
assert.equal(result.exitCode, 0, result.stderr);
|
|
assert.equal(server.seen[0].method, "POST");
|
|
assert.equal(server.seen[0].url, "/api/v1/simulation-runs");
|
|
assert.equal(server.seen[0].headers.authorization, "Bearer token-1");
|
|
assert.equal(server.seen[0].headers["x-extra"], "extra");
|
|
assert.deepEqual(server.seen[0].body, {
|
|
start_time: "2025-01-02T03:00:00+08:00",
|
|
duration: 60,
|
|
});
|
|
const payload = JSON.parse(result.stdout);
|
|
assert.equal(payload.ok, true);
|
|
assert.match(payload.next_commands[0], /--end-time 2025-01-02T04:00:00\+08:00/);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
test("get-all network commands collect every backend page", async () => {
|
|
const items = Array.from({ length: 2_005 }, (_, index) => ({
|
|
id: `P${index + 1}`,
|
|
node1: `N${index + 1}`,
|
|
node2: `N${index + 2}`,
|
|
}));
|
|
const server = await startJsonServer((req) => {
|
|
const url = new URL(req.url, "http://127.0.0.1");
|
|
const limit = Number(url.searchParams.get("limit") ?? 100);
|
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
return {
|
|
items: items.slice(offset, offset + limit),
|
|
limit,
|
|
offset,
|
|
total: items.length,
|
|
};
|
|
});
|
|
|
|
try {
|
|
const result = await runCli(
|
|
["--auth-stdin", "network", "get-all-pipes-properties"],
|
|
{
|
|
server: server.url,
|
|
access_token: "token-1",
|
|
project_id: "project-1",
|
|
},
|
|
);
|
|
|
|
assert.equal(result.exitCode, 0, result.stderr);
|
|
const payload = parseJsonResult(result);
|
|
assert.equal(payload.data.length, items.length);
|
|
assert.deepEqual(payload.data[0], items[0]);
|
|
assert.deepEqual(payload.data.at(-1), items.at(-1));
|
|
assert.deepEqual(
|
|
server.seen.map((request) => normalizeSeenRequest(request).query),
|
|
[
|
|
{ limit: "1000", offset: "0" },
|
|
{ limit: "1000", offset: "1000" },
|
|
{ limit: "1000", offset: "2000" },
|
|
],
|
|
);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
test("uses project scoped headers for realtime data commands", async () => {
|
|
const server = await startJsonServer([{ id: "P1" }]);
|
|
try {
|
|
const result = await runCli(
|
|
[
|
|
"--auth-stdin",
|
|
"data",
|
|
"timeseries",
|
|
"realtime",
|
|
"links",
|
|
"--start-time",
|
|
"2025-01-02T03:00:00+08:00",
|
|
"--end-time",
|
|
"2025-01-02T04:00:00+08:00",
|
|
],
|
|
{
|
|
server: server.url,
|
|
accessToken: "token-2",
|
|
projectId: "project-1",
|
|
},
|
|
);
|
|
|
|
assert.equal(result.exitCode, 0, result.stderr);
|
|
assert.equal(server.seen[0].method, "GET");
|
|
assert.equal(
|
|
server.seen[0].url,
|
|
"/api/v1/timeseries/realtime/links?start_time=2025-01-02T03%3A00%3A00%2B08%3A00&end_time=2025-01-02T04%3A00%3A00%2B08%3A00",
|
|
);
|
|
assert.equal(server.seen[0].headers.authorization, "Bearer token-2");
|
|
assert.equal(server.seen[0].headers["x-project-id"], "project-1");
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
test("maps CLI pipe and junction types to backend link and node types", async () => {
|
|
const server = await startJsonServer({ accepted: true });
|
|
const auth = { server: server.url, access_token: "token-3", project_id: "project-1" };
|
|
const at = "2025-01-02T03:30:00+08:00";
|
|
try {
|
|
for (const args of [
|
|
["data", "timeseries", "realtime", "simulation-by-id-time", "--id", "J1", "--type", "junction", "--time", at],
|
|
["data", "timeseries", "realtime", "simulation-by-time-property", "--type", "pipe", "--time", at, "--property", "flow"],
|
|
["data", "timeseries", "analysis", "values", "--run-id", "00000000-0000-0000-0000-000000000001", "--type", "pipe", "--time", at, "--field", "flow"],
|
|
]) {
|
|
const result = await runCli(["--auth-stdin", ...args], auth);
|
|
assert.equal(result.exitCode, 0, result.stderr);
|
|
}
|
|
const requests = server.seen.map(normalizeSeenRequest);
|
|
assert.deepEqual(
|
|
[requests[0].query.type, requests[1].query.type, requests[2].query.element_type],
|
|
["node", "link", "link"],
|
|
);
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
test("uses run-id based analysis, sensor placement, and SCADA contracts", async () => {
|
|
const server = await startJsonServer(defaultContractResponse);
|
|
const auth = { server: server.url, access_token: "token-4", project_id: "project-1" };
|
|
const runId = "00000000-0000-0000-0000-000000000001";
|
|
const start = "2025-01-02T03:00:00+08:00";
|
|
const end = "2025-01-02T04:00:00+08:00";
|
|
try {
|
|
for (const args of [
|
|
["analysis", "sensor-placement", "run", "--run-name", "placement-1", "--method", "kmeans", "--count", "5", "--min-diameter", "100"],
|
|
["analysis", "runs", "results", "--run-id", runId, "--result-type", "leakage_identification"],
|
|
["data", "timeseries", "analysis", "node-field", "--run-id", runId, "--node", "J1", "--field", "pressure", "--start-time", start, "--end-time", end],
|
|
["data", "scada", "get", "--device-id", "SCADA-001"],
|
|
]) {
|
|
const result = await runCli(["--auth-stdin", ...args], auth);
|
|
assert.equal(result.exitCode, 0, result.stderr);
|
|
}
|
|
|
|
const requests = server.seen.map(normalizeSeenRequest);
|
|
assert.deepEqual(requests[0], {
|
|
body: {
|
|
run_name: "placement-1",
|
|
sensor_type: "pressure",
|
|
method: "kmeans",
|
|
sensor_count: 5,
|
|
min_diameter: 100,
|
|
},
|
|
headers: { authorization: "Bearer token-4", "x-project-id": "project-1" },
|
|
method: "POST",
|
|
path: "/api/v1/sensor-placement-runs",
|
|
query: {},
|
|
});
|
|
assert.equal(requests[1].path, `/api/v1/analysis/runs/${runId}/results`);
|
|
assert.deepEqual(requests[1].query, { result_type: "leakage_identification" });
|
|
assert.equal(requests[2].path, `/api/v1/timeseries/analysis/runs/${runId}/nodes/J1`);
|
|
assert.deepEqual(requests[2].query, {
|
|
end_time: end,
|
|
field: "pressure",
|
|
start_time: start,
|
|
});
|
|
assert.equal(requests[3].path, "/api/v1/scada-devices/detail");
|
|
assert.deepEqual(requests[3].query, { device_id: "SCADA-001" });
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
});
|
|
|
|
test("does not expose removed scheme, risk, or valve-close commands", async () => {
|
|
for (const command of [
|
|
"analysis leakage schemes list",
|
|
"analysis risk network",
|
|
"data scheme list",
|
|
"data timeseries scheme simulation",
|
|
]) {
|
|
const result = await runCli(["help", ...command.split(" ")]);
|
|
assert.equal(result.exitCode, 0, result.stderr);
|
|
const payload = parseJsonResult(result);
|
|
assert.equal(payload.ok, false, command);
|
|
assert.equal(payload.error.code, "COMMAND_NOT_FOUND", command);
|
|
}
|
|
|
|
const valveClose = await runCli(["analysis", "valve", "--mode", "close"]);
|
|
assert.equal(valveClose.exitCode, 2, valveClose.stderr);
|
|
assert.equal(parseJsonResult(valveClose).error.code, "COMMAND_NOT_FOUND");
|
|
});
|
|
|
|
test("executes every command and key variant against the backend contract", async () => {
|
|
const tempDir = await mkdtemp(join(tmpdir(), "tjwater-cli-parity-"));
|
|
try {
|
|
const burstFile = join(tempDir, "burst.json");
|
|
const valveFile = join(tempDir, "valve.json");
|
|
const pressureFile = join(tempDir, "pressure.json");
|
|
const flowFile = join(tempDir, "flow.json");
|
|
await writeFile(burstFile, JSON.stringify([{ id: "B1", size: 12.5 }]));
|
|
await writeFile(valveFile, JSON.stringify([{ valve: "V1", opening: 0.5 }]));
|
|
await writeFile(pressureFile, JSON.stringify({ burst_pressure: [1.1], normal_pressure: [2.2] }));
|
|
await writeFile(flowFile, JSON.stringify({ burst_flow: [3.3], normal_flow: [4.4] }));
|
|
|
|
const auth = {
|
|
access_token: "token",
|
|
network: "tjwater",
|
|
project_id: "project-1",
|
|
username: "alice",
|
|
headers: { "x-extra": "extra" },
|
|
};
|
|
const start = "2025-01-02T03:00:00+08:00";
|
|
const end = "2025-01-02T04:00:00+08:00";
|
|
const at = "2025-01-02T03:30:00+08:00";
|
|
const runId = "00000000-0000-0000-0000-000000000001";
|
|
const cases = [
|
|
["network get-junction-properties", ["network", "get-junction-properties", "--junction", "J1"]],
|
|
["network get-pipe-properties", ["network", "get-pipe-properties", "--pipe", "P1"]],
|
|
["network get-all-pipes-properties", ["network", "get-all-pipes-properties"]],
|
|
["network get-reservoir-properties", ["network", "get-reservoir-properties", "--reservoir", "R1"]],
|
|
["network get-all-reservoirs-properties", ["network", "get-all-reservoirs-properties"]],
|
|
["network get-tank-properties", ["network", "get-tank-properties", "--tank", "T1"]],
|
|
["network get-all-tanks-properties", ["network", "get-all-tanks-properties"]],
|
|
["network get-pump-properties", ["network", "get-pump-properties", "--pump", "PU1"]],
|
|
["network get-all-pumps-properties", ["network", "get-all-pumps-properties"]],
|
|
["network get-valve-properties", ["network", "get-valve-properties", "--valve", "V1"]],
|
|
["network get-all-valves-properties", ["network", "get-all-valves-properties"]],
|
|
["component option schema", ["component", "option", "schema", "--kind", "time"]],
|
|
["component option get", ["component", "option", "get", "--kind", "pump-energy", "--pump", "P1"]],
|
|
["simulation run", ["simulation", "run", "--start-time", start, "--duration", "60"]],
|
|
["analysis burst", ["analysis", "burst", "--start-time", start, "--duration", "900", "--burst-file", burstFile, "--scheme", "burst_case"]],
|
|
["analysis valve isolation", ["analysis", "valve", "isolation", "--element", "E1", "--disabled-valve", "V3"]],
|
|
["analysis flushing", ["analysis", "flushing", "--start-time", start, "--valve-setting-file", valveFile, "--drainage-node", "N1", "--flow", "100.5", "--duration", "900", "--scheme", "flush_case"]],
|
|
["analysis age", ["analysis", "age", "--start-time", start, "--duration", "900"]],
|
|
["analysis contaminant", ["analysis", "contaminant", "--start-time", start, "--duration", "900", "--source-node", "N1", "--concentration", "10.5", "--pattern", "P1", "--scheme", "contam_case"]],
|
|
["analysis sensor-placement run", ["analysis", "sensor-placement", "run", "--run-name", "place_case", "--method", "kmeans", "--count", "5", "--min-diameter", "100"]],
|
|
["analysis sensor-placement list", ["analysis", "sensor-placement", "list"]],
|
|
["analysis sensor-placement get", ["analysis", "sensor-placement", "get", "--run-id", runId]],
|
|
["analysis runs list", ["analysis", "runs", "list"]],
|
|
["analysis runs get", ["analysis", "runs", "get", "--run-id", runId]],
|
|
["analysis runs results", ["analysis", "runs", "results", "--run-id", runId, "--result-type", "leakage_identification"]],
|
|
["analysis leakage identify", ["analysis", "leakage", "identify", "--start-time", start, "--end-time", end, "--scheme", "leak_case"]],
|
|
["analysis burst-detection detect", ["analysis", "burst-detection", "detect", "--start-time", start, "--end-time", end, "--scheme", "detect_case"]],
|
|
["analysis burst-location locate", ["analysis", "burst-location", "locate", "--start-time", start, "--end-time", end, "--burst-leakage", "50.5", "--scheme", "locate_case", "--data-source", "simulation", "--pressure-file", pressureFile, "--flow-file", flowFile, "--use-scada-flow"]],
|
|
["data realtime links", ["data", "timeseries", "realtime", "links", "--start-time", start, "--end-time", end]],
|
|
["data realtime nodes", ["data", "timeseries", "realtime", "nodes", "--start-time", start, "--end-time", end]],
|
|
["data realtime simulation-by-id-time", ["data", "timeseries", "realtime", "simulation-by-id-time", "--id", "J1", "--type", "junction", "--time", at]],
|
|
["data realtime simulation-by-time-property", ["data", "timeseries", "realtime", "simulation-by-time-property", "--type", "pipe", "--time", at, "--property", "flow"]],
|
|
["data analysis link-field", ["data", "timeseries", "analysis", "link-field", "--run-id", runId, "--link", "P1", "--field", "flow", "--start-time", start, "--end-time", end]],
|
|
["data analysis node-field", ["data", "timeseries", "analysis", "node-field", "--run-id", runId, "--node", "J1", "--field", "pressure", "--start-time", start, "--end-time", end]],
|
|
["data analysis values", ["data", "timeseries", "analysis", "values", "--run-id", runId, "--type", "pipe", "--time", at, "--field", "flow"]],
|
|
["data scada query", ["data", "timeseries", "scada", "query", "--device-id", "D1", "--device-id", "D2", "--start-time", start, "--end-time", end, "--field", "monitored_value"]],
|
|
["data composite scada-simulation", ["data", "timeseries", "composite", "--kind", "scada-simulation", "--feature", "D1", "--feature", "D2", "--start-time", start, "--end-time", end, "--run-id", runId]],
|
|
["data composite element-simulation", ["data", "timeseries", "composite", "--kind", "element-simulation", "--feature", "J1:pressure", "--start-time", start, "--end-time", end]],
|
|
["data composite element-scada", ["data", "timeseries", "composite", "--kind", "element-scada", "--feature", "J1", "--start-time", start, "--end-time", end, "--use-cleaned"]],
|
|
["data pipeline-health", ["data", "pipeline-health", "--time", end]],
|
|
["data scada get", ["data", "scada", "get", "--device-id", "SCADA-001"]],
|
|
["data scada list", ["data", "scada", "list"]],
|
|
["data scada schema", ["data", "scada", "schema"]],
|
|
];
|
|
|
|
for (const [name, args] of cases) {
|
|
const run = await runAgainstServer(name, runCli, args, auth);
|
|
assert.equal(run.exitCode, 0, `${name}: ${run.stderr}`);
|
|
assert.equal(run.payload.ok, true, name);
|
|
assert.equal(run.payload.schema_version, "tjwater-cli/v1", name);
|
|
assert.ok(run.requests.length > 0, `${name}: no backend request`);
|
|
for (const request of run.requests) {
|
|
assert.match(request.path, /^\/api\/v1\//, name);
|
|
assert.equal(request.headers.authorization, "Bearer token", name);
|
|
assert.equal(request.headers["x-project-id"], "project-1", name);
|
|
}
|
|
}
|
|
} finally {
|
|
await rm(tempDir, { force: true, recursive: true });
|
|
}
|
|
});
|