fix(invariants): enforce runtime relationships
This commit is contained in:
@@ -138,6 +138,20 @@ export const apply = (ctx: { invariants: { register(name: string, install: typeo
|
||||
.toContain('install function must use its bound failure reporter')
|
||||
})
|
||||
|
||||
it('rejects registering a different installer than the checked local function', () => {
|
||||
const decoy = fixture({
|
||||
source: `
|
||||
export const name = 'probe-invariant'
|
||||
export const inject = ['invariants']
|
||||
const install = (_ctx: unknown, fail: (message: string) => never) => { fail('checked decoy') }
|
||||
export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) =>
|
||||
ctx.invariants.register('@deepseek-ai/dsh-probe', () => {})
|
||||
`,
|
||||
})
|
||||
expect(collectPackageInvariantViolations(decoy).map(violation => violation.message))
|
||||
.toContain('line 6: ctx.invariants.register must use the checked local install function')
|
||||
})
|
||||
|
||||
it('accepts explained empty installers and rejects unexplained ones', () => {
|
||||
const explained = `
|
||||
export const name = 'probe-invariant'
|
||||
|
||||
@@ -167,12 +167,18 @@ function checkSource(
|
||||
const constants = topLevelStringConstants(sourceFile)
|
||||
const registrations: string[] = []
|
||||
const unresolved: number[] = []
|
||||
const mismatchedInstallers: number[] = []
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isCallExpression(node) && isInvariantRegistration(node.expression)) {
|
||||
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1
|
||||
const argument = node.arguments[0]
|
||||
const packageName = argument === undefined ? undefined : stringValue(argument, constants)
|
||||
if (packageName === undefined) unresolved.push(sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1)
|
||||
if (packageName === undefined) unresolved.push(line)
|
||||
else registrations.push(packageName)
|
||||
const installer = node.arguments[1]
|
||||
if (installer === undefined || !ts.isIdentifier(installer) || installer.text !== 'install') {
|
||||
mismatchedInstallers.push(line)
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
@@ -185,6 +191,13 @@ function checkSource(
|
||||
`line ${line}: ctx.invariants.register package name must resolve to a local string constant`,
|
||||
)
|
||||
}
|
||||
for (const line of mismatchedInstallers) {
|
||||
addViolation(
|
||||
violations,
|
||||
owner.sourcePath,
|
||||
`line ${line}: ctx.invariants.register must use the checked local install function`,
|
||||
)
|
||||
}
|
||||
if (registrations.length !== 1 || registrations[0] !== owner.packageName) {
|
||||
addViolation(
|
||||
violations,
|
||||
|
||||
@@ -83,6 +83,7 @@ describe('global test invariant host', () => {
|
||||
'/packages/fs/fs/tests/invariant.spec.ts',
|
||||
'/packages/hooks/hook-protocol/tests/invariant.spec.ts',
|
||||
'/packages/llm/llm/tests/invariant.spec.ts',
|
||||
'/packages/llm/llm-retry/tests/invariant.spec.ts',
|
||||
'/packages/sandbox/sandbox-policy/tests/invariant.spec.ts',
|
||||
'/packages/subagent/subagent/tests/invariant.spec.ts',
|
||||
'/packages/tasks/tasks/tests/invariant.spec.ts',
|
||||
|
||||
@@ -42,6 +42,7 @@ export const MANUAL_INVARIANT_TESTS = [
|
||||
'/packages/fs/fs/tests/invariant.spec.ts',
|
||||
'/packages/hooks/hook-protocol/tests/invariant.spec.ts',
|
||||
'/packages/llm/llm/tests/invariant.spec.ts',
|
||||
'/packages/llm/llm-retry/tests/invariant.spec.ts',
|
||||
'/packages/sandbox/sandbox-policy/tests/invariant.spec.ts',
|
||||
'/packages/subagent/subagent/tests/invariant.spec.ts',
|
||||
'/packages/tasks/tasks/tests/invariant.spec.ts',
|
||||
|
||||
Reference in New Issue
Block a user