Skip to content

Commit c133cfe

Browse files
fix(lint)!: the object publish door refuses an object whose own list view sorts by a field the list door refuses (#30095) (#30564)
domain:server#3 · os-bill · session_01Utp6CXQyiKbpV3REQxogrr <!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgJrQp25DwmU8PpHXY7H6t15)_ Fixes #30095 Clause-②: no (narrowing) — an object write the door accepted before is refused: an object whose built-in `listViews.*.sort` names a field the list door answers `400 INVALID_SORT` now saves `422 INVALID_METADATA`. Metadata-protocol: none Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/deployment/validating-metadata.mdx` lists the doors by rule family, not by reference-integrity member, so it names no object crossing to correct; the prescription ships in the refusal text. ## 维护者速读 - **改了什么:** 对象自带的列表视图(`listViews.*.sort`)如果按一个列表门会拒绝的字段排序(字段声明了 `sortable: false`、是公式字段、或对象根本没有这个字段),对象发布门现在直接拒收,返回 422 `INVALID_METADATA`,规则名 `sort-field-unsortable` 或 `sort-field-unknown`,指到具体排序键路径,不存任何行。 - **为什么改:** 以前 `os validate` 会报错,视图门和页面门也会拒,唯独对象门不查:Studio 里存对象照样成功,那个列表视图每次打开都答 `400 INVALID_SORT`。 - **风险与回滚:** 这是收窄:以前能存的这类对象,下次保存会被拒;已存的对象照常加载,另一个对象的写入不会因为已存的违规对象被拒。上线前普查了仓库里全部 127 个对象、102 个列表视图、113 个排序键,0 命中。回滚 = revert 本 PR。 - **先看:** `packages/lint/src/reference-integrity-suite.ts` 里 `validateSortableFields` 那一行(`runtimeTypes` 加了 `object`)和它上方的 #30095 注释;`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts` 末尾的 #30095 用例;`packages/lint/src/runtime-gate.object-writes.test.ts` 的成员清单钉子。 - **两分钟验证:** 运行 `pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.runtime-authoring-gate.test.ts -t 30095`,预期 3 个全绿:`sortable: false` 与不存在的字段各被 422 拒绝、没有存下任何行;对照组按可排序字段保存成功。 - **需要你做的:** 无,席位落地。 ## What changes - **`validateSortableFields` declares `object` in its `runtimeTypes`** (`packages/lint/src/reference-integrity-suite.ts`). The `validateReferenceIntegrity` entry already dispatches on `object` writes (#15254); the member axis decides which members judge that snapshot, and this member was the one list-view sibling #15495 held back for its own adjudication. Its first rung walks `objects[].listViews.*.sort`, so the object publish door (`PUT /api/v1/meta/object/NAME`; Studio, REST, MCP) now gives an object's own list view sort the verdicts `os validate`, the `view` door and the `page` door already give it: `sort-field-unsortable` (`sortable: false`, a virtual `formula`) and `sort-field-unknown`, both `error`, refused `422 INVALID_METADATA`, nothing stored. - **Why it is safe (the #15254 / #9313 terms).** The member resolves only against `stack.objects`, the collection every per-write snapshot carries, so it has no missing-collection false-positive channel; its `views[]` and `pages[]` rungs find nothing on an object snapshot. The gate's differential (#4463 D4) still never charges a stored sibling's violation to this write. - **Rollout measurement, before crossing** (base `c045c341`, through the door's own shape: `runRuntimeAuthoringRules`, type `object`, every other corpus object as context): 122 `*.object.ts` files under `packages/` and `examples/`, 127 objects, 45 of them carrying 102 built-in list views with 113 sort keys — **0 findings, precision 1.0**. The same sweep's synthetic probe (a list view sorting by a `sortable: false` field) drew `sort-field-unsortable` from the rule and nothing from the door: the defect, measured. The member note carries the population. - `authoring-rules.ts` is not touched: the entry already lists `object`. ## Tests - `packages/lint/src/runtime-gate.object-writes.test.ts`: the member-surface pin now lists `validateSortableFields` among the members that declare `object` (it used to assert the opposite and say this is where the adjudication has to be made); three refusals through the door's gate (`sortable: false`, `formula`, an unknown name), each one error at `objects.leave_request.listViews.inbox.sort[0]`; CONTROL: the same list view sorting by a field with no `sortable` key and by a `sortable: true` field publishes with the suite run. - `packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, new top-level `describe` (#30095), through the real `saveMetaItem` object publish route: `sortable: false` refused `422` / `INVALID_METADATA`, one `sort-field-unsortable` issue at `error` at the sort key path, `rulesRun` names `validateReferenceIntegrity`, no active row; an unknown name refused the same way as `sort-field-unknown`; CONTROL: the same object sorting by a sortable field saves one row. ## Acceptance notes - `Metadata-protocol: none` is the claim's line, copied. Reading for the seat: nothing an author may write changes, because `os validate` / `os build` / `os lint` already refused this sort; only the runtime door's accept set narrows, which the `(narrowing)` arm and the BREAKING banner carry. #30270 declared `narrowed` because it moved the CLI verdict too. - Draft saves (`?mode=draft`) are not judged, unchanged (#4716 D1). ## Verification All readings on head `0e5a281c` (base `c045c341`, the #30270 merge). - **Reproduced before the change** (`fa9c0aa2` and again on `c045c341`): through the same `saveMetaItem` harness, an object whose list view sorts by a `sortable: false` field saved (`success: true`, one active row), and so did one sorting by `gone_field`; `validateReferenceIntegrity` on the same body reported `sort-field-unsortable` at `error` (`objects[0].listViews.inbox.sort[0]`). The read half (`400 INVALID_SORT` at the list door for `sortable: false`) is pinned by `packages/rest/src/data-sort-field-sortable-flag-door.test.ts` (#29896). - `pnpm --filter @objectstack/lint test`: Test Files 195 passed (195), Tests 8086 passed (8086). `pnpm --filter @objectstack/lint typecheck`: exit 0. - `pnpm --filter @objectstack/metadata-protocol test`: Test Files 316 passed, 4 skipped (320); Tests 22434 passed, 26 skipped. `pnpm --filter @objectstack/metadata-protocol typecheck`: exit 0. - `pnpm lint`: exit 0. `pnpm --filter @objectstack/spec check:liveness`: exit 0. - `node scripts/pm/dispatch-gates.mjs --ran`: 58 derived, 58 run, 0 NOT-MEASURED, every exit code 0 (four dist-reading gates, `check:type-check-debt` and the teardown self-test re-run after a full `turbo run build --filter='!@objectstack/docs'` and a deepening fetch; their first run was exit 3, prerequisite not met, not red). - **Dogfood** (`pnpm --filter @objectstack/dogfood test` under the verify lock, after building its closure; the title carries `!`): Test Files 420 passed, 3 skipped (423); Tests 3496 passed, 36 skipped (3532). VERDICT command-exit 0. - **Ablation** (one, on the tested behaviour): `scripts/ablation-replace.mjs` replaced the member's `'object'` with a marker type (`ablate30095`) in `reference-integrity-suite.ts`; `@objectstack/lint` rebuilt; `ablation-dist-preflight.mjs` found the marker in all 4 built entry files. Result: `runtime-gate.object-writes.test.ts` 4 failed / 37 passed (the member-surface pin and the three refusals), the door suite `-t 30095` 2 failed / 1 passed (both refusals red, the object saved; the control green). Restored by the tool (blob equals HEAD, `git diff HEAD` empty), lint rebuilt, preflight `--absent` clean on all 20 built files, working tree clean, door suite 3/3 green again. <details><summary>Gate record (58 derived)</summary> ``` node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:docs-transcript-drift :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:durability-log-level :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 pnpm --filter @objectstack/spec check:liveness :: exit 0 pnpm lint :: exit 0 ``` </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CXQyiKbpV3REQxogrr)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2ffdb64 commit c133cfe

4 files changed

Lines changed: 176 additions & 4 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
fix(lint)!: the object publish door refuses an object whose own list view sorts by a field the list door refuses (`sortable: false`, a `formula`, or a name the object lacks) (#30095)
6+
7+
Clause-②: no (narrowing) — an object write the door accepted before is refused: an object whose built-in `listViews.*.sort` names a field the list door answers `400 INVALID_SORT` now saves `422 INVALID_METADATA`.
8+
Metadata-protocol: none
9+
Skills-docs: none — `skills/**` names no `sortable` field key, and `content/docs/deployment/validating-metadata.mdx` lists the doors by rule family, not by reference-integrity member, so it names no object crossing to correct; the prescription ships in the refusal text.
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed or renamed, and nothing an author may write changes: `os validate` / `os build` / `os lint` already refused this sort. The object publish door now gives an object's own list view sort the verdict the CLI, the view door and the page door already give it. A refused name never sorted anything (the list door answered every load 400 INVALID_SORT), so there is no rewrite that keeps what the author meant; the author sorts by a sortable field, and the refusal says which field and why. -->
12+
13+
**BREAKING** accept-set narrowing at the object publish door (`PUT /api/v1/meta/object/NAME`; Studio, REST and MCP authors).
14+
15+
- **Before.** `validateSortableFields` judged a list view's `sort` on the CLI, at the `view` door and at the `page` door, but not on an `object` write. An object whose built-in list view sorted by a field declaring `sortable: false`, by a virtual `formula`, or by a name the object does not have saved green, and that list view answered `400 INVALID_SORT` on its first load and every load after it.
16+
- **Now.** The member declares `object`, so the object door refuses that save `422 INVALID_METADATA` with `sort-field-unsortable` or `sort-field-unknown` at `error`, at the sort key's path (`objects.NAME.listViews.KEY.sort[i]`). Nothing is stored.
17+
- **Not changed.** A stored object already carrying such a sort keeps loading; only its next save is judged, and a stored sibling in violation is never charged to another object's write. A `?mode=draft` save is not judged, as before.
18+
19+
**What an author does.** Sort the list view by a sortable field, or remove `sortable: false` from the field it names. A refused sort never ordered the list, so there is nothing to keep.

‎packages/lint/src/reference-integrity-suite.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,38 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
295295
// missing-collection false-positive channel — the #9313 reason, one write
296296
// type over. It is the only member that declares `page`, so a page write
297297
// dispatches nothing else from this suite.
298-
{ name: 'validateSortableFields', runtimeTypes: ['flow', 'view', 'page'], run: validateSortableFields },
298+
//
299+
// [#30095] `object` joins, on the #15254 note's terms (below, at
300+
// `validateObjectFieldRefs`) and its own measurement. Its first rung walks
301+
// `objects[].listViews.*.sort`, a list view that lives on the OBJECT, the
302+
// artifact Studio's app builder mints; before this the object door read that
303+
// sort with nothing, so an object whose list view sorts by a field it
304+
// declares `sortable: false` (or a `formula`, or a name it does not have)
305+
// saved green and that list view answered `400 INVALID_SORT` on every load,
306+
// while `os validate` refused the same body. Its two list-view siblings
307+
// crossed in #15495; this member was held back for its own adjudication (the
308+
// note at `validateListViewFieldRefs` says so), and this is it.
309+
//
310+
// Safe for the #9313 reason, unchanged by the new type: it resolves only
311+
// against `stack.objects`, the collection every per-write snapshot carries,
312+
// so there is no missing-collection false-positive channel. Its `views[]`
313+
// and `pages[]` rungs find no such collection on an object snapshot and
314+
// contribute nothing — rungs that are ABSENT, not rungs that read dead.
315+
//
316+
// ⚠️ A REFUSAL narrowing, like #15254's: an object republished with a list
317+
// view sort the list door refuses is now refused (422) rather than stored.
318+
// The gate's differential keeps it honest in the one direction that matters
319+
// (#4463 D4): a STORED sibling already in violation is never charged to this
320+
// write. MEASURED before crossing, on the shipped object corpus, through the
321+
// door's own shape (`runRuntimeAuthoringRules`, type `object`, the other
322+
// objects as context): 127 objects, every `*.object.ts` under `packages/`
323+
// and `examples/`, 45 of them carrying 102 built-in list views with 113 sort
324+
// keys — 0 findings, precision 1.0. The zero is a fact about the corpus AND
325+
// the rule: the same sweep's synthetic probe (a list view sorting by a
326+
// `sortable: false` field) is refused with `sort-field-unsortable` at
327+
// `error`, and the door controls in `runtime-gate.object-writes.test.ts`
328+
// are that probe made permanent.
329+
{ name: 'validateSortableFields', runtimeTypes: ['flow', 'view', 'page', 'object'], run: validateSortableFields },
299330
// [#14107] The REST of the same list view's field surface. The two members
300331
// above own `sort` and `searchableFields`; every OTHER field-naming position
301332
// on a list view — `columns`, filter keys, `grouping`, `rowColor`,
@@ -336,6 +367,7 @@ export const REFERENCE_INTEGRITY_RULES: readonly ReferenceIntegrityRule[] = [
336367
// here: it measured equally clean (0 findings over the same corpus) but its
337368
// crossing is its own adjudication with its own non-vacuity control, and
338369
// this card was scoped to the two axes its title names.
370+
// [#30095] That adjudication is made: see the member's own note above.
339371
//
340372
// [#24563] A member that reads `packageScope` (#27827: so does
341373
// `validateObjectReferences` above, for its fix line only). Under a package

‎packages/lint/src/runtime-gate.object-writes.test.ts‎

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,18 @@ const cleanObject = (over: Record<string, unknown> = {}) => ({
9191
...over,
9292
});
9393

94+
/**
95+
* [#30095] A field map carrying each sort verdict: `owner` has no `sortable`
96+
* key, `priority` declares `sortable: true`, `body` declares `sortable: false`
97+
* and `score` is a virtual `formula`.
98+
*/
99+
const SORT_FIELDS = {
100+
owner: { type: 'text' },
101+
priority: { type: 'number', sortable: true },
102+
body: { type: 'textarea', sortable: false },
103+
score: { type: 'formula', expression: 'record.priority * 2' },
104+
};
105+
94106
const gateObject = (item: unknown, context: object = { objects: STORED }) =>
95107
runRuntimeAuthoringRules({ type: 'object', item, context });
96108

@@ -278,16 +290,19 @@ describe('the object write door dispatches at the adjudicated scope (#4716)', ()
278290
// `defineStack` gives a related-list name matching no inbound FK: resolved
279291
// against `stack.objects`, so no missing-collection channel. The keys are
280292
// new (0 shipped declarations, 0 findings); its controls are below.
293+
// [#30095] `validateSortableFields` is the sixth, the adjudication this pin
294+
// used to hold open: an object's built-in `listViews.*.sort`, resolved
295+
// against `stack.objects`. Measured over the shipped object corpus (127
296+
// objects, 102 list views, 113 sort keys, 0 findings); its controls are
297+
// below.
281298
expect(crossed).toEqual([
282299
'validateSearchableFields',
300+
'validateSortableFields',
283301
'validateListViewFieldRefs',
284302
'validateObjectFieldRefs',
285303
'validateRelatedListRefs',
286304
'validateChartBindings',
287305
]);
288-
// `validateSortableFields` measured equally clean but was NOT crossed:
289-
// that is its own adjudication, and this pin is where it has to be made.
290-
expect(crossed).not.toContain('validateSortableFields');
291306
// Every crossed member still judges flow snapshots — the #4463 P1 surface
292307
// is not narrowed by the member axis existing.
293308
const offFlow = REFERENCE_INTEGRITY_RULES
@@ -336,6 +351,39 @@ describe('the object write door dispatches at the adjudicated scope (#4716)', ()
336351
expect(f.message).toContain('gone_column');
337352
});
338353

354+
// [#30095] The sort axis of the same built-in list view. The list door
355+
// refuses each of these `400 INVALID_SORT` on the view's first fetch and
356+
// every fetch after it, so the object door refuses the declaration.
357+
it.each([
358+
['a field that declares `sortable: false`', 'body', 'sort-field-unsortable', 'declares `sortable: false`'],
359+
['a virtual `formula` field', 'score', 'sort-field-unsortable', "a virtual 'formula' field"],
360+
['a name the object does not have', 'gone_field', 'sort-field-unknown', 'gone_field'],
361+
])('REFUSES an object whose built-in list view sorts by %s', (_label, field, rule, said) => {
362+
const result = expectSingleRefusal(
363+
cleanObject({
364+
fields: SORT_FIELDS,
365+
listViews: { inbox: { label: 'Inbox', columns: ['owner'], sort: [{ field, order: 'asc' }] } },
366+
}),
367+
rule,
368+
);
369+
const sortErrors = result.errors.filter((e) => e.rule.startsWith('sort-field-'));
370+
expect(sortErrors.map((e) => e.path)).toEqual(['objects.leave_request.listViews.inbox.sort[0]']);
371+
expect(sortErrors[0]!.message).toContain(said);
372+
});
373+
374+
it('CONTROL — the same list view sorting by a sortable field publishes, and the member ran', () => {
375+
for (const field of ['owner', 'priority']) {
376+
const result = gateObject(
377+
cleanObject({
378+
fields: SORT_FIELDS,
379+
listViews: { inbox: { label: 'Inbox', columns: ['owner'], sort: [{ field, order: 'asc' }] } },
380+
}),
381+
);
382+
expect(result.errors, `${field}: ${JSON.stringify(result.errors)}`).toEqual([]);
383+
expect(result.rulesRun).toContain('validateReferenceIntegrity');
384+
}
385+
});
386+
339387
it('a clean object carrying BOTH declarations publishes — the refusal is about the reference', () => {
340388
// The other half of each control above: the same two keys, every name
341389
// resolving, and the door adds nothing. Without this, "refuses a dangling

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5531,3 +5531,76 @@ describe('runtime authoring gate — a stored page sort naming an unknown field
55315531
expect(activePages(rows)).toHaveLength(1);
55325532
});
55335533
});
5534+
5535+
// #30095 — an OBJECT whose own built-in list view sorts by a field the list
5536+
// door refuses is refused at the object publish door. `validateSortableFields`
5537+
// judged `listViews.*.sort` on `os validate` and on the view and page doors,
5538+
// but not on an object write, so the object saved green and that list view
5539+
// answered `400 INVALID_SORT` on every load (`sortable: false`: #29896). The
5540+
// member now declares `object`, on the #15254 terms its two list-view siblings
5541+
// crossed on.
5542+
describe('runtime authoring gate — an object list view sort the list door refuses (#30095)', () => {
5543+
const OBJECT = 'help_ticket';
5544+
/** A list view on the object, sorting by `field`. */
5545+
const ticket = (field: string) => ({
5546+
name: OBJECT,
5547+
label: 'Ticket',
5548+
sharingModel: 'private',
5549+
fields: {
5550+
subject: { type: 'text', label: 'Subject' },
5551+
body: { type: 'textarea', label: 'Body', sortable: false },
5552+
priority: { type: 'number', label: 'Priority', sortable: true },
5553+
},
5554+
listViews: {
5555+
inbox: { label: 'Inbox', columns: ['subject'], sort: [{ field, order: 'asc' }] },
5556+
},
5557+
});
5558+
const saveTicket = (protocol: any, field: string) =>
5559+
protocol.saveMetaItem({ type: 'object', name: OBJECT, item: ticket(field) });
5560+
const sortIssues = (err: any) => (err?.issues ?? []).filter((i: any) => String(i.rule).startsWith('sort-field-'));
5561+
const activeObjects = (rows: Map<string, Row>) =>
5562+
Array.from(rows.values()).filter((r) => r.type === 'object' && r.state === 'active');
5563+
5564+
let warn: ReturnType<typeof vi.spyOn>;
5565+
beforeEach(() => {
5566+
warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
5567+
delete process.env.OS_ALLOW_UNLINTED_METADATA_WRITES;
5568+
});
5569+
afterEach(() => {
5570+
warn.mockRestore();
5571+
});
5572+
5573+
it('refuses a list view sorting by a `sortable: false` field, 422 with the rule named, and stores nothing', async () => {
5574+
const { protocol, rows } = makeProtocol();
5575+
const err = await saveTicket(protocol, 'body').catch((e: any) => e);
5576+
expect(err?.status, JSON.stringify(err?.issues ?? err?.message)).toBe(422);
5577+
expect(err.code).toBe('INVALID_METADATA');
5578+
expect(sortIssues(err)).toEqual([expect.objectContaining({
5579+
rule: 'sort-field-unsortable',
5580+
severity: 'error',
5581+
path: `objects.${OBJECT}.listViews.inbox.sort[0]`,
5582+
message: expect.stringContaining('declares `sortable: false`'),
5583+
})]);
5584+
expect(err.rulesRun).toContain('validateReferenceIntegrity');
5585+
expect(activeObjects(rows)).toEqual([]);
5586+
});
5587+
5588+
it('refuses a list view sorting by a name the object does not have, the same way', async () => {
5589+
const { protocol, rows } = makeProtocol();
5590+
const err = await saveTicket(protocol, 'gone_field').catch((e: any) => e);
5591+
expect(err?.status, JSON.stringify(err?.issues ?? err?.message)).toBe(422);
5592+
expect(err.code).toBe('INVALID_METADATA');
5593+
expect(sortIssues(err).map((i: any) => [i.rule, i.severity, i.path])).toEqual([
5594+
['sort-field-unknown', 'error', `objects.${OBJECT}.listViews.inbox.sort[0]`],
5595+
]);
5596+
expect(activeObjects(rows)).toEqual([]);
5597+
});
5598+
5599+
it('CONTROL — the same list view sorting by a sortable field saves', async () => {
5600+
for (const field of ['subject', 'priority']) {
5601+
const { protocol, rows } = makeProtocol();
5602+
await expect(saveTicket(protocol, field), field).resolves.toMatchObject({ success: true });
5603+
expect(activeObjects(rows).map((r) => r.name)).toEqual([OBJECT]);
5604+
}
5605+
});
5606+
});

0 commit comments

Comments
 (0)