test(invariants): bound global companion topology

This commit is contained in:
Tianyi Cui
2026-07-20 01:51:28 +08:00
parent 6520f71f94
commit caaa1364ec
7 changed files with 72 additions and 18 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-19-package-owned-invariant-service.md: d73cb196820369e41a748ad7a1c3c50b889d636f
2026-07-19-package-owned-invariant-service.zh.md: 644fd1f0668a097f028ec805c5cb425e536e277a
2026-07-19-package-owned-invariant-service.md: d7b77439c8cfdfdf2fc4f01e779a6d3c7f345457
2026-07-19-package-owned-invariant-service.zh.md: b553d137f9f06b57f24672bf6984d087ca99ceb2

View File

@@ -84,7 +84,7 @@ Service tests cover defaults, global disablement, allow/block selection, blockli
Composition tests cover standard-spine forwarding and generated SDK entries. Loader tests preserve each companion namespace, while built plain-Node smokes exercise the compiled subpath exports. The scoped-event freshness gate reruns its semantic Program analysis.
Every Vitest configuration loads a test host that mounts an explicitly enabled service and all package companions before an ordinary Cordis root's first plugin. Focused service and owner tests construct their own invariant topology so they can exercise disablement, filtering, rollback, and reload without duplicate ownership. Gate tests also execute every companion's `apply` function and verify that it calls `register` with its manifest name, rather than accepting source text alone.
Every Vitest configuration loads a test host that mounts an explicitly enabled service before an ordinary Cordis root's first plugin. Package tests add their owner's companion, one exhaustive topology mounts every companion, and focused service and owner tests construct their own invariant topology so they can exercise disablement, filtering, rollback, and reload without duplicate ownership. Gate tests also execute every companion's `apply` function and verify that it calls `register` with its manifest name, rather than accepting source text alone.
## Alternatives considered
@@ -101,5 +101,5 @@ Every Vitest configuration loads a test host that mounts an explicitly enabled s
- Explicit companion entries make diagnostic cost and ownership visible in Cordis config and package exports.
- One selected contribution adds one child fiber and its listener/state cost; filtered registrations retain only name ownership.
- Regex sources are deployment configuration and remain fixed until the service reloads.
- Ordinary Vitest roots install every selected companion, trading extra child fibers during tests for repository-wide invariant coverage and immediate fixture failures.
- Ordinary Vitest roots install the current test package's companion; one exhaustive topology retains repository-wide registration coverage without multiplying every child fiber across every test root.
- Session storage validation, snapshotting, freezing, provenance, and surface acceptance remain always on and are not affected by invariant selection.

View File

@@ -84,7 +84,7 @@ Workspace 约束识别独立的不变式 bundle包 exports、项目引用、
组合测试覆盖标准 spine 转发和生成的 SDK 条目。Loader 测试固定每个伴随命名空间,构建后的纯 Node smoke 覆盖编译子路径 export。scoped event 新鲜度门禁会重新执行语义 Program 分析。
每个 Vitest 配置都会加载测试宿主;在普通 Cordis 根上下文启动第一个插件之前,宿主会挂载显式启用的服务以及所有的伴随插件服务与所有者的聚焦测试自行构建不变式拓扑,从而在不发生重复所有权冲突的前提下覆盖关闭、过滤、回滚与重载。门禁测试还会执行每个伴随插件的 `apply` 函数,并验证它调用 `register` 时使用包清单中的包名,而不是只检查源码文本。
每个 Vitest 配置都会加载测试宿主;在普通 Cordis 根上下文启动第一个插件之前,宿主会挂载显式启用的服务。包测试会添加其所有的伴随插件,一个完整拓扑会挂载所有伴随插件;服务与所有者的聚焦测试自行构建不变式拓扑,从而在不发生重复所有权冲突的前提下覆盖关闭、过滤、回滚与重载。门禁测试还会执行每个伴随插件的 `apply` 函数,并验证它调用 `register` 时使用包清单中的包名,而不是只检查源码文本。
## 考虑过的替代方案
@@ -101,5 +101,5 @@ Workspace 约束识别独立的不变式 bundle包 exports、项目引用、
- 显式伴随条目让诊断成本和所有权在 Cordis 配置与包 export 中可见。
- 每个选中贡献增加一个子 fiber 及其监听器和状态成本;被过滤注册只保留包名占用。
- 正则表达式源属于部署配置,在服务重载前保持固定。
- 普通 Vitest 根上下文会安装每个被选中的伴随插件,以增加测试期间的子 fiber 为代价,换取覆盖整个仓库的不变式检查和对 fixture测试前置数据错误的即时反馈
- 普通 Vitest 根上下文会安装当前测试包的伴随插件;一个完整拓扑保留全仓库注册覆盖,而不会在每个测试根上下文中重复创建所有子 fiber
- 会话存储验证、快照、冻结、provenance 与 surface 接受规则始终启用,不受不变式选择影响。

View File

@@ -9,7 +9,7 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning
- **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)).
- **Snapshot** (`pnpm run test:snapshot`): transport-specific keyless expected outputs cover external presentation. ACP suites boot the real example subprocess, replay a recorded session, and diff normalized JSON-RPC plus the re-persisted log ([ACP snapshot Agent Note](../.agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md)); the headless suite independently pins `stream-json` through its real one-shot subprocess. TUI completed journeys replay recorded primary/child JSONL through the real agent loop and tools before projecting ANSI into semantic terminal-state expected outputs; package-local snapshots retain transient renderer states, and a real PTY conversation covers the process boundary ([TUI snapshot Agent Note](../.agents/notes/implemented/testing/2026-07-18-tui-terminal-state-snapshots.md)). Use `pnpm run test:snapshot:record` when a model transcript must change and `pnpm run test:snapshot:refresh` when committed replay input remains correct; review every JSONL and expected-output diff. System-prompt/tool-schema content is pinned by one ACP scenario (`text-turn`) and tokenized in every other fixture, so a prompt or schema edit churns one committed line ([pinned-header Agent Note](../.agents/notes/implemented/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)).
All Vitest configurations mount enabled `ctx.invariants` and every package companion before ordinary Cordis roots start. Focused invariant topology tests compose enabled or deliberately disabled services explicitly, without competing global registrations.
Every Vitest configuration mounts enabled `ctx.invariants` before ordinary roots start. Package tests add their owner's companion; one exhaustive topology mounts them all. Focused invariant topology tests compose enabled or deliberately disabled services without competing global registrations.
## The with-key policy: inference is cheap here

View File

@@ -54,7 +54,7 @@ ctx.plugin(InvariantService, {
ctx.plugin(SessionInvariant)
```
The standard agent spine mounts the service and the four stateful companions. Custom compositions choose the companions they want and may disable or filter them without changing package entrypoints. Vitest mounts every package companion against an explicitly enabled service for ordinary Cordis roots, so baseline ownership and stateful checks execute across unit, snapshot, and e2e suites; focused invariant-service tests construct their own topology to exercise filtering and lifecycle behavior.
The standard agent spine mounts the service and the four stateful companions. Custom compositions choose the companions they want and may disable or filter them without changing package entrypoints. Vitest gives every ordinary root an explicitly enabled service and mounts the current test package's companion; one exhaustive topology mounts all companions once, while focused invariant-service tests construct their own topology to exercise filtering and lifecycle behavior.
## Model Experience

View File

@@ -2,7 +2,11 @@ import { describe, expect, it, vi } from 'vitest'
import { Context, Service } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
import { packageInvariantOwners } from './package-invariants.ts'
import { MANUAL_INVARIANT_TESTS, testInvariantCompanions } from './test-invariants.ts'
import {
MANUAL_INVARIANT_TESTS,
testInvariantCompanionPaths,
testInvariantCompanions,
} from './test-invariants.ts'
declare module 'cordis' {
interface Context {
@@ -17,7 +21,7 @@ class TestInvariantProbe extends Service {
}
describe('global test invariant host', () => {
it('loads every companion and reserves every package name with enabled checks', async () => {
it('uses one exhaustive topology to reserve every package name with enabled checks', async () => {
const ctx = new Context()
await ctx.plugin(TestInvariantProbe)
@@ -39,6 +43,14 @@ describe('global test invariant host', () => {
expect(unreserved).toEqual([])
})
it('mounts the owning package companion while leaving non-package roots service-only', () => {
expect(testInvariantCompanionPaths('/repo/packages/core/tools/tests/tools.spec.ts'))
.toEqual(['../packages/core/tools/src/invariant.ts'])
expect(testInvariantCompanionPaths('/repo/examples/echo-agent/tests/echo.spec.ts')).toEqual([])
expect(testInvariantCompanionPaths('/repo/scripts/test-invariants.spec.ts'))
.toEqual(Object.keys(testInvariantCompanions).sort())
})
it('executes each companion registration with its owning package name', async () => {
const owners = new Map(packageInvariantOwners(process.cwd()).map(owner => [owner.sourcePath, owner.packageName]))
const registrations = new Map<string, string>()

View File

@@ -1,7 +1,8 @@
/**
* Vitest-wide invariant host. Ordinary Cordis roots receive the invariant
* service with global enablement and every package companion before their first
* plugin starts. Focused invariant tests own their service topology explicitly.
* service with global enablement plus the current test package's companion.
* One topology test mounts every companion; focused invariant tests own their
* service topology explicitly.
*/
import { expect } from 'vitest'
@@ -40,6 +41,7 @@ export const MANUAL_INVARIANT_TESTS = [
interface InvariantHost {
readonly fibers: readonly PluginFiber[]
readonly byCallback: ReadonlyMap<unknown, PluginFiber>
readonly ready: Promise<void>
}
type PluginFiber = ReturnType<RegistryService['plugin']>
@@ -55,13 +57,15 @@ RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, ge
const host = hosts.get(root) ?? startInvariantHost(root)
const callback = this.resolve(plugin)
const existing = callback === undefined ? undefined : host.byCallback.get(callback)
if (existing !== undefined) return existing
if (existing !== undefined) {
return this.ctx === root ? joinInvariantStartup(existing, host.ready) : existing
}
const fiber = originalPlugin.call(this, plugin, config, getOuterStack)
// A root-level await is the test's composition boundary. Nested plugin
// fibers must not await their own companion parent through the global host.
if (this.ctx !== root) return fiber
return joinInvariantStartup(fiber, host.fibers)
return joinInvariantStartup(fiber, host.ready)
}
function usesManualInvariantTree(): boolean {
@@ -69,6 +73,30 @@ function usesManualInvariantTree(): boolean {
return MANUAL_INVARIANT_TESTS.some(path => testPath.endsWith(path))
}
const ALL_COMPANION_TESTS = ['/scripts/test-invariants.spec.ts'] as const
/**
* Select the package companions that an ordinary test root must register.
* Package tests receive their owner's checks; the dedicated topology test
* receives every owner so coverage and exhaustive runtime registration remain
* independently enforced.
* @param testPath - absolute or repo-relative normalized Vitest file path.
* @returns sorted `import.meta.glob` keys for companions to mount.
*/
export function testInvariantCompanionPaths(testPath: string): string[] {
const normalized = testPath.replaceAll('\\', '/')
const allPaths = Object.keys(testInvariantCompanions).sort()
if (ALL_COMPANION_TESTS.some(path => normalized.endsWith(path))) return allPaths
const owner = normalized.match(/\/packages\/([^/]+)\/([^/]+)\/tests\//)
if (owner === null) return []
const companionPath = `../packages/${owner[1]}/${owner[2]}/src/invariant.ts`
if (testInvariantCompanions[companionPath] === undefined) {
throw new Error(`test invariants: package test has no companion at ${companionPath}`)
}
return [companionPath]
}
function startInvariantHost(root: Context): InvariantHost {
const fibers: PluginFiber[] = []
const byCallback = new Map<unknown, PluginFiber>()
@@ -81,21 +109,35 @@ function startInvariantHost(root: Context): InvariantHost {
}
mount(InvariantService, { enabled: true })
for (const [path, companion] of Object.entries(testInvariantCompanions).sort(([left], [right]) => left.localeCompare(right))) {
const testPath = expect.getState().testPath ?? ''
const companionPaths = testInvariantCompanionPaths(testPath)
for (const path of companionPaths) {
const companion = testInvariantCompanions[path]
if (companion === undefined) {
throw new Error(`test invariants: selected companion vanished at ${path}`)
}
if (!companion.inject.includes('invariants')) {
throw new Error(`test invariants: ${path} must inject the invariant service`)
}
mount(companion)
}
const host = { fibers, byCallback }
const [serviceFiber, ...companionFibers] = fibers
if (serviceFiber === undefined) throw new Error('test invariants: service fiber was not mounted')
// A companion is initially PENDING on the invariant service, and Cordis
// Fiber.await() only joins work already in flight. Wait for the service to
// activate its dependants before joining their startup and failures.
const ready = serviceFiber.await()
.then(() => Promise.all(companionFibers.map(fiber => fiber.await())))
.then(() => undefined)
const host = { fibers, byCallback, ready }
hosts.set(root, host)
return host
}
function joinInvariantStartup(fiber: PluginFiber, invariantFibers: readonly PluginFiber[]): PluginFiber {
function joinInvariantStartup(fiber: PluginFiber, invariantReady: Promise<void>): PluginFiber {
const readiness = fiber.await().then(async (loaded) => {
await Promise.all(invariantFibers.map(invariant => invariant.await()))
await invariantReady
return loaded
})
const joined = Object.create(fiber) as PluginFiber