feat(invariants): require package-owned companions

This commit is contained in:
Tianyi Cui
2026-07-19 22:13:50 +08:00
parent 2c7822ddee
commit 433670a754
316 changed files with 5348 additions and 646 deletions

View File

@@ -93,37 +93,6 @@ function workspaceManifests(): WorkspaceManifest[] {
return manifests
}
const dshPackageFiles = [
'lib/index.js',
'lib/types/**/*.d.ts',
'lib/types/**/*.d.ts.map',
'src',
] as const
const dshBinPackageFiles = [
'lib/index.js',
'lib/bin.js',
'lib/types/**/*.d.ts',
'lib/types/**/*.d.ts.map',
'src',
] as const
const dshWorkerPackageFiles = [
'lib/index.js',
'lib/worker.cjs',
'lib/types/**/*.d.ts',
'lib/types/**/*.d.ts.map',
'src',
] as const
const dshInvariantPackageFiles = [
'lib/index.js',
'lib/invariant.js',
'lib/types/**/*.d.ts',
'lib/types/**/*.d.ts.map',
'src',
] as const
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
'@deepseek-ai/dsh-helper': ['lib/assets'],
'@deepseek-ai/dsh-scripts': [
@@ -139,25 +108,18 @@ function sameStringList(actual: readonly string[] | undefined, expected: readonl
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : []
if (extras.length > 0) {
return [
'lib/index.js',
...manifest.bin ? ['lib/bin.js'] : [],
...extras,
'lib/types/**/*.d.ts',
'lib/types/**/*.d.ts.map',
'src',
]
}
if (manifest.bin) return dshBinPackageFiles
// Package-owned diagnostic companions are separately bundled optional
// entries; their source stays in the owner package without bloating root.
if (manifest.exports?.['./invariant']) return dshInvariantPackageFiles
// A declared "./worker" subpath export sanctions the one extra runtime
// bundle a worker-thread entry needs (and NodeNext/publint then validate
// that subpath's targets like any other export).
if (manifest.exports?.['./worker']) return dshWorkerPackageFiles
return dshPackageFiles
return [
'lib/index.js',
// Every package publishes its invariant ownership companion as a separate
// bundle; the package-invariant gate validates the companion itself.
'lib/invariant.js',
...manifest.bin ? ['lib/bin.js'] : [],
...manifest.exports?.['./worker'] ? ['lib/worker.cjs'] : [],
...extras,
'lib/types/**/*.d.ts',
'lib/types/**/*.d.ts.map',
'src',
]
}
function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] {

View File

@@ -0,0 +1,44 @@
/** Generate or verify package-owned invariant companion baselines. */
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import {
GENERATED_INVARIANT_MARKER,
collectPackageInvariantViolations,
formatPackageInvariantViolation,
packageInvariantOwners,
renderBaselineInvariant,
} from './package-invariants.ts'
const root = resolve(import.meta.dirname, '..')
const check = process.argv.includes('--check')
if (!check) {
let generated = 0
for (const owner of packageInvariantOwners(root)) {
const path = resolve(root, owner.sourcePath)
let current: string | undefined
try {
current = readFileSync(path, 'utf8')
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
}
if (current !== undefined && !current.includes(GENERATED_INVARIANT_MARKER)) continue
const expected = renderBaselineInvariant(owner)
if (current === expected) continue
writeFileSync(path, expected)
generated += 1
}
console.log(`gen-package-invariants: wrote ${generated} generated baseline companion(s).`)
}
const violations = collectPackageInvariantViolations(root)
if (violations.length > 0) {
console.error('verify-package-invariants: violations found:')
for (const violation of violations) {
console.error(` ${formatPackageInvariantViolation(root, violation)}`)
}
process.exit(1)
}
console.log(`verify-package-invariants: ${packageInvariantOwners(root).length} package companion(s) conform.`)

View File

@@ -0,0 +1,104 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
collectPackageInvariantViolations,
packageInvariantOwners,
renderBaselineInvariant,
} from './package-invariants.ts'
const roots: string[] = []
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
})
function fixture(options: {
packageName?: string
source?: string
invariantExport?: boolean
invariantDependency?: boolean
invariantReference?: boolean
buildEntry?: boolean
} = {}): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-package-invariants-'))
roots.push(root)
const dir = join(root, 'packages/core/probe')
mkdirSync(join(dir, 'src'), { recursive: true })
const packageName = options.packageName ?? '@deepseek-ai/dsh-probe'
const manifest = {
name: packageName,
exports: options.invariantExport === false ? {} : {
'./invariant': {
types: './lib/types/invariant.d.ts',
default: './lib/invariant.js',
},
},
files: ['lib/index.js', 'lib/invariant.js', 'src'],
peerDependencies: options.invariantDependency === false ? {} : {
'@deepseek-ai/dsh-invariants': '^0.0.1',
},
devDependencies: options.invariantDependency === false ? {} : {
'@deepseek-ai/dsh-invariants': 'workspace:^',
},
}
writeFileSync(join(dir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`)
writeFileSync(join(dir, 'tsconfig.json'), `${JSON.stringify({
references: options.invariantReference === false ? [] : [{ path: '../../support/invariants' }],
}, null, 2)}\n`)
const owner = packageInvariantOwners(root)[0]!
writeFileSync(join(dir, 'src/invariant.ts'), options.source ?? renderBaselineInvariant(owner))
writeFileSync(
join(dir, 'tsdown.config.ts'),
options.buildEntry === false ? "export default { entry: ['lib/types/index.js'] }\n" : "export default { entry: ['lib/types/index.js', 'lib/types/invariant.js'] }\n",
)
return root
}
describe('package invariant gate', () => {
it('accepts a generated owner companion with publication metadata', () => {
expect(collectPackageInvariantViolations(fixture())).toEqual([])
})
it('rejects missing publication metadata and build output', () => {
const violations = collectPackageInvariantViolations(fixture({
invariantExport: false,
invariantDependency: false,
invariantReference: false,
buildEntry: false,
}))
expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([
expect.stringContaining('exports["./invariant"]'),
expect.stringContaining('peerDependency'),
expect.stringContaining('devDependency'),
expect.stringContaining('TypeScript project references'),
expect.stringContaining('must bundle lib/types/invariant.js'),
]))
})
it('rejects foreign, duplicate, and unresolved registrations', () => {
const source = `
export const name = 'probe-invariant'
export const inject = ['invariants']
const selected = process.env.PACKAGE_NAME
export const apply = (ctx: { invariants: { register(name: string, install: () => void): () => void } }) => {
ctx.invariants.register('@deepseek-ai/dsh-foreign', () => {})
return ctx.invariants.register(selected!, () => {})
}
`
const violations = collectPackageInvariantViolations(fixture({ source }))
expect(violations.map(violation => violation.message)).toEqual(expect.arrayContaining([
expect.stringContaining('must resolve to a local string constant'),
expect.stringContaining('must register exactly its own package name'),
]))
})
it('rejects edits to a generated baseline', () => {
const root = fixture()
const path = join(root, 'packages/core/probe/src/invariant.ts')
writeFileSync(path, `${renderBaselineInvariant(packageInvariantOwners(root)[0]!)}// stale\n`)
expect(collectPackageInvariantViolations(root).map(violation => violation.message))
.toContain('generated baseline is stale; run pnpm run gen-package-invariants')
})
})

View File

@@ -0,0 +1,283 @@
/**
* Package-invariant companion discovery, generation, and structural checks.
* The runtime registry stays product-independent; this gate makes ownership
* exhaustive across packages without centralizing package checks.
*/
import { existsSync, globSync, readFileSync } from 'node:fs'
import { basename, dirname, relative, resolve, sep } from 'node:path'
import ts from 'typescript'
/** Marker identifying baseline companions owned by this generator. */
export const GENERATED_INVARIANT_MARKER = '@generated scripts/gen-package-invariants.ts'
interface PackageManifest {
name?: string
exports?: Record<string, { types?: string; default?: string } | string | undefined>
files?: string[]
peerDependencies?: Record<string, string>
devDependencies?: Record<string, string>
}
/** One package and the files participating in its invariant publication contract. */
export interface PackageInvariantOwner {
readonly dir: string
readonly manifestPath: string
readonly sourcePath: string
readonly packageName: string
}
/** One gate violation with a repo-relative owner path. */
export interface PackageInvariantViolation {
readonly path: string
readonly message: string
}
/** Discover every package under the repository package tree. */
export function packageInvariantOwners(root: string): PackageInvariantOwner[] {
return globSync('packages/*/*/package.json', { cwd: root })
.map(path => path.split(sep).join('/'))
.sort()
.map((manifestPath) => {
const manifest = readManifest(resolve(root, manifestPath))
if (manifest.name === undefined || manifest.name === '') {
throw new Error(`${manifestPath}: package invariant owner must declare a package name`)
}
const dir = dirname(manifestPath)
return {
dir,
manifestPath,
sourcePath: `${dir}/src/invariant.ts`,
packageName: manifest.name,
}
})
}
/** Render the generated ownership-only companion for a package without custom checks. */
export function renderBaselineInvariant(owner: PackageInvariantOwner): string {
const serviceImport = owner.packageName === '@deepseek-ai/dsh-invariants'
? './index.ts'
: '@deepseek-ai/dsh-invariants'
const pluginName = `${basename(owner.dir)}-invariant`
return `/**
* Generated invariant ownership companion for \`${owner.packageName}\`.
* Replace this file with package-owned checks while preserving its registration.
*
* ${GENERATED_INVARIANT_MARKER}
* @module ${owner.packageName}/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '${serviceImport}'
const PACKAGE_NAME = '${owner.packageName}'
/** Cordis companion plugin name. */
export const name = '${pluginName}'
/** Services required before the companion can register. */
export const inject = ['invariants']
/** Reserve this package's invariant ownership until it adds relational checks. */
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */
`
}
/** Return all violations of the package-invariant companion contract. */
export function collectPackageInvariantViolations(root: string): PackageInvariantViolation[] {
const violations: PackageInvariantViolation[] = []
for (const owner of packageInvariantOwners(root)) {
const manifest = readManifest(resolve(root, owner.manifestPath))
checkManifest(owner, manifest, violations)
checkBuild(owner, root, violations)
checkSource(owner, root, violations)
}
return violations
}
function readManifest(path: string): PackageManifest {
return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
}
function addViolation(
violations: PackageInvariantViolation[],
path: string,
message: string,
): void {
violations.push({ path, message })
}
function checkManifest(
owner: PackageInvariantOwner,
manifest: PackageManifest,
violations: PackageInvariantViolation[],
): void {
const invariantExport = manifest.exports?.['./invariant']
if (typeof invariantExport !== 'object'
|| invariantExport.types !== './lib/types/invariant.d.ts'
|| invariantExport.default !== './lib/invariant.js') {
addViolation(
violations,
owner.manifestPath,
'exports["./invariant"] must target ./lib/types/invariant.d.ts and ./lib/invariant.js',
)
}
if (!manifest.files?.includes('lib/invariant.js')) {
addViolation(violations, owner.manifestPath, 'files must publish lib/invariant.js')
}
if (owner.packageName === '@deepseek-ai/dsh-invariants') return
if (manifest.peerDependencies?.['@deepseek-ai/dsh-invariants'] !== '^0.0.1') {
addViolation(
violations,
owner.manifestPath,
'@deepseek-ai/dsh-invariants must be a ^0.0.1 peerDependency',
)
}
if (manifest.devDependencies?.['@deepseek-ai/dsh-invariants'] !== 'workspace:^') {
addViolation(
violations,
owner.manifestPath,
'@deepseek-ai/dsh-invariants must also be a workspace:^ devDependency',
)
}
}
function checkBuild(
owner: PackageInvariantOwner,
root: string,
violations: PackageInvariantViolation[],
): void {
const tsconfigPath = `${owner.dir}/tsconfig.json`
const tsconfig = JSON.parse(readFileSync(resolve(root, tsconfigPath), 'utf8')) as {
references?: Array<{ path?: string }>
}
if (owner.packageName !== '@deepseek-ai/dsh-invariants'
&& !tsconfig.references?.some(reference => reference.path === '../../support/invariants')) {
addViolation(
violations,
tsconfigPath,
'TypeScript project references must include ../../support/invariants',
)
}
const configPath = `${owner.dir}/tsdown.config.ts`
if (!existsSync(resolve(root, configPath))) return
const source = readFileSync(resolve(root, configPath), 'utf8')
if (!source.includes('lib/types/invariant.js')) {
addViolation(violations, configPath, 'package build override must bundle lib/types/invariant.js')
}
}
function checkSource(
owner: PackageInvariantOwner,
root: string,
violations: PackageInvariantViolation[],
): void {
const absolutePath = resolve(root, owner.sourcePath)
if (!existsSync(absolutePath)) {
addViolation(violations, owner.sourcePath, 'missing package-owned invariant companion')
return
}
const sourceText = readFileSync(absolutePath, 'utf8')
if (sourceText.includes(GENERATED_INVARIANT_MARKER)
&& sourceText !== renderBaselineInvariant(owner)) {
addViolation(
violations,
owner.sourcePath,
'generated baseline is stale; run pnpm run gen-package-invariants',
)
}
const sourceFile = ts.createSourceFile(
absolutePath,
sourceText,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
)
const constants = topLevelStringConstants(sourceFile)
const registrations: string[] = []
const unresolved: number[] = []
const visit = (node: ts.Node): void => {
if (ts.isCallExpression(node) && isInvariantRegistration(node.expression)) {
const argument = node.arguments[0]
const packageName = argument === undefined ? undefined : stringValue(argument, constants)
if (packageName === undefined) unresolved.push(sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1)
else registrations.push(packageName)
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
for (const line of unresolved) {
addViolation(
violations,
owner.sourcePath,
`line ${line}: ctx.invariants.register package name must resolve to a local string constant`,
)
}
if (registrations.length !== 1 || registrations[0] !== owner.packageName) {
addViolation(
violations,
owner.sourcePath,
`must register exactly its own package name ${JSON.stringify(owner.packageName)}; saw ${JSON.stringify(registrations)}`,
)
}
for (const exportedName of ['name', 'inject', 'apply']) {
if (!hasNamedExport(sourceFile, exportedName)) {
addViolation(violations, owner.sourcePath, `must named-export ${exportedName}`)
}
}
}
function topLevelStringConstants(sourceFile: ts.SourceFile): ReadonlyMap<string, string> {
const constants = new Map<string, string>()
for (const statement of sourceFile.statements) {
if (!ts.isVariableStatement(statement)) continue
for (const declaration of statement.declarationList.declarations) {
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) continue
const value = stringValue(declaration.initializer, constants)
if (value !== undefined) constants.set(declaration.name.text, value)
}
}
return constants
}
function stringValue(node: ts.Expression, constants: ReadonlyMap<string, string>): string | undefined {
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text
if (ts.isIdentifier(node)) return constants.get(node.text)
return undefined
}
function isInvariantRegistration(expression: ts.LeftHandSideExpression): boolean {
return ts.isPropertyAccessExpression(expression)
&& expression.name.text === 'register'
&& ts.isPropertyAccessExpression(expression.expression)
&& expression.expression.name.text === 'invariants'
}
function hasNamedExport(sourceFile: ts.SourceFile, name: string): boolean {
return sourceFile.statements.some((statement) => {
if (!ts.isVariableStatement(statement)
|| !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) return false
return statement.declarationList.declarations.some(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === name)
})
}
/** Format violations for the command-line gate. */
export function formatPackageInvariantViolation(
root: string,
violation: PackageInvariantViolation,
): string {
const path = resolve(root, violation.path)
return `${relative(root, path)}: ${violation.message}`
}

View File

@@ -204,6 +204,7 @@ function ciPrimaryGates(): Gate[] {
return [
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
pnpmScript('constraints', 'constraints'),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
pnpmScript('typecheck', 'typecheck'),
lintGate(),
@@ -229,6 +230,7 @@ function ciStaticGates(): Gate[] {
return [
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
pnpmScript('constraints', 'constraints'),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }),
...staticDemoSmokeGates(),
...docSyncLeafGates(),
@@ -314,6 +316,7 @@ function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
pnpmScript('knip', 'knip'),
pnpmScript('publint', 'publint', artifactOptions),
pnpmScript('constraints', 'constraints'),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
pnpmScript('node-next-types', 'verify-node-next-types', {
label: 'node-next types',
...artifactOptions,

View File

@@ -0,0 +1,70 @@
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'
declare module 'cordis' {
interface Context {
testInvariantProbe: TestInvariantProbe
}
}
class TestInvariantProbe extends Service {
constructor(ctx: Context) {
super(ctx, 'testInvariantProbe')
}
}
describe('global test invariant host', () => {
it('loads every companion and reserves every package name with enabled checks', async () => {
const ctx = new Context()
await ctx.plugin(TestInvariantProbe)
const owners = packageInvariantOwners(process.cwd())
expect(Object.keys(testInvariantCompanions)).toHaveLength(owners.length)
const unreserved: string[] = []
for (const owner of owners) {
try {
const dispose = ctx.invariants.register(owner.packageName, () => {})
unreserved.push(owner.packageName)
dispose()
} catch (error) {
expect(error).toHaveProperty(
'message',
`invariants: package "${owner.packageName}" is already registered`,
)
}
}
expect(unreserved).toEqual([])
})
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>()
const register = vi.fn((_packageName: string, installer: InvariantInstaller) => {
expect(typeof installer).toBe('function')
return () => {}
})
const fakeContext = { invariants: { register } } as unknown as Context
for (const [rawPath, companion] of Object.entries(testInvariantCompanions)) {
const path = rawPath.replace(/^\.\.\//, '')
await companion.apply(fakeContext)
const call = register.mock.calls.at(-1)
if (call === undefined) throw new Error(`${path}: companion did not register`)
registrations.set(path, call[0])
}
expect(registrations).toEqual(owners)
})
it('limits manual composition to focused invariant topology tests', () => {
expect(MANUAL_INVARIANT_TESTS).toEqual([
'/packages/support/invariants/tests/service.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/examples/agent-spine-demo/tests/agent-core.spec.ts',
])
})
})

104
scripts/test-invariants.ts Normal file
View File

@@ -0,0 +1,104 @@
/**
* 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.
*/
import { expect } from 'vitest'
import { RegistryService } from 'cordis'
import type { Context, Plugin } from 'cordis'
import InvariantService from '@deepseek-ai/dsh-invariants'
declare global {
interface ImportMeta {
/** Eager Vite module-glob expansion used by the Vitest setup file. */
glob<TModule>(pattern: string, options: { eager: true }): Record<string, TModule>
}
}
/** Loader-safe shape shared by every package invariant companion. */
export interface TestInvariantCompanion {
readonly name: string
readonly inject: readonly string[]
apply(ctx: Context): Promise<() => void>
}
/** Every package companion, discovered eagerly so coverage observes each registration. */
export const testInvariantCompanions: Readonly<Record<string, TestInvariantCompanion>> =
import.meta.glob<TestInvariantCompanion>('../packages/*/*/src/invariant.ts', { eager: true })
/** 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/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/examples/agent-spine-demo/tests/agent-core.spec.ts',
] as const
interface InvariantHost {
readonly fibers: readonly PluginFiber[]
readonly byCallback: ReadonlyMap<unknown, PluginFiber>
}
type PluginFiber = ReturnType<RegistryService['plugin']>
const hosts = new WeakMap<Context, InvariantHost>()
// eslint-disable-next-line @typescript-eslint/unbound-method -- every call below supplies its RegistryService receiver explicitly.
const originalPlugin = RegistryService.prototype.plugin
RegistryService.prototype.plugin = function(plugin: Plugin, config?: unknown, getOuterStack?: () => string[]) {
if (usesManualInvariantTree()) return originalPlugin.call(this, plugin, config, getOuterStack)
const root = this.ctx.root
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
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)
}
function usesManualInvariantTree(): boolean {
const testPath = expect.getState().testPath?.replaceAll('\\', '/') ?? ''
return MANUAL_INVARIANT_TESTS.some(path => testPath.endsWith(path))
}
function startInvariantHost(root: Context): InvariantHost {
const fibers: PluginFiber[] = []
const byCallback = new Map<unknown, PluginFiber>()
const mount = (plugin: Plugin, config?: unknown): void => {
const fiber = originalPlugin.call(root.registry, plugin, config)
const callback = root.registry.resolve(plugin)
if (callback === undefined) throw new Error('test invariants: companion is not a valid Cordis plugin')
fibers.push(fiber)
byCallback.set(callback, fiber)
}
mount(InvariantService, { enabled: true })
for (const [path, companion] of Object.entries(testInvariantCompanions).sort(([left], [right]) => left.localeCompare(right))) {
if (!companion.inject.includes('invariants')) {
throw new Error(`test invariants: ${path} must inject the invariant service`)
}
mount(companion)
}
const host = { fibers, byCallback }
hosts.set(root, host)
return host
}
function joinInvariantStartup(fiber: PluginFiber, invariantFibers: readonly PluginFiber[]): PluginFiber {
const readiness = fiber.await().then(async (loaded) => {
await Promise.all(invariantFibers.map(invariant => invariant.await()))
return loaded
})
const joined = Object.create(fiber) as PluginFiber
joined.then = readiness.then.bind(readiness)
return joined
}