根因:inject: ['logger'] 声明了 logger 为硬依赖 service,但 DSH host 没有 provider 注册 logger service → entry 永远 pending → '1 entry did not activate' → 插件树加载失败 → 恢复模式循环。 对比 dsh-better-sidebar:其 inject 是 host 实际提供的 service (webServer/sessions/webRuntime/tools)。apes-sidebar host half 只用 ctx.logger 做可选链调用(log?.info?.() 在 try/catch 里),logger 是 软便利不是硬依赖——不该出现在 inject 列表里。 修正:inject 改空数组 [],host half 不声明任何硬依赖 service。 同步更新 smoke.spec.ts 断言(inject toEqual([]))+ verify-mount.cjs 检查(inject === [])。 门禁:typecheck rc=0 · build rc=0 · test 29/29
115 lines
4.4 KiB
TypeScript
115 lines
4.4 KiB
TypeScript
/**
|
|
* Phase 0 smoke spec: mounts the host plugin against a minimal fake context,
|
|
* verifies the plugin export shape (no stray default, so the cordis Loader
|
|
* keeps name/inject/apply), and exercises the invariant surface. Runs with
|
|
* `pnpm test`.
|
|
*
|
|
* The client half (lib/client.js closure factory) is a browser artifact — it
|
|
* registers tabs into the better-sidebar host registry at runtime, which
|
|
* needs a live DSH shell. The client registration contract is therefore
|
|
* covered by a focused unit spec (locales) + the build's purity gate; a full
|
|
* mount smoke (cordis scratch profile) lands in Phase 1 with the real tabs.
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import * as host from '../src/index.ts'
|
|
import {
|
|
InvariantError,
|
|
assert,
|
|
assertBetterSidebarPeer,
|
|
compareSemver,
|
|
} from '../src/invariant.ts'
|
|
|
|
/**
|
|
* Run the real namespace export through `Loader.unwrapExports`; a stray
|
|
* default would discard `name`, `inject`, and `apply`. Same guard the
|
|
* official plugin repos ship.
|
|
*/
|
|
describe('apes-sidebar host plugin shape', () => {
|
|
it('has the namespace-plugin export shape (no stray default)', () => {
|
|
expect('default' in host).toBe(false)
|
|
expect(typeof host.apply).toBe('function')
|
|
expect(host.name).toBe('apes-sidebar')
|
|
// inject MUST be empty: `logger` is used only via optional-chaining in
|
|
// apply(ctx) — declaring it as an injected service would make the entry
|
|
// wait on a provider DSH host does not register, blocking activation
|
|
// (recovery mode). See src/index.ts comment.
|
|
expect(host.inject).toEqual([])
|
|
})
|
|
|
|
it('apply mounts a logger line through the effect (Phase 0 minimal)', () => {
|
|
const logs: string[] = []
|
|
let effects = 0
|
|
const ctx = {
|
|
logger: { info: (msg: string) => logs.push(msg) },
|
|
effect: (fn: () => void | (() => void)) => {
|
|
effects += 1
|
|
const cleanup = fn()
|
|
return () => {
|
|
if (typeof cleanup === 'function') cleanup()
|
|
}
|
|
},
|
|
}
|
|
host.apply(ctx as never)
|
|
expect(effects).toBe(1)
|
|
expect(logs).toEqual(['[apes-sidebar] host half mounted (Phase 0 skeleton)'])
|
|
})
|
|
|
|
it('apply survives a missing logger (degrades, never throws)', () => {
|
|
let effects = 0
|
|
const ctx = {
|
|
effect: (fn: () => void | (() => void)) => {
|
|
effects += 1
|
|
fn()
|
|
},
|
|
}
|
|
expect(() => host.apply(ctx as never)).not.toThrow()
|
|
expect(effects).toBe(1)
|
|
})
|
|
|
|
it('apply without an effect API still runs once synchronously', () => {
|
|
const logs: string[] = []
|
|
const ctx = { logger: { info: (msg: string) => logs.push(msg) } }
|
|
host.apply(ctx as never)
|
|
expect(logs).toEqual(['[apes-sidebar] host half mounted (Phase 0 skeleton)'])
|
|
})
|
|
})
|
|
|
|
describe('apes-sidebar invariant surface', () => {
|
|
it('assert throws an InvariantError on a falsy condition', () => {
|
|
expect(() => assert(false, 'boom')).toThrow(InvariantError)
|
|
expect(() => assert(false, 'boom')).toThrow('boom')
|
|
// A truthy condition passes silently.
|
|
expect(() => assert(true, 'ok')).not.toThrow()
|
|
expect(() => assert(1, 'ok')).not.toThrow()
|
|
})
|
|
|
|
it('compareSemver orders versions (numeric + rc prerelease)', () => {
|
|
expect(compareSemver('0.17.9', '0.18.0')).toBeLessThan(0)
|
|
expect(compareSemver('0.18.0', '0.18.0')).toBe(0)
|
|
expect(compareSemver('0.19.0', '0.18.0')).toBeGreaterThan(0)
|
|
expect(compareSemver('1.0.0', '0.99.99')).toBeGreaterThan(0)
|
|
// Prerelease sorts below the release it qualifies.
|
|
expect(compareSemver('0.18.0-rc.1', '0.18.0')).toBeLessThan(0)
|
|
expect(compareSemver('0.18.0-rc.1', '0.18.0-rc.2')).toBeLessThan(0)
|
|
})
|
|
|
|
it('assertBetterSidebarPeer warns on an old peer (no logger → no throw)', () => {
|
|
const warnings: string[] = []
|
|
const warn = (msg: string) => warnings.push(msg)
|
|
// An old peer (0.17.x) triggers a warn.
|
|
;(globalThis as Record<string, unknown>).__apesPeer__ = { version: '0.17.9' }
|
|
assertBetterSidebarPeer({ warn })
|
|
expect(warnings).toHaveLength(1)
|
|
expect(warnings[0]).toContain('0.17.9')
|
|
// A new peer (0.18.x) does not.
|
|
warnings.length = 0
|
|
;(globalThis as Record<string, unknown>).__apesPeer__ = { version: '0.18.0' }
|
|
assertBetterSidebarPeer({ warn })
|
|
expect(warnings).toHaveLength(0)
|
|
// No logger at all: still no throw.
|
|
;(globalThis as Record<string, unknown>).__apesPeer__ = { version: '0.17.0' }
|
|
expect(() => assertBetterSidebarPeer(undefined)).not.toThrow()
|
|
delete (globalThis as Record<string, unknown>).__apesPeer__
|
|
})
|
|
})
|