Skip to content

Commit c107266

Browse files
committed
test(wire): 把「只增不改」从口号变成会变红的断言
docs/11-machine-output.md 承诺:同一 kindVersion 内字段只增不删不改名。而本模块 存在的理由之一,正是 `xlings interface --list` 那 20 个 capability —— 声明了 outputSchema,内容全是 `{"exitCode": integer}`。**没人能弄坏的 schema 不是 schema。** 所以把承诺钉住:信封、诊断、协议文档的已发布键集,以及每个 effect / severity 的 字面名(客户端按这些字符串匹配 —— 改名会悄悄改变 untrusted 门放行什么)。 加字段**不会**变红,这是契约允许的唯一变更。 验证过它真的会红:把 `kindVersion` 改成 `kind_version`、`exec-build-script` 改成 `exec-script`,5 个测试立刻失败;复原后 20/20 绿。
1 parent 56731be commit c107266

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

tests/unit/test_wire.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,4 +158,90 @@ TEST(WireFormat, LegacyJsonIsNotTheSameFormatAsJson) {
158158
EXPECT_NE(w::Format::LegacyJson, w::Format::Json);
159159
}
160160

161+
// ── Golden shapes, one per kind ────────────────────────────────────────────
162+
//
163+
// docs/11-machine-output.md promises that within a kindVersion fields are
164+
// added and never removed or renamed. A promise nobody can break is the thing
165+
// this whole module was written against — `xlings interface --list` declares
166+
// 20 capabilities whose outputSchema is, for all 20, only
167+
// `{"exitCode": integer}`. So the promise is enforced here: these lists are
168+
// the published key set, and removing or renaming one turns this file red.
169+
//
170+
// Adding a field does NOT turn it red, deliberately — that is the one change
171+
// the contract allows.
172+
namespace {
173+
174+
void expect_has_keys(const nlohmann::json& obj,
175+
std::initializer_list<const char*> keys,
176+
std::string_view what) {
177+
for (auto k : keys)
178+
EXPECT_TRUE(obj.contains(k))
179+
<< what << " lost published key '" << k
180+
<< "'. Removing or renaming one is a breaking change: bump the "
181+
"kind's version in mcpp.wire and say so in "
182+
"docs/11-machine-output.md.";
183+
}
184+
185+
} // namespace
186+
187+
TEST(WireGolden, EnvelopeKeySet) {
188+
auto j = w::to_json(w::Envelope{.kind = "mcpp.env"});
189+
expect_has_keys(j, {"schemaVersion", "kind", "kindVersion", "effects",
190+
"mcpp", "data", "diagnostics"}, "the envelope");
191+
expect_has_keys(j["mcpp"], {"version", "protocol"}, "envelope.mcpp");
192+
expect_has_keys(j["mcpp"]["protocol"], {"min", "max"}, "envelope.mcpp.protocol");
193+
}
194+
195+
TEST(WireGolden, DiagnosticKeySet) {
196+
auto j = w::to_json(w::Envelope{
197+
.kind = "mcpp.env",
198+
.diagnostics = {{.code = "C", .message = "m", .path = "p",
199+
.range = w::Range{{1, 1}, {1, 2}}}}});
200+
expect_has_keys(j["diagnostics"][0],
201+
{"code", "severity", "source", "message", "path", "range"},
202+
"a diagnostic");
203+
expect_has_keys(j["diagnostics"][0]["range"], {"start", "end"}, "a range");
204+
expect_has_keys(j["diagnostics"][0]["range"]["start"], {"line", "column"},
205+
"a position");
206+
}
207+
208+
TEST(WireGolden, ProtocolDocumentKeySet) {
209+
auto j = w::protocol_document({{"self env", {w::Effect::InitMcppHome}}});
210+
expect_has_keys(j, {"schemaVersion", "kind", "mcpp", "envelope", "kinds",
211+
"commands"}, "the protocol document");
212+
expect_has_keys(j["envelope"], {"min", "max"}, "protocol.envelope");
213+
expect_has_keys(j["commands"]["self env"], {"effects"}, "a command entry");
214+
}
215+
216+
// Every effect name is part of the contract: a client matches on these
217+
// strings, so renaming one silently changes what a gate lets through.
218+
TEST(WireGolden, EffectNamesAreStable) {
219+
struct { w::Effect e; const char* name; } const expected[]{
220+
{w::Effect::InitMcppHome, "init-mcpp-home"},
221+
{w::Effect::ReadProject, "read-project"},
222+
{w::Effect::WriteProject, "write-project"},
223+
{w::Effect::WriteGlobalCache, "write-global-cache"},
224+
{w::Effect::Network, "network"},
225+
{w::Effect::ExecBuildScript, "exec-build-script"},
226+
};
227+
for (auto const& x : expected)
228+
EXPECT_EQ(w::effect_name(x.e), x.name)
229+
<< "effect names are matched by clients; renaming one changes "
230+
"what an untrusted-workspace gate admits";
231+
}
232+
233+
TEST(WireGolden, SeverityNamesAreStable) {
234+
EXPECT_EQ(w::severity_name(w::Severity::Error), "error");
235+
EXPECT_EQ(w::severity_name(w::Severity::Warning), "warning");
236+
EXPECT_EQ(w::severity_name(w::Severity::Note), "note");
237+
}
238+
239+
// The kinds this build claims to speak. A client reads this list to decide
240+
// whether to bother calling; dropping one silently is a breaking change.
241+
TEST(WireGolden, DeclaredKinds) {
242+
auto j = w::protocol_document({});
243+
expect_has_keys(j["kinds"], {"mcpp.env", "mcpp.xpkg", "mcpp.cache"},
244+
"the kind list");
245+
}
246+
161247
} // namespace

0 commit comments

Comments
 (0)