Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .changeset/service-automation-test-tsc-program.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
"@objectstack/service-automation": patch
---

fix(service-automation): put the test layer in front of tsc, and repair the TS2341 x3 it was hiding (#15048)

`packages/services/service-automation` had **no `typecheck` script at all** —
its scripts were `build` and `test` — so no tsc program anywhere read this
package (`turbo run typecheck` selects only packages that declare the task, so
it skipped this one silently). `tsup` transpiles with esbuild and `vitest`
runs through esbuild type-**stripping**; neither type-checks. The package's
own `tsconfig.json` does include the tests and always did, so the program that
would have read them already existed and was simply never invoked. This is
the `packages/services/**` sibling of `@objectstack/service-cluster`'s same
graduation (#14181 / PR #15032), reached by the same road in.

What that hid was three `TS2341`s, all in
`src/nested-region-parity.test.ts` (lines 95/151/180):

```
error TS2341: Property 'flows' is private and only accessible within class 'AutomationEngine'.
```

Three tests dot-read the private `AutomationEngine#flows` map directly
instead of going through the class's own public accessor,
`await engine.getFlow(name)` — already the idiom every other test file in
this package uses. The fix replaces the three private reads with that
existing public call (making the two synchronous test bodies `async` where
they were not already); no source signature was widened, no cast was added.

Wired by the route the `packages/plugins/**` family settled on in #14062 and
`service-cluster` carried into `packages/services/**` in #14181: a sibling
`tsconfig.test.json` that changes **module semantics only** (`esnext` /
`bundler` / `lib: ES2022`, matching how vitest actually executes these files)
with **strictness inherited and untouched**, named by a new `typecheck`
script through the shared `check:test-typecheck` gate. Measured before the
repair: 3 errors under build semantics (`tsc -p tsconfig.json`, which already
included the tests), 3 under the new config — the two readings agree, so this
package carried no config-tier pile, and all 3 were genuinely code-tier from
the start. After: 0 and 0, across a 555-file program covering all 103 of its
`src/**/*.test.ts`.

No `test-typecheck-debt.json` is added, and its **absence is the zero**: the
gate reads a missing ledger as `{ entries: {} }`, under which any error in any
file here is red immediately. The package's `DEBT` entry in
`scripts/check-type-check-coverage.mjs` (`errors: 3`) is deleted in this PR
rather than lowered — that is the graduation the ratchet's own invariant
requires, and it is why the errors were fixed rather than ledgered.

`scripts/check-type-source-resolution.mjs` also gains a registry entry for
this package: onboarding `tsconfig.test.json` moved the package's tsc program
set (per that gate's documented onboarding-limb terms), exposing 9 workspace
deps whose types resolve through `dist/` with no pre-existing program for them
to have been laundered through. `paths` was measured and rejected as the
alternative — it takes this package's test layer from 0 errors to 648, nearly
all billed to other packages' source.

No runtime code changes: `src/**` (excluding the one edited test file, whose
own assertions are unchanged — only how it reaches the flow moved) is
otherwise byte-identical, so no shipped behaviour moves. The `patch` level
reflects the published `package.json` gaining `typecheck` /
`check:test-typecheck` scripts and a `tsx` devDependency.
5 changes: 4 additions & 1 deletion packages/services/service-automation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
},
"scripts": {
"build": "tsup --config ../../../tsup.config.ts && node ../../../scripts/check-dts-emitted.mjs",
"test": "vitest run"
"test": "vitest run",
"typecheck": "tsc --noEmit && pnpm check:test-typecheck",
"check:test-typecheck": "tsx ../../../scripts/check-test-typecheck.mts --self-test && tsx ../../../scripts/check-test-typecheck.mts --package packages/services/service-automation --project tsconfig.test.json"
},
"dependencies": {
"@objectstack/core": "workspace:*",
Expand All @@ -38,6 +40,7 @@
"@objectstack/service-job": "workspace:*",
"@objectstack/service-messaging": "workspace:*",
"@types/node": "^26.2.0",
"tsx": "^4.23.12",
"typescript": "^6.0.3",
"vitest": "^4.1.10"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ describe('#4347 — a loop-body predicate is canonicalized like a top-level one'
it.each([
['a bare string', CONDITION],
['an explicit CEL envelope', ENVELOPE],
])('stores %s as the canonical envelope on BOTH edges', (_label, condition) => {
])('stores %s as the canonical envelope on BOTH edges', async (_label, condition) => {
engine.registerFlow('repro', reproFlow(condition));
const flow = engine.flows.get('repro')!;
const flow = (await engine.getFlow('repro'))!;

const topEdge = flow.edges.find(e => e.id === 'e2')!.condition;
const bodyEdge = (flow.nodes.find(n => n.id === 'loop')!.config as any).body.edges[0].condition;
Expand Down Expand Up @@ -148,13 +148,13 @@ describe('#4347 — the conversion table reaches a node inside a region', () =>
edges: [{ id: 'e1', source: 'start', target: 'loop', type: 'default' }],
});

expect((engine.flows.get('callout')!.nodes[1]!.config as any).body.nodes[0].type).toBe('http');
expect(((await engine.getFlow('callout'))!.nodes[1]!.config as any).body.nodes[0].type).toBe('http');
const result = await engine.execute('callout', { params: {}, event: 'schedule' } as never);
expect(result.success).toBe(true);
expect(called).toEqual(['nested']);
});

it('canonicalizes a nested CRUD alias — an unconverted `filters` leaves no filter at all', () => {
it('canonicalizes a nested CRUD alias — an unconverted `filters` leaves no filter at all', async () => {
const engine = new AutomationEngine(silentLogger());
registerLoopNode(engine, ctx());
engine.registerNodeExecutor({ type: 'delete_record', async execute() { return { success: true }; } } as NodeExecutor);
Expand All @@ -177,7 +177,7 @@ describe('#4347 — the conversion table reaches a node inside a region', () =>
edges: [{ id: 'e1', source: 'start', target: 'loop', type: 'default' }],
});

expect((engine.flows.get('purge')!.nodes[1]!.config as any).body.nodes[0].config)
expect(((await engine.getFlow('purge'))!.nodes[1]!.config as any).body.nodes[0].config)
.toEqual({ objectName: 'lead', filter: { status: 'stale' } });
});
});
Expand Down
88 changes: 88 additions & 0 deletions packages/services/service-automation/tsconfig.test.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// The TEST-layer type-check program (#15048 — the `packages/services/**`
// instance of the class #14062 settled for `packages/plugins/**` and #14181
// carried to `service-cluster` (PR #15032), itself adopting the mechanism
// #5286 set for `packages/spec`, #5449 generalised, #12542 carried to
// `packages/rest` and #13176 to `packages/plugins/plugin-security`).
// `tsconfig.json` beside this one stays exactly as it is: it is the BUILD
// config. This sibling puts the test layer in front of tsc under the module
// semantics vitest really executes it with, and `package.json`'s `typecheck`
// script NAMES it (via `check:test-typecheck --project`), because a config no
// script invokes is exactly the phantom this whole change is about.
//
// ⚠️ WHY THIS PACKAGE COPIES `plugin-webhooks` / `service-cluster` RATHER THAN
// `plugin-auth` / `plugin-sharing` / `core`: the deciding property is what the
// BUILD config does with tests. `service-automation`'s `tsconfig.json` does
// NOT exclude `src/**/*.test.ts` and never did — its `include` is `["src"]`
// with no test exclusion — so the program that would have read them already
// existed; it was simply never invoked (no `typecheck` script at all, only
// `build` and `test`). That is the `plugin-webhooks`/`service-cluster` shape,
// not the `exclude`-and-compensate shape the other three packages carry, and
// AGENTS.md forbids ADDING such an exclusion, so their route does not
// transfer here.
//
// What differs from the build config, and what deliberately does NOT:
// - MODULE SEMANTICS ONLY, plus `lib`. The tests are written and executed as
// ESM by vitest (esbuild/vite). Matching that is FIDELITY, not laxity: it
// is the same subtraction `packages/spec`, `packages/rest`,
// `plugin-security`, `plugin-webhooks` and `service-cluster` each made.
// - ⛔ STRICTNESS IS UNTOUCHED. `strict`, `noUnusedLocals`,
// `noUnusedParameters`, `noImplicitReturns`, `noFallthroughCasesInSwitch`,
// `rootDir`, `paths` and `types` are all INHERITED from `tsconfig.json`
// (and through it the root config), and none of them is re-declared here.
// ⚠️ A child that declared its own `paths` would REPLACE the parent map
// rather than merge into it, silently sending a source-resolved specifier
// back to `dist/` — a BUILD ARTIFACT — so this file declares none.
// Nothing here may loosen a type rule; if a test does not compile, that is
// the finding.
// - `lib: ["ES2022"]`, for the same reason `packages/rest` states: the root
// config's `lib` is ES2020 and vitest runs on a Node that has es2022
// builtins, so the gap is reported as TS2550 about the CHECK. No `DOM`:
// nothing in this layer touches a browser global.
//
// MEASURED at 2cc4610304 (origin/main), workspace closure built first
// (`pnpm --filter '@objectstack/service-automation^...' build`, then
// `tsc --noEmit --pretty false --listFiles -p tsconfig.test.json`, and the
// same command without `--listFiles`), BEFORE any fix:
//
// files in this program 555
// own `src/**/*.test.ts` in it 103
// errors under BUILD semantics (tsc -p tsconfig.json, which already
// included the tests) 3
// errors under THIS config 3
//
// The two readings AGREE, so this package carried no config-tier pile at all
// — unlike `@objectstack/core` (#14916: 98 undivided -> 4 after the split,
// nearly all TS7006 cascading from one unresolved import) — and the 3 were
// genuinely code-tier from the start: all TS2341 ("Property 'flows' is
// private…"), all in `src/nested-region-parity.test.ts` (lines 95/151/180),
// where three tests dot-read the PRIVATE `AutomationEngine#flows` map
// directly instead of going through the class's own public accessor. That
// accessor already exists and is already the idiom every other test file in
// this package uses — `await engine.getFlow(name)` (defined at
// `src/engine.ts`, returns `this.flows.get(name) ?? null`) — so the fix is
// not a workaround: it replaces three private-internals reads with the public
// surface the class was already offering, exactly as the rest of the suite
// does. AFTER: 0 and 0, across a 555-file program covering all 103 of this
// package's `src/**/*.test.ts`.
//
// There is NO `test-typecheck-debt.json` beside this config, and its ABSENCE
// is the zero: `check:test-typecheck` reads a missing ledger as
// `{ entries: {} }`, under which ANY error in ANY file here is red
// immediately, with no entry to be added to. That is strictly stronger than a
// ledger holding nothing, and it is the same call `plugin-webhooks`,
// `plugin-security` (#13176) and `service-cluster` (#14181) each recorded for
// themselves. If this package ever acquires residue that cannot be fixed in
// the PR that causes it, THAT is when a ledger and a `gen:test-typecheck-debt`
// script are owed — and adding one is maintainer-only (#5286), exactly as the
// gate says when it refuses.
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true,
"module": "esnext",
"moduleResolution": "bundler",
"lib": ["ES2022"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 20 additions & 14 deletions scripts/check-type-check-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,26 @@ const ROOT_PROGRAM_COUPLED_SCRIPT = 'scripts/check-test-typecheck.mts';
// field the real call passes (TS2339). All 4 are fixed in the test file,
// matching each mock's type to the call site it stubs; `ExecutionContext`
// itself was not touched (it was correct -- the test's field name was stale).
//
// `@objectstack/service-automation` GRADUATED from this ledger (#15048; entry:
// 3 raw, repaired to 0), the `packages/services/**` sibling of the
// `service-cluster` graduation above (#14181/PR #15032) -- same road in: no
// `typecheck` script at all (only `build` and `test`), and a BUILD
// `tsconfig.json` that does NOT exclude tests, so the program that would have
// read them already existed and was simply never invoked. Measured BOTH ways
// (`tsc -p tsconfig.json`, which already included the tests, vs the new
// `tsconfig.test.json`): 3 and 3 -- the two readings AGREE, so this package
// carried no config-tier pile either, and the 3 were genuinely code-tier from
// the start. All 3 were TS2341 ("Property 'flows' is private..."), all in
// `src/nested-region-parity.test.ts` (95/151/180), where three tests dot-read
// the PRIVATE `AutomationEngine#flows` map directly instead of the class's own
// public accessor -- `await engine.getFlow(name)`, already the idiom every
// other test file in this package uses. Repaired by replacing the three
// private reads with that existing public call (no widened source signature,
// no cast, no bracket-notation workaround); the tests were made `async` where
// they were not already. Repaired by the #5286 route -- a `tsconfig.test.json`
// over the test layer, named by a new `typecheck` script -- so the entry is
// deleted rather than lowered.
const DEBT = {
'@objectstack/cloud-connection': {
errors: 13,
Expand All @@ -713,20 +733,6 @@ const DEBT = {
errors: 11,
note: 'all code-tier (TS2554 wrong arity x10, TS2552).',
},
'@objectstack/service-automation': {
errors: 3,
note: 'code-tier 3 (TS2341 x3), all in src/nested-region-parity.test.ts at 95/151/180, where the '
+ 'tests dot-read the private `engine.flows` -- not `engine[\'flows\']`, not `as any` (the casts on '
+ 'two of those lines sit on `.config`, not on the engine, so they do not suppress it). Re-measured '
+ '3 at 53a48c93f4, DOWN from 5 at 5ab08428: the two TS2741 in engine.test.ts this note used to '
+ 'itemise alongside them have graduated -- that file now builds its pausing fixtures through a '
+ 'single defineActionDescriptor helper that declares resumeAuthority (#5561), and engine.test.ts '
+ 'still compiles in this project (`--listFiles` lists it) while reporting nothing. The residue is '
+ 'therefore one decision, not an oversight: whether tests may read private state at all. This '
+ 'entry is the specimen #5278 cites for composition drift and has now drifted BOTH ways -- 2 -> 5 '
+ 'by acquiring a second file, then 5 -> 3 by graduating the first -- so re-read what the pile is '
+ 'made of before sizing it, never just the number.',
},
'@objectstack/service-storage': {
errors: 51,
note: 'code-tier 8 (TS2339 x4, TS2347 x4); config-tier 26 (TS2835 x23, TS2550 x3); noise 17 '
Expand Down
54 changes: 54 additions & 0 deletions scripts/check-type-source-resolution.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,60 @@ const KNOWN_DIST_RESOLVED_TYPE_IMPORTS = {
'@objectstack/lint', '@objectstack/mcp', '@objectstack/platform-objects',
'@objectstack/plugin-auth', '@objectstack/spec', '@objectstack/types',
],
// #15048 re-baseline (the onboarding limb above): a NEW entry, reached ONLY
// through `tsconfig.test.json` -- a program this card ADDED. Same shape as
// the `service-cluster` re-baseline below (#14181): `service-automation` had
// NO `typecheck` script AT ALL before (its scripts were `build` and `test`),
// and its build `tsconfig.json` -- which is ALWAYS a counted program per this
// gate's own design (see `programConfigsFor`'s doc-block) -- measured clean
// on its own, so there is no pre-existing program a dep could be laundered
// through. All 9 deps here are annotated `via tsconfig.test.json` by this
// gate's own failure text.
//
// Provenance measured four ways on one checkout, by varying only what the
// `typecheck` script NAMES (`--list`, totals as printed). RE-MEASURED on the
// merge of `origin/main` @ 919beca43b, which had landed the `service-knowledge`
// onboarding below (#15049) since this card's first reading: that merge moved
// every ABSOLUTE here (+1 program, +3 pairs, +1 package before this entry
// exists) and moved none of the DELTAS, which are what this block claims.
//
// no `typecheck` script (origin/main) absent 120 programs / 293 pairs
// names `tsconfig.json` only absent 120 programs / 293 pairs
// names `tsconfig.test.json` only PRESENT 121 programs / 302 pairs
// names both (this card) PRESENT 121 programs / 302 pairs
//
// Row 2 is the load-bearing one: the BUILD program (which already includes
// every test file -- `tsconfig.json`'s `include` has never excluded them)
// carries no dist-resolved workspace type import at all, so the exposure is
// not merely first SEEN through the onboarded program, it is only REACHABLE
// through it. Numbers, before/after on the same checkout:
//
// before 59 of 78 packages, 120 programs, 293 pairs, 19 clean
// after 60 of 78 packages, 121 programs, 302 pairs, 18 clean
//
// so +1 package, +1 program, +9 pairs (one per dep below) -- this entry and
// nothing else.
//
// Why the entry and not `paths`: MEASURED, not argued. Redirecting these 9
// deps to source takes this package's test layer from 0 errors to 648 (647
// TS6059 `not under rootDir` + 1 TS6133), ALL 647 of the TS6059 in ANOTHER
// package's source -- zero name a file under this package's own `src/`:
// `packages/spec/src/**` 379, `packages/core/src/**` 62,
// `packages/plugins/plugin-security/src/**` 60, `packages/objectql/src/**`
// 49, `packages/services/service-messaging/src/**` 41, `packages/
// metadata-core/src/**` 29, `packages/formula/src/**` 15, `packages/
// services/service-job/src/**` 6, `packages/drivers/driver-sql/src/**` 6 --
// billed to packages that cannot pay them down. Same finding as the
// `service-cluster` re-baseline below (#14181: 0 -> 435) and PR #12570's
// before it, reproduced again at a larger scale because this package pulls
// more workspace deps. The #5286 route this entry backs makes this
// package's OWN test files compile clean; `paths` would immediately re-bury
// that result under other packages' diagnostics.
'@objectstack/service-automation': [
'@objectstack/core', '@objectstack/driver-sql', '@objectstack/formula',
'@objectstack/metadata-core', '@objectstack/objectql', '@objectstack/plugin-security',
'@objectstack/service-job', '@objectstack/service-messaging', '@objectstack/spec',
],
// #14181 re-baseline (the onboarding limb above): a NEW entry, reached ONLY
// through `tsconfig.test.json` -- a program this card ADDED. This is the
// limb's cleanest case rather than a borderline one: `service-cluster` had NO
Expand Down
Loading