Skip to content

Commit 6fa2a8a

Browse files
claude[bot]claude
andauthored
test(cli): pin the per-package leg's resolution context, both directions (#17724)
* test(cli): pin the per-package leg's resolution context, both directions `compile.ts`'s `packageBodyAsStack` hands each package's body the artifact's own `packages[]` as resolution context, so a reference into an object a SIBLING package of the same artifact ships resolves while one no entry provides still errors. Neither half had an in-package pin: the firing direction was held only by `examples/app-multi-package`, and the non-degeneracy direction by a `packages/lint` control whose input is a local three-key replica of `packageBodyAsStack` -- green even if this command stops building that shape. One fixture measures both, because the discriminating fact is WHICH of two lookups on the package's object is reported. The top level carries the sibling object alone, so `objects` is present, `authoringRuleUnionStack` folds nothing (it fills absent keys only), and the union run never sees the referencing object's fields -- whatever the build reports about them comes from the per-package leg and nowhere else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c * test(cli): record the union-leg reading the pin rests on, measured The case's docblock argued that the union author-time rule run cannot be the reporter for `probe_order`'s two lookups. That argument is now three readings, each reddening only this case and leaving the other six in the file green: 1. drop `packages[]` from `packageBodyAsStack` -> the equality receives BOTH paths, `objects[0].fields.account.reference` first; 2. hand the per-package leg no `objects` -- the "skip the site per package" option the ruling rejected -- and `os build` exits 0 with `success: true`, so the union run is not a second reporter for these paths and nothing else in the command catches the dangling reference either; 3. add `probeOrder` to the TOP-LEVEL `objects` and the union run does report it, answering `error: 'author-time rules failed'` -- which is why that string is asserted before the paths are read. The fold-skip premise the fixture's placement relies on is not re-derived here either: `src/utils/stack-collections.test.ts` already pins it BY IDENTITY on this exact shape, so the docblock cites that pin instead of re-arguing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0641640 commit 6fa2a8a

1 file changed

Lines changed: 91 additions & 1 deletion

File tree

packages/cli/test/build-multi-package-artifact.e2e.test.ts

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,77 @@ export default {
132132
};
133133
`;
134134

135+
/**
136+
* [#17169 — F1 of #17066's verdict] The per-package leg's RESOLUTION CONTEXT,
137+
* both directions in ONE build.
138+
*
139+
* `compile.ts`'s `packageBodyAsStack` hands each package's body the artifact's
140+
* own `packages[]` as resolution context (#16611), so a reference into an
141+
* object a SIBLING package of the same artifact ships RESOLVES, while one no
142+
* entry provides still ERRORS — the second half being the whole difference
143+
* between the ruled fix and "skip the site per package".
144+
*
145+
* One fixture measures both, because the discriminating fact is WHICH of the
146+
* two lookups on `probe_order` is reported: `account` (a sibling provides it)
147+
* must not be, `ghost` (nothing provides it) must be. Dropping the `packages[]`
148+
* pass-through reports both; dropping the judging reports neither.
149+
*
150+
* ⚠️ The top level carries `probe_account`, so `objects` is PRESENT and
151+
* `authoringRuleUnionStack` folds nothing into it — it only ever fills ABSENT
152+
* keys, which `src/utils/stack-collections.test.ts` pins BY IDENTITY on exactly
153+
* this shape (a stack carrying both its collections and `packages[]`). So the
154+
* union run never sees `probe_order`'s fields, and this is a pin on
155+
* `compile.ts`'s half rather than a second copy of `packages/lint`'s rule test,
156+
* whose input is a local three-key REPLICA of `packageBodyAsStack`
157+
* (`perPackageStack`) and stays green if this command stops building that
158+
* shape.
159+
*
160+
* ⭐ That last claim is MEASURED rather than argued. Three ablations, each
161+
* reddening THIS case alone and leaving the other six in this file green:
162+
*
163+
* 1. drop `packages[]` from `packageBodyAsStack` (the pre-#16611 shape) — the
164+
* equality below receives BOTH paths, `account` first;
165+
* 2. hand the per-package leg no `objects` at all, i.e. the "skip the site per
166+
* package" option the ruling rejected — `os build` exits **0** with
167+
* `success: true`, so the union run is NOT a second reporter for these
168+
* paths and nothing else in the command catches the dangling reference
169+
* either;
170+
* 3. add `probeOrder` to the TOP-LEVEL `objects` — the union run does report
171+
* it, and `error` reads `author-time rules failed`. That is why the error
172+
* string is asserted BEFORE the paths are read: it is the only thing that
173+
* tells the two exits apart from outside.
174+
*/
175+
const CONFIG_PKG_REFS = `
176+
const coreManifest = { id: 'com.example.probe.core', name: 'core', version: '1.0.0', type: 'app', namespace: 'probe' };
177+
const probeAccount = {
178+
name: 'probe_account', label: 'Account', sharingModel: 'private',
179+
fields: { name: { type: 'text', label: 'Name' } },
180+
};
181+
const probeOrder = {
182+
name: 'probe_order', label: 'Order', sharingModel: 'private',
183+
fields: {
184+
name: { type: 'text', label: 'Number' },
185+
account: { type: 'lookup', label: 'Account', reference: 'probe_account' },
186+
ghost: { type: 'lookup', label: 'Ghost', reference: 'probe_nothing' },
187+
},
188+
};
189+
190+
export default {
191+
manifest: coreManifest,
192+
objects: [probeAccount],
193+
packages: [
194+
{ manifest: { ...coreManifest, objects: [probeAccount] } },
195+
{
196+
manifest: {
197+
id: 'com.example.probe.orders', name: 'orders', version: '1.0.0', type: 'module', namespace: 'probe',
198+
dependencies: { 'com.example.probe.core': '^1.0.0' },
199+
objects: [probeOrder],
200+
},
201+
},
202+
],
203+
};
204+
`;
205+
135206
interface Artifact {
136207
manifest?: { id?: string };
137208
objects?: Array<{ name: string }>;
@@ -147,7 +218,7 @@ const artifactOf = (dir: string): Artifact =>
147218

148219
beforeAll(() => {
149220
root = mkdtempSync(join(tmpdir(), 'os-multi-package-'));
150-
for (const [name, config] of Object.entries({ multi: CONFIG_MULTI, single: CONFIG_SINGLE, globs: CONFIG_GLOBS })) {
221+
for (const [name, config] of Object.entries({ multi: CONFIG_MULTI, single: CONFIG_SINGLE, globs: CONFIG_GLOBS, refs: CONFIG_PKG_REFS })) {
151222
const dir = join(root, name);
152223
mkdirSync(dir, { recursive: true });
153224
writeFileSync(join(dir, 'objectstack.config.ts'), config);
@@ -217,6 +288,25 @@ describe('ADR-0130 D4 — `os build` emits one artifact carrying `packages[]`',
217288
const paths = (payload.errors ?? []).map((e) => (e.path ?? []).join('.'));
218289
expect(paths).toContain('packages.0.manifest.objects.0');
219290
}, 180_000);
291+
292+
it('resolves a SIBLING package\'s object on the per-package leg, and still errors on an artifact-wide dangling one', async () => {
293+
const run = await runCli(['build', '--json'], dirs.refs);
294+
expect(run.code, `${run.stdout}\n${run.stderr}`).toBe(1);
295+
const payload = JSON.parse(run.stdout) as {
296+
error?: string;
297+
issues?: Array<{ rule: string; path: string; package?: string }>;
298+
};
299+
// WHICH leg spoke, asserted rather than assumed: the union run's own
300+
// failure exit carries `error: 'author-time rules failed'`, so this string
301+
// is the only thing that distinguishes the two exits from outside.
302+
expect(payload.error).toBe('author-time rules failed for one or more packages');
303+
const refs = (payload.issues ?? []).filter((i) => i.rule === 'object-reference-unknown');
304+
// Both directions in one equality: `ghost` is present (the leg still
305+
// JUDGES) and `account` is absent (the leg RESOLVED it through the
306+
// artifact's `packages[]`). A pass-through that went missing reports both.
307+
expect(refs.map((i) => i.path)).toEqual(['objects[0].fields.ghost.reference']);
308+
expect(refs[0].package).toBe('com.example.probe.orders');
309+
}, 180_000);
220310
});
221311

222312
describe('ADR-0130 D7 — the single-package path is untouched', () => {

0 commit comments

Comments
 (0)