Skip to content

Commit 6b47b8d

Browse files
dmealingclaude
andcommitted
test(runtime-ts): #368 — pin @sourceRefField at both relation-resolver call sites
Both existing tests resolve by NAME PAIRING (ladder step 3), so dropping the 4th argument from either findReferenceFkField call in relation-resolver.ts left them green — nothing proved @sourceRefField was read at runtime at all. The new model names its relationships so they pair with NOTHING ("winner"/"loser" against alphaRef/betaRef) and crosses the declared FKs against declaration order, so only ladder step 2 can resolve them. Verified: dropping the 4th argument at either call site turns both new tests red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfd8nat1WcSpXffd8YetiC
1 parent 3937057 commit 6b47b8d

1 file changed

Lines changed: 57 additions & 0 deletions

File tree

‎server/typescript/packages/runtime-ts/test/relation-resolver-two-refs.test.ts‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,33 @@ const MANY_SIDE_MODEL = {
7474
},
7575
};
7676

77+
// @sourceRefField model: neither relationship name pairs with either reference
78+
// ("winner"/"loser" vs alphaRef(alphaFk)/betaRef(betaFk)), so ladder step 3 cannot
79+
// resolve them and only the declared @sourceRefField (step 2) can. Declaration
80+
// order is deliberate: alphaRef comes first, so a lookup that ignored
81+
// @sourceRefField would return alphaFk for BOTH relations.
82+
const SOURCE_REF_FIELD_MODEL = {
83+
"metadata.root": {
84+
package: "repro",
85+
children: [
86+
{ "object.entity": { name: "Team", children: [
87+
{ "field.int": { name: "id" } },
88+
{ "identity.primary": { name: "id", "@fields": ["id"], "@generation": "increment" } },
89+
] } },
90+
{ "object.entity": { name: "Match", children: [
91+
{ "field.int": { name: "id" } },
92+
{ "field.int": { name: "alphaFk" } },
93+
{ "field.int": { name: "betaFk" } },
94+
{ "identity.primary": { name: "id", "@fields": ["id"], "@generation": "increment" } },
95+
{ "identity.reference": { name: "alphaRef", "@fields": ["alphaFk"], "@references": "Team" } },
96+
{ "identity.reference": { name: "betaRef", "@fields": ["betaFk"], "@references": "Team" } },
97+
{ "relationship.association": { name: "winner", "@objectRef": "Team", "@cardinality": "one", "@sourceRefField": "betaFk" } },
98+
{ "relationship.association": { name: "loser", "@objectRef": "Team", "@cardinality": "one", "@sourceRefField": "alphaFk" } },
99+
] } },
100+
],
101+
},
102+
};
103+
77104
async function load(model: unknown): Promise<MetaRoot> {
78105
const { root, errors } = await new MetaDataLoader().load([
79106
new InMemoryStringSource(JSON.stringify(model), { id: "meta.repro.json" }),
@@ -104,4 +131,34 @@ describe("resolveRelationDescriptor with two references onto one target (#368)",
104131
expect(desc.targetEntityName).toBe("Match");
105132
expect(desc.targetField).toBe("homeTeamId"); // was "awayTeamId"
106133
});
134+
135+
// Both tests above resolve by NAME PAIRING (ladder step 3), so they stay green if
136+
// the @sourceRefField argument is dropped at either call site — nothing proved it
137+
// was ever read here. These two models name the relationships so they pair with
138+
// NOTHING ("winner" / "loser" against alphaRef / betaRef), which makes a declared
139+
// @sourceRefField (ladder step 2) the only thing that can resolve them: drop the
140+
// 4th argument at either call site and the resolver throws "has no
141+
// identity.reference targeting 'Team'".
142+
143+
test("one-side: @sourceRefField reaches the resolver when the name pairs with nothing", async () => {
144+
const root = await load(SOURCE_REF_FIELD_MODEL);
145+
const match = root.findObject("Match")!;
146+
// Deliberately crossed against declaration order: winner -> betaFk (the SECOND
147+
// reference), loser -> alphaFk.
148+
expect(resolveRelationDescriptor(match, "winner", root).sourceField).toBe("betaFk");
149+
expect(resolveRelationDescriptor(match, "loser", root).sourceField).toBe("alphaFk");
150+
});
151+
152+
test("many-side: @sourceRefField reaches the inverse resolver too", async () => {
153+
const root = await load(SOURCE_REF_FIELD_MODEL);
154+
const team = root.findObject("Team")!;
155+
// Team's only reachable inverse name is "matches", which resolves to Match's
156+
// FIRST cardinality:one relationship targeting Team — "winner", whose
157+
// @sourceRefField names betaFk here. alphaRef is declared FIRST, so a resolver
158+
// that ignored @sourceRefField would answer "alphaFk".
159+
const desc = resolveRelationDescriptor(team, "matches", root);
160+
expect(desc.cardinality).toBe("many");
161+
expect(desc.targetEntityName).toBe("Match");
162+
expect(desc.targetField).toBe("betaFk");
163+
});
107164
});

0 commit comments

Comments
 (0)