/** * Native right-sidebar bridge tests (Phase A spike). * * `registerApesNativeTabs` publishes apes:profile as an OFFICIAL * `@deepseek-ai/dsh-client-ui-sidebar-right` tab type over the public * two-stage protocol: the type declaration on `ctx.sidebarRightTabs`, then the * keyed body on the `sidebar.right.pane.tab` seat. The bridge reaches the * registry through `ctx.inject(['sidebarRightTabs'], …)` so activation order * cannot strand it, and a deployment without the official sidebar must stay a * silent no-op instead of breaking the fork's own shell. * * The spec drives that contract with a fake cordis context (a recording * `inject` + `effect`), so it pins: the dependency name, the definition's * identity/band/guide entry, the body's seat key, the disposer's unwind, and * both no-op paths. No DOM and no real DSH host — same node environment as * apes-tabs.spec.ts. */ import { describe, expect, it, vi } from 'vitest' // Browser globals before the client graph loads (mirror of apes-tabs.spec.ts). import './browser-globals.ts' import { registerApesNativeTabs } from '../src/client/apes/native-tabs.tsx' /** One recorded type declaration. */ interface RecordedDefinition { id: string kind: string priority?: string title: () => string guide?: Array<{ order: number; title: () => string; description?: () => string; icon?: unknown }> } /** One recorded body registration. */ interface RecordedBody { options: { name: string; key?: string; inject?: (...args: unknown[]) => Record } component: unknown } /** Build a fake client context plus the registries it hands the bridge. */ function harness(options: { service?: boolean } = {}) { const definitions: RecordedDefinition[] = [] const bodies: RecordedBody[] = [] let typeUnwound = false let bodyUnwound = false const registry = { register(definition: RecordedDefinition) { definitions.push(definition) return () => { if (!typeUnwound) typeUnwound = true } }, } const slots = { inject(key: string, callback: () => () => void) { // The seat exists: run the declaration callback like the real registry. expect(key).toBe('sidebar.right.pane.tab') return callback() }, register(registration: RecordedBody['options'], component: unknown) { bodies.push({ options: registration, component }) return () => { if (!bodyUnwound) bodyUnwound = true } }, } const effects: Array<() => void> = [] const fctx = { get: (name: string) => (options.service === false ? undefined : name === 'sidebarRightTabs' ? registry : undefined), slots, effect: (callback: () => () => void) => { effects.push(callback()) }, } const inject = vi.fn((deps: string[], callback: (inner: unknown) => void) => { expect(deps).toEqual(['sidebarRightTabs']) callback(fctx) return { dispose: () => { for (const dispose of effects) dispose() } } }) return { ctx: { inject }, inject, definitions, bodies, get typeUnwound() { return typeUnwound }, get bodyUnwound() { return bodyUnwound }, } } describe('apes native right-sidebar bridge (spike)', () => { it('waits on ctx.inject for the official registry service', () => { const h = harness() const dispose = registerApesNativeTabs(h.ctx) expect(h.inject).toHaveBeenCalledTimes(1) expect(h.inject.mock.calls[0]![0]).toEqual(['sidebarRightTabs']) dispose() }) it('registers the profile type in the extension band with a guide entry', () => { const h = harness() const dispose = registerApesNativeTabs(h.ctx) try { expect(h.definitions).toHaveLength(1) const def = h.definitions[0]! expect(def.id).toBe('dsh-apes-sidebar/profile') expect(def.kind).toBe('apes:profile') // `extension` is the band for types contributed from outside the product. expect(def.priority).toBe('extension') // The title is read fresh (locale switches must follow). expect(def.title()).toBe('画像') expect(def.guide).toHaveLength(1) const entry = def.guide![0]! expect(entry.order).toBe(70) expect(entry.title()).toBe('画像') expect(typeof entry.description?.()).toBe('string') expect(entry.icon).toBeDefined() } finally { dispose() } }) it('registers the body under the type id on the keyed seat', () => { const h = harness() const dispose = registerApesNativeTabs(h.ctx) try { expect(h.bodies).toHaveLength(1) const body = h.bodies[0]! expect(body.options.name).toBe('sidebar.right.pane.tab') // The seat key MUST equal the registered id — that is how the panel // dispatches a tab body to our type. expect(body.options.key).toBe(h.definitions[0]!.id) expect(body.component).toBeDefined() // The body's injected props carry the client context the view reads. expect(body.options.inject?.()).toEqual({ ctx: h.ctx }) } finally { dispose() } }) it('the disposer unwinds both stages (HMR-safe)', () => { const h = harness() const dispose = registerApesNativeTabs(h.ctx) expect(h.definitions).toHaveLength(1) expect(h.bodies).toHaveLength(1) dispose() expect(h.typeUnwound).toBe(true) expect(h.bodyUnwound).toBe(true) }) it('is a silent no-op when the official registry service never arrives', () => { const h = harness({ service: false }) const dispose = registerApesNativeTabs(h.ctx) expect(h.inject).toHaveBeenCalledTimes(1) expect(h.definitions).toHaveLength(0) expect(h.bodies).toHaveLength(0) expect(() => dispose()).not.toThrow() }) it('is a no-op for a context without inject (non-cordis harness)', () => { expect(() => registerApesNativeTabs({} as never)()).not.toThrow() }) })