Skip to content

Commit c91ace1

Browse files
committed
build(types): type-check this package's tests so the spec-derivation guards actually run (objectstack#4074)
`spec-derived-unions.test.ts` exists to stop a spec-derived union from being re-forked into a hand-written copy, and its header claimed the `satisfies` checks in it "are the real enforcement". They were not. `tsconfig.json` excludes test files — correctly, since it is the package build with `rootDir` / `composite` / `declaration`, so tests would emit into dist — and no other `tsc` invocation read them. Measured, not assumed: reverting `ActionParamFieldType` from the spec's `FieldType` back to a hand-written subset produced ZERO type errors before this change, and now produces TS1360 on the `satisfies` line. The sibling guards over `ChartType`, `ReportType`, `ActionType` and `PageType` were equally inert, so the anti-regression mechanism left by #2944/#2901 was not running in CI. `tsconfig.test.json` follows the shape this package already uses for `tsconfig.examples.json`: a separate, emit-free project chained from `type-check`. Kept separate rather than deleting the exclude so the BUILD stays honest — the reexport guard's source scan needs `types: ["node"]`, and folding that into `tsconfig.json` would let package SOURCE reference Node APIs and still compile, in a package that ships to browsers. Turning it on surfaced 39 pre-existing errors in test files. Fixed: - `p2-spec-exports.test.ts` imported eight `…Schema` names as types from `../index`. #2561 decision (a) removed those and the sibling `spec-ui-schema-reexports.test.ts` asserts their absence, so this file contradicted its own guard for the whole interval — a type-only import of a nonexistent name erases at runtime, so the suite stayed green. Its minimal fixtures were also typed as parsed OUTPUT while being parse INPUT (these schemas `.default()` several fields); they use `z.input<>` now, the distinction spec draws itself with `ActionInput`. `operator: 'eq'` is a legacy alias spec folds at parse time — valid input, absent from the canonical output union. The value-type re-exports the file exists to pin are now asserted explicitly instead of implicitly through fixture annotations. - `app-creation-types.test.ts` / `system-fields.test.ts` imported the package by its own name; turbo's `type-check` dependsOn `^build` (upstream only), so the package's own dist does not exist when it runs. They use the relative import every sibling test uses. Declared as debt rather than fixed: `p1-spec-alignment.test.ts` is excluded with a written reason. Its 14 errors are not hygiene — every one sits in a test named "should accept <shape>" whose whole purpose is asserting the type accepts that shape, and the type rejects it. The clearest case, sharing as `{ visibility, enabled }`, describes a shape that IS handled by `foldSharing` in core's `normalize-list-view.ts`, but only as untyped input (`normalizeListViewSchema<T>(schema: T): T`), so no type names it. Each site is a separate decision — widen the type so the claim becomes true, or drop the claim — and several touch the public surface, so rewriting them silently here would bury the finding. Scope is deliberately narrow: only `packages/types` is converted. 28 other packages still exclude their tests, and 5 (`fields`, `cli`, `data-objectstack`, `plugin-charts`, `plugin-editor`) already include them. This establishes the pattern rather than sweeping the repo. Refs objectstack#4074, #3003, #2561, #2944, #2901, #2911 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DBMo8gnFduzgpuSHd5Eje
1 parent 8864971 commit c91ace1

7 files changed

Lines changed: 176 additions & 39 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
'@object-ui/types': patch
3+
---
4+
5+
**`@object-ui/types`' tests are type-checked, so the spec-derivation guards actually run (framework#4074).**
6+
7+
`spec-derived-unions.test.ts` exists to stop a spec-derived union from being
8+
re-forked into a hand-written copy, and its header claimed the `satisfies` checks
9+
in it "are the real enforcement". They were not. `tsconfig.json` excludes test
10+
files — correctly, since it is the package build with `rootDir` / `composite` /
11+
`declaration`, so tests would emit into dist — and no other `tsc` invocation read
12+
them. Measured, not assumed: reverting `ActionParamFieldType` from the spec's
13+
`FieldType` back to its old hand-written subset produced **zero** type errors.
14+
15+
It now produces `TS1360` on the `satisfies` line. Same for the sibling guards over
16+
`ChartType`, `ReportType`, `ActionType` and `PageType`, which were equally inert —
17+
the anti-regression mechanism left by #2944/#2901 was not running.
18+
19+
`packages/types/tsconfig.test.json` follows the shape the package already uses for
20+
`tsconfig.examples.json`: a separate, emit-free project chained from `type-check`.
21+
Kept separate rather than deleting the exclude so the BUILD stays honest — the
22+
reexport guard's source scan needs `types: ["node"]`, and folding that into
23+
`tsconfig.json` would let package source reference Node APIs and still compile, in
24+
a package that ships to browsers.
25+
26+
Turning it on surfaced 39 pre-existing type errors in test files, all fixed here
27+
except one declared gap:
28+
29+
- **`p2-spec-exports.test.ts`** imported eight `…Schema` names as types from
30+
`../index`. #2561 decision (a) removed those, and the sibling
31+
`spec-ui-schema-reexports.test.ts` asserts their absence — so this file
32+
contradicted its own guard for the whole interval. A type-only import of a
33+
nonexistent name erases at runtime, so the suite stayed green. Its minimal
34+
fixtures were also typed as parsed OUTPUT while being parse INPUT (these schemas
35+
`.default()` several fields); they now use `z.input<>`, the distinction spec
36+
draws itself with `ActionInput`. `operator: 'eq'` is likewise a legacy alias spec
37+
folds at parse time, valid as input and absent from the canonical output union.
38+
- **`app-creation-types.test.ts` / `system-fields.test.ts`** imported the package
39+
by its own name. `turbo`'s `type-check` depends on `^build` (upstream only), so
40+
the package's own `dist` does not exist when it runs; they now use the relative
41+
import every sibling test uses.
42+
- **`p1-spec-alignment.test.ts`** is excluded with a written reason, and is real
43+
debt rather than hygiene: all 14 of its errors sit in tests named
44+
"should accept &lt;shape&gt;" whose entire purpose is asserting the type accepts
45+
that shape, and the type rejects it. The clearest case —
46+
"should accept sharing in ObjectUI format `{ visibility, enabled }`" — describes
47+
a shape that IS handled, by `foldSharing` in core's `normalize-list-view.ts`, but
48+
only as untyped input (`normalizeListViewSchema<T>(schema: T): T`), so no type
49+
names it. Each site is a separate decision (widen the type so the claim becomes
50+
true, or drop the claim) and several touch the public surface, so they are
51+
tracked on framework#4074 instead of being silently rewritten here.
52+
53+
Only `packages/types` is converted. 28 other packages still exclude their tests
54+
from type-checking, and 5 (`fields`, `cli`, `data-objectstack`, `plugin-charts`,
55+
`plugin-editor`) already include them — this establishes the pattern for the rest
56+
rather than sweeping them.

packages/types/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"scripts": {
6565
"build": "tsc",
6666
"clean": "rm -rf dist",
67-
"type-check": "tsc --noEmit && tsc -p tsconfig.examples.json",
67+
"type-check": "tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json",
6868
"lint": "eslint ."
6969
},
7070
"keywords": [

packages/types/src/__tests__/app-creation-types.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
*/
88

99
import { describe, it, expect } from 'vitest';
10-
import { isValidAppName, wizardDraftToAppSchema } from '@object-ui/types';
10+
import { isValidAppName, wizardDraftToAppSchema } from '../index';
1111
import type {
1212
AppWizardDraft,
1313
AppWizardStep,
1414
BrandingConfig,
1515
ObjectSelection,
1616
EditorMode,
1717
AppSchema,
18-
} from '@object-ui/types';
18+
} from '../index';
1919

2020
describe('App Creation Types', () => {
2121
describe('isValidAppName', () => {

packages/types/src/__tests__/p2-spec-exports.test.ts

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,49 @@
1717
* 2. Runtime Zod schemas from @objectstack/spec/ui validate data correctly
1818
*/
1919
import { describe, it, expect } from 'vitest';
20+
// Minimal fixtures below are parse INPUT, not parsed output: these schemas
21+
// `.default()` several fields, so `{ enabled: true }` is valid input while the
22+
// inferred output type requires them. Spec draws the same distinction itself
23+
// (`ActionInput = z.input<typeof ActionSchema>`). Typing them as the output type
24+
// was wrong, and went unnoticed because nothing type-checked this file
25+
// (objectstack#4074).
26+
import type { z } from 'zod';
27+
// The `…Schema` names are deliberately NOT part of this package's surface:
28+
// #2561 decision (a) dropped the spec/ui zod-validator re-exports, and
29+
// `spec-ui-schema-reexports.test.ts` asserts their absence. This file went on
30+
// importing eight of them as types for the whole interval, which no `tsc` run
31+
// ever read (objectstack#4074) — a type-only import of a nonexistent name erases
32+
// at runtime, so the suite stayed green while contradicting its own sibling
33+
// guard. Only the value types are re-exported, so only they are imported here;
34+
// the runtime validators come from `@objectstack/spec/ui` below, as #2561
35+
// prescribes.
2036
import type {
2137
SharingConfig,
22-
SharingConfigSchema,
2338
EmbedConfig,
24-
EmbedConfigSchema,
2539
AddRecordConfig,
26-
AddRecordConfigSchema,
2740
AppearanceConfig,
28-
AppearanceConfigSchema,
2941
UserActionsConfig,
30-
UserActionsConfigSchema,
3142
ViewTab,
32-
ViewTabSchema,
3343
ViewFilterRule,
34-
ViewFilterRuleSchema,
3544
} from '../index';
3645

37-
import type {
38-
ThemeModeSchema,
39-
} from '../theme';
46+
/**
47+
* The type-level half of this file's contract (point 1 of the header): the
48+
* non-`…Schema` value types ARE part of this package's surface — #2561 dropped
49+
* only the zod validators — so importing them must keep compiling. Asserted here
50+
* rather than left implicit in fixture annotations, because most fixtures are
51+
* now typed as parse INPUT and stopped referencing these names.
52+
*/
53+
type _ReexportedValueTypes = [
54+
SharingConfig,
55+
EmbedConfig,
56+
AddRecordConfig,
57+
AppearanceConfig,
58+
UserActionsConfig,
59+
ViewTab,
60+
ViewFilterRule,
61+
];
62+
void 0 as unknown as _ReexportedValueTypes;
4063

4164
// Runtime Zod schemas are imported directly from the spec package
4265
import {
@@ -62,7 +85,7 @@ describe('P2.3 Spec Protocol Type Re-exports — Sharing & Embedding', () => {
6285
});
6386

6487
it('should validate a minimal SharingConfig', () => {
65-
const config: SharingConfig = { enabled: true };
88+
const config: z.input<typeof SharingConfigZod> = { enabled: true };
6689
const result = SharingConfigZod.safeParse(config);
6790
expect(result.success).toBe(true);
6891
});
@@ -89,7 +112,7 @@ describe('P2.3 Spec Protocol Type Re-exports — Sharing & Embedding', () => {
89112
});
90113

91114
it('should validate a minimal EmbedConfig', () => {
92-
const config: EmbedConfig = { enabled: true };
115+
const config: z.input<typeof EmbedConfigZod> = { enabled: true };
93116
const result = EmbedConfigZod.safeParse(config);
94117
expect(result.success).toBe(true);
95118
});
@@ -122,7 +145,7 @@ describe('P2.4 Spec Protocol Type Re-exports — View Configuration', () => {
122145
});
123146

124147
it('should validate a minimal AddRecordConfig', () => {
125-
const config: AddRecordConfig = { enabled: true };
148+
const config: z.input<typeof AddRecordConfigZod> = { enabled: true };
126149
const result = AddRecordConfigZod.safeParse(config);
127150
expect(result.success).toBe(true);
128151
});
@@ -147,7 +170,7 @@ describe('P2.4 Spec Protocol Type Re-exports — View Configuration', () => {
147170
});
148171

149172
it('should validate a minimal AppearanceConfig', () => {
150-
const config: AppearanceConfig = {};
173+
const config: z.input<typeof AppearanceConfigZod> = {};
151174
const result = AppearanceConfigZod.safeParse(config);
152175
expect(result.success).toBe(true);
153176
});
@@ -170,13 +193,13 @@ describe('P2.4 Spec Protocol Type Re-exports — View Configuration', () => {
170193
});
171194

172195
it('should validate a minimal UserActionsConfig', () => {
173-
const config: UserActionsConfig = {};
196+
const config: z.input<typeof UserActionsConfigZod> = {};
174197
const result = UserActionsConfigZod.safeParse(config);
175198
expect(result.success).toBe(true);
176199
});
177200

178201
it('should validate a full UserActionsConfig', () => {
179-
const config: UserActionsConfig = {
202+
const config: z.input<typeof UserActionsConfigZod> = {
180203
sort: true,
181204
search: true,
182205
filter: true,
@@ -195,13 +218,15 @@ describe('P2.4 Spec Protocol Type Re-exports — View Configuration', () => {
195218
});
196219

197220
it('should validate a minimal ViewTab', () => {
198-
const tab: ViewTab = { name: 'all', label: 'All Records' };
221+
const tab: z.input<typeof ViewTabZod> = { name: 'all', label: 'All Records' };
199222
const result = ViewTabZod.safeParse(tab);
200223
expect(result.success).toBe(true);
201224
});
202225

203226
it('should validate a full ViewTab', () => {
204-
const tab: ViewTab = {
227+
// `operator: 'eq'` is a legacy alias spec folds onto `equals` at parse time,
228+
// so it is valid INPUT and absent from the canonical output union.
229+
const tab: z.input<typeof ViewTabZod> = {
205230
name: 'active',
206231
label: 'Active',
207232
icon: 'CheckCircle',
@@ -229,7 +254,7 @@ describe('v3.0.10 Spec Protocol New Types', () => {
229254
});
230255

231256
it('should validate a ViewFilterRule', () => {
232-
const rule: ViewFilterRule = { field: 'status', operator: 'eq', value: 'active' };
257+
const rule: z.input<typeof ViewFilterRuleZod> = { field: 'status', operator: 'eq', value: 'active' };
233258
const result = ViewFilterRuleZod.safeParse(rule);
234259
expect(result.success).toBe(true);
235260
});
@@ -262,17 +287,17 @@ describe('Type re-exports from @object-ui/types index', () => {
262287

263288
it('should allow type annotations with P2.3 Sharing & Embedding types', () => {
264289
// Compile-time check: these lines would fail to compile if types were not re-exported
265-
const sharing: SharingConfig = { enabled: true };
266-
const embed: EmbedConfig = { enabled: false };
290+
const sharing: z.input<typeof SharingConfigZod> = { enabled: true };
291+
const embed: z.input<typeof EmbedConfigZod> = { enabled: false };
267292
expect(sharing.enabled).toBe(true);
268293
expect(embed.enabled).toBe(false);
269294
});
270295

271296
it('should allow type annotations with P2.4 View Configuration types', () => {
272-
const addRecord: AddRecordConfig = { enabled: true };
297+
const addRecord: z.input<typeof AddRecordConfigZod> = { enabled: true };
273298
const appearance: AppearanceConfig = { showDescription: true };
274-
const userActions: UserActionsConfig = { sort: true };
275-
const tab: ViewTab = { name: 'main', label: 'Main' };
299+
const userActions: z.input<typeof UserActionsConfigZod> = { sort: true };
300+
const tab: z.input<typeof ViewTabZod> = { name: 'main', label: 'Main' };
276301
expect(addRecord.enabled).toBe(true);
277302
expect(appearance.showDescription).toBe(true);
278303
expect(userActions.sort).toBe(true);

packages/types/src/__tests__/spec-derived-unions.test.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,21 @@
2727
* implements it, so a host app typing against @object-ui/types got an error
2828
* on working code.
2929
*
30-
* The `satisfies` checks below document that contract at the type level — but they
31-
* are NOT enforcement today, and the previous version of this comment claiming
32-
* they were "the real enforcement" was wrong (#4074). Every package tsconfig
33-
* excludes test files by glob (see `packages/types/tsconfig.json`) and there is no
34-
* vitest `typecheck` project, so no `tsc` invocation reads this file at all. They would
35-
* bite the day tests are type-checked; until then the RUNTIME assertions are the
36-
* only thing that fails CI, which is why the #4074 cases below are written as
37-
* identity and membership checks.
30+
* The `satisfies` checks below ARE enforcement — but only since #3005 added
31+
* `packages/types/tsconfig.test.json` and chained it from this package's
32+
* `type-check` script.
33+
*
34+
* For the whole interval before that they were decorative, and an earlier version
35+
* of this comment asserting they were "the real enforcement" was simply false:
36+
* `tsconfig.json` excludes test files (it is the package build, with `rootDir`,
37+
* `composite` and `declaration`, so tests would emit into dist), and no other
38+
* `tsc` invocation read this file. Measured at the time: reverting a derived alias
39+
* to its old hand-written fork produced ZERO type errors. It now produces
40+
* `TS1360` pointing at the `satisfies` line below.
41+
*
42+
* The runtime assertions further down are still the stronger check for anything
43+
* with a runtime witness — a type alias erases, so only reference identity
44+
* distinguishes a re-export from a faithful copy. Keep both.
3845
*
3946
* The last case is the inverse: `navigation` is a name objectui runs that the
4047
* spec does NOT have. It is asserted absent from the spec so that the day the
@@ -82,9 +89,6 @@ const _pageCovers = null as unknown as 'record' | 'home' | 'app' | 'utility' | '
8289
const _vizCovers = null as unknown as PageVisualizationAlias satisfies PageType;
8390
const _runnableCovers = null as unknown as ActionType satisfies RunnableActionType;
8491
// #4074: the action sub-vocabularies that were restated rather than derived.
85-
// `ActionComponent`'s real compile-time enforcement lives in `ui-action.ts`,
86-
// which IS type-checked: `ActionSchema.component?: ActionComponent` stops
87-
// compiling there if the derivation breaks.
8892
const _componentCovers = null as unknown as
8993
| 'action:button'
9094
| 'action:icon'

packages/types/src/__tests__/system-fields.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { describe, it, expect } from 'vitest';
10-
import { isSystemManagedField, SYSTEM_MANAGED_FIELD_NAMES } from '@object-ui/types';
10+
import { isSystemManagedField, SYSTEM_MANAGED_FIELD_NAMES } from '../index';
1111

1212
describe('isSystemManagedField', () => {
1313
it('treats fields flagged `system: true` as system-managed (the single source of truth)', () => {

packages/types/tsconfig.test.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
// Type-checks this package's TESTS, which `tsconfig.json` excludes.
3+
//
4+
// Why they were excluded, and why that was a hole: `tsconfig.json` is the
5+
// package BUILD (`tsc` -> dist) with "rootDir": "./src", "composite" and
6+
// "declaration", so test files would emit into dist. Excluding them keeps the
7+
// build clean — but it also meant no `tsc` invocation ever read them, and
8+
// `spec-derived-unions.test.ts` had built its whole contract on `satisfies`
9+
// checks that therefore never ran. Its header claimed they were "the real
10+
// enforcement"; reverting a derived alias to its old hand-written fork
11+
// produced zero errors (objectstack#4074).
12+
//
13+
// Same reasoning and same shape as `tsconfig.examples.json`: a separate,
14+
// emit-free project chained from the package's `type-check` script. Keeping it
15+
// separate rather than deleting the exclude also keeps the BUILD honest — the
16+
// "types": ["node"] below is needed by the reexport guard's `readFileSync`
17+
// source scan, and folding that into `tsconfig.json` would let package SOURCE
18+
// reference Node APIs and still compile, in a package that ships to browsers.
19+
"extends": "../../tsconfig.json",
20+
"compilerOptions": {
21+
"noEmit": true,
22+
"lib": ["ES2020", "DOM"],
23+
// `spec-ui-schema-reexports.test.ts` reads sibling sources off disk to prove
24+
// no spec/ui zod value hides inside an `export type` block (#2561).
25+
"types": ["node"],
26+
// Same reason as tsconfig.json: drop the root tsconfig's source-tree paths
27+
// so @objectstack/spec resolves through the workspace dependency.
28+
"paths": {}
29+
},
30+
"include": ["src/**/*.test.ts", "src/**/*.test.tsx"],
31+
// ── Known gap, declared rather than silent ─────────────────────────────────
32+
// `p1-spec-alignment.test.ts` has 14 type errors across 8 sites, and they are
33+
// not hygiene: every one sits in a test named "should accept <shape>" whose
34+
// whole purpose is to assert the type ACCEPTS that shape. The type rejects it,
35+
// and the test passed anyway because nothing compiled the file. Two examples:
36+
//
37+
// - "should accept sharing in ObjectUI format { visibility, enabled }" —
38+
// `ListViewSchema.sharing` is `{ type, lockedBy }` only. The legacy shape is
39+
// real and IS handled, by `foldSharing` in core's `normalize-list-view.ts`,
40+
// but as untyped input (`normalizeListViewSchema<T>(schema: T): T`), so no
41+
// type names it.
42+
// - several minimal fixtures assigned to output types whose fields carry
43+
// `.default()`, the same input/output confusion fixed in
44+
// `p2-spec-exports.test.ts` in this change.
45+
//
46+
// Each one is a separate decision — widen the type so the test's claim becomes
47+
// true, or delete the claim — and several touch the public surface. Doing that
48+
// silently while landing the type-check plumbing would bury it, so the file is
49+
// excluded here WITH this reason and tracked in objectstack#4074. Removing this
50+
// entry is the definition of done; it must only ever shrink.
51+
"exclude": ["src/__tests__/p1-spec-alignment.test.ts"]
52+
}

0 commit comments

Comments
 (0)