从 0 重写的 v2.0.0 DSH 侧边栏插件第一阶段: - 工程骨架:tsdown 双构建(host ESM + client CJS closure)、vitest、strict TS - host 半:namespace 插件形状(name/inject/apply,无 default),minimal apply - client 半:closure factory 注册 5 个 apes:* tab 占位 + zh/en i18n - 不变量面:assert/semver/peer guard(dsh-better-sidebar >= 0.18.0) - 依赖策略:dsh-better-sidebar=peerDependency(不打包),cordis=devDependency(仅类型) - 门禁:pnpm typecheck + build + test 全绿(14 单测通过) - AGENTS.md 工程约定 + .gitignore(排除 lib/node_modules/maxkb-integration) 模板:dsh-better-sidebar。作用域锁定信息层(tasks/agents/apps/messages/profile), 不含 file/terminal/browser(归 better-sidebar)、不含 team/room(归 background-agents)。
111 lines
4.2 KiB
TypeScript
111 lines
4.2 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')
|
|
expect(host.inject).toEqual(['logger'])
|
|
})
|
|
|
|
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__
|
|
})
|
|
})
|