Skip to content
Merged
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
92 changes: 91 additions & 1 deletion packages/cli/test/build-multi-package-artifact.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,77 @@ export default {
};
`;

/**
* [#17169 — F1 of #17066's verdict] The per-package leg's RESOLUTION CONTEXT,
* both directions in ONE build.
*
* `compile.ts`'s `packageBodyAsStack` hands each package's body the artifact's
* own `packages[]` as resolution context (#16611), so a reference into an
* object a SIBLING package of the same artifact ships RESOLVES, while one no
* entry provides still ERRORS — the second half being the whole difference
* between the ruled fix and "skip the site per package".
*
* One fixture measures both, because the discriminating fact is WHICH of the
* two lookups on `probe_order` is reported: `account` (a sibling provides it)
* must not be, `ghost` (nothing provides it) must be. Dropping the `packages[]`
* pass-through reports both; dropping the judging reports neither.
*
* ⚠️ The top level carries `probe_account`, so `objects` is PRESENT and
* `authoringRuleUnionStack` folds nothing into it — it only ever fills ABSENT
* keys, which `src/utils/stack-collections.test.ts` pins BY IDENTITY on exactly
* this shape (a stack carrying both its collections and `packages[]`). So the
* union run never sees `probe_order`'s fields, and this is a pin on
* `compile.ts`'s half rather than a second copy of `packages/lint`'s rule test,
* whose input is a local three-key REPLICA of `packageBodyAsStack`
* (`perPackageStack`) and stays green if this command stops building that
* shape.
*
* ⭐ That last claim is MEASURED rather than argued. Three ablations, each
* reddening THIS case alone and leaving the other six in this file green:
*
* 1. drop `packages[]` from `packageBodyAsStack` (the pre-#16611 shape) — the
* equality below receives BOTH paths, `account` first;
* 2. hand the per-package leg no `objects` at all, i.e. the "skip the site per
* package" option the ruling rejected — `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` — the union run does report
* it, and `error` reads `author-time rules failed`. That is why the error
* string is asserted BEFORE the paths are read: it is the only thing that
* tells the two exits apart from outside.
*/
const CONFIG_PKG_REFS = `
const coreManifest = { id: 'com.example.probe.core', name: 'core', version: '1.0.0', type: 'app', namespace: 'probe' };
const probeAccount = {
name: 'probe_account', label: 'Account', sharingModel: 'private',
fields: { name: { type: 'text', label: 'Name' } },
};
const probeOrder = {
name: 'probe_order', label: 'Order', sharingModel: 'private',
fields: {
name: { type: 'text', label: 'Number' },
account: { type: 'lookup', label: 'Account', reference: 'probe_account' },
ghost: { type: 'lookup', label: 'Ghost', reference: 'probe_nothing' },
},
};

export default {
manifest: coreManifest,
objects: [probeAccount],
packages: [
{ manifest: { ...coreManifest, objects: [probeAccount] } },
{
manifest: {
id: 'com.example.probe.orders', name: 'orders', version: '1.0.0', type: 'module', namespace: 'probe',
dependencies: { 'com.example.probe.core': '^1.0.0' },
objects: [probeOrder],
},
},
],
};
`;

interface Artifact {
manifest?: { id?: string };
objects?: Array<{ name: string }>;
Expand All @@ -147,7 +218,7 @@ const artifactOf = (dir: string): Artifact =>

beforeAll(() => {
root = mkdtempSync(join(tmpdir(), 'os-multi-package-'));
for (const [name, config] of Object.entries({ multi: CONFIG_MULTI, single: CONFIG_SINGLE, globs: CONFIG_GLOBS })) {
for (const [name, config] of Object.entries({ multi: CONFIG_MULTI, single: CONFIG_SINGLE, globs: CONFIG_GLOBS, refs: CONFIG_PKG_REFS })) {
const dir = join(root, name);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'objectstack.config.ts'), config);
Expand Down Expand Up @@ -217,6 +288,25 @@ describe('ADR-0130 D4 — `os build` emits one artifact carrying `packages[]`',
const paths = (payload.errors ?? []).map((e) => (e.path ?? []).join('.'));
expect(paths).toContain('packages.0.manifest.objects.0');
}, 180_000);

it('resolves a SIBLING package\'s object on the per-package leg, and still errors on an artifact-wide dangling one', async () => {
const run = await runCli(['build', '--json'], dirs.refs);
expect(run.code, `${run.stdout}\n${run.stderr}`).toBe(1);
const payload = JSON.parse(run.stdout) as {
error?: string;
issues?: Array<{ rule: string; path: string; package?: string }>;
};
// WHICH leg spoke, asserted rather than assumed: the union run's own
// failure exit carries `error: 'author-time rules failed'`, so this string
// is the only thing that distinguishes the two exits from outside.
expect(payload.error).toBe('author-time rules failed for one or more packages');
const refs = (payload.issues ?? []).filter((i) => i.rule === 'object-reference-unknown');
// Both directions in one equality: `ghost` is present (the leg still
// JUDGES) and `account` is absent (the leg RESOLVED it through the
// artifact's `packages[]`). A pass-through that went missing reports both.
expect(refs.map((i) => i.path)).toEqual(['objects[0].fields.ghost.reference']);
expect(refs[0].package).toBe('com.example.probe.orders');
}, 180_000);
});

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