Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ jobs:
- name: Bring up Redis test service
run: docker compose -f docker-compose.test.yml up -d redis --wait

- name: C2S gate matrix contract
run: |
python3 scripts/check_c2s_gate_matrix.py
python3 -m unittest scripts/tests/check_c2s_gate_matrix_test.py

- name: Schema stage (build + check + test + generate)
working-directory: agent/packages/schema
run: |
Expand Down
354 changes: 354 additions & 0 deletions docs/plan-refactor-c2s-gate-v1.md

Large diffs are not rendered by default.

49 changes: 0 additions & 49 deletions docs/plans-skeleton/plan-refactor-c2s-gate-v1.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/plans-skeleton/plan-refactor-master-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@

1. 9 条轨道全部归档(各自 bot 场景常绿 + 吸收 plan 全部归档/验伪结案);
2. 三个 2 万行级 god file(inventory/mod.rs、client_request_handler.rs、persistence/mod.rs)不复存在,最大单文件 < 3000 行;
3. `qi_current` 裸写编译不过;client 无未登记的会话态 store;113 C2S 变体全部有显式 GateSpec/no_gate 声明;28 旁路 channel 收编或豁免登记;
3. `qi_current` 裸写编译不过;client 无未登记的会话态 store;届时现行 `ClientRequestV1` 全部变体均有显式 GateSpec/no_gate 声明(2026-08-03 P0 基线为 104,新增变体自动纳入);28 旁路 channel 收编或豁免登记;
4. bot 场景数从 ~30 增至 ≥80,CI e2e 是唯一主门禁且无已知假绿。
5. `flash-review` label 下 open issue 全部显式处置(fixed / dup / 验伪关闭 / 促升 skeleton,见 §10),无静默积压。

Expand Down
2 changes: 1 addition & 1 deletion docs/plans-skeleton/plan-refactor-wire-s2c-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
- ⬜ P1 emit builder + scope 落地:builder 上线,vfx/audio/env 三类先挂 scope(跨维 bleed 立灭);跨位面切换时 env/season 全量重发。
- ⬜ P2 client 桥接层收敛:枚举前缀剥离收敛到单点(含 forge-session 修复);`ServerDataRouter` 注册表整备(分域注册文件,不再单个 1547 行 switch 追加)。
- ⬜ P3 旁路归一批次:28 channel 逐批收编入 server_data envelope 或登记豁免(资源包/握手类可豁免);删除散装 receiver。
- ⬜ P4 契约 pin 全量化:双向 sample 对拍测试补齐(113 C2S + 144 S2C 每变体至少一条正反 sample,schema 改动连 sample 一起改);emit 迁移到 builder 的长尾批次。
- ⬜ P4 契约 pin 全量化:双向 sample 对拍测试补齐(届时现行 `ClientRequestV1` 全部变体,2026-08-03 基线 104;以及届时现行 `ServerDataType` 全部变体,2026-07-27 侦察基线 144;每变体至少一条正反 sample,schema 改动连 sample 一起改);emit 迁移到 builder 的长尾批次。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
set -eu

python3 - <<'PY'
from pathlib import Path
import re

text = Path("agent/packages/schema/src/server-data.ts").read_text(encoding="utf-8")
block = text.split("export const ServerDataType = Type.Union([", 1)[1]
block = block.split("]);", 1)[0]
tags = re.findall(r'Type\.Literal\("([^"]+)"\)', block)

print(f"ServerDataType literal count: {len(tags)}")
if len(tags) != 144:
    raise SystemExit("ServerDataType count does not match the documented 144 baseline")
PY

Repository: Kizunad/Bong

Length of output: 248


🏁 Script executed:

#!/bin/sh
set -eu

echo "== plan =="
sed -n '1,80p' docs/plans-skeleton/plan-refacor-wire-s2c-v1.md 2>/dev/null || true

echo "== file match =="
fd -a 'plan-refactor-wire-s2c-v1\.md|server-data\.ts' . | sed 's#^\./##'

echo "== schema outline/search =="
wc -l agent/packages/schema/src/server-data.ts
python3 - <<'PY'
from pathlib import Path
text = Path("agent/packages/schema/src/server-data.ts").read_text(encoding="utf-8")
print("contains ServerDataType:", "export const ServerDataType = Type.Union([" in text)
print("line 190-305:")
for i,line in enumerate(text.splitlines(), 1):
    if 190 <= i <= 305:
        print(f"{i}: {line}")
PY

echo "== enumerate all Type.Literal values in server-data.ts =="
python3 - <<'PY'
import re
from pathlib import Path
text = Path("agent/packages/schema/src/server-data.ts").read_text(encoding="utf-8")
print("all exported/labeled datatypes:")
for i,line in enumerate(text.splitlines(), 1):
    if "export const " in line or "Type.Union" in line:
        print(f"{i}: {line}")
print("ServerDataType total literals:", len(re.findall(r'Type\.Literal\("([^"]+)"\)', text)))
PY

echo "== search for 144 and ServerDataType =="
rg -n "144|ServerDataType|ClientRequestV1|Client.*base|基线" docs/plans-skeleton docs agent packages 2>/dev/null | head -n 200

Repository: Kizunad/Bong

Length of output: 50368


🏁 Script executed:

#!/bin/sh
set -eu

echo "== plan exact =="
sed -n '1,70p' docs/plans-skeleton/plan-refactor-wire-s2c-v1.md

echo "== schema TypeScript exports from server-data.ts =="
python3 - <<'PY'
import re
from pathlib import Path
text = Path("agent/packages/schema/src/server-data.ts").read_text()
lines = text.splitlines()
for i,line in enumerate(lines, 1):
    if i < 20 or line.startswith(("export const ", "export type ") or "Type.Union" in line):
        if i <= 400:
            print(f"{i}: {line}")
PY

echo "== Rust ServerData enum search =="
fd -a 'server_data\.(rs|rs)$$' . | sed 's#^\./##'
rg -n "enum ServerData|ServerData(v1)?|ServerDataType|Type::" server agent 2>/dev/null | sed -n '1,240p'

echo "== agent package source files near ServerDataV1 =="
rg -n "ServerDataV1|ServerDataType" docs/plans-skeleton/plan-refactor-wire-s2c-v1.md agent/packages/schema src 2>/dev/null | sed -n '1,200p'

echo "== precise literal count in ServerDataType union block =="
python3 - <<'PY'
import re
from pathlib import Path
text = Path("agent/packages/schema/src/server-data.ts").read_text()
block = text.split("export const ServerDataType = Type.Union([",1)[1]
block = block.split("]);",1)[0]
vals = re.findall(r'Type\.Literal\("([^"]+)"\)', block)
print("count", len(vals))
PY

Repository: Kizunad/Bong

Length of output: 44261


修正 ServerDataType 基线或补全说明。

P4 的 ServerDataType 侦察基线写为 144,但 agent schema 的 ServerDataType literal 计数为 100。若这 144 包含旁路 channel 或其他服务端 Rust 变体,请标注来源;否则将基线改为当前 agent union 数量,避免 P4 按错误总量留空验证。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/plans-skeleton/plan-refactor-wire-s2c-v1.md` at line 25, 修正 P4 计划中
ServerDataType 基线的来源或数值:核对 agent schema 的 ServerDataType union 计数;若 144 包含旁路
channel 或其他服务端 Rust 变体,明确标注其来源和范围,否则将基线改为当前 agent union 的实际数量,避免按错误总量规划验证。

- ⬜ P5 bot 验收 + 吸收 plan 批量归档。

## 吸收清单(短名省略 plan-bughunt- 前缀与 -v1 后缀)
Expand Down
188 changes: 188 additions & 0 deletions scripts/check_c2s_gate_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
#!/usr/bin/env python3
from __future__ import annotations

import re
import sys
from collections import Counter
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
ENUM_PATH = ROOT / "server/src/schema/client_request.rs"
PLAN_PATH = ROOT / "docs/plan-refactor-c2s-gate-v1.md"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ENUM_SERDE_RE = re.compile(
r'#\[serde\(([^]]*)\)\]\s*pub enum ClientRequestV1\s*\{', re.MULTILINE
)
MATRIX_RE = re.compile(r"^\|\s*(\d+)\s*\|\s*`([^`]+)`\s*\|")
VARIANT_DECL_RE = re.compile(r"^([A-Z][A-Za-z0-9_]*)\s*(.*)$")


def _without_line_comment(line: str) -> str:
return line.split("//", 1)[0]


def _leading_attributes(code: str) -> tuple[list[str], str]:
attributes: list[str] = []
while code.startswith("#["):
bracket_depth = 0
in_string = False
escaped = False
end = None
for index, char in enumerate(code):
if in_string:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
in_string = False
continue
if char == '"':
in_string = True
elif char == "[":
bracket_depth += 1
elif char == "]":
bracket_depth -= 1
if bracket_depth == 0:
end = index + 1
break
if end is None:
raise RuntimeError(f"unterminated ClientRequestV1 attribute: {code!r}")
attributes.append(code[:end])
code = code[end:].lstrip()
if code.startswith("#"):
raise RuntimeError(f"unsupported ClientRequestV1 attribute syntax: {code!r}")
return attributes, code


def parse_enum_variants(source: str) -> list[str]:
serde = ENUM_SERDE_RE.search(source)
if not serde or not re.search(r'\btag\s*=\s*"type"', serde.group(1)):
raise RuntimeError("ClientRequestV1 must use serde tag = \"type\"")
if not re.search(r'\brename_all\s*=\s*"snake_case"', serde.group(1)):
raise RuntimeError("ClientRequestV1 must use serde rename_all = \"snake_case\"")

lines = source.splitlines()
variants: list[str] = []
inside = False
depth = 0
tuple_depth = 0
pending_attributes: list[str] = []

for line in lines:
if not inside:
if line == "pub enum ClientRequestV1 {":
inside = True
depth = 1
continue

code = _without_line_comment(line).strip()
if depth == 1 and tuple_depth:
tuple_depth += code.count("(") - code.count(")")
if tuple_depth < 0:
raise RuntimeError(f"unbalanced tuple variant syntax: {line!r}")
continue
if depth == 1 and code == "}":
depth = 0
break
if depth == 1 and code:
attributes, code = _leading_attributes(code)
pending_attributes.extend(attributes)
if not code:
continue
if any("serde" in attribute and "rename" in attribute for attribute in pending_attributes):
raise RuntimeError("ClientRequestV1 variant-level serde rename is unsupported")
pending_attributes.clear()
Comment on lines +92 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -U \
  '^\s*#\[serde\([^]]*(alias|skip|other|rename|rename_all)' \
  server/src/schema/client_request.rs

Repository: Kizunad/Bong

Length of output: 974


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- files ---\n'
git ls-files | rg '(^|/)check_c2s_gate_matrix(\.py)?$|(^|/)check_c2s_gate_matrix_test\.py$' || true

printf '\n--- checker outline/contents ---\n'
wc -l scripts/check_c2s_gate_matrix.py
sed -n '1,150p' scripts/check_c2s_gate_matrix.py

printf '\n--- tests relevant section ---\n'
sed -n '1,140p' scripts/tests/check_c2s_gate_matrix_test.py

printf '\n--- schema excerpts client_request.rs relevant ---\n'
sed -n '1,50p' server/src/schema/client_request.rs
sed -n '760,800p' server/src/schema/client_request.rs

Repository: Kizunad/Bong

Length of output: 241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- files ---\n'
git lg ls-files | rg '(^|/)check_c2s_gate_matrix(\.py)?$|(^|/)check_c2s_gate_matrix_test\.py$' || true

printf '\n--- checker outline/contents ---\n'
wc -l scripts/check_c2s_gate_matrix.py
sed -n '1,150p' scripts/check_c2s_gate_matrix.py

printf '\n--- tests relevant section ---\n'
sed -n '1,140p' scripts/tests/check_c2s_gate_matrix_test.py

printf '\n--- schema excerpts client_request.rs relevant ---\n'
sed -n '1,50p' server/src/schema/client_request.rs
sed -n '760,800p' server/src/schema/client_request.rs

Repository: Kizunad/Bong

Length of output: 241


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- files ---'
git lg ls-files | rg '(^|/)check_c2s_gate_matrix(\.py)?$|(^|/)check_c2s_gate_matrix_test\.py$' || true

printf '%s\n' ''
printf '%s\n' '--- checker outline/contents ---'
wc -l scripts/check_c2s_gate_matrix.py
sed -n '1,150p' scripts/check_c2s_gate_matrix.py

printf '%s\n' ''
printf '%s\n' '--- tests relevant section ---'
sed -n '1,140p' scripts/tests/check_c2s_gate_matrix_test.py

printf '%s\n' ''
printf '%s\n' '--- schema excerpts client_request.rs relevant ---'
sed -n '1,50p' server/src/schema/client_request.rs
sed -n '760,800p' server/src/schema/client_request.rs

Repository: Kizunad/Bong

Length of output: 12717


🌐 Web query:

serde RenameAll applies to enum variant names only enum variant attributes alias other

💡 Result:

In Serde, the rename_all attribute acts differently depending on where it is applied, and it does not affect alias attributes [1][2][3]. When rename_all is used as a container attribute on an enum, it applies the specified casing convention (e.g., camelCase, snake_case) to the enum's variant names [1][4]. It does not affect the fields within those variants [4][5]. If you need to rename the fields of enum variants using a casing convention, you must either: 1. Apply rename_all individually to each struct-like variant [4][2]. 2. Use rename_all_fields if available/appropriate for your specific Serde version (note: rename_all_fields is specifically intended to apply a casing rule to the fields of variants) [1]. Regarding aliases, the alias attribute on an enum variant provides an alternative name for deserialization that is independent of any rename_all or rename attributes [6]. If a variant has a rename_all rule applied (via the container) or a rename attribute, the alias attribute still functions as an additional valid name for that variant during deserialization [6]. Explicit rename attributes on a specific variant will override the casing convention applied by rename_all at the container level [3]. Top results: [1][4][2][3][6]

Citations:


🌐 Web query:

serde enum variant syntax alias skip default other documentation example

💡 Result:

In Serde, enum variants support several attributes to control their serialization and deserialization behavior [1][2][3]. ### Alias The #[serde(alias = "name")] attribute allows you to deserialize a variant from a different name in addition to its Rust name [1][3]. This can be repeated to specify multiple aliases [1][3]. ### Skip The #[serde(skip)] attribute prevents a variant from being serialized or deserialized [1][3]. Note: In some data formats, especially non-self-describing ones like bincode, using #[serde(skip)] on an enum variant can lead to unexpected behavior or errors because the serializer still expects to be able to represent that variant [4][5]. ### Other The #[serde(other)] attribute is used during deserialization to catch any unknown variant values and map them to a specific "catch-all" variant [5]. It is typically applied to a unit variant in an enum. If the deserializer encounters a value that does not match any other defined variants, it will deserialize it into the variant marked with #[serde(other)] [5]. ### Default While #[serde(default)] is a standard field attribute for structs (to fill missing fields with Default::default()) [6][7], it is not a direct variant attribute for enums in the same way. However, for enum variants that contain fields (e.g., struct-like variants), you can apply #[serde(default)] to those individual fields to provide default values during deserialization [2][7]. ### Example Below is an example demonstrating the use of alias and other: rust use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] enum Status { Active, #[serde(alias = "inactive", alias = "disabled")] Suspended, #[serde(other)] Unknown, } fn main() { // Deserializing "inactive" maps to Status::Suspended let s: Status = serde_json::from_str("\"inactive\"").unwrap(); println!("{:?}", s); // Output: Suspended // Deserializing "anything_else" maps to Status::Unknown due to #[serde(other)] let s: Status = serde_json::from_str("\"anything_else\"").unwrap(); println!("{:?}", s); // Output: Unknown } Top results for this query include the official Serde documentation for variant and field attributes [1][2][6].

Citations:


对所有 variant-level serde attribute 执行 fail-closed。

scripts/check_c2s_gate_matrix.py:92-94 只拒绝 renamealias 可以增加未登记的 wire 名称,skipother 也会改变反序列化集合;rename_all 也需要在 variant 上拒绝,因为它可能覆盖 enum 级别的 rename_all

  • 拒绝所有 variant-level #[serde(...)],或至少拒绝 aliasskipotherrename 以及 variant 级别的 rename_all
  • scripts/tests/check_c2s_gate_matrix_test.py:54-87 补充 aliasskipother 和 variant-level rename_all 的拒绝用例。
建议修复
-            if any("serde" in attribute and "rename" in attribute for attribute in pending_attributes):
-                raise RuntimeError("ClientRequestV1 variant-level serde rename is unsupported")
+            if any("serde" in attribute for attribute in pending_attributes):
+                raise RuntimeError("ClientRequestV1 variant-level serde attribute is unsupported")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if any("serde" in attribute and "rename" in attribute for attribute in pending_attributes):
raise RuntimeError("ClientRequestV1 variant-level serde rename is unsupported")
pending_attributes.clear()
if any("serde" in attribute for attribute in pending_attributes):
raise RuntimeError("ClientRequestV1 variant-level serde attribute is unsupported")
pending_attributes.clear()
🧰 Tools
🪛 Ruff (0.16.0)

[warning] 93-93: Avoid specifying long messages outside the exception class

(TRY003)

📍 Affects 2 files
  • scripts/check_c2s_gate_matrix.py#L92-L94 (this comment)
  • scripts/tests/check_c2s_gate_matrix_test.py#L54-L87
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check_c2s_gate_matrix.py` around lines 92 - 94, Update the
variant-level serde attribute validation in
scripts/check_c2s_gate_matrix.py:92-94 to fail closed for every #[serde(...)]
attribute, or at minimum reject alias, skip, other, rename, and variant-level
rename_all instead of only rename. Add rejection test cases for alias, skip,
other, and variant-level rename_all in
scripts/tests/check_c2s_gate_matrix_test.py:54-87.

match = VARIANT_DECL_RE.match(code)
if not match:
raise RuntimeError(f"unsupported ClientRequestV1 syntax: {line!r}")
suffix = match.group(2).strip()
if not suffix or suffix[0] not in "{(,":
raise RuntimeError(f"unsupported ClientRequestV1 variant syntax: {line!r}")
variants.append(match.group(1))
if suffix[0] == "(":
tuple_depth = suffix.count("(") - suffix.count(")")
if tuple_depth < 0:
raise RuntimeError(f"unbalanced tuple variant syntax: {line!r}")

depth += code.count("{") - code.count("}")
if depth == 0:
break

if not inside or depth != 0 or not variants:
raise RuntimeError(f"cannot parse ClientRequestV1 from {ENUM_PATH}")
return variants


def enum_variants() -> list[str]:
return parse_enum_variants(ENUM_PATH.read_text(encoding="utf-8"))


def matrix_variants() -> tuple[list[int], list[str]]:
rows: list[tuple[int, str]] = []
for line in PLAN_PATH.read_text(encoding="utf-8").splitlines():
if match := MATRIX_RE.match(line):
rows.append((int(match.group(1)), match.group(2)))
if not rows:
raise RuntimeError(f"cannot parse C2S matrix from {PLAN_PATH}")
return [number for number, _ in rows], [variant for _, variant in rows]


def duplicates(values: list[str]) -> list[str]:
return sorted(value for value, count in Counter(values).items() if count > 1)


def first_order_mismatch(left: list[str], right: list[str]) -> tuple[int, str, str] | None:
for index in range(max(len(left), len(right))):
left_value = left[index] if index < len(left) else "<missing>"
right_value = right[index] if index < len(right) else "<missing>"
if left_value != right_value:
return index, left_value, right_value
return None


def main() -> int:
errors: list[str] = []
try:
enum = enum_variants()
numbers, matrix = matrix_variants()
except RuntimeError as error:
print(f"C2S gate matrix check failed:\n- {error}", file=sys.stderr)
return 1

expected_numbers = list(range(1, len(matrix) + 1))
if numbers != expected_numbers:
errors.append(f"matrix numbering is not contiguous: {numbers}")

for label, values in (("enum", enum), ("matrix", matrix)):
duplicate_values = duplicates(values)
if duplicate_values:
errors.append(f"duplicate {label} variants: {duplicate_values}")

enum_matrix_missing = [variant for variant in enum if variant not in matrix]
matrix_enum_extra = [variant for variant in matrix if variant not in enum]
if enum_matrix_missing:
errors.append(f"missing matrix variants: {enum_matrix_missing}")
if matrix_enum_extra:
errors.append(f"extra matrix variants: {matrix_enum_extra}")
if set(matrix) != set(enum):
errors.append("matrix and Rust enum variant sets differ")

mismatch = first_order_mismatch(enum, matrix)
if mismatch:
errors.append(
"first enum/matrix order mismatch at row "
f"{mismatch[0] + 1}: enum={mismatch[1]} matrix={mismatch[2]}"
)

if errors:
print("C2S gate matrix check failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1

print(f"C2S gate matrix matches all {len(enum)} Rust ClientRequestV1 variants")
return 0


if __name__ == "__main__":
raise SystemExit(main())
120 changes: 120 additions & 0 deletions scripts/tests/check_c2s_gate_matrix_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
from __future__ import annotations

import importlib.util
import io
import unittest
from contextlib import redirect_stderr
from pathlib import Path
from unittest.mock import patch

CHECKER = Path(__file__).resolve().parents[1] / "check_c2s_gate_matrix.py"
spec = importlib.util.spec_from_file_location("check_c2s_gate_matrix", CHECKER)
assert spec and spec.loader
checker = importlib.util.module_from_spec(spec)
spec.loader.exec_module(checker)

ENUM_PREFIX = '#[serde(deny_unknown_fields, tag = "type", rename_all = "snake_case")]\n'


class ParserTests(unittest.TestCase):
def test_accepts_struct_unit_and_tuple_variants(self) -> None:
source = ENUM_PREFIX + """pub enum ClientRequestV1 {
StructVariant {
value: u8,
},
UnitVariant,
TupleVariant(u8),
}
"""
self.assertEqual(
checker.parse_enum_variants(source),
["StructVariant", "UnitVariant", "TupleVariant"],
)

def test_accepts_multiline_tuple_variant(self) -> None:
source = ENUM_PREFIX + """pub enum ClientRequestV1 {
TupleVariant(
u8,
String,
),
}
"""
self.assertEqual(checker.parse_enum_variants(source), ["TupleVariant"])

def test_fails_closed_on_unknown_top_level_syntax(self) -> None:
source = ENUM_PREFIX + """pub enum ClientRequestV1 {
#[cfg(test)]
Unsupported = 1,
}
"""
with self.assertRaisesRegex(RuntimeError, "unsupported ClientRequestV1 variant syntax"):
checker.parse_enum_variants(source)

def test_fails_closed_on_serde_wire_renames(self) -> None:
camel = '#[serde(tag = "type", rename_all = "camelCase")]\npub enum ClientRequestV1 { Variant, }'
with self.assertRaisesRegex(RuntimeError, 'rename_all = "snake_case"'):
checker.parse_enum_variants(camel)

renamed = ENUM_PREFIX + """pub enum ClientRequestV1 {
#[serde(rename = "other")]
Variant,
}
"""
with self.assertRaisesRegex(RuntimeError, "variant-level serde rename"):
checker.parse_enum_variants(renamed)

stacked = ENUM_PREFIX + """pub enum ClientRequestV1 {
#[serde(rename = "other")]
#[doc = "variant docs"]
Variant,
}
"""
with self.assertRaisesRegex(RuntimeError, "variant-level serde rename"):
checker.parse_enum_variants(stacked)

inline = ENUM_PREFIX + """pub enum ClientRequestV1 {
#[serde(rename = "other")] Variant,
}
"""
with self.assertRaisesRegex(RuntimeError, "variant-level serde rename"):
checker.parse_enum_variants(inline)

inline_cfg = ENUM_PREFIX + """pub enum ClientRequestV1 {
#[cfg(test)] Variant,
}
"""
self.assertEqual(checker.parse_enum_variants(inline_cfg), ["Variant"])

def test_main_rejects_each_contract_drift(self) -> None:
baseline = {
"enum_variants": ["Alpha", "Beta"],
"matrix_variants": ([1, 2], ["Alpha", "Beta"]),
}
cases = {
"mismatched sets": {"matrix_variants": ([1, 2], ["Alpha", "Gamma"])},
"reordered variants": {"matrix_variants": ([1, 2], ["Beta", "Alpha"])},
"duplicate enum variants": {"enum_variants": ["Alpha", "Alpha"]},
"duplicate matrix variants": {"matrix_variants": ([1, 2], ["Alpha", "Alpha"])},
"non-contiguous rows": {"matrix_variants": ([1, 3], ["Alpha", "Beta"])},
}
for label, override in cases.items():
values = baseline | override
with self.subTest(label=label), patch.object(
checker, "enum_variants", return_value=values["enum_variants"]
), patch.object(
checker, "matrix_variants", return_value=values["matrix_variants"]
), redirect_stderr(io.StringIO()):
self.assertEqual(checker.main(), 1)

def test_main_accepts_matching_enum_and_matrix(self) -> None:
with patch.object(
checker, "enum_variants", return_value=["Alpha", "Beta"]
), patch.object(
checker, "matrix_variants", return_value=([1, 2], ["Alpha", "Beta"])
):
self.assertEqual(checker.main(), 0)


if __name__ == "__main__":
unittest.main()
Loading