diff --git a/.changeset/20439-hook-condition-expression-row.md b/.changeset/20439-hook-condition-expression-row.md new file mode 100644 index 00000000000..3c2258a236e --- /dev/null +++ b/.changeset/20439-hook-condition-expression-row.md @@ -0,0 +1,11 @@ +--- +'@objectstack/spec': patch +--- + +`hook.form.ts`'s `condition` row now declares `language: 'expression'`, matching the CEL predicate `HookSchema.condition` actually is. + +Clause-②: no + +The row previously declared `language: 'javascript'` — the same declared language as a real script row (`body.source`) — over a field that is `EvaluatedExpressionInputSchema`, a CEL predicate. A consumer keyed on the row's declared language could not tell the predicate apart from a script. The `helpText` moves from "Optional formula — skip the hook when this evaluates to false" to "CEL predicate — the hook runs only when TRUE", matching the phrasing every sibling predicate row (`field.form.ts` / `object.form.ts`'s `visibleWhen` / `readonlyWhen` / `requiredWhen`, and the formula `expression` row) already uses. + +No key is added, removed, narrowed or widened, and no parse verdict changes — `type: 'code'` and `language` are already-declared form-DSL vocabulary. `metadata-form-declared-rows.pin.test.ts` pins the new value, with a control against a sibling predicate row. diff --git a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts index ca8d0084293..0ee1119e6e5 100644 --- a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts @@ -811,7 +811,7 @@ export const enMetadataForms: NonNullable = { }, condition: { label: "Condition", - helpText: "Optional formula — skip the hook when this evaluates to false" + helpText: "CEL predicate — the hook runs only when TRUE" }, retryPolicy: { label: "Retry Policy", diff --git a/packages/spec/src/data/hook.form.ts b/packages/spec/src/data/hook.form.ts index d39e4a8f01b..2d21c410f54 100644 --- a/packages/spec/src/data/hook.form.ts +++ b/packages/spec/src/data/hook.form.ts @@ -74,7 +74,7 @@ export const hookForm = defineForm({ { label: 'System (elevated)', value: 'system' }, { label: 'User (triggering user)', value: 'user' }, ] }, - { field: 'condition', type: 'code', language: 'javascript', colSpan: 2, helpText: 'Optional formula — skip the hook when this evaluates to false' }, + { field: 'condition', type: 'code', language: 'expression', colSpan: 2, helpText: 'CEL predicate — the hook runs only when TRUE' }, { field: 'retryPolicy', type: 'composite', diff --git a/packages/spec/src/system/metadata-form-declared-rows.pin.test.ts b/packages/spec/src/system/metadata-form-declared-rows.pin.test.ts index b13bbc3ffe9..8ffa6100a8b 100644 --- a/packages/spec/src/system/metadata-form-declared-rows.pin.test.ts +++ b/packages/spec/src/system/metadata-form-declared-rows.pin.test.ts @@ -48,6 +48,7 @@ type FormEntry = { field?: string; widget?: string; type?: string; + language?: string; helpText?: string; visibleWhen?: string | { dialect?: string; source?: string }; fields?: FormEntry[]; @@ -168,3 +169,36 @@ describe('declared keys that now have a form row (#19085)', () => { expect(row.helpText).toContain('"name"'); }); }); + +/** + * **A `type: 'code'` row's `language` must name the slot it actually edits** + * (#20439). + * + * `hook.form.ts`'s `condition` row declared `language: 'javascript'` over + * `HookSchema.condition`, which is `EvaluatedExpressionInputSchema` — a CEL + * predicate, not a script. Every sibling predicate row (`field.form.ts` / + * `object.form.ts`'s `visibleWhen` / `readonlyWhen` / `requiredWhen`, and the + * formula `expression` row) already declares `language: 'expression'`; a + * consumer keyed on the declared language (objectui#10963's `CodeWidget` + * fix) cannot otherwise tell this row apart from a real script row + * (`body.source`, `action.source`, both genuinely `'javascript'`). + */ +describe('hook.condition declares the CEL predicate language it edits (#20439)', () => { + it('hook.condition is a `code` row declaring language: expression, not javascript', () => { + const [row, ...extra] = rowFor('hook', 'condition'); + expect(row, '`condition` is declared by HookSchema; the hook form must offer it').toBeDefined(); + expect(extra, 'one row only').toEqual([]); + expect(row.type).toBe('code'); + expect(row.language).toBe('expression'); + }); + + it('CONTROL: a sibling predicate row (field.visibleWhen) also declares expression', () => { + // Proves the assertion above actually discriminates: run the identical + // probe against a row known to already be correct, so a helper that + // stopped reading `language` at all would fail here rather than passing + // hook.condition vacuously. + const [row] = rowFor('field', 'visibleWhen'); + expect(row.type).toBe('code'); + expect(row.language).toBe('expression'); + }); +});