From be1c321cd86255eb45f6e83075e04be8bb415200 Mon Sep 17 00:00:00 2001 From: yunyaozhou Date: Sat, 8 Aug 2026 07:19:51 +0800 Subject: [PATCH] feat: show canonical account source and groups --- README.md | 2 +- pyproject.toml | 2 +- sub2api_quota_tui.py | 131 +++++++++++++++++++++++++++++-------- tests/test_payload.py | 146 +++++++++++++++++++++++++++++------------- uv.lock | 2 +- 5 files changed, 210 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 22ea818..db7e706 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ refresh。组件失败时服务端保留对应 last-good 数据,并在 `compon Workspace 当前提供以下有界 projection: -- `accounts`:按完整 `(account_id, account_name)` 对齐 server6/server4 fresh binding 和 scheduled-test plan 的账号摘要、quota window 与当日用量;`active_state`、`schedulable_state` 是 `yes` / `partial` / `no`,`last_test_state` 另可为 `NA`。每个节点先选择自身最新的有效测试结果,聚合 `last_test_time` / `last_test_model` 取两者中较早的一次;详情保留脱敏 `node_status` 用于定位 partial。当前 server6 upstream key-group binding 精确关联时仍包含 `account_rate_multiplier` 原始有效倍率和 `account_rate_multiplier_cny` 展示倍率。 +- `accounts`:按完整 `(account_id, account_name)` 对齐 server6/server4 fresh binding 和 scheduled-test plan 的账号摘要、quota window 与当日用量;`active_state`、`schedulable_state` 是 `yes` / `partial` / `no`,`last_test_state` 另可为 `NA`。每个节点先选择自身最新的有效测试结果,聚合 `last_test_time` / `last_test_model` 取两者中较早的一次;详情保留脱敏 `node_status` 用于定位 partial。当前 server6 upstream key-group binding 精确关联时仍包含 `account_rate_multiplier` 原始有效倍率和 `account_rate_multiplier_cny` 展示倍率,并输出 canonical `pricing_source` / `pricing_collector_source`、对应 source 的 `pricing_balance_cny`、native `account_group_names` 和 `pricing_last_fetch_at`。客户端账户表的最后一列显示去重后的全部 native group 名称,`Last fetch` 显示 key/rate binding collector 时间;source、group 或 fetch 不可用时保持 `-` / failure 状态。 - `status`:channel monitor 摘要。 - `sources`:Pricing Monitor 已有的脱敏 source 余额与健康状态。 - `traffic.requests`:仅 server6 的最新有界请求样本;每行带对应 account 的当前 `account_rate_multiplier` / `account_rate_multiplier_cny`,无法稳定关联时为 `null`。 diff --git a/pyproject.toml b/pyproject.toml index 77a835a..8ccb6d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "shusub2" -version = "0.3.6" +version = "0.3.7" description = "Aggregated operations TUI for Sub2API" readme = "README.md" requires-python = ">=3.11" diff --git a/sub2api_quota_tui.py b/sub2api_quota_tui.py index 6dc4765..7223a9d 100644 --- a/sub2api_quota_tui.py +++ b/sub2api_quota_tui.py @@ -27,7 +27,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError APP_NAME = "shusub2" -FALLBACK_VERSION = "0.3.6" +FALLBACK_VERSION = "0.3.7" DEFAULT_WORKSPACE_URL = "https://price.tailbeb9ad.ts.net/api/ui-data?view=workspace" DEFAULT_WORKSPACE_URL_CONFIG_FILE = "~/.config/shusub2/workspace-url" DEFAULT_API_URL = "http://127.0.0.1:18318/api/tui/accounts" @@ -99,7 +99,7 @@ MONITOR_OK_STATUSES = {"operational", "ok", "success"} MONITOR_FAILED_STATUSES = {"error", "failed", "failure"} MONITOR_STOPWORDS = {"response", "responses", "monitor"} PRICING_ACCOUNT_SOURCE_RE = re.compile( - r"^[a-z0-9]+-quota(?:only)?-(?P[a-z0-9]+(?:-[a-z0-9]+)*)$", + r"^[a-z0-9]+-quota-(?P[a-z0-9]+)-(?P[a-z0-9_]+)$", re.IGNORECASE, ) ERROR_DISPLAY_LABEL_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9 ._/+\-]{0,95}$") @@ -672,6 +672,26 @@ def workspace_account_contract_is_valid(rows: list[dict[str, Any]]) -> bool: return False if str(row.get("last_test_state") or "") not in {"yes", "partial", "no", "NA"}: return False + for field in ( + "pricing_source", + "pricing_collector_source", + "pricing_source_kind", + "pricing_fetch_status", + ): + if field in row and not isinstance(row.get(field), str): + return False + group_names = row.get("account_group_names") + if group_names is not None and ( + not isinstance(group_names, list) + or len(group_names) > 16 + or any(not isinstance(name, str) for name in group_names) + ): + return False + fetch_at = row.get("pricing_last_fetch_at") + if fetch_at not in (None, "") and ( + not isinstance(fetch_at, str) or parse_time(fetch_at) is None + ): + return False if not isinstance(row.get("last_test_time"), str) or not isinstance( row.get("last_test_model"), str ): @@ -1783,14 +1803,24 @@ def normalize_account_rows( for account in payload.get("accounts") or []: if not isinstance(account, dict): continue - pricing_source = pricing_source_for_account(account.get("name"), pricing_sources) + explicit_source_name = str( + account.get("pricing_source") or account.get("pricing_collector_source") or "" + ).strip() + pricing_source = ( + pricing_sources.get(pricing_source_key(explicit_source_name)) + if explicit_source_name + else pricing_source_for_account(account.get("name"), pricing_sources) + ) + groups = account_group_display(account) haystack = " ".join( str(account.get(key) or "") for key in ( "id", "name", "routing_group", + "account_group_names", "provider", + "pricing_source", "kind", "status", "account_type", @@ -1809,10 +1839,27 @@ def normalize_account_rows( "id": as_int(account.get("id")), "name": str(account.get("name") or ""), "routing_group": routing_group, + "groups": groups, "base_url_hash": str(account.get("base_url_hash") or ""), - "pricing_source": str(pricing_source.get("name") or "-") if pricing_source else "-", - "pricing_cny": pricing_source.get("balance_cny") if pricing_source and pricing_source.get("status") == "healthy" else None, - "pricing_state": str(pricing_source.get("status") or "-") if pricing_source else "-", + "pricing_source": ( + explicit_source_name + or (str(pricing_source.get("name") or "-") if pricing_source else "-") + ), + "pricing_cny": ( + optional_number(account.get("pricing_balance_cny")) + if explicit_source_name and "pricing_balance_cny" in account + else ( + pricing_source.get("balance_cny") + if pricing_source and pricing_source.get("status") == "healthy" + else None + ) + ), + "pricing_state": ( + str(account.get("pricing_fetch_status") or "-") + if explicit_source_name + else (str(pricing_source.get("status") or "-") if pricing_source else "-") + ), + "pricing_fetch": account_pricing_fetch_label(account, pricing_source), "pricing_balance": pricing_source.get("balance") if pricing_source else None, "pricing_unit": str(pricing_source.get("unit") or "-") if pricing_source else "-", "pricing_updated": str(pricing_source.get("updated") or "-") if pricing_source else "-", @@ -1905,7 +1952,7 @@ def normalize_pricing_rows(payload: dict[str, Any], filter_text: str = "") -> li def pricing_source_key(value: Any) -> str: - return re.sub(r"[^a-z0-9]+", "", str(value or "").strip().casefold()) + return str(value or "").strip().casefold() def pricing_source_lookup(payload: dict[str, Any]) -> dict[str, dict[str, Any] | None]: @@ -1930,6 +1977,32 @@ def pricing_source_for_account( return pricing_sources.get(pricing_source_key(match.group("source"))) +def account_group_display(account: dict[str, Any]) -> str: + raw_names = account.get("account_group_names") + if isinstance(raw_names, list): + names: list[str] = [] + for raw_name in raw_names[:16]: + name = str(raw_name or "").strip() + if name and name not in names: + names.append(name) + if names: + return "、".join(names) + fallback = str(account.get("routing_group") or "").strip() + return fallback or "-" + + +def account_pricing_fetch_label( + account: dict[str, Any], pricing_source: dict[str, Any] | None +) -> str: + fetched = short_time(account.get("pricing_last_fetch_at")) + status = str(account.get("pricing_fetch_status") or "").strip() + if fetched == "-": + return str(pricing_source.get("updated") or "-") if pricing_source else (status or "-") + if status in {"", "matched", "reused_primary", "ok"}: + return fetched + return f"{fetched} {status}" + + def pricing_summary(payload: dict[str, Any], rows: list[dict[str, Any]] | None = None) -> str: source_rows = rows if rows is not None else normalize_pricing_rows(payload) healthy = sum(row["status"] == "healthy" for row in source_rows) @@ -1961,9 +2034,10 @@ def account_pricing_detail(row: dict[str, Any]) -> str: cny = format_cny(row.get("pricing_cny")) raw_balance = format_amount(row.get("pricing_balance")) unit = str(row.get("pricing_unit") or "-") - state = str(row.get("pricing_state") or "-") - updated = str(row.get("pricing_updated") or "-") - return f"source {source} | CNY {cny} | raw {raw_balance} {unit} | {state} | updated {updated}" + return ( + f"source {source} | CNY {cny} | raw {raw_balance} {unit} | " + f"last fetch {row.get('pricing_fetch') or '-'}" + ) def print_pricing_once(payload: dict[str, Any], filter_text: str = "") -> None: @@ -2011,16 +2085,15 @@ def print_once( print(f"sources: {pricing_error}") elif pricing_payload: print(pricing_summary(pricing_payload)) - print("name provider group source multiplier src cny src state daily today tokens req kind 5h 7d reset status active schedulable last-test last-test-time last-test-model availability") + print("name provider source multiplier src cny last fetch daily today tokens req kind 5h 7d reset status active schedulable last-test last-test-time last-test-model availability groups") for row in normalize_account_rows(payload, filter_text, pricing_payload): print( f"{row['name'][:30]:<30} " f"{row['provider']:<9} " - f"{row['routing_group']:<6} " f"{row['pricing_source']:<16} " f"{format_multiplier(row['account_multiplier']):<11} " f"{format_cny(row['pricing_cny']):<13} " - f"{row['pricing_state']:<12} " + f"{row['pricing_fetch']:<21} " f"{row['daily_quota_cell']:<10} " f"{format_cost(row['today_cost_usd']):<10} " f"{format_count(row['today_tokens']):<8} " @@ -2035,7 +2108,8 @@ def print_once( f"{row['last_test']:<10} " f"{row['last_test_time']:<13} " f"{row['last_test_model'][:23]:<23} " - f"{monitor_availability(row, status_payload or {})}" + f"{monitor_availability(row, status_payload or {}):<12} " + f"{row['groups']:<28}" ) @@ -2167,16 +2241,16 @@ def run_textual( # Content-sized columns preserve complete values on wide terminals; # narrow terminals use DataTable's horizontal scrolling instead. for label in columns: - table.add_column(label) + table.add_column(label, width=32 if label == "Groups" else None) def on_mount(self) -> None: self.app.sub_title = "Dashboard" account_columns = ( - "ACCOUNT", "Group", "Source", "Multiplier", "Src CNY", "Src state", "Today", "Daily", "5h", "7d" + "ACCOUNT", "Source", "Multiplier", "Src CNY", "Last fetch", "Today", "Daily", "5h", "7d" ) if not legacy_direct: account_columns += ("Active", "Schedulable", "Last test", "Last test time", "Last test model") - account_columns += ("Avail",) + account_columns += ("Avail", "Groups") self.configure_table( self.query_one("#accounts", DataTable), account_columns, @@ -2341,11 +2415,10 @@ def run_textual( self.account_by_key[key] = row cells: list[Any] = [ row["name"], - row["routing_group"], row["pricing_source"], format_multiplier(row["account_multiplier"]), format_cny(row["pricing_cny"]), - row["pricing_state"], + row["pricing_fetch"], format_cost(row["today_cost_usd"]), row["daily_quota_cell"], row["five_hour"], @@ -2362,6 +2435,7 @@ def run_textual( ) ) cells.append(monitor_availability(row, self.status_payload)) + cells.append(row["groups"]) table.add_row(*cells, key=key) def render_keys(self) -> None: @@ -2531,7 +2605,7 @@ def run_textual( def render_detail(self, table_id: str, row: dict[str, Any]) -> None: if table_id == "accounts": detail = ( - f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | " + f"{row['name']} | {row['provider']} | groups {row['groups']} | {row['kind_label']} | " f"multiplier {format_multiplier(row['account_multiplier'])} | " f"daily {row['daily_quota_cell']} | " f"5h {row['five_hour']} | " @@ -2618,12 +2692,13 @@ def run_textual( table.cursor_type = "row" table.zebra_stripes = True account_columns = [ - "Name", "Provider", "Group", "Source", "Multiplier", "Src CNY", "Src state", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status" + "Name", "Provider", "Source", "Multiplier", "Src CNY", "Last fetch", "Daily", "Today", "Tokens", "Req", "Kind", "5h", "7d", "Reset", "Status" ] if not legacy_direct: account_columns.extend(("Active", "Schedulable", "Last test", "Last test time", "Last test model")) - account_columns.append("Availability") - table.add_columns(*account_columns) + account_columns.extend(("Availability", "Groups")) + for label in account_columns: + table.add_column(label, width=32 if label == "Groups" else None) keys_table = self.query_one("#keys", DataTable) keys_table.cursor_type = "row" keys_table.zebra_stripes = True @@ -2719,11 +2794,10 @@ def run_textual( cells: list[Any] = [ row["name"], row["provider"], - row["routing_group"], row["pricing_source"], format_multiplier(row["account_multiplier"]), format_cny(row["pricing_cny"]), - row["pricing_state"], + row["pricing_fetch"], row["daily_quota_cell"], format_cost(row["today_cost_usd"]), format_count(row["today_tokens"]), @@ -2744,7 +2818,10 @@ def run_textual( row["last_test_model"], ) ) - cells.append(monitor_availability(row, self.status_payload)) + cells.extend(( + monitor_availability(row, self.status_payload), + row["groups"], + )) table.add_row(*cells, key=key) self.query_one("#summary", Static).update(summary_line(self.payload)) if self.rows: @@ -2761,7 +2838,7 @@ def run_textual( def render_detail(self, row: dict[str, Any]) -> None: raw = row.get("raw") if isinstance(row.get("raw"), dict) else {} detail = ( - f"{row['name']} | {row['provider']} | group {row['routing_group']} | {row['kind_label']} | {row['account_type']} | " + f"{row['name']} | {row['provider']} | groups {row['groups']} | {row['kind_label']} | {row['account_type']} | " f"multiplier {format_multiplier(row['account_multiplier'])} | " f"daily {row['daily_quota_cell']} ({format_percent(row['daily_quota_used_percent'])} used, " f"{row['daily_quota_remaining'] if row['daily_quota_remaining'] is not None else '-'} left) | " diff --git a/tests/test_payload.py b/tests/test_payload.py index 270664f..7413bdc 100644 --- a/tests/test_payload.py +++ b/tests/test_payload.py @@ -43,7 +43,7 @@ def workspace_payload_fixture() -> dict[str, object]: }, "sources": [ { - "name": "code-plan", + "name": "codeplan", "source_kind": "newapi", "health_state": "healthy", "balance_available": True, @@ -62,7 +62,7 @@ def workspace_payload_fixture() -> dict[str, object]: "accounts": [ { "id": "9007199254740993", - "name": "oai-quota-code-plan", + "name": "oai-quota-codeplan-primary", "routing_group": "fast", "provider": "openai", "kind": "quota_limited", @@ -98,7 +98,7 @@ def workspace_payload_fixture() -> dict[str, object]: "items": [ { "id": "9007199254740995", - "name": "oai-quota-code-plan", + "name": "oai-quota-codeplan-primary", "provider": "openai", "latest_status": "success", } @@ -127,7 +127,7 @@ def workspace_payload_fixture() -> dict[str, object]: "api_key_id": "7", "api_key_name": "wmy", "account_id": "9", - "account_name": "oai-quota-code-plan", + "account_name": "oai-quota-codeplan-primary", "account_rate_multiplier": 0.06, "account_rate_multiplier_cny": 0.003, "model": "gpt-5.5", @@ -149,7 +149,7 @@ def workspace_payload_fixture() -> dict[str, object]: "api_key_id": "7", "api_key_name": "wmy", "account_id": "9", - "account_name": "oai-quota-code-plan", + "account_name": "oai-quota-codeplan-primary", "model": "gpt-5.5", "error_type": "api_error", "error_source": "upstream_http", @@ -245,19 +245,19 @@ class Sub2APIQuotaTUITests(unittest.TestCase): mod = load_module() accounts_payload = { "accounts": [ - {"id": 1, "name": "oai-quota-code-plan", "routing_group": "fast", "kind": "quota_limited"}, - {"id": 2, "name": "oai-quotaonly-code-plan", "routing_group": "fast", "kind": "quota_limited"}, - {"id": 3, "name": "oai-sub-code-plan", "routing_group": "fast", "kind": "quota_limited"}, + {"id": 1, "name": "oai-quota-codeplan-primary", "routing_group": "fast", "kind": "quota_limited"}, + {"id": 2, "name": "ordinary-account", "routing_group": "fast", "kind": "quota_limited"}, + {"id": 3, "name": "oai-sub-codeplan-primary", "routing_group": "fast", "kind": "quota_limited"}, {"id": 4, "name": "oai-quota-unknown", "routing_group": "fast", "kind": "quota_limited"}, - {"id": 5, "name": "oai-quota-ycy", "routing_group": "fast", "kind": "quota_limited"}, - {"id": 6, "name": "oai-quota-code--plan", "routing_group": "fast", "kind": "quota_limited"}, - {"id": 7, "name": "oai-quota-code-plan-", "routing_group": "fast", "kind": "quota_limited"}, + {"id": 5, "name": "oai-quota-ycy-primary", "routing_group": "fast", "kind": "quota_limited"}, + {"id": 6, "name": "oai-quota-codeplan--primary", "routing_group": "fast", "kind": "quota_limited"}, + {"id": 7, "name": "oai-quota-codeplan-primary-", "routing_group": "fast", "kind": "quota_limited"}, ] } pricing_payload = { "sources": [ { - "name": "code-plan", + "name": "codeplan", "source_kind": "newapi", "health_state": "healthy", "last_success_at": "2026-08-02T12:00:00Z", @@ -275,20 +275,72 @@ class Sub2APIQuotaTUITests(unittest.TestCase): rows = {row["name"]: row for row in mod.normalize_account_rows(accounts_payload, pricing_payload=pricing_payload)} - self.assertEqual(rows["oai-quota-code-plan"]["pricing_source"], "code-plan") - self.assertEqual(rows["oai-quota-code-plan"]["pricing_cny"], 10) - self.assertEqual(rows["oai-quotaonly-code-plan"]["pricing_source"], "code-plan") - self.assertEqual(rows["oai-sub-code-plan"]["pricing_source"], "-") + self.assertEqual(rows["oai-quota-codeplan-primary"]["pricing_source"], "codeplan") + self.assertEqual(rows["oai-quota-codeplan-primary"]["pricing_cny"], 10) + self.assertEqual(rows["ordinary-account"]["pricing_source"], "-") + self.assertEqual(rows["oai-sub-codeplan-primary"]["pricing_source"], "-") self.assertEqual(rows["oai-quota-unknown"]["pricing_source"], "-") - self.assertEqual(rows["oai-quota-ycy"]["pricing_source"], "ycy") - self.assertEqual(rows["oai-quota-ycy"]["pricing_state"], "error") - self.assertIsNone(rows["oai-quota-ycy"]["pricing_cny"]) - self.assertEqual(rows["oai-quota-code--plan"]["pricing_source"], "-") - self.assertEqual(rows["oai-quota-code-plan-"]["pricing_source"], "-") + self.assertEqual(rows["oai-quota-ycy-primary"]["pricing_source"], "ycy") + self.assertEqual(rows["oai-quota-ycy-primary"]["pricing_state"], "error") + self.assertIsNone(rows["oai-quota-ycy-primary"]["pricing_cny"]) + self.assertEqual(rows["oai-quota-codeplan--primary"]["pricing_source"], "-") + self.assertEqual(rows["oai-quota-codeplan-primary-"]["pricing_source"], "-") ambiguous = dict(pricing_payload, sources=[*pricing_payload["sources"], {"name": "codeplan", "health_state": "healthy", "balance": {"available_cny": 12}}]) ambiguous_rows = {row["name"]: row for row in mod.normalize_account_rows(accounts_payload, pricing_payload=ambiguous)} - self.assertEqual(ambiguous_rows["oai-quota-code-plan"]["pricing_source"], "-") + self.assertEqual(ambiguous_rows["oai-quota-codeplan-primary"]["pricing_source"], "-") + + def test_explicit_account_source_balance_groups_and_fetch_metadata_win_over_name(self) -> None: + mod = load_module() + accounts_payload = { + "accounts": [ + { + "id": 1, + "name": "oai-quota-codeplan-primary", + "routing_group": "legacy-key-group", + "pricing_source": "canonical-provider", + "pricing_collector_source": "canonical-provider", + "pricing_balance_cny": 12.5, + "pricing_last_fetch_at": "2026-08-03T11:59:00Z", + "pricing_fetch_status": "matched", + "account_group_names": ["GPT", "quota", "GPT"], + "kind": "quota_limited", + }, + { + "id": 2, + "name": "ordinary-failed", + "pricing_source": "canonical-provider", + "pricing_balance_cny": None, + "pricing_last_fetch_at": "2026-08-03T12:00:00Z", + "pricing_fetch_status": "upstream_error", + "account_group_names": [], + "kind": "quota_limited", + }, + ] + } + pricing_payload = { + "sources": [ + { + "name": "codeplan", + "health_state": "healthy", + "balance": {"available_cny": 99}, + } + ] + } + + rows = { + row["name"]: row + for row in mod.normalize_account_rows(accounts_payload, pricing_payload=pricing_payload) + } + + canonical = rows["oai-quota-codeplan-primary"] + self.assertEqual(canonical["pricing_source"], "canonical-provider") + self.assertEqual(canonical["pricing_cny"], 12.5) + self.assertEqual(canonical["groups"], "GPT、quota") + self.assertEqual(canonical["pricing_fetch"], "08-03 19:59") + failed = rows["ordinary-failed"] + self.assertIsNone(failed["pricing_cny"]) + self.assertIn("upstream_error", failed["pricing_fetch"]) def test_normalize_pricing_rows_uses_pricing_monitor_source_balances(self) -> None: mod = load_module() @@ -296,7 +348,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase): "generated_at": "2026-08-02T12:00:00Z", "sources": [ { - "name": "code-plan", + "name": "codeplan", "source_kind": "newapi", "health_state": "healthy", "last_success_at": "2026-08-02T11:59:00Z", @@ -316,11 +368,13 @@ class Sub2APIQuotaTUITests(unittest.TestCase): rows = mod.normalize_pricing_rows(payload) - self.assertEqual([row["name"] for row in rows], ["code-plan", "kedaya"]) + self.assertEqual([row["name"] for row in rows], ["codeplan", "kedaya"]) self.assertEqual(rows[0]["balance"], 5000000) self.assertEqual(rows[0]["balance_cny"], 10) self.assertEqual(rows[0]["unit"], "quota") self.assertEqual(rows[0]["status"], "healthy") + self.assertEqual(rows[0]["updated_at"], "2026-08-02T11:59:00Z") + self.assertNotEqual(rows[0]["age"], "-") self.assertEqual(rows[1]["status"], "error") self.assertIsNone(rows[1]["balance"]) self.assertIsNone(rows[1]["balance_cny"]) @@ -345,7 +399,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase): out = io.StringIO() with contextlib.redirect_stdout(out): mod.print_pricing_once(payload) - self.assertIn("code-plan", out.getvalue()) + self.assertIn("codeplan", out.getvalue()) self.assertIn("5,000,000", out.getvalue()) self.assertIn("¥10", out.getvalue()) @@ -386,7 +440,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase): "view": "accounts", "sources": [ { - "name": "code-plan", + "name": "codeplan", "source_kind": "newapi", "health_state": "healthy", "balance_available": True, @@ -403,7 +457,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase): for invalid_payload in ( {"view": "overview", "sources": []}, {"view": "accounts"}, - {"view": "accounts", "sources": [{"name": "code-plan"}]}, + {"view": "accounts", "sources": [{"name": "codeplan"}]}, { "view": "accounts", "sources": [ @@ -618,7 +672,7 @@ class Sub2APIQuotaTUITests(unittest.TestCase): self.assertEqual(mod.monitor_availability(rows[0], status_payload), "ok") self.assertIn("availability", out.getvalue()) self.assertNotIn("5h bal", out.getvalue()) - self.assertIn(" ok\n", out.getvalue()) + self.assertIn(" ok ", out.getvalue()) def test_monitor_availability_is_dash_without_bound_monitor(self) -> None: mod = load_module() @@ -823,7 +877,7 @@ class WorkspaceTests(unittest.TestCase): self.assertEqual(accounts["accounts"][0]["id"], "9007199254740993") self.assertEqual(status["channel_monitors"]["items"][0]["id"], "9007199254740995") - self.assertEqual(pricing["sources"][0]["name"], "code-plan") + self.assertEqual(pricing["sources"][0]["name"], "codeplan") self.assertEqual(len(logs["data"]["items"]), 1) self.assertEqual(logs["data"]["items"][0]["id"], "9007199254740997") self.assertEqual(logs["data"]["items"][0]["instance"], "server6") @@ -927,6 +981,12 @@ class WorkspaceTests(unittest.TestCase): with self.assertRaisesRegex(RuntimeError, "account contract"): mod.fetch_workspace_payload("https://workspace.example.test/data", 3) + invalid_pricing_timestamp = workspace_payload_fixture() + invalid_pricing_timestamp["accounts"]["accounts"][0]["pricing_last_fetch_at"] = "not-a-timestamp" + with mock.patch.object(mod, "fetch_workspace_response", return_value=(invalid_pricing_timestamp, "")): + with self.assertRaisesRegex(RuntimeError, "account contract"): + mod.fetch_workspace_payload("https://workspace.example.test/data", 3) + invalid_period = workspace_payload_fixture() invalid_period["traffic"].pop("ends_at") with mock.patch.object(mod, "fetch_workspace_response", return_value=(invalid_period, "")): @@ -1341,7 +1401,7 @@ class WorkspaceTests(unittest.TestCase): "https://workspace.example.test/api/ui-data?view=workspace", 10 ) token_read.assert_not_called() - self.assertIn("oai-quota-code-plan", out.getvalue()) + self.assertIn("oai-quota-codeplan-primary", out.getvalue()) self.assertIn("wmy", out.getvalue()) def test_empty_workspace_url_falls_back_to_legacy_direct_mode(self) -> None: @@ -1732,7 +1792,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase): "accounts": [ { "id": 1, - "name": "oai-quota-code-plan", + "name": "oai-quota-codeplan-primary", "routing_group": "fast", "provider": "openai", "kind": "pay_as_you_go", @@ -1745,7 +1805,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase): "generated_at": "2026-07-24T12:00:00+08:00", "sources": [ { - "name": "code-plan", + "name": "codeplan", "source_kind": "newapi", "health_state": "healthy", "last_success_at": "2026-07-24T12:00:00+08:00", @@ -1839,7 +1899,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase): self.assertGreater(logs_table.max_scroll_x, 0) self.assertGreater(errors_table.max_scroll_x, 0) self.assertEqual(screen.log_rows[0]["cache_tokens"], 4150) - self.assertEqual(screen.account_rows[0]["pricing_source"], "code-plan") + self.assertEqual(screen.account_rows[0]["pricing_source"], "codeplan") self.assertEqual(screen.account_rows[0]["pricing_cny"], 10) self.assertGreater(screen.log_rows[0]["tokens_per_second"], 0) self.assertEqual(len({table.styles.background for table in tables}), 4) @@ -1852,7 +1912,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase): await pilot.pause() self.assertEqual(type(app.screen).__name__, "PricingScreen") self.assertEqual(app.screen.focused.id, "pricing") - self.assertEqual(app.screen.rows[0]["name"], "code-plan") + self.assertEqual(app.screen.rows[0]["name"], "codeplan") async def test_workspace_dashboard_handles_initial_endpoint_failure(self) -> None: @@ -1936,7 +1996,7 @@ class DashboardLayoutTests(unittest.IsolatedAsyncioTestCase): screen = app.screen self.assertEqual(type(screen).__name__, "DashboardScreen") self.assertEqual(app.sub_title, "Dashboard") - self.assertEqual(screen.account_rows[0]["name"], "oai-quota-code-plan") + self.assertEqual(screen.account_rows[0]["name"], "oai-quota-codeplan-primary") self.assertEqual(screen.error_rows[0]["key"], "wmy") self.assertNotIn("count", screen.error_rows[0]) self.assertEqual(len(fetch.call_args_list), 1) @@ -1960,7 +2020,7 @@ class PageSelectionTests(unittest.TestCase): "generated_at": "2026-08-02T12:00:00Z", "sources": [ { - "name": "code-plan", + "name": "codeplan", "source_kind": "newapi", "health_state": "healthy", "last_success_at": "2026-08-02T11:59:00Z", @@ -1976,7 +2036,7 @@ class PageSelectionTests(unittest.TestCase): mod.fetch_pricing_payload = original_fetch self.assertEqual(rc, 0) - self.assertIn("code-plan", out.getvalue()) + self.assertIn("codeplan", out.getvalue()) self.assertIn("¥10", out.getvalue()) def test_once_pricing_failure_is_redacted(self) -> None: @@ -2004,14 +2064,14 @@ class PageSelectionTests(unittest.TestCase): "source_name": "test", "totals": {"total_accounts": 2, "usable_accounts": 2, "today_cost_usd": 0}, "accounts": [ - {"id": 1, "name": "oai-quota-code-plan", "routing_group": "fast", "kind": "quota_limited"}, - {"id": 2, "name": "oai-sub-code-plan", "routing_group": "fast", "kind": "quota_limited"}, + {"id": 1, "name": "oai-quota-codeplan-primary", "routing_group": "fast", "kind": "quota_limited"}, + {"id": 2, "name": "oai-sub-codeplan-primary", "routing_group": "fast", "kind": "quota_limited"}, ], } pricing_payload = { "sources": [ { - "name": "code-plan", + "name": "codeplan", "source_kind": "newapi", "health_state": "healthy", "last_success_at": "2026-08-02T12:00:00Z", @@ -2044,9 +2104,9 @@ class PageSelectionTests(unittest.TestCase): text = out.getvalue() self.assertEqual(rc, 0) self.assertIn("src cny", text) - self.assertIn("oai-quota-code-plan", text) - self.assertIn("oai-sub-code-plan", text) - self.assertGreaterEqual(text.count("code-plan"), 2) + self.assertIn("oai-quota-codeplan-primary", text) + self.assertIn("oai-sub-codeplan-primary", text) + self.assertGreaterEqual(text.count("codeplan"), 2) self.assertIn("¥10", text) def test_dedicated_page_flags_are_mutually_exclusive(self) -> None: diff --git a/uv.lock b/uv.lock index e5ed45c..39543e4 100644 --- a/uv.lock +++ b/uv.lock @@ -85,7 +85,7 @@ wheels = [ [[package]] name = "shusub2" -version = "0.3.6" +version = "0.3.7" source = { editable = "." } dependencies = [ { name = "textual" },