test(cli): remove Python parity dependency
This commit is contained in:
@@ -20,6 +20,7 @@ src/chat/ 聊天流和 SSE 事件适配
|
|||||||
src/runtime/ OpenCode 运行时管理
|
src/runtime/ OpenCode 运行时管理
|
||||||
src/session/ 会话映射和运行上下文
|
src/session/ 会话映射和运行上下文
|
||||||
src/mcp/ MCP 服务与工具桥接
|
src/mcp/ MCP 服务与工具桥接
|
||||||
|
cli/ Agent 使用的 TypeScript 后端 API CLI
|
||||||
.opencode/agents/ Agent prompt 和模型行为配置
|
.opencode/agents/ Agent prompt 和模型行为配置
|
||||||
.opencode/tools/ OpenCode 自定义工具
|
.opencode/tools/ OpenCode 自定义工具
|
||||||
.opencode/skills/ 可复用分析工作流
|
.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` 后会自动重启。
|
`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
|
```bash
|
||||||
|
|||||||
@@ -8,7 +8,59 @@ import { fileURLToPath } from "node:url";
|
|||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
|
||||||
const cliPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../cli/tjwater-cli");
|
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 = {}) {
|
function runCommand(command, args, input, options = {}) {
|
||||||
return new Promise((resolveRun, reject) => {
|
return new Promise((resolveRun, reject) => {
|
||||||
@@ -35,10 +87,6 @@ function runCli(args, input) {
|
|||||||
return runCommand(cliPath, args, input);
|
return runCommand(cliPath, args, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runPythonCli(args, input) {
|
|
||||||
return runCommand("python", ["-m", "tjwater_cli", ...args], input, { cwd: pythonCliCwd });
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseJsonResult(result) {
|
function parseJsonResult(result) {
|
||||||
return JSON.parse(result.stdout);
|
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>");
|
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 () => {
|
test("discovers every visible command and keeps internal commands hidden", async () => {
|
||||||
for (const args of [["help"], ["help", "analysis"]]) {
|
const rootResult = await runCli(["help"]);
|
||||||
const [nodeResult, pythonResult] = await Promise.all([runCli(args), runPythonCli(args)]);
|
assert.equal(rootResult.exitCode, 0, rootResult.stderr);
|
||||||
assert.equal(nodeResult.exitCode, pythonResult.exitCode);
|
assert.deepEqual(
|
||||||
assert.deepEqual(parseJsonResult(nodeResult), parseJsonResult(pythonResult));
|
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([
|
for (const command of hiddenCommandPaths) {
|
||||||
runCli(["help", "simulation", "run"]),
|
const result = await runCli(["help", ...command.split(" ")]);
|
||||||
runPythonCli(["help", "simulation", "run"]),
|
assert.equal(result.exitCode, 0, `${command}: ${result.stderr}`);
|
||||||
]);
|
const payload = parseJsonResult(result);
|
||||||
assert.equal(nodeLeaf.exitCode, pythonLeaf.exitCode);
|
assert.equal(payload.ok, false, command);
|
||||||
const nodePayload = parseJsonResult(nodeLeaf);
|
assert.equal(payload.error.code, "COMMAND_NOT_FOUND", command);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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-"));
|
const tempDir = await mkdtemp(join(tmpdir(), "tjwater-cli-parity-"));
|
||||||
try {
|
try {
|
||||||
const burstFile = join(tempDir, "burst.json");
|
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) {
|
for (const [name, args] of cases) {
|
||||||
const [nodeRun, pythonRun] = await Promise.all([
|
const run = await runAgainstServer(name, runCli, args, auth);
|
||||||
runAgainstServer(`${name} node`, runCli, args, auth),
|
assert.equal(run.exitCode, 0, `${name}: ${run.stderr}`);
|
||||||
runAgainstServer(`${name} python`, runPythonCli, args, auth),
|
assert.equal(run.payload.ok, true, name);
|
||||||
]);
|
assert.equal(run.payload.schema_version, "tjwater-cli/v1", name);
|
||||||
assert.equal(nodeRun.exitCode, pythonRun.exitCode, `${name}: exit\nnode=${nodeRun.stderr}\npython=${pythonRun.stderr}`);
|
assert.ok(run.requests.length > 0, `${name}: no backend request`);
|
||||||
assert.deepEqual(nodeRun.requests, pythonRun.requests, name);
|
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 {
|
} finally {
|
||||||
await rm(tempDir, { force: true, recursive: true });
|
await rm(tempDir, { force: true, recursive: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user