import json from pathlib import Path from typer.testing import CliRunner from tjwater_cli import common, core from tjwater_cli.main import app, main runner = CliRunner() class DummyResponse: def __init__(self, *, status_code=200, json_data=None, text="", headers=None, content=None): self.status_code = status_code self._json_data = json_data self.text = text self.headers = headers or {"content-type": "application/json"} self.content = content if content is not None else text.encode("utf-8") @property def ok(self): return 200 <= self.status_code < 300 def json(self): if self._json_data is None: raise ValueError("no json") return self._json_data def test_load_auth_context_supports_aliases(monkeypatch): monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_PROJECT_ID", "p1") monkeypatch.setenv("TJWATER_USER_ID", "u1") monkeypatch.setenv("TJWATER_USERNAME", "tester") monkeypatch.setenv("TJWATER_NETWORK", "net1") auth = core.load_auth_context(auth_stdin=False) assert auth.server == "http://server" assert auth.access_token == "abc" assert auth.project_id == "p1" assert auth.user_id == "u1" assert auth.username == "tester" assert auth.network == "net1" def test_build_runtime_context_uses_default_server(monkeypatch): monkeypatch.delenv("TJWATER_SERVER", raising=False) monkeypatch.delenv("TJWATER_ACCESS_TOKEN", raising=False) monkeypatch.delenv("TJWATER_PROJECT_ID", raising=False) monkeypatch.delenv("TJWATER_USER_ID", raising=False) monkeypatch.delenv("TJWATER_USERNAME", raising=False) monkeypatch.delenv("TJWATER_NETWORK", raising=False) monkeypatch.delenv("TJWATER_EXTRA_HEADERS", raising=False) runtime = core.build_runtime_context( server=None, scheme=None, timeout=core.DEFAULT_TIMEOUT, request_id="req-1", ) assert runtime.server == core.DEFAULT_SERVER def test_auth_stdin_can_be_reused_with_runtime_context_cache(monkeypatch): observed_runtime_ids: list[int] = [] def fake_request_json(ctx, **kwargs): observed_runtime_ids.append(id(ctx)) assert ctx.auth.access_token == "token-1" assert kwargs["params"] == {"network": "tjwater", "junction": "11"} return {"id": "11"}, 5 monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke( app, ["--auth-stdin", "network", "get-junction-properties", "--junction", "11"], input=json.dumps( { "server": "http://server", "access_token": "token-1", "project_id": "project-1", "network": "tjwater", } ), ) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == {"id": "11"} assert len(observed_runtime_ids) == 1 def test_network_get_junction_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return {"id": "J1"}, 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-junction-properties", "--junction", "J1"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == {"id": "J1"} assert captured == { "access_token": "abc", "path": "/getjunctionproperties/", "params": {"network": "tjwater", "junction": "J1"}, } def test_network_get_pipe_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return {"id": "P1"}, 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-pipe-properties", "--pipe", "P1"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == {"id": "P1"} assert captured == { "access_token": "abc", "path": "/getpipeproperties/", "params": {"network": "tjwater", "pipe": "P1"}, } def test_network_get_all_pipes_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return [{"id": "P1"}], 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-all-pipes-properties"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == [{"id": "P1"}] assert captured == { "access_token": "abc", "path": "/getallpipeproperties/", "params": {"network": "tjwater"}, } def test_network_get_reservoir_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return {"id": "R1"}, 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-reservoir-properties", "--reservoir", "R1"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == {"id": "R1"} assert captured == { "access_token": "abc", "path": "/getreservoirproperties/", "params": {"network": "tjwater", "reservoir": "R1"}, } def test_network_get_all_reservoir_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return [{"id": "R1"}], 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-all-reservoirs-properties"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == [{"id": "R1"}] assert captured == { "access_token": "abc", "path": "/getallreservoirproperties/", "params": {"network": "tjwater"}, } def test_network_get_tank_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return {"id": "T1"}, 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-tank-properties", "--tank", "T1"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == {"id": "T1"} assert captured == { "access_token": "abc", "path": "/gettankproperties/", "params": {"network": "tjwater", "tank": "T1"}, } def test_network_get_all_tank_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return [{"id": "T1"}], 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-all-tanks-properties"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == [{"id": "T1"}] assert captured == { "access_token": "abc", "path": "/getalltankproperties/", "params": {"network": "tjwater"}, } def test_network_get_pump_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return {"id": "PU1"}, 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-pump-properties", "--pump", "PU1"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == {"id": "PU1"} assert captured == { "access_token": "abc", "path": "/getpumpproperties/", "params": {"network": "tjwater", "pump": "PU1"}, } def test_network_get_all_pump_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return [{"id": "PU1"}], 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-all-pumps-properties"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == [{"id": "PU1"}] assert captured == { "access_token": "abc", "path": "/getallpumpproperties/", "params": {"network": "tjwater"}, } def test_network_get_valve_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return {"id": "V1"}, 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-valve-properties", "--valve", "V1"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == {"id": "V1"} assert captured == { "access_token": "abc", "path": "/getvalveproperties/", "params": {"network": "tjwater", "valve": "V1"}, } def test_network_get_all_valve_properties_uses_network_context(monkeypatch): captured = {} def fake_request_json(ctx, **kwargs): captured["access_token"] = ctx.auth.access_token captured["path"] = kwargs["path"] captured["params"] = kwargs["params"] return [{"id": "V1"}], 5 monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "tjwater") monkeypatch.setattr(common, "request_json", fake_request_json) result = runner.invoke(app, ["network", "get-all-valves-properties"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["ok"] is True assert payload["data"] == [{"id": "V1"}] assert captured == { "access_token": "abc", "path": "/getallvalveproperties/", "params": {"network": "tjwater"}, } def test_help_outputs_json_lists_commands(): result = runner.invoke(app, ["help"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["schema_version"] == "tjwater-cli/v1" assert any(command["command"] == "analysis" for command in payload["commands"]) assert all(command["command"] != "project" for command in payload["commands"]) assert payload["menu_level"] == 1 assert all(command["command"] != "project list" for command in payload["commands"]) def test_help_option_json_is_removed(): result = runner.invoke(app, ["help", "--json"]) assert result.exit_code == 2 assert "No such option: --json" in result.output def test_simulation_help_lists_subcommands(): result = runner.invoke(app, ["simulation", "help"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["summary"] == "模拟运行与调度相关命令。" commands = {command["command"]: command for command in payload["commands"]} assert commands["simulation run"]["summary"] == "触发指定绝对时间的模拟运行" assert commands["simulation run"]["usage"] == "tjwater-cli simulation run --start-time --duration " assert "tjwater-cli" in commands["simulation run"]["example"] assert "simulation run" in commands["simulation run"]["example"] def test_nested_group_help_lists_examples(): result = runner.invoke(app, ["analysis", "leakage", "help"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["summary"] == "漏损分析相关命令。" commands = {command["command"]: command for command in payload["commands"]} assert commands["analysis leakage identify"]["summary"] == "执行漏损识别" assert commands["analysis leakage identify"]["example"] == "tjwater-cli analysis leakage identify --start-time 2025-01-02T03:00:00+08:00 --end-time 2025-01-02T04:00:00+08:00 --scheme leak_case_01" def test_analysis_help_uses_group_summaries_for_nested_groups(): result = runner.invoke(app, ["analysis", "help"]) payload = json.loads(result.stdout) commands = {command["command"]: command for command in payload["commands"]} assert result.exit_code == 0 assert commands["analysis leakage"]["summary"] == "漏损分析相关命令。" assert commands["analysis burst-detection"]["summary"] == "爆管检测相关命令。" assert "analysis burst-location" not in commands assert "analysis risk" not in commands assert commands["analysis burst"]["example"] == "tjwater-cli analysis burst --start-time 2025-01-02T03:04:05+08:00 --duration 900 --burst-file ./burst.json --scheme burst_case_01" assert commands["analysis valve"]["example"] == "tjwater-cli analysis valve --mode close --start-time 2025-01-02T03:04:05+08:00 --valve V1 --valve V2 --duration 900 --scheme valve_case_01" def test_bare_analysis_uses_typer_help_with_descriptions(): result = runner.invoke(app, ["analysis"]) assert result.exit_code == 2 assert "分析计算与诊断相关命令。" in result.stdout assert "burst 执行爆管分析" in result.stdout assert "valve" in result.stdout assert "leakage 漏损分析相关命令。" in result.stdout assert "burst-location" not in result.stdout assert "risk" not in result.stdout def test_leaf_help_outputs_json(): result = runner.invoke(app, ["help", "simulation", "run"]) payload = json.loads(result.stdout) assert result.exit_code == 0 assert payload["command"] == "simulation run" assert payload["output"] == "模拟触发结果;实时数据需通过 data timeseries 命令按时间段查询" assert payload["usage"] == "tjwater-cli simulation run --start-time --duration " assert len(payload["examples"]) == 1 assert "simulation run" in payload["examples"][0] def test_root_help_flag_uses_typer_style_with_examples(): result = runner.invoke(app, ["--help"], prog_name="tjwater-cli") assert result.exit_code == 0 assert "Usage: tjwater-cli" in result.stdout assert "Examples:" in result.stdout assert "tjwater-cli help simulation run" in result.stdout def test_leaf_help_flag_includes_usage_and_example(): result = runner.invoke(app, ["simulation", "run", "--help"], prog_name="tjwater-cli") assert result.exit_code == 0 assert "Usage: tjwater-cli simulation run [OPTIONS]" in result.stdout assert "Usage example:" in result.stdout assert "--start-time " in result.stdout assert "--duration" in result.stdout assert "Examples:" in result.stdout assert "tjwater-cli simulation run" in result.stdout assert "START_TIME" in result.stdout assert "DURATION" in result.stdout def test_realtime_simulation_help_clarifies_type_values(): result = runner.invoke( app, ["data", "timeseries", "realtime", "simulation-by-id-time", "--help"], prog_name="tjwater-cli", ) assert result.exit_code == 0 assert "links/nodes 是子命令" in result.stdout assert "pipe" in result.stdout assert "junction" in result.stdout def test_realtime_property_help_lists_supported_fields(): result = runner.invoke( app, ["data", "timeseries", "realtime", "simulation-by-time-property", "--help"], prog_name="tjwater-cli", ) assert result.exit_code == 0 assert "flow" in result.stdout assert "pressure" in result.stdout assert "actual_demand" in result.stdout assert "velocity" in result.stdout def test_analysis_burst_returns_next_step_to_fetch_scheme(monkeypatch, tmp_path: Path): monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "demo") burst_path = tmp_path / "burst.json" burst_path.write_text('[{"id":"P1","size":3.5}]', encoding="utf-8") def fake_request(**kwargs): return DummyResponse(text="success", headers={"content-type": "text/plain"}) monkeypatch.setattr(core.requests, "request", fake_request) result = runner.invoke( app, [ "analysis", "burst", "--start-time", "2025-01-02T03:04:05+08:00", "--duration", "30", "--burst-file", str(burst_path), "--scheme", "burst_case_01", ], ) assert result.exit_code == 0 assert '"summary": "爆管分析执行成功"' in result.stdout assert "tjwater-cli data scheme get --name burst_case_01" in result.stdout assert "tjwater-cli data scheme list" in result.stdout def test_analysis_contaminant_sends_required_scheme_name(monkeypatch): monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "demo") captured = {} def fake_request(**kwargs): captured.update(kwargs) return DummyResponse(text="success", headers={"content-type": "text/plain"}) monkeypatch.setattr(core.requests, "request", fake_request) result = runner.invoke( app, [ "analysis", "contaminant", "--start-time", "2025-01-02T03:04:05+08:00", "--duration", "900", "--source-node", "N1", "--concentration", "10.0", "--scheme", "contam_case_01", ], ) assert result.exit_code == 0 assert captured["params"] == { "network": "demo", "start_time": "2025-01-02T03:04:05+08:00", "source": "N1", "concentration": 10.0, "duration": 900, "scheme_name": "contam_case_01", } def test_analysis_flushing_sends_required_scheme_name(monkeypatch, tmp_path: Path): monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "demo") captured = {} valve_path = tmp_path / "valve.json" valve_path.write_text('[{"valve":"V1","opening":0.5}]', encoding="utf-8") def fake_request(**kwargs): captured.update(kwargs) return DummyResponse(text="success", headers={"content-type": "text/plain"}) monkeypatch.setattr(core.requests, "request", fake_request) result = runner.invoke( app, [ "analysis", "flushing", "--start-time", "2025-01-02T03:04:05+08:00", "--valve-setting-file", str(valve_path), "--drainage-node", "N1", "--flow", "100.0", "--duration", "900", "--scheme", "flush_case_01", ], ) assert result.exit_code == 0 assert captured["params"] == { "network": "demo", "start_time": "2025-01-02T03:04:05+08:00", "valves": ["V1"], "valves_k": [0.5], "drainage_node_ID": "N1", "flush_flow": 100.0, "duration": 900, "scheme_name": "flush_case_01", } def test_analysis_valve_close_sends_required_scheme_name(monkeypatch): monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "demo") captured = {} def fake_request(**kwargs): captured.update(kwargs) return DummyResponse(text="success", headers={"content-type": "text/plain"}) monkeypatch.setattr(core.requests, "request", fake_request) result = runner.invoke( app, [ "analysis", "valve", "--mode", "close", "--start-time", "2025-01-02T03:04:05+08:00", "--valve", "V1", "--duration", "900", "--scheme", "valve_case_01", ], ) assert result.exit_code == 0 assert captured["params"] == { "network": "demo", "start_time": "2025-01-02T03:04:05+08:00", "valves": ["V1"], "duration": 900, "scheme_name": "valve_case_01", } def test_analysis_contaminant_requires_scheme(monkeypatch, capsys): monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "demo") exit_code = main( [ "analysis", "contaminant", "--start-time", "2025-01-02T03:04:05+08:00", "--duration", "900", "--source-node", "N1", "--concentration", "10.0", ], ) stdout = capsys.readouterr().out assert exit_code == 2 assert '"code": "SCHEME_REQUIRED"' in stdout def test_analysis_flushing_requires_scheme(monkeypatch, tmp_path: Path, capsys): monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "demo") valve_path = tmp_path / "valve.json" valve_path.write_text('[{"valve":"V1","opening":0.5}]', encoding="utf-8") exit_code = main( [ "analysis", "flushing", "--start-time", "2025-01-02T03:04:05+08:00", "--valve-setting-file", str(valve_path), "--drainage-node", "N1", "--flow", "100.0", ], ) stdout = capsys.readouterr().out assert exit_code == 2 assert '"code": "SCHEME_REQUIRED"' in stdout def test_analysis_valve_close_requires_scheme(monkeypatch, capsys): monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "demo") exit_code = main( [ "analysis", "valve", "--mode", "close", "--start-time", "2025-01-02T03:04:05+08:00", "--valve", "V1", "--duration", "900", ], ) stdout = capsys.readouterr().out assert exit_code == 2 assert '"code": "SCHEME_REQUIRED"' in stdout def test_main_missing_option_error_includes_usage_and_next_step(capsys): exit_code = main(["simulation", "run"]) stdout = capsys.readouterr().out assert exit_code == 2 assert '"summary": "缺少参数"' in stdout assert '"code": "MISSING_PARAMETER"' in stdout assert '"usage": "tjwater-cli simulation run --start-time --duration "' in stdout assert '"tjwater-cli help simulation run"' in stdout def test_main_invalid_enum_value_is_rejected_before_request(capsys): exit_code = main( [ "data", "timeseries", "realtime", "simulation-by-id-time", "--id", "J1", "--type", "links", "--time", "2025-01-02T03:30:00+08:00", ] ) stdout = capsys.readouterr().out assert exit_code == 2 assert '"summary": "参数无效"' in stdout assert '"code": "INVALID_PARAMETER"' in stdout assert "links" in stdout assert "pipe" in stdout assert "junction" in stdout def test_main_invalid_pipe_property_is_rejected_before_request(capsys): exit_code = main( [ "data", "timeseries", "realtime", "simulation-by-time-property", "--type", "pipe", "--time", "2025-01-02T03:30:00+08:00", "--property", "pressure", ] ) stdout = capsys.readouterr().out assert exit_code == 2 assert '"code": "INVALID_PROPERTY"' in stdout assert "flow" in stdout assert "velocity" in stdout def test_main_invalid_scada_field_is_rejected_before_request(capsys): exit_code = main( [ "data", "timeseries", "scada", "query", "--device-id", "D1", "--start-time", "2025-01-02T03:00:00+08:00", "--end-time", "2025-01-02T04:00:00+08:00", "--field", "flow", ] ) stdout = capsys.readouterr().out assert exit_code == 2 assert '"code": "INVALID_FIELD"' in stdout assert "monitored_value" in stdout assert "cleaned_value" in stdout def test_data_scada_get_rejects_removed_kind_before_request(capsys): exit_code = main(["data", "scada", "get", "--kind", "device", "--id", "D1"]) stdout = capsys.readouterr().out assert exit_code == 2 assert '"code": "INVALID_PARAMETER"' in stdout assert "device" in stdout assert "info" in stdout def test_data_scada_list_help_only_shows_info_kind(): result = runner.invoke(app, ["data", "scada", "list", "--help"]) assert result.exit_code == 0 assert "info" in result.stdout assert "device" not in result.stdout assert "element" not in result.stdout def test_data_scada_help_no_longer_lists_schema(): result = runner.invoke(app, ["data", "scada", "help"]) payload = json.loads(result.stdout) assert result.exit_code == 0 commands = {command["command"] for command in payload["commands"]} assert "data scada get" in commands assert "data scada list" in commands assert "data scada schema" not in commands def test_data_scada_schema_command_is_removed(): result = runner.invoke(app, ["data", "scada", "schema", "--kind", "info"]) assert result.exit_code == 2 assert "No such command 'schema'" in result.output def test_data_help_no_longer_lists_extension_or_misc(): result = runner.invoke(app, ["data", "help"]) payload = json.loads(result.stdout) assert result.exit_code == 0 commands = {command["command"] for command in payload["commands"]} assert "data timeseries" in commands assert "data scada" in commands assert "data scheme" in commands assert "data extension" not in commands assert "data misc" not in commands def test_removed_data_extension_and_misc_commands_fail(): extension_result = runner.invoke(app, ["data", "extension", "list"]) misc_result = runner.invoke(app, ["data", "misc", "sensor-placements"]) assert extension_result.exit_code == 2 assert "No such command 'extension'" in extension_result.output assert misc_result.exit_code == 2 assert "No such command 'misc'" in misc_result.output def test_main_bare_analysis_returns_typer_help_without_json_error(capsys): exit_code = main(["analysis"]) stdout = capsys.readouterr().out assert exit_code == 0 assert "Usage: tjwater-cli analysis" in stdout assert "分析计算与诊断相关命令。" in stdout assert '"ok": false' not in stdout def test_simulation_run_translates_rfc3339(monkeypatch): monkeypatch.setenv("TJWATER_SERVER", "http://server") monkeypatch.setenv("TJWATER_ACCESS_TOKEN", "abc") monkeypatch.setenv("TJWATER_NETWORK", "demo") captured = {} def fake_request(**kwargs): captured.update(kwargs) return DummyResponse(json_data={"status": "success", "message": "Simulation started"}) monkeypatch.setattr(core.requests, "request", fake_request) result = runner.invoke( app, [ "simulation", "run", "--start-time", "2025-01-02T03:04:05+08:00", "--duration", "30", ], ) assert result.exit_code == 0 assert captured["json"] == { "name": "demo", "start_time": "2025-01-02T03:04:05+08:00", "duration": 30, } assert "tjwater-cli data timeseries realtime links" in result.stdout assert "tjwater-cli data timeseries realtime nodes" in result.stdout def test_removed_project_command_returns_not_found(capsys): exit_code = main(["project", "list"]) stdout = capsys.readouterr().out assert exit_code == 2 assert '"code": "COMMAND_NOT_FOUND"' in stdout or "No such command: project" in stdout