Skip to content

Commit a58c796

Browse files
committed
test(lint): the translation walk and planned-verdict pins plant their warned group
The shipped translation ledger warns on no group now that flows is live, so the locale-keyed walk pins and the planned-with-authorHint verdict read a planted synthetic group row through lintLivenessPropertiesFromLedgerDir. The shipped flows row is pinned silent, and the authorWarnedProperties anti-drift pin asks over every governed translation group and over the object type's warned set, so it cannot pass by iterating nothing. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 68f653f commit a58c796

1 file changed

Lines changed: 125 additions & 40 deletions

File tree

‎packages/lint/src/lint-liveness-properties.test.ts‎

Lines changed: 125 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,32 @@ import {
3838

3939
const objStack = (obj: Record<string, unknown>) => ({ objects: [{ name: 'widget', ...obj }] });
4040
const paths = (findings: { message: string }[]) => findings.map((f) => f.message);
41+
42+
/**
43+
* The shipped `translation` ledger warns on no group since `flows` went `live`
44+
* (both halves read by the console's screen-flow runner), so every pin below
45+
* that needs a WARNED translation group plants one: a copy of the shipped
46+
* ledger directory whose `translation.json` gains a single synthetic
47+
* `planned` + `authorWarn` group row, read through the same
48+
* `lintLivenessPropertiesFromLedgerDir` seam the synthetic `field` ledger
49+
* further down uses. What those pins hold is the locale-keyed WALK and the
50+
* planned verdict's wording, not any shipped verdict; the shipped `flows` row
51+
* is pinned SILENT on the real ledger beside them.
52+
*/
53+
const SYNTH_WARNED_GROUP = 'synthWarnedGroup';
54+
const SYNTH_WARNED_GROUP_HINT = 'synthetic — this group row exists only in a test ledger directory';
55+
const synthWarnedGroup = { probe: { label: '探针' } };
56+
const plantedTranslationLedgerDir = () => ledgerDirWith((dir) => {
57+
const ledger = JSON.parse(readFileSync(join(dir, 'translation.json'), 'utf8'));
58+
ledger.props[SYNTH_WARNED_GROUP] = {
59+
status: 'planned',
60+
authorWarn: true,
61+
authorHint: SYNTH_WARNED_GROUP_HINT,
62+
};
63+
writeLedger(dir, 'translation', ledger);
64+
});
65+
const lintWithPlantedGroup = (stack: Record<string, unknown>) =>
66+
lintLivenessPropertiesFromLedgerDir(plantedTranslationLedgerDir(), stack);
4167
/** The rule id of the finding about `path`, or `undefined` when the rule said nothing about it. */
4268
const ruleOf = (findings: { message: string; rule: string }[], path: string) =>
4369
findings.find((f) => f.message.includes(`sets \`${path}\``))?.rule;
@@ -616,7 +642,9 @@ describe('lintLivenessProperties', () => {
616642
// (That enumeration is dated to its own commit and has since lost a member:
617643
// `field.relatedListFilter` left the warned set in #19187 when objectui#4664
618644
// reached the pin. The claim it supports — every warned entry is TOP-LEVEL, so
619-
// no dotted subject is left — is unchanged by a top-level row leaving.)
645+
// no dotted subject is left — is unchanged by a top-level row leaving.
646+
// `translation.flows` has left it too, by the same route: it went `live` once
647+
// the runner reading the flow's own `label` reached the pin.)
620648
// Filed as **#10262**, which also carries the recommendation not to play this
621649
// round a third time (test the WALKER against a synthetic warn map, and leave
622650
// the ledger-driven coupling to the assertions that are genuinely about the
@@ -820,40 +848,49 @@ describe('lintLivenessProperties', () => {
820848
// BROKEN walk, so it would have been green from the day the bug shipped and
821849
// pinned nothing.
822850
describe('translation bundles are locale-keyed (#11288)', () => {
823-
/** `flows` is the one `authorWarn` row on the shipped `translation` ledger. */
824-
const flowsGroup = { lead_conversion: { screens: { screen_1: { title: '转化详情' } } } };
851+
/**
852+
* A `flows` group as an author writes it. `flows` is `live` on the shipped
853+
* ledger, so it is authored below only where the pin is SILENCE; the walk
854+
* pins author the planted {@link SYNTH_WARNED_GROUP} instead.
855+
*/
856+
const flowsGroup = { lead_conversion: { label: '线索转化', screens: { screen_1: { title: '转化详情' } } } };
825857

826858
it('warns on a warned group authored under a locale entry', () => {
827-
const findings = lintLivenessProperties({
828-
translations: [{ 'zh-CN': { flows: flowsGroup } }],
859+
const findings = lintWithPlantedGroup({
860+
translations: [{ 'zh-CN': { [SYNTH_WARNED_GROUP]: synthWarnedGroup } }],
829861
});
830-
expect(paths(findings).some((m) => m.includes('`flows`'))).toBe(true);
862+
expect(paths(findings).some((m) => m.includes(`\`${SYNTH_WARNED_GROUP}\``))).toBe(true);
831863
expect(findings.map((f) => f.where)).toEqual(["translation bundle #0 · locale 'zh-CN'"]);
832-
expect(findings[0]?.hint).toContain('screen-flow runner');
864+
expect(findings[0]?.hint).toBe(SYNTH_WARNED_GROUP_HINT);
833865
});
834866

835867
// The walk has two levels and both can stop early. Authored on the SECOND
836868
// locale of the SECOND bundle, so neither an outer nor an inner walk that
837869
// visits only index 0 can pass this.
838870
it('reaches every locale of every bundle, not just the first of each', () => {
839-
const findings = lintLivenessProperties({
871+
const findings = lintWithPlantedGroup({
840872
translations: [
841873
{ en: { messages: { 'common.save': 'Save' } } },
842874
{
843875
ja: { messages: { 'common.save': '保存' } },
844-
'zh-CN': { flows: flowsGroup },
876+
'zh-CN': { [SYNTH_WARNED_GROUP]: synthWarnedGroup },
845877
},
846878
],
847879
});
848880
expect(findings.map((f) => f.where)).toEqual(["translation bundle #1 · locale 'zh-CN'"]);
849881
});
850882

883+
// `flows` is among the live groups authored here: the shipped row went
884+
// `live` once the runner read the flow's own `label` as well as `screens`,
885+
// so a half-reverted flip (the row `live` again but still opted in to a
886+
// warning, or `planned` again) surfaces as a finding on this bundle.
851887
it('stays silent on a bundle that authors only live groups', () => {
852888
const findings = lintLivenessProperties({
853889
translations: [{
854890
'zh-CN': {
855891
objects: { crm_lead: { label: '线索' } },
856892
messages: { 'common.save': '保存' },
893+
flows: flowsGroup,
857894
},
858895
}],
859896
});
@@ -869,7 +906,7 @@ describe('lintLivenessProperties', () => {
869906
it('the translation silence is a real verdict, not a lint that stopped loading ledgers', () => {
870907
const findings = lintLivenessProperties({
871908
objects: [{ name: 'widget', externalSharingModel: 'read' }],
872-
translations: [{ 'zh-CN': { objects: { crm_lead: { label: '线索' } } } }],
909+
translations: [{ 'zh-CN': { objects: { crm_lead: { label: '线索' } }, flows: flowsGroup } }],
873910
});
874911
const messages = findings.map((f) => f.message);
875912
expect(messages.some((m) => m.includes('externalSharingModel'))).toBe(true);
@@ -882,23 +919,26 @@ describe('lintLivenessProperties', () => {
882919
// (`packages/spec/src/kernel/metadata-type-schemas.ts:158`), not this
883920
// collection. It warns on the broken walk and must not here:
884921
// `stack.translations` is `z.record(LocaleSchema, TranslationDataSchema)`, so
885-
// this object would have to mean a locale named `flows` whose value is
886-
// `TranslationData` — a parse error two tiers before this advisory ever runs.
922+
// this object would have to mean a locale named after the group, whose value
923+
// is `TranslationData` — a parse error two tiers before this advisory runs.
887924
// Runtime-authored items reach this lint through no door at all: no stack
888925
// collection carries them, and the rule is `surfaces: CLI_ONLY`, so it does
889926
// not run at the runtime publish gate either.
927+
// Driven against the planted ledger, where the same group authored under a
928+
// locale DOES warn (the first pin of this block): on the shipped ledger no
929+
// translation group warns, so a silence there would pin nothing.
890930
it('does not treat a runtime `TranslationItem` shape as a bundle', () => {
891-
const findings = lintLivenessProperties({
892-
translations: [{ name: 'zh_cn', locale: 'zh-CN', flows: flowsGroup }],
931+
const findings = lintWithPlantedGroup({
932+
translations: [{ name: 'zh_cn', locale: 'zh-CN', [SYNTH_WARNED_GROUP]: synthWarnedGroup }],
893933
});
894934
expect(findings).toEqual([]);
895935
});
896936

897937
// "Advisory only — returns findings, never throws" is the function's own
898938
// contract, and a bundle walk adds two levels that can be malformed.
899939
it('never throws on a malformed bundle, and keeps walking past it', () => {
900-
const findings = lintLivenessProperties({
901-
translations: [null, { 'zh-CN': null }, { en: { flows: flowsGroup } }],
940+
const findings = lintWithPlantedGroup({
941+
translations: [null, { 'zh-CN': null }, { en: { [SYNTH_WARNED_GROUP]: synthWarnedGroup } }],
902942
});
903943
// The bundle is the third list item the author wrote and is reported as
904944
// `#1`: since #15636 this walk reads through `recordsOf`, which drops the
@@ -1098,8 +1138,10 @@ describe('the array fan-out, against a synthetic warn map (#10262)', () => {
10981138
// the `dead` branch, so the finding's MESSAGE told the author to remove
10991139
// something the ledger's own `authorHint`/`note` on the SAME finding said to
11001140
// keep. `field.relatedListFilter`, `object.externalSharingModel` and
1101-
// `translation.flows` are the three shipped rows this hit — two of them still,
1102-
// the third only until #19187 flipped `field.relatedListFilter` `live`.
1141+
// `translation.flows` are the three shipped rows this hit. Only
1142+
// `object.externalSharingModel` still does: `field.relatedListFilter` and
1143+
// `translation.flows` have both gone `live`, each once its reader reached the
1144+
// `.objectui-sha` pin.
11031145
//
11041146
// The real ledgers currently have PLANNED rows and EXPERIMENTAL rows, but — as
11051147
// this file's other comments document at length (#2377, #3896, #4509) — no
@@ -1114,29 +1156,48 @@ describe('dead / experimental / planned / live-elsewhere verdicts are distinct,
11141156
const oneEntry = (entry: Record<string, unknown>) => new Map([['gizmo', entry]]);
11151157

11161158
// ── REAL LEDGER: the three rows the card captured ──────────────────────
1117-
it('REAL LEDGER: translation.flows (planned) — planned rule id, non-contradictory message, hint preserved', () => {
1159+
//
1160+
// `translation.flows` no longer has a planned REAL LEDGER case: it went
1161+
// `live` once the console's screen-flow runner read the flow's own `label`
1162+
// as well as `screens`, so it warns about nothing and can pin no verdict.
1163+
// What its case held is split in two below and nothing is dropped: the
1164+
// shipped row's SILENCE on the real ledger, and the planned verdict's wording
1165+
// WITH an authorHint (rule id, non-contradictory message, hint shown) on a
1166+
// planted translation group, the one shape no shipped row carries any more.
1167+
it('REAL LEDGER: translation.flows is `live` — authoring it warns nothing, in a call that still warns', () => {
11181168
const findings = lintLivenessProperties({
1169+
objects: [{ name: 'widget', externalSharingModel: 'read' }],
11191170
translations: [{
1120-
'zh-CN': { flows: { lead_conversion: { screens: { screen_1: { title: '转化详情' } } } } },
1171+
'zh-CN': { flows: { lead_conversion: { label: '线索转化', screens: { screen_1: { title: '转化详情' } } } } },
11211172
}],
11221173
});
1174+
// Anti-vacuity: the same call, the same ledger load, still raises the one
1175+
// planned row in tree, so the silence is a reading and not an unloaded lint.
1176+
expect(findings.map((f) => f.message).filter((m) => m.includes('externalSharingModel'))).toHaveLength(1);
1177+
expect(findings.filter((f) => f.where.startsWith('translation bundle'))).toEqual([]);
1178+
});
1179+
1180+
it('PLANTED: a planned + authorWarn translation group with an authorHint — planned rule id, non-contradictory message, hint preserved', () => {
1181+
const findings = lintWithPlantedGroup({
1182+
translations: [{ 'zh-CN': { [SYNTH_WARNED_GROUP]: synthWarnedGroup } }],
1183+
});
11231184
expect(findings).toHaveLength(1);
11241185
const [f] = findings;
11251186
expect(f.rule).toBe('liveness-planned-property');
11261187
expect(f.message).not.toContain('dead');
11271188
expect(f.message).toContain('is planned');
1128-
// The card's own captured hint — unchanged by this fix, just no longer
1129-
// contradicted by the message sitting next to it.
1130-
expect(f.hint).toContain('screen-flow runner');
1189+
// The row's own authorHint, shown as written and not contradicted by the
1190+
// message sitting next to it.
1191+
expect(f.hint).toBe(SYNTH_WARNED_GROUP_HINT);
11311192
});
11321193

11331194
// ⚠️ The third of the card's rows, `field.relatedListFilter`, no longer has a
11341195
// REAL LEDGER case here: #19187 flipped it `live` once objectui#4664 reached
11351196
// the `.objectui-sha` pin, so it warns about nothing and can pin no verdict.
11361197
// Its silence is pinned in the `field related-list filter (#19187)` block
11371198
// above, where a half-reverted flip surfaces. The PLANNED branch stays a
1138-
// contract test on the two rows that are still planned — `translation.flows`
1139-
// just above and `object.externalSharingModel` just below.
1199+
// contract test on the one row that is still planned,
1200+
// `object.externalSharingModel` just below.
11401201

11411202
it('REAL LEDGER: object.externalSharingModel (planned, no authorHint) — planned rule id, the planned DEFAULT hint, never its ledger note', () => {
11421203
const findings = lintLivenessProperties({ objects: [{ name: 'widget', externalSharingModel: 'read' }] });
@@ -1343,30 +1404,54 @@ describe('dead / experimental / planned / live-elsewhere verdicts are distinct,
13431404
// The walker now asks THIS function which groups are warned instead of reading
13441405
// the same JSON a second time. These pins hold the two readings equal.
13451406
describe('authorWarnedProperties', () => {
1346-
it('reports the `flows` translation group as warned (the row the CLI gate reads)', () => {
1347-
expect(authorWarnedProperties('translation').has('flows')).toBe(true);
1407+
it('reports no translation group as warned now that `flows` is live (the set the CLI gate reads)', () => {
1408+
// `flows` was the one warned group, and it went `live` once the runner read
1409+
// both its halves. An equality, so the day a translation group acquires a
1410+
// warn this goes red and the CLI's coverage gate is looked at with it.
1411+
expect([...authorWarnedProperties('translation')]).toEqual([]);
1412+
// The positive the empty answer needs, from the same reader in the same
1413+
// process: the one planned + authorWarn row still in tree.
1414+
expect(authorWarnedProperties('object').has('externalSharingModel')).toBe(true);
13481415
});
13491416

13501417
it('can say no — a live group in the same ledger is absent', () => {
1351-
// The negative half of the instrument: `objects` is `live` and sits in the
1352-
// same file as the positive above, so a set that swallowed everything or
1353-
// returned everything would fail one of the two.
1418+
// The negative half of the instrument, beside the positive above (a warned
1419+
// `object` row): a set that swallowed everything or returned everything
1420+
// would fail one of the two.
13541421
const warned = authorWarnedProperties('translation');
13551422
expect(warned.has('objects')).toBe(false);
13561423
expect(warned.has('pages')).toBe(false);
1424+
expect(warned.has('flows')).toBe(false);
13571425
});
13581426

1359-
it('agrees with the rule itself on every path it names — the anti-drift pin', () => {
1427+
it('agrees with the rule itself on every translation group — the anti-drift pin', () => {
13601428
// The whole point of exporting this is that the demand side and the warn
1361-
// side cannot disagree. So: authoring each warned top-level group in a
1362-
// locale bundle must actually produce a finding naming that group. A path
1363-
// this set reports but the rule stays silent on is the drift that reopens
1364-
// the collision from the other end.
1365-
for (const path of authorWarnedProperties('translation')) {
1366-
if (path.includes('.')) continue; // container rows only — see getNested
1367-
const findings = lintLivenessProperties({
1368-
translations: [{ 'zh-CN': { [path]: { probe: { label: 'x' } } } }],
1369-
});
1429+
// side cannot disagree. With no translation group warned, a loop over the
1430+
// exported set would assert nothing, so this asks over the WHOLE domain:
1431+
// every top-level row the shipped `translation` ledger governs is authored
1432+
// in one locale entry, and the groups the rule names must be exactly the
1433+
// groups this set names, in both directions.
1434+
const ledger = JSON.parse(readFileSync(join(shippedLedgerDir(), 'translation.json'), 'utf8'));
1435+
const groups = Object.keys(ledger.props);
1436+
expect(groups).toContain('flows');
1437+
expect(groups.length).toBeGreaterThanOrEqual(10);
1438+
const findings = lintLivenessProperties({
1439+
translations: [{ 'zh-CN': Object.fromEntries(groups.map((g) => [g, { probe: { label: 'x' } }])) }],
1440+
});
1441+
const named = groups.filter((g) => findings.some((f) => f.message.includes(`sets \`${g}\``)));
1442+
const exported = [...authorWarnedProperties('translation')].filter((p) => !p.includes('.'));
1443+
expect(named.sort()).toEqual(exported.sort());
1444+
});
1445+
1446+
it('agrees with the rule itself on every object path it names — the same pin over a non-empty set', () => {
1447+
// The direction the translation pin above cannot exercise today ("a path
1448+
// this set reports but the rule stays silent on"), held on the type that
1449+
// still has a warned row. `true` is authored because the rule warns on a
1450+
// boolean only when it is true and on anything else when present.
1451+
const warned = [...authorWarnedProperties('object')].filter((p) => !p.includes('.'));
1452+
expect(warned.length).toBeGreaterThan(0);
1453+
for (const path of warned) {
1454+
const findings = lintLivenessProperties({ objects: [{ name: 'widget', [path]: true }] });
13701455
expect(findings.map((f) => f.message).join('\n')).toContain(`sets \`${path}\``);
13711456
}
13721457
});

0 commit comments

Comments
 (0)