Skip to content

Commit 21ac7e6

Browse files
committed
test(service-automation): get_record's empty-fields case pins the engine's refusal the node now carries
The engine refuses an empty projection (#27008), so a get_record node authored with fields: [] fails with that refusal instead of reading the whole row. The control for an absent fields stays; the crud-nodes comment says what the empty list now meets. Claude-Session: https://claude.ai/code/session_01FEkQ5mbUpy4ebfWUHxAv87 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2e447bb commit 21ac7e6

2 files changed

Lines changed: 28 additions & 14 deletions

File tree

‎packages/services/service-automation/src/builtin/crud-nodes.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,10 @@ async function serveFamilyRead<A>(
307307
* node's `outputVariable` agrees with `{<node>.id}`; it is not taken back off,
308308
* which would be a second projection layer over the engine's answer.
309309
*
310-
* Only a non-empty projection is touched: no `fields`, or an empty list, is a
311-
* whole-row read already, and a projection that names `id` is handed over as
312-
* written. The `find` branch (`limit > 1`) answers rows only, with no
310+
* Only a non-empty projection is touched: no `fields` is a whole-row read
311+
* already, an empty list is handed over as written for the engine to refuse
312+
* (#27008: an empty projection is never read as the whole row), and a
313+
* projection that names `id` is handed over as written. The `find` branch (`limit > 1`) answers rows only, with no
313314
* top-level `id`, and does not come through here.
314315
*/
315316
function projectionNamingId(fields: string[] | undefined): string[] | undefined {

‎packages/services/service-automation/src/builtin/get-record-id-projection.integration.test.ts‎

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
* engine on that branch, and only there:
1515
* - a projection that already names `id` is handed over as written (no
1616
* second `id`);
17-
* - no projection, or an empty one, stays a whole-row read (no projection is
18-
* invented);
17+
* - no projection stays a whole-row read (no projection is invented);
18+
* - an empty projection is handed over as written, and the engine refuses it
19+
* (#27008): the node fails with that refusal carried, never a whole-row read;
1920
* - the `find` branch (`limit > 1`) declares no top-level `id` and is not
2021
* touched.
2122
*
@@ -152,15 +153,27 @@ describe('get_record answers the row id as output.id whatever its fields project
152153
expect(result.output.record).toEqual({ id: rowId, title: 'alpha' });
153154
});
154155

155-
for (const [label, config] of [['no fields', {}], ['empty fields', { fields: [] }]] as const) {
156-
it(`control, ${label}: the whole row is read and the output is the engine's own answer`, async () => {
157-
const whole: any = await ql.findOne(OBJECT.name, { where: { title: 'alpha' }, context: SYS });
158-
expect(whole.status, 'control: a whole-row read carries the unnamed columns').toBe('open');
159-
const { result, fields } = await readOne(config);
160-
expect(fields, 'no projection is invented').toEqual((config as { fields?: unknown }).fields);
161-
expect(result.output).toEqual({ record: whole, id: rowId, object: OBJECT.name });
162-
});
163-
}
156+
it("control, no fields: the whole row is read and the output is the engine's own answer", async () => {
157+
const whole: any = await ql.findOne(OBJECT.name, { where: { title: 'alpha' }, context: SYS });
158+
expect(whole.status, 'control: a whole-row read carries the unnamed columns').toBe('open');
159+
const { result, fields } = await readOne({});
160+
expect(fields, 'no projection is invented').toBeUndefined();
161+
expect(result.output).toEqual({ record: whole, id: rowId, object: OBJECT.name });
162+
});
163+
164+
it("empty fields: handed to the engine as written, and the node fails with the engine's refusal carried (#27008)", async () => {
165+
handed.length = 0;
166+
const result = await getRecord.execute(
167+
{ id: 'read', type: 'get_record', config: { objectName: OBJECT.name, filter: { title: 'alpha' }, fields: [] } },
168+
new Map(),
169+
{ runAs: 'system', flowName: 'pin_get_record_id' },
170+
);
171+
expect(handed, 'the node hands the empty projection over unchanged, once').toEqual([{ method: 'findOne', fields: [] }]);
172+
expect(result.success, 'an empty projection is refused, never read as the whole row').toBe(false);
173+
expect(result.output).toBeUndefined();
174+
expect(result.innerRefusal).toMatchObject({ code: 'VALIDATION_ERROR' });
175+
expect(result.error).toMatch(/^get_record\(pin_get_record_id\) failed: ObjectQL\.findOne\('pin_get_record_id'\) names no field in 'fields'/);
176+
});
164177

165178
it('a downstream update_record filtering on read.id updates the row the narrowed get_record read', async () => {
166179
await ql.insert(OBJECT.name, { title: 'beta', status: 'open' }, { context: SYS });

0 commit comments

Comments
 (0)