Skip to content

Commit 1aba315

Browse files
os-muskclaude
andauthored
test(objectql): declare the recorded-by fixture's lookup with the canonical reference key (#14633)
`protocol-recorded-by-null.test.ts` declared `recorded_by` as `{ type: 'lookup', referenceTo: 'sys_user', readonly: true }` under a comment claiming it is "the real declaration". `referenceTo` is an alias `FieldSchema` refuses by name, and `referenceTargetOf` — the single arbiter the #4441 write-path referential check resolves through — does not read it, so the lookup presented as target-less. Measured on the fixture's own write path before renaming anything, by counting the guard's target probe: with `referenceTo` spelled, no probe ran even with the `readonly` exemption removed; with `reference` spelled, it runs. So the exemption the file's second half claims to exercise had never admitted these writes — the target-less skip did. Renames the key and adds the two pins that keep the header's claim honest: the declaration resolves through `referenceTargetOf`, and an actor id no `sys_user` row matches is still admitted (which is the exemption, and is now red if the exemption is deleted). Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2aa8456 commit 1aba315

1 file changed

Lines changed: 47 additions & 1 deletion

File tree

packages/objectql/src/protocol-recorded-by-null.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,23 @@
1919
* check precisely because this column held a value no `sys_user` row
2020
* matched. With the sentinel gone the ordinary authoring paths must still
2121
* pass — that is the regression #4441 was bitten by.
22+
*
23+
* [#14535] "The real thing" is a claim about the TARGET KEY too, and it was
24+
* false here until this change. The declaration spelled `referenceTo` — an
25+
* alias `FieldSchema` refuses by name (#11567) and `referenceTargetOf`, the
26+
* single arbiter the write-path guard resolves through, does not read at all.
27+
* So the lookup presented as TARGET-LESS and the guard skipped it at
28+
* `if (!target) continue`, whatever the `readonly` exemption did. Measured on
29+
* this very write path by counting the guard's own target probe: with the
30+
* alias spelled no probe ran even with the exemption deleted; with `reference`
31+
* spelled it runs. The exemption had therefore never been what admitted these
32+
* writes. Two pins below keep both halves honest — the declaration resolves,
33+
* and the exemption is what admits an actor id no `sys_user` row matches.
2234
*/
2335

2436
import { describe, it, expect, beforeEach } from 'vitest';
2537
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
38+
import { referenceTargetOf } from '@objectstack/spec/data';
2639
import { ObjectQL } from './engine.js';
2740

2841
const sysUserObject = {
@@ -78,9 +91,11 @@ const sysMetadataHistoryObject = {
7891
source: { name: 'source', label: 'Source', type: 'text' as const },
7992
organization_id: { name: 'organization_id', label: 'Org', type: 'text' as const },
8093
// The real declaration, not a `text` stand-in — see the file header.
94+
// The target key is `reference`, the ONLY spelling `referenceTargetOf`
95+
// reads and the one `Field.lookup` emits; the pin below holds it there.
8196
recorded_by: {
8297
name: 'recorded_by', label: 'Recorded By',
83-
type: 'lookup' as const, referenceTo: 'sys_user', readonly: true,
98+
type: 'lookup' as const, reference: 'sys_user', readonly: true,
8499
},
85100
recorded_at: { name: 'recorded_at', label: 'At', type: 'datetime' as const, required: true },
86101
},
@@ -255,4 +270,35 @@ describe('#4556 — protocol write paths store NULL, not the sentinel string', (
255270
expect(v === null || users.has(v)).toBe(true);
256271
}
257272
});
273+
274+
it('[#14535] the declaration resolves to sys_user through the platform arbiter', () => {
275+
// The fidelity claim in the header, as an assertion instead of prose.
276+
// A raw object literal handed to the registry is never parsed by
277+
// `FieldSchema`, so the alias this fixture used to spell could never
278+
// be refused where it was written; `referenceTargetOf` is the reader
279+
// that decides whether the lookup has a target at all, and it is the
280+
// one the write-path guard resolves through.
281+
expect(referenceTargetOf(sysMetadataHistoryObject.fields.recorded_by)).toBe('sys_user');
282+
});
283+
284+
it('[#14535] an actor id with no sys_user row is still admitted — the #4441 readonly exemption', async () => {
285+
// The second half of the file, now that the target resolves. This is
286+
// NOT the #4556 sentinel returning: `'system'` was a string the
287+
// PLATFORM minted for every actor-less write, which is what this suite
288+
// refuses above. An actor the caller named is the caller's own value,
289+
// and #4441 deliberately does not police a `readonly` lookup — the
290+
// value there was minted outside the check's stated scope, and the
291+
// residual is reported by the #4551 audit rather than refused here.
292+
//
293+
// Before the `reference` rename this passed for the wrong reason: the
294+
// field was target-less, so the guard skipped it whether or not the
295+
// exemption existed. Delete the exemption now and this goes red.
296+
await protocol.saveMetaItem({
297+
type: 'view', name: 'cases', organizationId: 'org_x', item: viewBody('A'), actor: 'usr_not_a_row',
298+
});
299+
300+
const rows = await historyRows();
301+
expect(rows).toHaveLength(1);
302+
expect(rows[0].recorded_by).toBe('usr_not_a_row');
303+
});
258304
});

0 commit comments

Comments
 (0)