fix(invariants): assert runtime relationships, not API shapes
This commit is contained in:
@@ -16,8 +16,10 @@ function handwrittenInvariant(packageName: string): string {
|
||||
return `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const install = (_ctx: unknown, fail: (message: string) => never) => {
|
||||
if (typeof ${JSON.stringify(packageName)} !== 'string') fail('package name must remain a string')
|
||||
const install = (ctx: { on(name: string, listener: (value: number) => void): void }, fail: (message: string) => never) => {
|
||||
ctx.on('probe/value', (value) => {
|
||||
if (value < 0) fail('observed values must be non-negative')
|
||||
})
|
||||
}
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
|
||||
Promise.resolve(ctx.invariants.register(${JSON.stringify(packageName)}, install))
|
||||
@@ -65,41 +67,6 @@ function fixture(options: {
|
||||
return root
|
||||
}
|
||||
|
||||
function addConformingPackage(root: string, slug: string, packageName: string, source: string): void {
|
||||
const dir = join(root, `packages/core/${slug}`)
|
||||
mkdirSync(join(dir, 'src'), { recursive: true })
|
||||
writeFileSync(join(dir, 'package.json'), `${JSON.stringify({
|
||||
name: packageName,
|
||||
exports: {
|
||||
'./invariant': {
|
||||
types: './lib/types/invariant.d.ts',
|
||||
default: './lib/invariant.js',
|
||||
},
|
||||
},
|
||||
files: ['lib/invariant.js'],
|
||||
peerDependencies: { '@deepseek-ai/dsh-invariants': '^0.0.1' },
|
||||
devDependencies: { '@deepseek-ai/dsh-invariants': 'workspace:^' },
|
||||
}, null, 2)}\n`)
|
||||
writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
|
||||
references: [{ path: '../../support/invariants' }],
|
||||
}, null, 2)}\n`)
|
||||
writeFileSync(join(dir, 'src/invariant.ts'), source)
|
||||
writeFileSync(join(dir, 'tsdown.config.ts'), "export default { entry: ['lib/types/invariant.js'] }\n")
|
||||
}
|
||||
|
||||
function nameObservedInvariant(packageName: string, pluginName: string): string {
|
||||
return `
|
||||
import { observePluginInvariant } from '@deepseek-ai/dsh-invariants'
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const install = (ctx: never, fail: (message: string) => never) => {
|
||||
observePluginInvariant(ctx, fail, { name: ${JSON.stringify(pluginName)} })
|
||||
}
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
|
||||
Promise.resolve(ctx.invariants.register(${JSON.stringify(packageName)}, install))
|
||||
`
|
||||
}
|
||||
|
||||
describe('package invariant gate', () => {
|
||||
it('accepts a hand-owned checking companion with publication metadata', () => {
|
||||
expect(collectPackageInvariantViolations(fixture())).toEqual([])
|
||||
@@ -139,27 +106,24 @@ export const apply = (ctx: { invariants: { register(name: string, install: typeo
|
||||
]))
|
||||
})
|
||||
|
||||
it('rejects generated markers and empty or reporter-free installers', () => {
|
||||
it('rejects generated markers and reporter-free executable installers', () => {
|
||||
const generated = fixture({
|
||||
source: `/** @generated */\n${handwrittenInvariant('@deepseek-ai/dsh-probe')}`,
|
||||
})
|
||||
expect(collectPackageInvariantViolations(generated).map(violation => violation.message))
|
||||
.toContain('invariant companions must be hand-owned and may not carry @generated markers')
|
||||
|
||||
const empty = fixture({
|
||||
const reporterFree = fixture({
|
||||
source: `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const install = () => {}
|
||||
const install = () => { void 0 }
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: typeof install): () => void } }) =>
|
||||
Promise.resolve(ctx.invariants.register('@deepseek-ai/dsh-probe', install))
|
||||
`,
|
||||
})
|
||||
expect(collectPackageInvariantViolations(empty).map(violation => violation.message))
|
||||
.toEqual(expect.arrayContaining([
|
||||
'install function must contain a package-owned invariant check',
|
||||
'install function must accept the bound failure reporter as its second parameter',
|
||||
]))
|
||||
expect(collectPackageInvariantViolations(reporterFree).map(violation => violation.message))
|
||||
.toContain('install function must accept the bound failure reporter as its second parameter')
|
||||
|
||||
const unused = fixture({
|
||||
source: `
|
||||
@@ -174,22 +138,19 @@ export const apply = (ctx: { invariants: { register(name: string, install: typeo
|
||||
.toContain('install function must use its bound failure reporter')
|
||||
})
|
||||
|
||||
it('rejects duplicate name-based plugin observers across packages', () => {
|
||||
const root = fixture({
|
||||
source: nameObservedInvariant('@deepseek-ai/dsh-probe', 'shared-runtime-name'),
|
||||
})
|
||||
addConformingPackage(
|
||||
root,
|
||||
'probe-two',
|
||||
'@deepseek-ai/dsh-probe-two',
|
||||
nameObservedInvariant('@deepseek-ai/dsh-probe-two', 'shared-runtime-name'),
|
||||
)
|
||||
expect(collectPackageInvariantViolations(root).map(violation => violation.message))
|
||||
.toContain('name-based plugin invariant "shared-runtime-name" is already owned by "@deepseek-ai/dsh-probe-two"')
|
||||
})
|
||||
it('accepts explained empty installers and rejects unexplained ones', () => {
|
||||
const explained = `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-probe'
|
||||
/** No runtime invariant: this pure package owns no events or mutable data. */
|
||||
const install = () => {}
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) =>
|
||||
ctx.invariants.register(PACKAGE_NAME, install)
|
||||
`
|
||||
expect(collectPackageInvariantViolations(fixture({ source: explained }))).toEqual([])
|
||||
|
||||
it('rejects an unexplained empty package installer', () => {
|
||||
const source = `
|
||||
const unexplained = `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-probe'
|
||||
@@ -197,7 +158,7 @@ const install = () => {}
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) =>
|
||||
ctx.invariants.register(PACKAGE_NAME, install)
|
||||
`
|
||||
expect(collectPackageInvariantViolations(fixture({ source })).map(violation => violation.message))
|
||||
expect(collectPackageInvariantViolations(fixture({ source: unexplained })).map(violation => violation.message))
|
||||
.toContain('empty install function must explain why with a "No runtime invariant:" comment')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,6 +8,9 @@ import { existsSync, globSync, readFileSync } from 'node:fs'
|
||||
import { dirname, relative, resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
|
||||
/** Required explanation marker for an intentionally empty installer. */
|
||||
export const NO_RUNTIME_INVARIANT_MARKER = 'No runtime invariant:'
|
||||
|
||||
interface PackageManifest {
|
||||
name?: string
|
||||
exports?: Record<string, { types?: string; default?: string } | string | undefined>
|
||||
@@ -53,23 +56,11 @@ export function packageInvariantOwners(root: string): PackageInvariantOwner[] {
|
||||
/** Return all violations of the package-invariant companion contract. */
|
||||
export function collectPackageInvariantViolations(root: string): PackageInvariantViolation[] {
|
||||
const violations: PackageInvariantViolation[] = []
|
||||
const observedPluginNames = new Map<string, PackageInvariantOwner>()
|
||||
for (const owner of packageInvariantOwners(root)) {
|
||||
const manifest = readManifest(resolve(root, owner.manifestPath))
|
||||
checkManifest(owner, manifest, violations)
|
||||
checkBuild(owner, root, violations)
|
||||
for (const pluginName of checkSource(owner, root, violations)) {
|
||||
const existing = observedPluginNames.get(pluginName)
|
||||
if (existing === undefined) {
|
||||
observedPluginNames.set(pluginName, owner)
|
||||
} else {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.sourcePath,
|
||||
`name-based plugin invariant ${JSON.stringify(pluginName)} is already owned by ${JSON.stringify(existing.packageName)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
checkSource(owner, root, violations)
|
||||
}
|
||||
return violations
|
||||
}
|
||||
@@ -151,11 +142,11 @@ function checkSource(
|
||||
owner: PackageInvariantOwner,
|
||||
root: string,
|
||||
violations: PackageInvariantViolation[],
|
||||
): string[] {
|
||||
): void {
|
||||
const absolutePath = resolve(root, owner.sourcePath)
|
||||
if (!existsSync(absolutePath)) {
|
||||
addViolation(violations, owner.sourcePath, 'missing package-owned invariant companion')
|
||||
return []
|
||||
return
|
||||
}
|
||||
const sourceText = readFileSync(absolutePath, 'utf8')
|
||||
if (sourceText.includes('@generated')) {
|
||||
@@ -206,49 +197,26 @@ function checkSource(
|
||||
addViolation(violations, owner.sourcePath, `must named-export ${exportedName}`)
|
||||
}
|
||||
}
|
||||
checkInstaller(owner, sourceFile, violations)
|
||||
return nameOnlyObservedPlugins(sourceFile)
|
||||
}
|
||||
|
||||
function nameOnlyObservedPlugins(sourceFile: ts.SourceFile): string[] {
|
||||
const names: string[] = []
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isCallExpression(node)
|
||||
&& ts.isIdentifier(node.expression)
|
||||
&& node.expression.text === 'observePluginInvariant') {
|
||||
const contract = node.arguments[2]
|
||||
if (contract !== undefined && ts.isObjectLiteralExpression(contract)) {
|
||||
let hasExactPlugin = false
|
||||
let name: string | undefined
|
||||
for (const property of contract.properties) {
|
||||
if (!ts.isPropertyAssignment(property)) continue
|
||||
const key = ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)
|
||||
? property.name.text
|
||||
: undefined
|
||||
if (key === 'plugin') hasExactPlugin = true
|
||||
if (key === 'name') name = stringValue(property.initializer, new Map())
|
||||
}
|
||||
if (!hasExactPlugin && name !== undefined) names.push(name)
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(sourceFile)
|
||||
return names
|
||||
checkInstaller(owner, sourceFile, sourceText, violations)
|
||||
}
|
||||
|
||||
function checkInstaller(
|
||||
owner: PackageInvariantOwner,
|
||||
sourceFile: ts.SourceFile,
|
||||
sourceText: string,
|
||||
violations: PackageInvariantViolation[],
|
||||
): void {
|
||||
let initializer: ts.Expression | undefined
|
||||
let declarationStatement: ts.VariableStatement | undefined
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (!ts.isVariableStatement(statement)) continue
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
if (ts.isIdentifier(declaration.name)
|
||||
&& declaration.name.text === 'install'
|
||||
&& declaration.initializer !== undefined) initializer = declaration.initializer
|
||||
&& declaration.initializer !== undefined) {
|
||||
initializer = declaration.initializer
|
||||
declarationStatement = statement
|
||||
}
|
||||
}
|
||||
}
|
||||
const installer = initializer === undefined ? undefined : installerFunction(initializer)
|
||||
@@ -257,7 +225,17 @@ function checkInstaller(
|
||||
return
|
||||
}
|
||||
if (ts.isBlock(installer.body) && installer.body.statements.length === 0) {
|
||||
addViolation(violations, owner.sourcePath, 'install function must contain a package-owned invariant check')
|
||||
const declarationText = declarationStatement === undefined
|
||||
? ''
|
||||
: sourceText.slice(declarationStatement.getFullStart(), declarationStatement.getEnd())
|
||||
if (!declarationText.includes(NO_RUNTIME_INVARIANT_MARKER)) {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.sourcePath,
|
||||
`empty install function must explain why with a "${NO_RUNTIME_INVARIANT_MARKER}" comment`,
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
const reporter = installer.parameters[1]?.name
|
||||
if (reporter === undefined || !ts.isIdentifier(reporter)) {
|
||||
|
||||
@@ -72,10 +72,24 @@ describe('global test invariant host', () => {
|
||||
it('limits manual composition to focused invariant topology tests', () => {
|
||||
expect(MANUAL_INVARIANT_TESTS).toEqual([
|
||||
'/packages/support/invariants/tests/service.spec.ts',
|
||||
'/packages/bash/bash/tests/invariant.spec.ts',
|
||||
'/packages/compact/compact/tests/invariant.spec.ts',
|
||||
'/packages/context/time-context/tests/invariant.spec.ts',
|
||||
'/packages/core/session/tests/invariant.spec.ts',
|
||||
'/packages/core/agent/tests/invariant.spec.ts',
|
||||
'/packages/core/scope/tests/invariant.spec.ts',
|
||||
'/packages/core/agent-loop/tests/invariant.spec.ts',
|
||||
'/packages/core/system-prompt/tests/invariant.spec.ts',
|
||||
'/packages/core/tools/tests/invariant.spec.ts',
|
||||
'/packages/fs/fs/tests/invariant.spec.ts',
|
||||
'/packages/hooks/hook-protocol/tests/invariant.spec.ts',
|
||||
'/packages/llm/llm/tests/invariant.spec.ts',
|
||||
'/packages/subagent/subagent/tests/invariant.spec.ts',
|
||||
'/packages/tasks/tasks/tests/invariant.spec.ts',
|
||||
'/packages/todo/tool-todo/tests/invariant.spec.ts',
|
||||
'/packages/ui/permission/tests/invariant.spec.ts',
|
||||
'/packages/ui/user-approval/tests/invariant.spec.ts',
|
||||
'/packages/workflow/workflow/tests/invariant.spec.ts',
|
||||
'/packages/examples/agent-spine-demo/tests/agent-core.spec.ts',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -31,10 +31,24 @@ export const testInvariantCompanions: Readonly<Record<string, TestInvariantCompa
|
||||
/** Tests that exercise selection or companion lifecycle with a deliberately hand-built service tree. */
|
||||
export const MANUAL_INVARIANT_TESTS = [
|
||||
'/packages/support/invariants/tests/service.spec.ts',
|
||||
'/packages/bash/bash/tests/invariant.spec.ts',
|
||||
'/packages/compact/compact/tests/invariant.spec.ts',
|
||||
'/packages/context/time-context/tests/invariant.spec.ts',
|
||||
'/packages/core/session/tests/invariant.spec.ts',
|
||||
'/packages/core/agent/tests/invariant.spec.ts',
|
||||
'/packages/core/scope/tests/invariant.spec.ts',
|
||||
'/packages/core/agent-loop/tests/invariant.spec.ts',
|
||||
'/packages/core/system-prompt/tests/invariant.spec.ts',
|
||||
'/packages/core/tools/tests/invariant.spec.ts',
|
||||
'/packages/fs/fs/tests/invariant.spec.ts',
|
||||
'/packages/hooks/hook-protocol/tests/invariant.spec.ts',
|
||||
'/packages/llm/llm/tests/invariant.spec.ts',
|
||||
'/packages/subagent/subagent/tests/invariant.spec.ts',
|
||||
'/packages/tasks/tasks/tests/invariant.spec.ts',
|
||||
'/packages/todo/tool-todo/tests/invariant.spec.ts',
|
||||
'/packages/ui/permission/tests/invariant.spec.ts',
|
||||
'/packages/ui/user-approval/tests/invariant.spec.ts',
|
||||
'/packages/workflow/workflow/tests/invariant.spec.ts',
|
||||
'/packages/examples/agent-spine-demo/tests/agent-core.spec.ts',
|
||||
] as const
|
||||
|
||||
|
||||
Reference in New Issue
Block a user