From a5e91ac2b8851776809f12cdcc758d8f1648cf59 Mon Sep 17 00:00:00 2001 From: Huarch Date: Thu, 6 Aug 2026 15:58:47 +0800 Subject: [PATCH] test(cli): remove Python parity dependency --- README.md | 5 + node-tests/cli/tjwaterCli.node.mjs | 165 +++++++++++++++-------------- 2 files changed, 92 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 453e6aa..ee57613 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ src/chat/ 聊天流和 SSE 事件适配 src/runtime/ OpenCode 运行时管理 src/session/ 会话映射和运行上下文 src/mcp/ MCP 服务与工具桥接 +cli/ Agent 使用的 TypeScript 后端 API CLI .opencode/agents/ Agent prompt 和模型行为配置 .opencode/tools/ OpenCode 自定义工具 .opencode/skills/ 可复用分析工作流 @@ -39,6 +40,10 @@ bun run dev `bun install` 会通过 `postinstall` 安装 `.opencode` 子目录依赖。`bun run dev` 以 watch 模式启动 `src/server.ts`,修改 `src/**`、`.opencode/**`、`opencode.json` 或 `.local.env` 后会自动重启。 +`cli/tjwater-cli` 是当前唯一的 TJWater 业务 CLI 入口,由 Bun 直接执行 +`cli/tjwater-cli.ts` 及 `cli/src/` 源码,并随 Agent 镜像一起交付,不需要 +Python 或 PyInstaller 构建步骤。 + ## 常用命令 ```bash diff --git a/node-tests/cli/tjwaterCli.node.mjs b/node-tests/cli/tjwaterCli.node.mjs index e04470e..530fb42 100644 --- a/node-tests/cli/tjwaterCli.node.mjs +++ b/node-tests/cli/tjwaterCli.node.mjs @@ -8,7 +8,59 @@ import { fileURLToPath } from "node:url"; import { dirname, join, resolve } from "node:path"; const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../cli/tjwater-cli"); -const pythonCliCwd = resolve(dirname(fileURLToPath(import.meta.url)), "../../../TJWaterServerBinary/cli"); + +const visibleCommandPaths = [ + "analysis age", + "analysis burst", + "analysis burst-detection detect", + "analysis burst-detection schemes get", + "analysis burst-detection schemes list", + "analysis contaminant", + "analysis flushing", + "analysis leakage identify", + "analysis leakage schemes get", + "analysis leakage schemes list", + "analysis sensor-placement kmeans", + "analysis valve", + "component option get", + "component option schema", + "data scada get", + "data scada list", + "data scheme get", + "data scheme list", + "data scheme schema", + "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", + "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 = [ + "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", +]; function runCommand(command, args, input, options = {}) { return new Promise((resolveRun, reject) => { @@ -35,10 +87,6 @@ function runCli(args, input) { return runCommand(cliPath, args, input); } -function runPythonCli(args, input) { - return runCommand("python", ["-m", "tjwater_cli", ...args], input, { cwd: pythonCliCwd }); -} - function parseJsonResult(result) { return JSON.parse(result.stdout); } @@ -118,74 +166,31 @@ test("emits structured JSON help compatible with tjwater-cli/v1", async () => { assert.equal(payload.usage, "tjwater-cli simulation run --start-time --duration "); }); -test("matches Python CLI help discovery and hidden command behavior", async () => { - for (const args of [["help"], ["help", "analysis"]]) { - const [nodeResult, pythonResult] = await Promise.all([runCli(args), runPythonCli(args)]); - assert.equal(nodeResult.exitCode, pythonResult.exitCode); - assert.deepEqual(parseJsonResult(nodeResult), parseJsonResult(pythonResult)); +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`); } - const [nodeLeaf, pythonLeaf] = await Promise.all([ - runCli(["help", "simulation", "run"]), - runPythonCli(["help", "simulation", "run"]), - ]); - assert.equal(nodeLeaf.exitCode, pythonLeaf.exitCode); - const nodePayload = parseJsonResult(nodeLeaf); - const pythonPayload = parseJsonResult(pythonLeaf); - assert.equal(nodePayload.ok, pythonPayload.ok); - assert.equal(nodePayload.schema_version, pythonPayload.schema_version); - assert.equal(nodePayload.command, pythonPayload.command); - assert.equal(nodePayload.summary, pythonPayload.summary); - assert.equal(nodePayload.usage, pythonPayload.usage); - assert.deepEqual(nodePayload.options.map(({ name, required, repeated }) => ({ name, required, repeated })), pythonPayload.options.map(({ name, required, repeated }) => ({ name, required, repeated }))); - assert.deepEqual(nodePayload.examples, pythonPayload.examples); - assert.deepEqual(nodePayload.next_commands, pythonPayload.next_commands); - - const [nodeHidden, pythonHidden] = await Promise.all([ - runCli(["help", "analysis", "risk"]), - runPythonCli(["help", "analysis", "risk"]), - ]); - assert.equal(nodeHidden.exitCode, pythonHidden.exitCode); - const nodeError = parseJsonResult(nodeHidden); - const pythonError = parseJsonResult(pythonHidden); - delete nodeError.metadata.generated_at; - delete pythonError.metadata.generated_at; - assert.deepEqual(nodeError, pythonError); -}); - -test("matches Python CLI leaf help for every visible command", async () => { - const listResult = await runCommand( - "python", - [ - "-c", - "from tjwater_cli.registry import COMMAND_DOCS, is_hidden_path\nimport json\nprint(json.dumps([' '.join(path) for path in COMMAND_DOCS if not is_hidden_path(path)], ensure_ascii=False))", - ], - undefined, - { cwd: pythonCliCwd }, - ); - assert.equal(listResult.exitCode, 0, listResult.stderr); - const commands = JSON.parse(listResult.stdout); - - for (const command of commands) { - const args = ["help", ...command.split(" ")]; - const [nodeResult, pythonResult] = await Promise.all([runCli(args), runPythonCli(args)]); - assert.equal(nodeResult.exitCode, pythonResult.exitCode, command); - - const nodePayload = parseJsonResult(nodeResult); - const pythonPayload = parseJsonResult(pythonResult); - const comparable = (payload) => ({ - command: payload.command, - summary: payload.summary, - usage: payload.usage, - examples: payload.examples, - next_commands: payload.next_commands, - options: (payload.options ?? []).map(({ name, required, repeated }) => ({ - name, - required, - repeated, - })), - }); - assert.deepEqual(comparable(nodePayload), comparable(pythonPayload), command); + 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); } }); @@ -282,7 +287,7 @@ test("maps CLI pipe and junction types to backend link and node types", async () } }); -test("matches Python CLI backend request shape for every command and key variants", async () => { +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"); @@ -359,12 +364,16 @@ test("matches Python CLI backend request shape for every command and key variant ]; for (const [name, args] of cases) { - const [nodeRun, pythonRun] = await Promise.all([ - runAgainstServer(`${name} node`, runCli, args, auth), - runAgainstServer(`${name} python`, runPythonCli, args, auth), - ]); - assert.equal(nodeRun.exitCode, pythonRun.exitCode, `${name}: exit\nnode=${nodeRun.stderr}\npython=${pythonRun.stderr}`); - assert.deepEqual(nodeRun.requests, pythonRun.requests, name); + 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 });