Skip to content

Commit 83b3d32

Browse files
fix(metadata-protocol,metadata-core,rest): saving or publishing a public form a walled posture cannot take intake for says why (#21608)
Fixes #21476 Clause-②: yes (widening) This is part 2 of 2 of #21476, the publish half. Part 1 (PR #21580, `a7ab047cf`) delivered the anonymous doors and the administrator's read. This PR delivers the seat's answer A (ACCEPT `5968878000`) to part 1's open question: - a gate-local warning advisory in `runtime-authoring-gate.ts`; - the predicate moved into `@objectstack/metadata-core`; - the gate fed the posture in force from `protocol.ts`. With this, every surface triage's ruling `5962758813` names is delivered: both doors, the administrator's read, and the administrator's publish. ## What On a walled posture in force (`group` or `isolated`), saving a view (`PUT /meta/view/:name`) or publishing its draft (`POST /meta/view/:name/publish`, and the package batch publish) can carry an open public form whose object is walled by an organization column. That write now answers success with **one `warning` advisory per such form** under `advisories`: - rule `public-form-intake-unavailable`; - located at the form's `sharing`, under the write's root (`views[0].formViews.contact.sharing`, `views[0].config.sharing` or `views[0].form.sharing`); - `message` is the administrator's read's reason, byte for byte; - `hint` is the remedy: declare `tenancy: { enabled: false }` if the rows belong to no organization. It never blocks and never 422s. It is omitted-when-empty as before, and a draft save is not judged (#4463 D1). - **One predicate, moved.** `anonymousFormIntakeUnavailability(object, posture, readObjectSchema)` now lives in `@objectstack/metadata-core` (`anonymous-form-intake.ts`). Next to it are: - its posture reader `anonymousFormIntakePosture(tenancy)`; - the reason, `anonymousFormIntakeUnavailableMessage`, built from `anonymousFormIntakeUnavailableRemedy`; - the location, `anonymousFormSharingPath`; - the target object, `anonymousFormObjectName`; - the type `AnonymousFormIntakeUnavailable`. Three readers call those exports: both anonymous doors, the admin read (`rest-server.ts`), and the gate rule. No copy is left in `rest`. The reason text is the same bytes as part 1's, proven by evaluating part 1's function from `$BASE` against the export over 8 inputs: byte-identical. - **The gate rule** is `findPublicFormIntakeGaps`, beside `findPlatformScheduleOrgGaps`. It is pure, and it reads only what the gate already holds: - the object universe `assertRuntimeAuthoringRules` already gathers (registry plus stored rows), folded with this batch's pending drafts, now computed once and shared with the shared rules; - one new pure input, `tenancyPostureInForce`. It adds no network or engine call. - **The posture input** is `tenancyPostureInForce()` in `protocol.ts`. It reads `anonymousFormIntakePosture(this.getServicesRegistry().get('tenancy'))`, the same service and the same reader the doors use, and the same channel `anonymousFormIntakeOrgScopeRefusal` already reads `tenancy` through. ### Two deviations from the dispatch's mechanism hypotheses, each measured 1. **The predicate judges the object's EFFECTIVE schema.** It now applies metadata-core's `applyInjectedSystemColumns` before resolving the wall column. The doors read served object documents, which already carry the injected `organization_id`, so for them this is the same reference and their answers are unchanged. The rest suite is 4883 / 4883 before and after, the same count as part 1. The gate's universe is different. Its stored-row winners and a batch's pending drafts are raw bodies, because `foldStoredCollection` does not apply the read exits' `governServedItem`. Judged raw, a Studio-authored object reads as unwalled, and the advisory would disagree with the doors. 2. **The predicate is synchronous for a synchronous reader.** The gate is pure and synchronous. The doors need the object read to stay lazy: part 1's pin asserts that the single posture reads no object. So the export has two overloads: - a synchronous reader gets a synchronous answer; - a reader returning a promise gets a promise, or `null` without reading when no wall is in force. The doors' call sites are unchanged. ### `orgWallEnforced()` is NOT aligned (Zone 2 #3: measured, then left as is) The advisory reads the posture IN FORCE. The #6285 schedule refusal keeps reading the REQUESTED posture through `orgWallEnforced()`. I measured the alternative with a one-off mutation: `orgWallEnforced()` reading the in-force posture, its throw arm kept. My prediction was that every #6285 refusal row driven through `saveMetaItem` with no tenancy service would turn red. Observed: **6 red / 28 passed**, across `protocol.platform-schedule-org-gate.test.ts` and `protocol.bracketed-refusal-opener-absence.test.ts`: - `refuses the publish …`; - `… under the group posture`; - `… refuses the publish that promotes it`; - `OS_ALLOW_UNLINTED … loud log`; - `[#6710] DOES gate an unscoped kernel`; - `survives a deployment whose OS_TENANCY_POSTURE is unparseable …`. So aligning would narrow a refusal that the docblock and ADR-0105 defend (the unparseable-posture row). It would also contradict the #6155 Q3=A ruling, which names `postureEnforcesWall(resolveTenancyPosture())` as that input verbatim. Per the dispatch, it stays. The split is documented on both inputs, and it is reported as a finding below. The mutation was restored, proven by blob == HEAD and an empty `git diff HEAD`. ## Pins - `packages/metadata-core/src/anonymous-form-intake.test.ts`, +15 cases (13 → 28): - both walled postures; - the effective schema; - a declared `tenancy.tenantField`; - controls: tenancy-disabled, absent object, no wall column; - `single` and no tenancy service, with zero object reads; - the asynchronous reader; - posture-in-force reading (degraded reads `single`, legacy `multi`); - sharing path across all three form shapes; - object name; - message ending with the remedy. - `packages/metadata-protocol/src/runtime-authoring-gate.public-form-intake.test.ts` (new, pure), 12 cases: - per walled posture, exactly one advisory, compared with `toEqual` against the metadata-core reason and remedy; - each form shape's path; - a pending raw object in the batch; - controls: tenancy-disabled, `single`, no posture, a withdrawn form, a draft, a non-view write; - `orgWallEnforced: true` with `single` in force raises nothing; - a refused view write (`422 INVALID_METADATA`) discloses the rule in `rulesRun`. - `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, +8 end-to-end rows through `saveMetaItem` and `publishMetaItem`, with a real `tenancy` service in the services table: - `isolated` and `group` × PUT and publish: success, exactly one advisory, the reason equal to `anonymousFormIntakeUnavailableMessage`, and the row landed active; - controls on PUT and publish: tenancy-disabled object, `single`, no tenancy service, and a **degraded** deployment (`OS_TENANCY_POSTURE=isolated`, service in force `single`), where the doors serve the form and nothing is raised. - Part 1's `packages/rest/src/public-form-intake-availability.test.ts` is **unchanged** and green (16/16). It now exercises the moved export through `dist/`. - **Real-boot measurement** (a one-off file, not committed): `bootStack(showcaseStack, { multiTenant: 'posture-only' })`, posture `isolated`. - The admin read warning is `config.sharing` with the reason. - `PUT /meta/view/showcase_inquiry.contact` answered 200 with exactly one advisory: `path: "views[0].config.sharing"` and `message` identical (`toBe`) to the admin read's warning. - `PUT ?mode=draft` answered 200 with no advisories. - `POST …/publish` answered 200 with the same advisory. ## Ablations, direction predicted before each run | Ablation | Predicted | Observed | |---|---|---| | A: the gate rule's findings removed from the verdict (`runtime-authoring-gate.ts`, src) | only the advisory rows: 8 red (4 pure, 4 end-to-end) | full metadata-protocol suite **8 failed / 3204 passed**, exactly those 8 | | B: the predicate answers "available" everywhere (metadata-core, rebuilt) | 20 red: metadata-core 5, the rest door and admin-read rows 7, advisory rows 8; every control green | **5 + 7 + 8 = 20 red**, every control green | Both mutations went through `scripts/ablation-replace.mjs` in WRAP mode: the anchor hit 1 → 0, and the restore was proven by blob == HEAD and an empty `git diff HEAD`. B is dist-mediated, so it used a type-valid mutation carrying a string-literal marker (part 1 measured that a DTS refusal leaves the mutated JS in `dist/`): - `ablation-dist-preflight` found the marker in 2 built files before the run. - After restore and rebuild, `--absent` reported the marker absent from all 12 built files and the tree clean against HEAD. - Positive control: the restored guard is present in `dist/index.js` and `dist/index.cjs`. ## Tests and gates The code is final at `dc0a93d4c4`. `0687a7f17e` adds only docs and the changeset (`git diff dc0a93d 0687a7f` touches no `packages/` path). - metadata-core `test`: 17 files, **326 passed**. - metadata-protocol full suite: 208 files, **3212 passed**, 19 skipped (at `dc0a93d4c4`). - rest `test` (`--project local`): 258 files, **4883 passed**, 326 skipped. `test:repo`: 5 files, 177 passed. - typecheck: metadata-core, metadata-protocol and rest all clean. rest's includes `check:test-typecheck`. - Five public-form dogfood files (walled intake, walled withdrawal, showcase withdrawal, showcase public form, read-back masking): **5 files / 20 passed**. - `dispatch-gates --repo objectstack-ai/objectstack --commands` at `0687a7f17e` derived 95 commands. All 95 exit 0. - Two first answered exit 3 `PREREQUISITE NOT MET`: `check:skill-examples` (client-react unbuilt) and `check:dual-build-cjs-loads` (8 packages unbuilt). Both were re-run green after building those packages, so they are measured, not skipped. - `--ran`: 95 derived, 95 run, 0 NOT-MEASURED, 0 UNRUN. - eslint, narrowed: `eslint --no-inline-config --format json` on the 7 changed `.ts` files gave 7 files, 0 errors, 0 warnings, none ignored. `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`), so the narrowing cannot move an untouched file's verdict. Full `pnpm lint` is CI's. - `main` moved 4 commits past `$BASE` (`6c5697dffb`). The only overlap with these packages is one new metadata-protocol test file (the spec-validation 422 face inventory), which does not touch the authoring gate. The branch is not merged; CI runs the merge ref. ## Docs - `content/docs/deployment/validating-metadata.mdx`: - adds the runtime-only row "Public-form anonymous intake on this deployment's tenancy posture — advisory only" (`✓ᵛ`); - rewrites the sentence that called the platform-schedule row "the one deliberate exception". There are now two deployment-fact rows. - `content/docs/ui/forms.mdx`: the "wires the anonymous REST endpoints automatically" rule list gains the walled-posture rule. The form is not offered, the admin read and the save/publish response say why, and the remedy is given. - `skills/**` is governed and not edited. Two published skill sentences are already false, made so by the `sharing.enabled` rule and by part 1, not by this PR: - `skills/objectstack-api/SKILL.md` "Any `FormView` declared with `sharing.allowAnonymous: true` and a `publicLink` slug is auto-mounted"; - `skills/objectstack-ui/SKILL.md`'s "Public / anonymous form" row. ## Acceptance notes - **Finding, the posture split, kept deliberately.** The #6285 refusal reads the REQUESTED posture, while the doors, the engine and this advisory read the posture IN FORCE. On a degraded deployment the refusal turns away a schedule-flow publish the engine would stamp. Aligning it is a ruling's call (#6155 Q3=A names the input), measured above at 6 pinned refusals. This is a code read with no public-door reach measured, so it is not filed; it is noted for the seat. - The advisory's object read is the gate's universe. A form bound to an object that is in neither the live universe nor this batch gets no advisory, and the doors offer such a form too, so the two agree. - The intake reason still rides only RestServer's single-item view read and the write responses. The list read (`GET /meta/view`), `/layers` and the runtime dispatcher's `/meta` read carry none (part 1's note, unchanged). - The console's rendering of `advisories` and `_diagnostics.warnings` for this rule is NOT MEASURED: no objectui checkout. --- _Generated by [Claude Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 54521f0 commit 83b3d32

10 files changed

Lines changed: 761 additions & 98 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/metadata-core': minor
3+
'@objectstack/metadata-protocol': patch
4+
'@objectstack/rest': patch
5+
---
6+
7+
Public forms on a walled tenancy posture: saving or publishing a view whose public form cannot take anonymous intake now tells the author why, on the response.
8+
9+
Clause-②: yes (widening)
10+
11+
On a walled posture (`group` or `isolated` in force), an open public form whose object is walled by an organization column cannot take an anonymous submission: the submission carries no organization, and an insert without one into a walled object is refused. The two anonymous form endpoints already answer such a form as a withdrawn one (`404 FORM_NOT_FOUND`), and the administrator's read of the view (`GET /meta/view/:name`) already states why in `_diagnostics.warnings`.
12+
13+
- **`@objectstack/metadata-protocol`**: saving the view (`PUT /meta/view/:name`) or publishing its draft (`POST /meta/view/:name/publish`, and a package's batch publish) now answers success with one `warning` advisory per such form, under `advisories`, with rule `public-form-intake-unavailable`. It is located at the form's `sharing` (for example `views[0].formViews.contact.sharing`), its `message` is the same text the administrator's read states, and its `hint` is the remedy: if the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it. The write is never refused. The advisory reads the posture in force from the `tenancy` service, which is what the anonymous endpoints read: a single-posture deployment, a deployment whose walled posture is degraded to `single`, a deployment with no tenancy service, and a form bound to a tenancy-disabled object get no advisory, and a draft save is not judged. The publish refusal for an unstamped platform schedule flow still reads the requested posture, as before.
14+
- **`@objectstack/metadata-core`**: the intake-availability rule moved here from `@objectstack/rest` and is exported, so the anonymous endpoints, the administrator's read and the publish advisory read one answer: `anonymousFormIntakeUnavailability(object, posture, readObjectSchema)` (`null` when the form can take intake, otherwise the object, the posture and the wall column; it judges the object's effective schema, with the injected `organization_id`), `anonymousFormIntakePosture(tenancy)` (the posture in force, as a tenancy service reports it), `anonymousFormIntakeUnavailableMessage` and `anonymousFormIntakeUnavailableRemedy` (the reason and its remedy), `anonymousFormSharingPath` and `anonymousFormObjectName`, and the type `AnonymousFormIntakeUnavailable`.
15+
- **`@objectstack/rest`**: the anonymous form endpoints and the administrator's read import that rule instead of holding their own copy. Their answers are unchanged.

‎content/docs/deployment/validating-metadata.mdx‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ orthogonal to both, and no cell here can carry it; it is written out in
478478
| Declared enforcement that cannot run, **declared on the object being written** — a validation rule's regex / JSON Schema (#4762) and its `format` names (#5178) | ✓ | ✓ | ✓ | ✓ᵒ |
479479
| Declared enforcement that cannot run, **declared on another collection** — sharing-rule conditions (#4698), row-level-security predicates (#4983) | ✓ | ✓ | ✓ | — |
480480
| Platform-schedule `create_record` organization (#6285) | — | — | — | ✓ᶠ |
481+
| Public-form anonymous intake on this deployment's tenancy posture — advisory only (#21476) | — | — | — | ✓ᵛ |
481482
| Autonumber `{field}` interpolation | ✓ | ✓ | ✓ | ✓ᵒ |
482483
| View references — form targets, view-key collisions (#2554) | ✓ | ✓ | ✓ | — |
483484
| Flow authoring anti-patterns (#1874) | ✓ | ✓ | ✓ | ✓ᶠ |
@@ -594,11 +595,17 @@ The fourth door does not weaken that, because it is held to the CLI's verdicts
594595
rather than to its own: a test fails if a rule runs at the runtime publish gate
595596
but not on `os build` — the two publish verbs must not disagree. What that
596597
column narrows is which *types* it judges, never which *verdict* it reaches. The
597-
one deliberate exception is the platform-schedule row (#6285), runtime-only by ruling:
598-
both of its inputs are facts about the **deployment** (the organization this
599-
write lands in, and whether this deployment walls organizations), and a build
600-
machine's environment is a false signal for them — so `os build` must not judge
601-
it at all.
598+
deliberate exceptions are the two rows whose inputs are facts about the
599+
**deployment**, and a build machine's environment is a false signal for those —
600+
so `os build` must not judge them at all. The platform-schedule row (#6285) is
601+
runtime-only by ruling: its inputs are the organization this write lands in and
602+
whether this deployment walls organizations. The public-form intake row (#21476)
603+
reads the tenancy posture **in force**: on a walled posture, an open public form
604+
whose object is walled by an organization column cannot take an anonymous
605+
submission, so the anonymous form endpoints do not offer it, and a save or
606+
publish of the view answers success with a `public-form-intake-unavailable`
607+
warning in `advisories`, located at the form's `sharing`. It never refuses the
608+
write.
602609

603610
Some rows are deliberately not universal across the three commands, and each is
604611
one-directional (none lets a stack through a gate another command enforces):

‎content/docs/ui/forms.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export default defineView({
100100
> - Anything not in the `sections[].fields[]` whitelist is silently stripped at submit time. Treat the whitelist as the form's authoritative "what the public is allowed to set" list.
101101
> - A form whose sections declare **no** fields collects nothing, so the submit is **refused** (`400 VALIDATION_ERROR`) rather than accepting whatever the caller sent (#6920). Its `GET /forms/:slug` publishes no schema either (#6601) — declare the fields and both planes come alive together.
102102
> - Multiple form views per object are fine — only the one(s) with `sharing.enabled === true` and `sharing.allowAnonymous === true` are exposed.
103+
> - On a **walled** tenancy posture (`group` or `isolated` in force), a form whose object is walled by an organization column is **not offered**. An anonymous submission carries no organization, and an insert without one into a walled object is refused, so both anonymous endpoints answer the form exactly as they answer a withdrawn one (`404 FORM_NOT_FOUND`). The administrator is told why, at the form's `sharing`: the view's read (`GET /api/v1/meta/view/:name`) carries it in `_diagnostics.warnings`, and a save or publish of the view answers success with a `public-form-intake-unavailable` warning in `advisories`. If the object's rows belong to no organization, declare `tenancy: { enabled: false }` on it and the form is offered again.
103104
104105
## 2. (Optional) Create the `guest_portal` permission set
105106

‎packages/metadata-core/src/anonymous-form-intake.test.ts‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ import { describe, it, expect } from 'vitest';
44
import { SharingConfigSchema } from '@objectstack/spec/ui';
55
import {
66
anonymousFormIntakeCandidates,
7+
anonymousFormIntakePosture,
78
anonymousFormIntakeSlug,
89
anonymousFormIntakeSlugs,
10+
anonymousFormIntakeUnavailability,
11+
anonymousFormIntakeUnavailableMessage,
12+
anonymousFormIntakeUnavailableRemedy,
13+
anonymousFormObjectName,
14+
anonymousFormSharingPath,
915
publicFormSlug,
1016
} from './anonymous-form-intake.js';
1117

@@ -79,3 +85,102 @@ describe('anonymousFormIntakeCandidates / anonymousFormIntakeSlugs — the three
7985
expect(publicFormSlug('//forms/x')).toBe('x');
8086
});
8187
});
88+
89+
// [#21476] Whether an open form can take an anonymous submission on this
90+
// posture — the one predicate both anonymous doors, the admin read and the
91+
// runtime authoring gate's advisory read.
92+
describe('anonymousFormIntakeUnavailability — the intake-availability predicate', () => {
93+
/** The object as a served document carries it: the registry injects `organization_id`. */
94+
const served = (extra: Record<string, unknown> = {}) => ({
95+
name: 'inquiry',
96+
fields: { organization_id: { type: 'lookup', reference: 'sys_organization' }, email: { type: 'text' } },
97+
...extra,
98+
});
99+
/** The same object as a stored or pending body carries it: declared fields only. */
100+
const raw = (extra: Record<string, unknown> = {}) => ({ name: 'inquiry', fields: { email: { type: 'text' } }, ...extra });
101+
102+
it.each(['isolated', 'group'] as const)("'%s': a walled object is unavailable, naming the object, the posture and the column", (posture) => {
103+
expect(anonymousFormIntakeUnavailability('inquiry', posture, () => served()))
104+
.toEqual({ object: 'inquiry', posture, tenantField: 'organization_id' });
105+
});
106+
107+
it('judges the EFFECTIVE schema: a stored body with no declared organization_id is walled all the same', () => {
108+
expect(anonymousFormIntakeUnavailability('inquiry', 'isolated', () => raw()))
109+
.toEqual({ object: 'inquiry', posture: 'isolated', tenantField: 'organization_id' });
110+
});
111+
112+
it('a declared tenancy.tenantField the object really has is the column named', () => {
113+
const schema = served({ tenancy: { tenantField: 'company_id' }, fields: { company_id: { type: 'text' } } });
114+
expect(anonymousFormIntakeUnavailability('inquiry', 'isolated', () => schema)?.tenantField).toBe('company_id');
115+
});
116+
117+
it.each<[string, unknown]>([
118+
['tenancy: { enabled: false } (ADR-0066)', served({ tenancy: { enabled: false } })],
119+
['an object the universe does not hold', undefined],
120+
['an object with no fields record and no wall', { name: 'inquiry', systemFields: false }],
121+
])('CONTROL, walled posture — %s: available', (_label, schema) => {
122+
expect(anonymousFormIntakeUnavailability('inquiry', 'isolated', () => schema)).toBeNull();
123+
});
124+
125+
it.each<[string, 'single' | undefined]>([
126+
["the 'single' posture", 'single'],
127+
['no tenancy service (no posture)', undefined],
128+
])('CONTROL — %s: available, and the object is never read', (_label, posture) => {
129+
let reads = 0;
130+
expect(anonymousFormIntakeUnavailability('inquiry', posture, () => { reads += 1; return served(); })).toBeNull();
131+
expect(reads).toBe(0);
132+
});
133+
134+
it('an asynchronous reader gets a promise when a wall is in force, and null without reading otherwise', async () => {
135+
const walled = anonymousFormIntakeUnavailability('inquiry', 'group', async () => served());
136+
expect(walled).toBeInstanceOf(Promise);
137+
expect(await walled).toEqual({ object: 'inquiry', posture: 'group', tenantField: 'organization_id' });
138+
expect(anonymousFormIntakeUnavailability('inquiry', 'single', async () => served())).toBeNull();
139+
});
140+
});
141+
142+
describe('anonymousFormIntakePosture — the posture IN FORCE, as the tenancy service reports it', () => {
143+
it('reads `posture`, never `requestedPosture`: a degraded walled request is single', () => {
144+
expect(anonymousFormIntakePosture({ posture: 'single', requestedPosture: 'isolated' })).toBe('single');
145+
expect(anonymousFormIntakePosture({ posture: 'group' })).toBe('group');
146+
expect(anonymousFormIntakePosture({ posture: 'multi' })).toBe('isolated');
147+
});
148+
149+
it('no service, or no recognisable posture: undefined', () => {
150+
for (const tenancy of [undefined, null, {}, { posture: 'walled' }, 'isolated']) {
151+
expect(anonymousFormIntakePosture(tenancy)).toBeUndefined();
152+
}
153+
});
154+
});
155+
156+
describe('where the reason is located, and the reason itself', () => {
157+
it('anonymousFormSharingPath: form.sharing, formViews.KEY.sharing, config.sharing', () => {
158+
const view = {
159+
name: 'inquiry.contact',
160+
form: { sharing: { ...OPEN, publicLink: '/forms/nested' } },
161+
formViews: { contact: { sharing: { ...OPEN, publicLink: '/forms/a' } } },
162+
viewKind: 'form',
163+
config: { sharing: { ...OPEN, publicLink: '/forms/flat' } },
164+
};
165+
expect(anonymousFormIntakeCandidates(view).map((c) => anonymousFormSharingPath(view, c)))
166+
.toEqual(['form.sharing', 'formViews.contact.sharing', 'config.sharing']);
167+
});
168+
169+
it('anonymousFormObjectName: the form\'s own data.object first, then the view\'s', () => {
170+
const view = { object: 'v_obj', list: { data: { object: 'list_obj' } } };
171+
expect(anonymousFormObjectName(view, { data: { object: 'form_obj' } })).toBe('form_obj');
172+
expect(anonymousFormObjectName(view, {})).toBe('list_obj');
173+
expect(anonymousFormObjectName({ object: 'v_obj' }, {})).toBe('v_obj');
174+
expect(anonymousFormObjectName(undefined, undefined)).toBeUndefined();
175+
});
176+
177+
it('the message names the slug, the object, the column and the posture, and ends with the remedy', () => {
178+
const u = { object: 'inquiry', posture: 'isolated' as const, tenantField: 'organization_id' };
179+
const message = anonymousFormIntakeUnavailableMessage('contact-us', u);
180+
for (const named of ["'/forms/contact-us'", "'inquiry'", "'organization_id'", "'isolated'"]) {
181+
expect(message).toContain(named);
182+
}
183+
expect(anonymousFormIntakeUnavailableRemedy(u)).toContain('tenancy: { enabled: false }');
184+
expect(message.endsWith(` ${anonymousFormIntakeUnavailableRemedy(u)}`)).toBe(true);
185+
});
186+
});

0 commit comments

Comments
 (0)