Skip to content

Commit 80b0a99

Browse files
committed
test: the dry-run doubles answer one verdict per submitted row, and the header-read control counts the write's reference check
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5krDpBpMjEHKqjJ6u8
1 parent 1b2324f commit 80b0a99

4 files changed

Lines changed: 40 additions & 16 deletions

File tree

‎packages/core/src/utils/import-runner-dropped-fields.test.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ const withDrops = (row: Record<string, unknown>) => {
7171
return d.length > 0 ? { droppedFields: d } : {};
7272
};
7373

74+
/** The submitted rows of a `validateData` call: one verdict is owed per row (#26684 previews a batch). */
75+
const rowsOf = (args: ValidateArgs) =>
76+
(Array.isArray(args.data) ? args.data : [args.data]) as Array<Record<string, unknown>>;
77+
7478
/** The row's key set — a clean row must not grow an empty `droppedFields`. */
7579
const keysOf = (r: ImportRowResult | undefined) => Object.keys(r ?? {}).sort();
7680

@@ -85,7 +89,7 @@ describe('[#20701] the dry run copies validateData\'s per-row drops onto the row
8589
it('a create-resolved row carries the verdict\'s droppedFields verbatim; a clean row carries no key', async () => {
8690
const validateData = vi.fn(async (args: ValidateArgs) => ({
8791
object: OBJECT, mode: args.mode ?? 'insert', valid: true,
88-
results: [{ valid: true, errors: [], warnings: [], ...withDrops(args.data as Record<string, unknown>) }],
92+
results: rowsOf(args).map((row) => ({ valid: true, errors: [], warnings: [], ...withDrops(row) })),
8993
posture: { valueShapeStrict: false, mediaValueShapeStrict: false },
9094
}));
9195
const p: ImportProtocolLike = { findData: vi.fn(async () => []), createData: vi.fn(), updateData: vi.fn(), validateData };
@@ -116,12 +120,12 @@ describe('[#20701] the dry run copies validateData\'s per-row drops onto the row
116120
it('a row the verdict refuses carries no droppedFields, and an admitted warning rides beside the drops', async () => {
117121
const warning = { field: 'name', code: 'invalid_format', message: 'admitted' };
118122
const validateData = vi.fn(async (args: ValidateArgs) => {
119-
const refused = (args.data as Record<string, unknown>).name === 'bad';
123+
const results = rowsOf(args).map((row) => (row.name === 'bad'
124+
? { valid: false, errors: [{ field: 'name', code: 'required', message: 'name is required' }], warnings: [], droppedFields: [COMPUTED] }
125+
: { valid: true, errors: [], warnings: [warning], droppedFields: [COMPUTED] }));
120126
return {
121-
object: OBJECT, mode: 'insert' as const, valid: !refused,
122-
results: [refused
123-
? { valid: false, errors: [{ field: 'name', code: 'required', message: 'name is required' }], warnings: [], droppedFields: [COMPUTED] }
124-
: { valid: true, errors: [], warnings: [warning], droppedFields: [COMPUTED] }],
127+
object: OBJECT, mode: 'insert' as const, valid: results.every((r) => r.valid),
128+
results,
125129
posture: { valueShapeStrict: false, mediaValueShapeStrict: false },
126130
};
127131
});

‎packages/core/src/utils/import-runner-kept-options.test.ts‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ function makeProvider(shape: 'inline' | 'createMany' | 'insertMany' | 'update' |
7777
}
7878
if (shape === 'validate') {
7979
p.validateData = vi.fn(async (args: ValidateArgs) => {
80-
calls.push({ kind: 'validateData', rows: [args.data as Record<string, unknown>], context: args.context });
81-
return { results: [{ valid: true, errors: [] }] } as any;
80+
const rows = (Array.isArray(args.data) ? args.data : [args.data]) as Array<Record<string, unknown>>;
81+
calls.push({ kind: 'validateData', rows, context: args.context });
82+
return { results: rows.map(() => ({ valid: true, errors: [] })) } as any;
8283
});
8384
}
8485
return { p, calls };
@@ -135,14 +136,22 @@ describe('runImport — createMissingOptions hands the kept values to the write
135136
expect(calls[0].context.keptOptionValues).toEqual({ priority: ['Bogus'] });
136137
});
137138

138-
it('the dry run asks validateData with the same per-row context the write would carry', async () => {
139+
it('the dry run asks validateData with the context the batch write would carry: the union of its rows\' kept values', async () => {
140+
// [#26684] The dry run previews the create rows as the batch the commit
141+
// writes them in, so its one call carries that batch call's context.
139142
const { p, calls } = makeProvider('validate');
140143
const summary = await runImport({ ...baseOpts, p, dryRun: true, rows: ROWS });
141144
expect(summary.created).toBe(3);
142-
expect(calls.map((c) => c.kind)).toEqual(['validateData', 'validateData', 'validateData']);
143-
expect(calls[0].context.keptOptionValues).toEqual({ priority: ['Bogus'] });
144-
expect(calls[1].context).not.toHaveProperty('keptOptionValues');
145-
expect(calls[2].context.keptOptionValues).toEqual({ tags: ['Other'] });
145+
expect(calls.map((c) => c.kind)).toEqual(['validateData']);
146+
expect(calls[0].rows).toHaveLength(3);
147+
expect(calls[0].context.keptOptionValues).toEqual({ priority: ['Bogus'], tags: ['Other'] });
148+
});
149+
150+
it('a dry run whose rows kept nothing carries the import\'s write context unchanged', async () => {
151+
const { p, calls } = makeProvider('validate');
152+
await runImport({ ...baseOpts, p, dryRun: true, rows: [{ code: 'b', priority: 'High' }] });
153+
expect(calls).toHaveLength(1);
154+
expect(calls[0].context).not.toHaveProperty('keptOptionValues');
146155
});
147156

148157
it('with the option off, an unmatched cell fails at coercion and no write carries the key', async () => {

‎packages/core/src/utils/import-runner-update-preview-address.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ describe('[#22445] the dry run of a matched row names the stored row it would up
7878
await runImport({ ...baseOpts, p, writeMode: 'upsert', dryRun: true, rows: [{ code: 'c8', title: 'fresh' }] });
7979

8080
expect(validateData.mock.calls[0]![0]).toMatchObject({ mode: 'insert' });
81-
expect(validateData.mock.calls[0]![0].data).toEqual({ code: 'c8', title: 'fresh' });
81+
// [#26684] A create row is previewed with the rows of its batch: an array.
82+
expect(validateData.mock.calls[0]![0].data).toEqual([{ code: 'c8', title: 'fresh' }]);
8283
});
8384
});

‎packages/objectql/src/validate-preview-parent.test.ts‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,14 +261,24 @@ describe('#22474 — the preview binds the master-detail header the write binds'
261261
return count;
262262
}
263263

264-
it('insert and update previews of a plain line issue no read of the header object', async () => {
264+
it('an update preview of a plain line issues no read of the header object', async () => {
265265
storeFor(PLAIN_LINE).set('p1', { id: 'p1', invoice: 'h_sent', quantity: 1 });
266266
const count = countHeaderReads(engine);
267-
expect(await preview(engine, PLAIN_LINE, { invoice: 'h_sent', quantity: 1 }, 'insert')).toEqual(ADMITTED);
268267
expect(await preview(engine, PLAIN_LINE, { quantity: 2, id: 'p1' }, 'update')).toEqual(ADMITTED);
269268
expect(count.reads).toBe(0);
270269
});
271270

271+
it('an insert preview of a plain line reads the header only as the insert does', async () => {
272+
// [#26684] The insert preview runs the write's reference check, which
273+
// asks the header's read door whether `h_sent` exists: the read the
274+
// insert itself makes. The header binding adds none.
275+
const count = countHeaderReads(engine);
276+
expect(await preview(engine, PLAIN_LINE, { invoice: 'h_sent', quantity: 1 }, 'insert')).toEqual(ADMITTED);
277+
const previewReads = count.reads;
278+
expect(await write(() => engine.insert(PLAIN_LINE, { invoice: 'h_sent', quantity: 1 }))).toEqual(ADMITTED);
279+
expect(count.reads - previewReads).toBe(previewReads);
280+
});
281+
272282
it('positive control: the parent-scoped line does read it', async () => {
273283
const count = countHeaderReads(engine);
274284
await preview(engine, LINE, { invoice: 'h_draft', quantity: 1 }, 'insert');

0 commit comments

Comments
 (0)