From c58beda3f5eff1c25acd91b9bd082b78f3a32693 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 19:54:00 +0000 Subject: [PATCH] =?UTF-8?q?fix(qa,dx):=20=E8=AE=A9=20dogfood=20=E7=9A=84?= =?UTF-8?q?=20strict=20tsconfig=20=E7=9C=9F=E6=AD=A3=E8=A2=AB=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=20=E2=80=94=E2=80=94=20=E4=BF=AE=2014=20=E6=9D=A1?= =?UTF-8?q?=E7=A7=AF=E5=8E=8B=E5=B9=B6=E6=8E=A5=E8=BF=9B=20turbo=20typeche?= =?UTF-8?q?ck=20(#4855)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packages/qa/dogfood/tsconfig.json` 是一份认真的配置(strict、NodeNext、 include 覆盖 test/**),但 package.json 的 scripts 里只有 `test`。根 `pnpm typecheck` 是 `turbo run typecheck`,只跑声明了该 script 的包 —— 三个 qa 包里这是唯一没声明的一个,于是这份配置从未被执行过。 #4311 的覆盖率闸门看见了这个洞,但把它记成 DEBT(errors: 12)冻结下来。 DEBT 是「暂缓」不是「豁免」:在最新 main 上实测是 14 条,比冻结时多了 2 条 —— 漂移本身就是不能长期用冻结替代执行的证据。 14 条全部修掉: - NodeNext 缺扩展名(2 条 TS2307 + 1 条级联 TS7006):两处 `./field-zoo.matrix` 补 `.js`。包内另外 33 处相对 import 全都带 `.js`,这两处是仅有的例外。 - flow fixture 的 `type` 没收窄(8 条 TS2322):四个 fixture 的 flow 是裸对象 字面量。按 examples/app-todo 的既有写法标注 `: Flow`,而不是 `as const` —— 整份 fixture 因此被 spec 的真实契约检查,而不只是让报错闭嘴。 - 条件展开出的 headers(2 条 TS2322):给 auth 常量标注 Record。 - 连接器 handler 少传参(1 条 TS2554):`handler!({})` 改为 `handler!({}, {})`。 `McpConnectorBundle.handlers` 声明的是 (input, ctx),引擎侧 connector-nodes.ts 也始终按两参数派发;这条一直「能跑」只因这个 handler 恰好忽略 ctx。按声明 修正调用方,没有放松任何契约。 接进执行:package.json 加 `"typecheck": "tsc --noEmit"`,并按闸门的 RECONCILED 不变式在同一个 PR 里删掉 DEBT 条目。覆盖率 60/77 → 61/77,DEBT 17 → 16 个包。 反向验证:塞一个非法 flow `type`,`turbo run typecheck` 判红并精确指出该行; 移回后判绿(命中修复前那次绿跑的同一个 turbo hash)。 `pnpm --filter @objectstack/dogfood test` 481 passed / 3 skipped,行为未变。 Fixes #4855 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ --- .changeset/dogfood-typecheck-wired.md | 15 +++++++++++++++ packages/qa/dogfood/package.json | 1 + .../attachments-permission-matrix.dogfood.test.ts | 6 +++++- .../test/field-zoo-roundtrip.dogfood.test.ts | 2 +- .../qa/dogfood/test/field-zoo-value-shape.test.ts | 2 +- .../test/fixtures/flow-durable-suspend-fixture.ts | 3 ++- .../test/fixtures/flow-function-effect-fixture.ts | 3 ++- .../dogfood/test/fixtures/flow-runas-fixture.ts | 5 +++-- .../dogfood/test/fixtures/flow-touch-fixture.ts | 3 ++- .../showcase-mcp-self-connection.dogfood.test.ts | 6 +++++- scripts/check-type-check-coverage.mjs | 4 ---- 11 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 .changeset/dogfood-typecheck-wired.md diff --git a/.changeset/dogfood-typecheck-wired.md b/.changeset/dogfood-typecheck-wired.md new file mode 100644 index 0000000000..835950a553 --- /dev/null +++ b/.changeset/dogfood-typecheck-wired.md @@ -0,0 +1,15 @@ +--- +--- + +ci(dx): `packages/qa/dogfood` 的 strict tsconfig 现在真的被执行 —— 加上 `typecheck` script 并从 `scripts/check-type-check-coverage.mjs` 的 DEBT 账本毕业(#4855)。Dev scripts / CI only;releases nothing。 + +dogfood 有一份认真的 tsconfig(`strict`、`NodeNext`、`include: ["src/**/*", "test/**/*"]`),但 package.json 的 scripts 里只有 `test`。根 `pnpm typecheck` 是 `turbo run typecheck`,只跑声明了该 script 的包 —— 三个 qa 包里,这是唯一没声明的一个,所以这份配置**从未被执行过**。#4311 的覆盖率闸门确实看见了这个洞,但把它记成 DEBT(`errors: 12`)冻结了下来,而 DEBT 是「暂缓」不是「豁免」:门后的错误只会继续涨。在最新 main 上实测是 **14** 条,比账本冻结时多了 2 条 —— 正是这种漂移说明冻结不能长期替代执行。 + +14 条全部修掉,分四类: + +- **NodeNext 缺扩展名(2 条,TS2307)** —— `field-zoo-roundtrip.dogfood.test.ts` 与 `field-zoo-value-shape.test.ts` 写 `from './field-zoo.matrix'`。包内另外 33 处相对 import 全都带 `.js`,这两处是仅有的例外。vitest 能解析,tsc 不能。附带消掉 1 条级联的 TS7006(未解析的 import 让符号退化成 `any`,回调参数随即报 implicit-any)。 +- **flow fixture 的 `type` 没有收窄(8 条,TS2322)** —— 四个 fixture 的 flow 是裸对象字面量,`type: 'autolaunched'` 推成 `string`,喂给 `defineStack` 时对不上字面量联合。修法不是 `as const`,而是按 `examples/app-todo/src/flows/task.flow.ts` 的既有写法标注 `: Flow`(`import type { Flow } from '@objectstack/spec/automation'`)—— 这样整份 fixture 都被 spec 的真实契约检查,而不只是让报错闭嘴。 +- **条件展开出的 headers(2 条,TS2322)** —— `attachments-permission-matrix.dogfood.test.ts` 的 `token ? { Authorization } : {}` 在匿名分支上推出 `Authorization?: undefined`,展开进 `headers` 后 `HeadersInit`(`Record< string, string >`)拒收。给该常量标注 `Record< string, string >`。 +- **连接器 handler 少传一个参数(1 条,TS2554)** —— `showcase-mcp-self-connection.dogfood.test.ts` 调 `handler!({})`,而 `McpConnectorBundle.handlers` 声明的是 `(input, ctx)`,引擎侧 `connector-nodes.ts` 也始终按两参数派发。这条一直「能跑」,只是因为 MCP 这个 handler 恰好忽略 `ctx` —— 契约上它是错的。改为按声明传两参数,与 connector-mcp 自己的测试一致;**没有放松任何契约**(把 `ctx` 改成可选才是错误方向)。 + +修完接进执行:package.json 加 `"typecheck": "tsc --noEmit"`,并按闸门的 RECONCILED 不变式在同一个 PR 里删掉 DEBT 条目(graduated)。覆盖率从 60/77 走到 61/77,DEBT 从 17 个包降到 16 个。反向验证:在 fixture 里塞一个非法的 flow `type`,`turbo run typecheck` 判红并精确指出该行;移回后判绿(命中修复前那一次绿跑的同一个 turbo hash)。`pnpm --filter @objectstack/dogfood test` 481 passed / 3 skipped,行为未变。 diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index 31f43e70fe..61507465b5 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -6,6 +6,7 @@ "description": "Dogfood regression gate — hand-written golden tests that boot real example apps through @objectstack/verify's in-process HTTP stack, pinning historical runtime regressions (#2018 timezone bucketing, #1994 cross-owner RLS, #2004 field fidelity) that static checks miss.", "type": "module", "scripts": { + "typecheck": "tsc --noEmit", "test": "vitest run" }, "dependencies": { diff --git a/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts b/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts index c6435d06a6..5013414067 100644 --- a/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts +++ b/packages/qa/dogfood/test/attachments-permission-matrix.dogfood.test.ts @@ -61,7 +61,11 @@ function bootFixture(extra: { multiTenant?: boolean } = {}) { /** Drive the REAL presigned three-step upload; returns the fileId. */ async function uploadFile(stack: VerifyStack, token: string | null, name = 'hello.txt'): Promise { - const auth = token ? { Authorization: `Bearer ${token}` } : {}; + // Typed as a header record rather than inferred: the ternary would otherwise + // widen to `{ Authorization?: undefined }` on the anonymous branch, which the + // spread carries into `headers` and `HeadersInit` (Record< string, string >) + // rightly refuses. + const auth: Record = token ? { Authorization: `Bearer ${token}` } : {}; const presignRes = await stack.api('/storage/upload/presigned', { method: 'POST', headers: { 'Content-Type': 'application/json', ...auth }, diff --git a/packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts b/packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts index 226f741a3b..5a2795f2ed 100644 --- a/packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts +++ b/packages/qa/dogfood/test/field-zoo-roundtrip.dogfood.test.ts @@ -21,7 +21,7 @@ import showcaseStack from '@objectstack/example-showcase'; import { SECRET_MASK } from '@objectstack/objectql'; import { bootStack, type VerifyStack } from '@objectstack/verify'; -import { MATRIX, REFERENCE_TARGETS } from './field-zoo.matrix'; +import { MATRIX, REFERENCE_TARGETS } from './field-zoo.matrix.js'; describe('dogfood: field-type capability matrix round-trips over HTTP (#2004)', () => { let stack: VerifyStack; let record: Record; diff --git a/packages/qa/dogfood/test/field-zoo-value-shape.test.ts b/packages/qa/dogfood/test/field-zoo-value-shape.test.ts index 5a9a91b57b..46c2f6c834 100644 --- a/packages/qa/dogfood/test/field-zoo-value-shape.test.ts +++ b/packages/qa/dogfood/test/field-zoo-value-shape.test.ts @@ -14,7 +14,7 @@ import { describe, it, expect } from 'vitest'; import { valueSchemaFor } from '@objectstack/spec/data'; -import { MATRIX } from './field-zoo.matrix'; +import { MATRIX } from './field-zoo.matrix.js'; describe('ADR-0104: field-zoo MATRIX write vectors parse under valueSchemaFor(stored)', () => { const writable = MATRIX.filter( diff --git a/packages/qa/dogfood/test/fixtures/flow-durable-suspend-fixture.ts b/packages/qa/dogfood/test/fixtures/flow-durable-suspend-fixture.ts index 50fef79b44..0a90767f63 100644 --- a/packages/qa/dogfood/test/fixtures/flow-durable-suspend-fixture.ts +++ b/packages/qa/dogfood/test/fixtures/flow-durable-suspend-fixture.ts @@ -22,6 +22,7 @@ import { defineStack } from '@objectstack/spec'; import { ObjectSchema, Field } from '@objectstack/spec/data'; +import type { Flow } from '@objectstack/spec/automation'; /** The record the resumed half of the run stamps, so "it continued" is observable. */ export const SuspendNote = ObjectSchema.create({ @@ -51,7 +52,7 @@ export const SuspendNote = ObjectSchema.create({ * snapshot across the restart) cannot accidentally pass: the variables have to * survive the round-trip through `sys_automation_run` for the right row to move. */ -export const flowDurableSuspend = { +export const flowDurableSuspend: Flow = { name: 'flow_durable_suspend', label: 'Flow Durable Suspend', type: 'screen', diff --git a/packages/qa/dogfood/test/fixtures/flow-function-effect-fixture.ts b/packages/qa/dogfood/test/fixtures/flow-function-effect-fixture.ts index bda1b73a31..e84b708eb3 100644 --- a/packages/qa/dogfood/test/fixtures/flow-function-effect-fixture.ts +++ b/packages/qa/dogfood/test/fixtures/flow-function-effect-fixture.ts @@ -20,6 +20,7 @@ import { defineStack } from '@objectstack/spec'; import { ObjectSchema, Field } from '@objectstack/spec/data'; +import type { Flow } from '@objectstack/spec/automation'; /** One object, so the sweep flow has something real to select. */ export const FxInvoice = ObjectSchema.create({ @@ -36,7 +37,7 @@ export const FxInvoice = ObjectSchema.create({ }); /** start → get_record → script → end. `fn` is the only thing that varies. */ -const sweepFlow = (name: string, fn: string) => ({ +const sweepFlow = (name: string, fn: string): Flow => ({ name, label: name, type: 'autolaunched', diff --git a/packages/qa/dogfood/test/fixtures/flow-runas-fixture.ts b/packages/qa/dogfood/test/fixtures/flow-runas-fixture.ts index 877504088e..f9f0ac0f10 100644 --- a/packages/qa/dogfood/test/fixtures/flow-runas-fixture.ts +++ b/packages/qa/dogfood/test/fixtures/flow-runas-fixture.ts @@ -30,6 +30,7 @@ import { defineStack } from '@objectstack/spec'; import { ObjectSchema, Field } from '@objectstack/spec/data'; import { PermissionSetSchema, RLS, type PermissionSet } from '@objectstack/spec/security'; +import type { Flow } from '@objectstack/spec/automation'; import { SecurityPlugin, securityDefaultPermissionSets } from '@objectstack/plugin-security'; /** The one object under test: an owner-scoped note (isolated via `created_by`). */ @@ -51,7 +52,7 @@ export const RunAsNote = ObjectSchema.create({ * whose id is passed as the `noteId` input variable. The two variants differ * ONLY in `runAs`, isolating the identity switch as the single variable. */ -function touchFlow(name: string, runAs: 'system' | 'user', stamp: string) { +function touchFlow(name: string, runAs: 'system' | 'user', stamp: string): Flow { return { name, label: `RunAs ${runAs} touch`, @@ -81,7 +82,7 @@ function touchFlow(name: string, runAs: 'system' | 'user', stamp: string) { * `data.output.found`. Under `system` the elevated read returns the record; * under `user` the RLS-scoped read returns null. */ -function readFlow(name: string, runAs: 'system' | 'user') { +function readFlow(name: string, runAs: 'system' | 'user'): Flow { return { name, label: `RunAs ${runAs} read`, diff --git a/packages/qa/dogfood/test/fixtures/flow-touch-fixture.ts b/packages/qa/dogfood/test/fixtures/flow-touch-fixture.ts index a01cabbff4..dec8cfb952 100644 --- a/packages/qa/dogfood/test/fixtures/flow-touch-fixture.ts +++ b/packages/qa/dogfood/test/fixtures/flow-touch-fixture.ts @@ -21,6 +21,7 @@ import { defineStack } from '@objectstack/spec'; import { ObjectSchema, Field } from '@objectstack/spec/data'; +import type { Flow } from '@objectstack/spec/automation'; /** The one object under test: a note the flow stamps as processed. */ export const FlowNote = ObjectSchema.create({ @@ -42,7 +43,7 @@ export const FlowNote = ObjectSchema.create({ * `status` to `processed`. Triggered via `POST /automation/flow_touch/trigger` * with `{ params: { noteId } }`. */ -export const flowTouch = { +export const flowTouch: Flow = { name: 'flow_touch', label: 'Flow Touch', type: 'autolaunched', diff --git a/packages/qa/dogfood/test/showcase-mcp-self-connection.dogfood.test.ts b/packages/qa/dogfood/test/showcase-mcp-self-connection.dogfood.test.ts index 6f18a229a1..ea8662514f 100644 --- a/packages/qa/dogfood/test/showcase-mcp-self-connection.dogfood.test.ts +++ b/packages/qa/dogfood/test/showcase-mcp-self-connection.dogfood.test.ts @@ -81,7 +81,11 @@ describe('showcase: the platform connects to its OWN MCP endpoint (#3167 self-co try { const handler = bundle.handlers['list_objects']; expect(handler, 'list_objects handler present').toBeDefined(); - const result = await handler!({}); + // Two args, as the engine itself dispatches them (connector-nodes.ts calls + // `handler(input, handlerCtx)`); the connector's own tests call it the same + // way. The one-arg call here only ever worked because this MCP handler + // happens to ignore `ctx` — it was not the declared contract. + const result = await handler!({}, {}); // The app's own object list, round-tripped back through its own MCP surface. expect(JSON.stringify(result), `self list_objects result: ${JSON.stringify(result)}`).toContain('showcase_'); } finally { diff --git a/scripts/check-type-check-coverage.mjs b/scripts/check-type-check-coverage.mjs index 4c1489971e..714126bd1f 100644 --- a/scripts/check-type-check-coverage.mjs +++ b/scripts/check-type-check-coverage.mjs @@ -129,10 +129,6 @@ const DEBT = { errors: 91, note: 'code-tier 3; the rest is config-tier (TS2835/TS2347 module resolution) and noise (TS7006).', }, - '@objectstack/dogfood': { - errors: 12, - note: 'code-tier 8 (TS2322/TS2554) + 3 config-tier + 1 noise.', - }, '@objectstack/hono': { errors: 3, note: 'all code-tier (TS2769/TS18046).',