Skip to content

Commit b51eeae

Browse files
committed
test(core): pin the import's stored-title rung and the formula skip
Wren resolves through a stored displayNameField / nameField after the id and the lookup's displayField; a formula title (John Smith, Acme (EMEA)) is never asked by where and still answers reference_not_found; two records sharing a stored title answer reference_ambiguous; a target whose exposure refuses list is still never read. Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5b32d74 commit b51eeae

1 file changed

Lines changed: 162 additions & 0 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#24981] An import cell is resolved against a lookup TARGET's own title
5+
* field: ADR-0079's `resolveDisplayField`, the one rule the list export writes
6+
* a lookup cell by. It is asked after the id and the lookup's own
7+
* `displayField`, and before the fixed fallback keys, which are unchanged.
8+
*
9+
* Only a STORED title is asked. A title the target declares virtual (a
10+
* `formula`) has no column, and the engine's filter seam
11+
* (`assertFilterIsMaterializable`) refuses every `where` over one by the spec
12+
* predicate `isVirtualSearchField`; the resolver asks the same predicate and
13+
* never sends that query. How a formula title should match is the open half of
14+
* the card, with the maintainer, so such a cell still answers
15+
* `reference_not_found`.
16+
*
17+
* The fake's `findData` answers like the engine: equality over the stored row,
18+
* and an `INVALID_FIELD` / 400 refusal for a `where` over a formula field.
19+
*/
20+
21+
import { describe, it, expect, vi } from 'vitest';
22+
import { runImport, type ImportProtocolLike } from './import-runner';
23+
import type { ExportFieldMeta } from './import-field-meta.js';
24+
25+
type FindArgs = Parameters<ImportProtocolLike['findData']>[0];
26+
type Row = Record<string, unknown>;
27+
type Declaration = { fields: Record<string, Row>; nameField?: string; displayNameField?: string; enable?: unknown };
28+
29+
const TEXT = { type: 'text' };
30+
const FORMULA = { type: 'formula', returnType: 'text' };
31+
32+
/** The card's three targets, plus one whose stored title two records share. */
33+
const DECLARATIONS: Record<string, Declaration> = {
34+
// `Wren`: a STORED title off the fixed key list.
35+
xt_person: { displayNameField: 'nickname', fields: { first_name: TEXT, nickname: TEXT } },
36+
// `John Smith`: a FORMULA title whose name IS on the fixed key list.
37+
xt_contact: {
38+
nameField: 'full_name',
39+
fields: { first_name: TEXT, last_name: TEXT, title: TEXT, full_name: { ...FORMULA, expression: 'first_name + " " + last_name' } },
40+
},
41+
// `Acme (EMEA)`: a FORMULA title off the fixed key list.
42+
xt_account: {
43+
nameField: 'display_title',
44+
fields: { name: TEXT, region: TEXT, display_title: { ...FORMULA, expression: 'name + " (" + region + ")"' } },
45+
},
46+
xt_member: { nameField: 'nickname', fields: { nickname: TEXT } },
47+
};
48+
49+
const ROWS: Record<string, Row[]> = {
50+
xt_person: [{ id: 'p1', first_name: 'Ann', nickname: 'Wren' }],
51+
xt_contact: [{ id: 'c1', first_name: 'John', last_name: 'Smith', title: 'VP of Engineering' }],
52+
xt_account: [{ id: 'a1', name: 'Acme', region: 'EMEA' }],
53+
xt_member: [{ id: 'm1', nickname: 'Robin' }, { id: 'm2', nickname: 'Robin' }],
54+
};
55+
56+
const lookup = (name: string, reference: string, extra: Partial<ExportFieldMeta> = {}): [string, ExportFieldMeta] =>
57+
[name, { name, type: 'lookup', reference, ...extra }];
58+
59+
const metaMap = new Map<string, ExportFieldMeta>([
60+
['name', { name: 'name', type: 'text' }],
61+
// The lookup's own `displayField` is asked before the target's title.
62+
lookup('person', 'xt_person', { displayField: 'first_name' }),
63+
lookup('contact', 'xt_contact'),
64+
lookup('account', 'xt_account'),
65+
lookup('member', 'xt_member'),
66+
]);
67+
68+
function protocol(overrides: Record<string, Partial<Declaration>> = {}) {
69+
const refusals: Array<{ object: string; field: string; code: string; status: number }> = [];
70+
const findData = vi.fn(async (args: FindArgs) => {
71+
const decl = { ...DECLARATIONS[args.object], ...overrides[args.object] };
72+
const [[field, value]] = Object.entries(args.query!.where!);
73+
if (decl.fields?.[field]?.type === 'formula') {
74+
const err = Object.assign(new Error(`filters on '${field}', a virtual formula field`), { code: 'INVALID_FIELD', status: 400 });
75+
refusals.push({ object: args.object, field, code: err.code, status: err.status });
76+
throw err;
77+
}
78+
return (ROWS[args.object] ?? []).filter((r) => r[field] === value);
79+
});
80+
const getMetaItem = vi.fn(async ({ name }: { type: string; name: string }) =>
81+
({ type: 'object', name, item: { name, ...DECLARATIONS[name], ...overrides[name] } }));
82+
const p: ImportProtocolLike = { findData, getMetaItem, createData: vi.fn(async (a) => ({ id: 'new', ...a.data })), updateData: vi.fn() };
83+
return { p, findData, getMetaItem, refusals };
84+
}
85+
86+
const run = (p: ImportProtocolLike, rows: Row[]) =>
87+
runImport({
88+
p, context: { userId: 'u_member', roles: ['member'] }, objectName: 'xt_source', metaMap, writeMode: 'insert',
89+
matchFields: [], dryRun: true, runAutomations: false, trimWhitespace: true, createMissingOptions: false,
90+
skipBlankMatchKey: false, rows,
91+
});
92+
93+
/** The `where` keys asked of one target, in order. */
94+
const askedOf = (findData: ReturnType<typeof protocol>['findData'], object: string) =>
95+
findData.mock.calls.filter(([a]) => a.object === object).map(([a]) => Object.keys(a.query!.where!)[0]);
96+
97+
const verdict = (r: { ok: boolean; code?: string; field?: string }) => ({ ok: r.ok, code: r.code, field: r.field });
98+
const OK = { ok: true, code: undefined, field: undefined };
99+
100+
describe('[#24981] import resolves a lookup cell by the target object\'s stored title field', () => {
101+
for (const [label, override] of [
102+
['displayNameField (the card\'s cell, as authored)', {}],
103+
['nameField (the alias as the registry normalizes it)', { displayNameField: undefined, nameField: 'nickname' }],
104+
] as const) {
105+
it(`Wren resolves through a stored ${label}, asked after the id and the lookup's displayField`, async () => {
106+
const { p, findData, refusals } = protocol({ xt_person: override });
107+
const summary = await run(p, [{ name: 'a', person: 'Wren' }]);
108+
expect(summary.results.map(verdict)).toEqual([OK]);
109+
expect(askedOf(findData, 'xt_person')).toEqual(['id', 'first_name', 'nickname']);
110+
expect(refusals).toEqual([]);
111+
});
112+
}
113+
114+
it('the stored title stores the matched record\'s id', async () => {
115+
const { p, getMetaItem } = protocol();
116+
const createData = vi.fn(async (a: { data: Row }) => ({ id: 'new', ...a.data }));
117+
p.createData = createData;
118+
const summary = await runImport({
119+
p, context: {}, objectName: 'xt_source', metaMap, writeMode: 'insert', matchFields: [], dryRun: false,
120+
runAutomations: false, trimWhitespace: true, createMissingOptions: false, skipBlankMatchKey: false,
121+
rows: [{ name: 'a', person: 'Wren' }, { name: 'b', person: 'Wren' }],
122+
});
123+
expect(summary.results.map(verdict)).toEqual([OK, OK]);
124+
expect(createData.mock.calls.map(([a]) => a.data.person)).toEqual(['p1', 'p1']);
125+
// The title comes off the declaration the exposure check already reads:
126+
// one metadata read per target per import, however many rows ask it.
127+
expect(getMetaItem.mock.calls.map(([r]) => r.name)).toEqual(['xt_person']);
128+
});
129+
130+
for (const [cell, object, field, title] of [
131+
['John Smith', 'xt_contact', 'contact', 'full_name'],
132+
['Acme (EMEA)', 'xt_account', 'account', 'display_title'],
133+
] as const) {
134+
it(`a formula title (${object}.${title}) is never asked by where: '${cell}' still answers reference_not_found`, async () => {
135+
const { p, findData, refusals } = protocol();
136+
const summary = await run(p, [{ name: 'a', [field]: cell }]);
137+
expect(summary.results.map(verdict)).toEqual([{ ok: false, code: 'reference_not_found', field }]);
138+
const asked = askedOf(findData, object);
139+
expect(asked).not.toContain(title);
140+
// The fixed fallback keys are unchanged; only a member the target
141+
// declares virtual is skipped, which the engine could only refuse.
142+
expect(asked).toEqual(['id', 'name', 'title', 'label', 'full_name', 'email', 'username'].filter((f) => f !== title));
143+
expect(refusals).toEqual([]);
144+
});
145+
}
146+
147+
it('two records sharing a stored title answer the import\'s ambiguity refusal, and link neither', async () => {
148+
const { p, findData } = protocol();
149+
const summary = await run(p, [{ name: 'a', member: 'Robin' }]);
150+
expect(summary.results.map(verdict)).toEqual([{ ok: false, code: 'reference_ambiguous', field: 'member' }]);
151+
expect(askedOf(findData, 'xt_member')).toEqual(['id', 'nickname']);
152+
});
153+
154+
it('[#22739] a target whose exposure refuses list still answers reference_not_found, and is never read', async () => {
155+
const { p, findData, getMetaItem } = protocol({ xt_person: { enable: { apiEnabled: false } } });
156+
const summary = await run(p, [{ name: 'a', person: 'Wren' }, { name: 'b', person: 'Missing' }]);
157+
const NOT_FOUND = { ok: false, code: 'reference_not_found', field: 'person' };
158+
expect(summary.results.map(verdict)).toEqual([NOT_FOUND, NOT_FOUND]);
159+
expect(findData).not.toHaveBeenCalled();
160+
expect(getMetaItem).toHaveBeenCalledTimes(1);
161+
});
162+
});

0 commit comments

Comments
 (0)