test(cli): remove Python parity dependency

This commit is contained in:
2026-08-06 15:58:47 +08:00
parent 258f4996eb
commit a5e91ac2b8
2 changed files with 92 additions and 78 deletions
+5
View File
@@ -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
+87 -78
View File
@@ -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 <START_TIME> --duration <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 });