test(ui): guard the fallback catalog against drifting from the config - #173
Merged
Conversation
ui/js/data.js mirrors config.example.toml's model catalog: MODELS feeds the publish-form picker and MARKET is what guests browse. app.js joins the two by model NAME, so a rename in one place silently degrades the other to a 'not published' cell -- which is exactly how ce6d0db broke it (it updated the config, missed MARKET, and left a phantom model in the fallback). This adds a tests-only gate (no production code, no new dependency) asserting MODELS == the config catalog, every MARKET row resolves in MODELS, PLANS ids match, and shared rows agree on provider/prices/context/max in each row's own currency -- with panicking positive controls so a broken extractor cannot read as clean.
9 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ui/js/data.jsis the front-end's fallback catalog. Its header comment says it is kept aligned withconfig/config.example.toml [[models]], and two of its tables are load-bearing:MODELS— the publish form's model picker (andPROVIDERSis derived from it), i.e. a mirror of the config catalog;MARKET— the guest marketplace listing, i.e. a subset ofMODELS.The coupling is not cosmetic.
ui/js/app.jsrenders the detail view by looking the model up by name across the two tables:When the lookup misses, the cell degrades to "未上架 / not published". Nothing errors, no key goes missing, and no existing test notices — a guest just sees a model name that no longer exists, an outdated price, and an empty max-tokens column.
That is exactly what commit
ce6d0dbdid: it updatedconfig/config.example.tomland "synced"ui/js/data.js, but missedMARKET(leavingglm-5.2,gpt-5.5-pro,claude-opus-4.7and a staledeepseek-v4-flashprice) and added agemini-3.5-flash-literow that never existed in any config. A later PR fixed the data; this PR adds the guard that was missing.Related Issue
None — this is a self-contained test-only addition.
Changes
src/catalog_gate.rs: a tests-only#[cfg(test)]gate that pins the two fallback tables to the config catalog.src/main.rs: one#[cfg(test)] mod catalog_gate;declaration.ui/.What it asserts
MODELSname-set == config[[models]]name-set, both directions — so it catches a phantom row and a model added to the config but never synced into the fallback.MARKETmodel name resolves inMODELS(theapp.jsjoin).PLANSid-set == config[[plans]]id-set (the publish form submitspayload.planstraight to the backend).MODELSrow agrees with the config onprovider/ input price / output price /context_length/max_output, compared in each row's own currency (USD(x)/CNY(x)is a label; no FX factor is invented).deepseek-v4-flash= 1.5/4.5), so a silently broken extractor aborts instead of "passing" on empty sets.Why it is cheap and safe
The config side reuses the production deserializer —
tomlis already a dependency andconfig.example.tomlis alreadyinclude_str!-embedded in the binary (main.rs'sDEFAULT_CONFIG), so embedding it in a test adds nothing. The JS side reuses the byte-scan approach the existingi18n_pack.rsgate uses on the other two front-end files; the repo has noregexdependency and deliberately no JS toolchain, so neither is introduced.Tests
cargo test全部通过 — 161 passed / 0 failed (155 existing + 6 new)cargo fmt --check通过cargo clippy --all-targets -- -D warnings通过Beyond the suite, I verified the gate has teeth by injecting each real defect form into the actual
ui/js/data.jsand confirming the right assertion fails with the offending model named:gemini-3.5-flash-liteMODELS 含配置中不存在的模型:["gemini-3.5-flash-lite"]glm-5.3→glm-5.2inMARKETonly (counts unchanged)游客市场模型的名称在 MODELS 中查不到:["glm-5.2"]deepseek-v4-flashprice → 1.008/2.016 (counts unchanged)输入价不一致(兜底 1.008 vs 配置 1.5 CNY)+ output lineThe second and third are the important ones: every count control stays green, so only the substantive comparison can catch them — the gate is not merely counting rows.
ui/js/data.jswas restored byte-identically afterwards (md5ebfeac67aef03fa56b1e64266d3a7acd, verified against the committed blob).Checklist
test/前缀不在 CONTRIBUTING 的列举中,但test是其允许的 commit type,且本改动不含任何生产代码 —— 与既有i18n_pack.rs门禁同型;如需改为refactor/或chore/请告知)ui/、不改数据、不动既有测试