Skip to content

Commit efc12ff

Browse files
committed
test(metadata-protocol): pin the object save door's refusal of a reference into a stored-metadata-body table
Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK Co-authored-by: Claude <noreply@anthropic.com>
1 parent 91b562b commit efc12ff

2 files changed

Lines changed: 131 additions & 3 deletions

File tree

‎packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3069,6 +3069,123 @@ describe('runtime authoring gate on OBJECT writes — the object action predicat
30693069
});
30703070
});
30713071

3072+
/**
3073+
* [#24677] The object save door refuses a `lookup` or `master_detail` whose
3074+
* `reference` is a stored-metadata-body table.
3075+
*
3076+
* The engine never hydrates such a field (`expand` keeps the stored id; a
3077+
* master-detail `parent` root over a family header stays unbound), and this
3078+
* door saved it with a 200, as `os validate` and `os build` built it. The
3079+
* refusal is in `@objectstack/lint`'s expression rule, beside the refusal of a
3080+
* formula reading through such a field; no code here moves. Pinned through
3081+
* the REAL `saveMetaItem` / `publishMetaItem`:
3082+
*
3083+
* (a) each family table, as a `lookup` and as a `master_detail`, is refused
3084+
* with the 422 `INVALID_METADATA` envelope carrying the build's located
3085+
* finding, on an active save AND on a draft's promotion, and nothing
3086+
* lands;
3087+
* (b) the control: a reference to an ordinary platform table still saves;
3088+
* (d) the door's issue and the build's finding are the same finding.
3089+
*
3090+
* The rule's own pins are `@objectstack/lint`'s
3091+
* `validate-expressions.stored-metadata-body-reference.test.ts`.
3092+
*
3093+
* ⚠️ As in the #22019 block above: this package reaches `@objectstack/lint`
3094+
* through its built `dist/`, so an edit to the rule is invisible here until
3095+
* `pnpm --filter @objectstack/lint build` has run.
3096+
*/
3097+
describe('runtime authoring gate on OBJECT writes — a relationship into a stored-metadata-body table (#24677)', () => {
3098+
/** `sharingModel` is authored so `security-owd-unset` stays quiet and the refusal is the reference's. */
3099+
const fxRef = (type: string, target: string) => ({
3100+
name: 'fx_ref',
3101+
label: 'Reference Probe',
3102+
sharingModel: 'private',
3103+
fields: {
3104+
name: { type: 'text', label: 'Name' },
3105+
meta: { type, label: 'Meta', reference: target },
3106+
},
3107+
});
3108+
const FAMILY = ['sys_metadata', 'sys_metadata_history'] as const;
3109+
const TYPES = ['lookup', 'master_detail'] as const;
3110+
/** Where the build locates the refusal — the KEY the author edits. */
3111+
const WHERE = "object 'fx_ref' · field 'meta' reference";
3112+
3113+
const refRows = (rows: Map<string, Row>) =>
3114+
Array.from(rows.values()).filter((r) => r.type === 'object' && r.name === 'fx_ref');
3115+
const buildFindings = (obj: unknown) => {
3116+
const stack = { objects: [obj] };
3117+
return runAuthoringRules('build', { normalized: stack, parsed: stack })
3118+
.filter((f) => f.rule === EXPRESSION_INVALID && f.where === WHERE);
3119+
};
3120+
3121+
for (const target of FAMILY) {
3122+
for (const type of TYPES) {
3123+
it(`(a) REFUSES an active save of a \`${type}\` to '${target}' with a 422 carrying the build's located finding`, async () => {
3124+
const { protocol, rows } = makeProtocol();
3125+
3126+
const err = await protocol
3127+
.saveMetaItem({ type: 'object', name: 'fx_ref', item: fxRef(type, target) })
3128+
.catch((e: any) => e);
3129+
3130+
expect(err, 'the save resolved — the door still accepts the reference').toBeInstanceOf(Error);
3131+
expect({ code: err.code, status: err.status }).toEqual({ code: 'INVALID_METADATA', status: 422 });
3132+
expect(err.rulesRun).toContain('validateStackExpressions');
3133+
const issue = err.issues.find((i: any) => i.rule === EXPRESSION_INVALID && i.where === WHERE);
3134+
expect(issue, `issues: ${JSON.stringify(err.issues)}`).toBeDefined();
3135+
expect(issue.path).toBe(WHERE);
3136+
expect(issue.severity).toBe('error');
3137+
// The named subjects: the field, its type and the target.
3138+
expect(issue.message).toContain(`\`meta\` is a \`${type}\` whose \`reference\` names '${target}'`);
3139+
// And nothing landed.
3140+
expect(refRows(rows)).toEqual([]);
3141+
});
3142+
}
3143+
}
3144+
3145+
it("(a) REFUSES the same body on a draft's PROMOTION — the draft door is not a bypass", async () => {
3146+
const { protocol, rows } = makeProtocol();
3147+
await expect(
3148+
protocol.saveMetaItem({ type: 'object', name: 'fx_ref', item: fxRef('master_detail', 'sys_metadata'), mode: 'draft' }),
3149+
).resolves.toMatchObject({ success: true });
3150+
3151+
const err = await protocol.publishMetaItem({ type: 'object', name: 'fx_ref' }).catch((e: any) => e);
3152+
3153+
expect({ code: err?.code, status: err?.status }).toEqual({ code: 'INVALID_METADATA', status: 422 });
3154+
const issue = err.issues.find((i: any) => i.rule === EXPRESSION_INVALID && i.where === WHERE);
3155+
expect(issue, `issues: ${JSON.stringify(err.issues)}`).toBeDefined();
3156+
expect(refRows(rows).map((r) => r.state)).not.toContain('active');
3157+
});
3158+
3159+
for (const type of TYPES) {
3160+
it(`(b) the control: a \`${type}\` to an ordinary platform table (\`sys_user\`) still saves, and the row lands`, async () => {
3161+
const { protocol, rows } = makeProtocol();
3162+
3163+
const result = await protocol.saveMetaItem({ type: 'object', name: 'fx_ref', item: fxRef(type, 'sys_user') });
3164+
3165+
expect(result.success).toBe(true);
3166+
expect(refRows(rows).map((r) => r.state)).toEqual(['active']);
3167+
});
3168+
}
3169+
3170+
it('(d) the door and `os build` give the SAME finding for the same field', async () => {
3171+
const { protocol } = makeProtocol();
3172+
const err = await protocol
3173+
.saveMetaItem({ type: 'object', name: 'fx_ref', item: fxRef('lookup', 'sys_metadata') })
3174+
.catch((e: any) => e);
3175+
const atDoor = (err.issues ?? []).filter((i: any) => i.rule === EXPRESSION_INVALID && i.where === WHERE);
3176+
3177+
const atBuild = buildFindings(fxRef('lookup', 'sys_metadata'));
3178+
3179+
expect(atBuild).toHaveLength(1);
3180+
expect(atBuild[0]!.severity).toBe('error');
3181+
expect(atDoor).toHaveLength(1);
3182+
for (const k of ['rule', 'where', 'path', 'message', 'hint'] as const) {
3183+
expect(atDoor[0][k], `door and build disagree on '${k}'`).toBe(atBuild[0]![k]);
3184+
}
3185+
expect(buildFindings(fxRef('lookup', 'sys_user'))).toEqual([]);
3186+
});
3187+
});
3188+
30723189
// #23388 — the two page component rules at the runtime save door, ruling of
30733190
// record 6161173492 (letter B): `validateComponentProps` and
30743191
// `validateComponentTypes` run on a `page` write (`runtimeTypes: ['page']`),

‎packages/metadata-protocol/src/stored-metadata-body-family.pin.test.ts‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@
4343
* - [#24358] the engine's two relationship hydrations outside that loader,
4444
* `expand` and the master-detail `parent` root → `@objectstack/objectql`
4545
* (`engine-hydration-stored-metadata-body.test.ts`: the engine never
46-
* hydrates a reference or a header into the family);
46+
* hydrates a reference or a header into the family), and [#24677] the
47+
* author-time refusal of the reference field itself → `@objectstack/lint`
48+
* (`validate-expressions.stored-metadata-body-reference.test.ts`);
4749
* - the generic data door reads / groupBy / filter+sort → this package
4850
* (`protocol.data-door-stored-metadata-redaction.test.ts`, and the behaviour
4951
* asserted below).
@@ -166,6 +168,14 @@ const FAMILY_SURFACES = [
166168
disposition: 'refuses',
167169
pin: '@objectstack/objectql (engine-hydration-stored-metadata-body.test.ts)',
168170
},
171+
// [#24677] The declaration both hydrations above presuppose, refused where it
172+
// is written: the engine guards stay for installed artifacts, which run no
173+
// authoring rule.
174+
{
175+
surface: 'author time: a lookup or master_detail field whose reference is in the family (os validate, os build, the object save door)',
176+
disposition: 'refuses',
177+
pin: '@objectstack/lint (validate-expressions.stored-metadata-body-reference.test.ts) + this package (protocol.runtime-authoring-gate.test.ts)',
178+
},
169179
] as const;
170180

171181
/** A stored datasource body as it sits in the `metadata` column: serialized JSON with credential material. */
@@ -238,8 +248,9 @@ describe('[#21120] stored-metadata-body family — the data door exposes only co
238248
// + [#21207] the thirteen surfaces of the stored content hash, and the two
239249
// of the history change note that can quote one, + [#24316] the three of
240250
// the one-hop read through a reference, + [#24358] the two relationship
241-
// hydrations outside it.
242-
expect(FAMILY_SURFACES).toHaveLength(28);
251+
// hydrations outside it, + [#24677] the author-time refusal of the
252+
// reference field they presuppose.
253+
expect(FAMILY_SURFACES).toHaveLength(29);
243254
});
244255
});
245256

0 commit comments

Comments
 (0)