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
15 changes: 15 additions & 0 deletions .changeset/dogfood-typecheck-wired.md
Original file line number Diff line number Diff line change
@@ -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,行为未变。
1 change: 1 addition & 0 deletions packages/qa/dogfood/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
const presignRes = await stack.api('/storage/upload/presigned', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...auth },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
Expand Down
2 changes: 1 addition & 1 deletion packages/qa/dogfood/test/field-zoo-value-shape.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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',
Expand Down
5 changes: 3 additions & 2 deletions packages/qa/dogfood/test/fixtures/flow-runas-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`). */
Expand All @@ -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`,
Expand Down Expand Up @@ -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`,
Expand Down
3 changes: 2 additions & 1 deletion packages/qa/dogfood/test/fixtures/flow-touch-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 0 additions & 4 deletions scripts/check-type-check-coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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).',
Expand Down
Loading