refactor(cli)!: align commands with analysis run APIs
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.
This commit is contained in:
@@ -13,32 +13,32 @@ const visibleCommandPaths = [
|
||||
"analysis age",
|
||||
"analysis burst",
|
||||
"analysis burst-detection detect",
|
||||
"analysis burst-detection schemes get",
|
||||
"analysis burst-detection schemes list",
|
||||
"analysis burst-location locate",
|
||||
"analysis contaminant",
|
||||
"analysis flushing",
|
||||
"analysis leakage identify",
|
||||
"analysis leakage schemes get",
|
||||
"analysis leakage schemes list",
|
||||
"analysis sensor-placement kmeans",
|
||||
"analysis valve",
|
||||
"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 scheme get",
|
||||
"data scheme list",
|
||||
"data scheme schema",
|
||||
"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 composite pipeline-health",
|
||||
"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",
|
||||
"data timeseries scheme links",
|
||||
"data timeseries scheme node-field",
|
||||
"data timeseries scheme simulation",
|
||||
"network get-all-pipes-properties",
|
||||
"network get-all-pumps-properties",
|
||||
"network get-all-reservoirs-properties",
|
||||
@@ -53,14 +53,7 @@ const visibleCommandPaths = [
|
||||
"simulation run",
|
||||
];
|
||||
|
||||
const hiddenCommandPaths = [
|
||||
"analysis burst-location locate",
|
||||
"analysis burst-location schemes get",
|
||||
"analysis burst-location schemes list",
|
||||
"analysis risk network",
|
||||
"analysis risk pipe-history",
|
||||
"analysis risk pipe-now",
|
||||
];
|
||||
const hiddenCommandPaths = [];
|
||||
|
||||
function runCommand(command, args, input, options = {}) {
|
||||
return new Promise((resolveRun, reject) => {
|
||||
@@ -144,7 +137,7 @@ function normalizeSeenRequest(request) {
|
||||
|
||||
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"].includes(url.pathname)) {
|
||||
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")),
|
||||
@@ -338,18 +331,86 @@ test("maps CLI pipe and junction types to backend link and node types", async ()
|
||||
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", "scheme", "simulation", "--query", "by-id-time", "--id", "P1", "--type", "pipe", "--time", at, "--scheme", "scheme_case"],
|
||||
["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 queries = server.seen.map(normalizeSeenRequest).map((request) => request.query.type);
|
||||
assert.deepEqual(queries, ["node", "link", "link"]);
|
||||
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 {
|
||||
@@ -372,6 +433,7 @@ test("executes every command and key variant against the backend contract", asyn
|
||||
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"]],
|
||||
@@ -388,42 +450,34 @@ test("executes every command and key variant against the backend contract", asyn
|
||||
["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 close", ["analysis", "valve", "--mode", "close", "--start-time", start, "--valve", "V1", "--valve", "V2", "--duration", "900", "--scheme", "valve_case"]],
|
||||
["analysis valve isolation", ["analysis", "valve", "--mode", "isolation", "--element", "E1", "--disabled-valve", "V3"]],
|
||||
["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 kmeans", ["analysis", "sensor-placement", "kmeans", "--count", "5", "--min-diameter", "100", "--scheme", "place_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 leakage schemes list", ["analysis", "leakage", "schemes", "list"]],
|
||||
["analysis leakage schemes get", ["analysis", "leakage", "schemes", "get", "leak_case"]],
|
||||
["analysis burst-detection detect", ["analysis", "burst-detection", "detect", "--start-time", start, "--end-time", end, "--scheme", "detect_case"]],
|
||||
["analysis burst-detection schemes list", ["analysis", "burst-detection", "schemes", "list"]],
|
||||
["analysis burst-detection schemes get", ["analysis", "burst-detection", "schemes", "get", "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"]],
|
||||
["analysis burst-location schemes list", ["analysis", "burst-location", "schemes", "list"]],
|
||||
["analysis burst-location schemes get", ["analysis", "burst-location", "schemes", "get", "locate_case"]],
|
||||
["analysis risk pipe-now", ["analysis", "risk", "pipe-now", "--pipe", "P1"]],
|
||||
["analysis risk pipe-history", ["analysis", "risk", "pipe-history", "--pipe", "P1"]],
|
||||
["analysis risk network", ["analysis", "risk", "network"]],
|
||||
["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 scheme links", ["data", "timeseries", "scheme", "links", "--start-time", start, "--end-time", end, "--scheme", "scheme_case", "--scheme-type", "simulation"]],
|
||||
["data scheme node-field", ["data", "timeseries", "scheme", "node-field", "--node", "J1", "--field", "pressure", "--start-time", start, "--end-time", end, "--scheme", "scheme_case"]],
|
||||
["data scheme simulation by-id", ["data", "timeseries", "scheme", "simulation", "--query", "by-id-time", "--id", "J1", "--time", at, "--type", "junction", "--scheme", "scheme_case"]],
|
||||
["data scheme simulation by-property", ["data", "timeseries", "scheme", "simulation", "--query", "by-scheme-time-property", "--time", at, "--type", "pipe", "--property", "flow", "--scheme", "scheme_case"]],
|
||||
["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, "--scheme", "scheme_case"]],
|
||||
["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 composite pipeline-health", ["data", "timeseries", "composite", "pipeline-health", "--pipe", "P1", "--start-time", start, "--end-time", end]],
|
||||
["data scada get", ["data", "scada", "get", "--kind", "info", "--id", "SCADA-001"]],
|
||||
["data scada list", ["data", "scada", "list", "--kind", "info"]],
|
||||
["data scheme schema", ["data", "scheme", "schema"]],
|
||||
["data scheme get", ["data", "scheme", "get", "--name", "scheme_case"]],
|
||||
["data scheme list", ["data", "scheme", "list"]],
|
||||
["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) {
|
||||
|
||||
Reference in New Issue
Block a user