Skip to content

Commit b46217a

Browse files
os-zhuangclaude
andauthored
test(objectql): guard overlay-only cross-package collision provenance (ADR-0048 #1828) (#3252)
Adds the getMetaItems case that unit tests missed and dogfooding caught: two packages with ONLY a sys_metadata overlay each (no registered artifact) shipping the same `type/name`. The list-decorate step grafts each row's artifact via `getArtifactItem(type, name, <row's own package>)`; with no composite artifact it must not fall back to the OTHER package's bare-key hydrated overlay, or both rows get stamped with the last-hydrated package's `_packageId`. Reverting the `getArtifactItem` package-scope guard fails this test (`expected 'Globex home' to be 'Acme home'`), so it locks the fix. Test-only; no runtime change. Claude-Session: https://claude.ai/code/session_01DKFjwN2EVXPB2CJwNzPs8c Co-authored-by: Claude <noreply@anthropic.com>
1 parent 15dbe18 commit b46217a

1 file changed

Lines changed: 24 additions & 0 deletions

File tree

packages/objectql/src/protocol-meta.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,30 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => {
891891
expect(byPkg['com.globex.crm']).toBe('Globex (overlay)');
892892
});
893893

894+
it('keeps both colliding rows when NEITHER package ships an artifact — overlay-only (ADR-0048 #1828)', async () => {
895+
// The case unit tests missed and dogfooding caught: two packages with
896+
// ONLY a sys_metadata overlay each (NO registered artifact — registry
897+
// stays empty for `page`). The list-decorate step grafts each row's
898+
// artifact via getArtifactItem(type, name, <row's own package>); with no
899+
// composite artifact it must NOT fall back to the OTHER package's
900+
// bare-key hydrated overlay, or both rows get stamped with the
901+
// last-hydrated package's `_packageId` (the exact provenance bug the
902+
// getArtifactItem package-scope fix closes).
903+
mockEngine.find.mockResolvedValue([
904+
{ type: 'page', name: 'home', state: 'active', package_id: 'com.acme.crm', metadata: JSON.stringify({ name: 'home', label: 'Acme home' }) },
905+
{ type: 'page', name: 'home', state: 'active', package_id: 'com.globex.crm', metadata: JSON.stringify({ name: 'home', label: 'Globex home' }) },
906+
]);
907+
908+
const result = await protocol.getMetaItems({ type: 'page' });
909+
const homes = (result.items as any[]).filter((i) => i.name === 'home');
910+
911+
expect(homes).toHaveLength(2);
912+
const byPkg = Object.fromEntries(homes.map((h: any) => [h._packageId, h.label]));
913+
// Each row keeps its OWN package — not both stamped with one id.
914+
expect(byPkg['com.acme.crm']).toBe('Acme home');
915+
expect(byPkg['com.globex.crm']).toBe('Globex home');
916+
});
917+
894918
it('single-package env-wide (package-less) overlay still overlays the artifact (ADR-0005 unchanged, #1828)', async () => {
895919
// A package-less overlay (package_id IS NULL) must still WIN over the
896920
// one artifact it customizes and stay a single row — the package-aware

0 commit comments

Comments
 (0)