mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
The data layer no longer depends on the React glue package, and business plugins no longer depend on web-react at all: - The store engine (zustand vanilla + immer + persist + dev freeze), defineStore, and shallowEqual move to @deepseek-ai/dsh-client-runtime, exported from the ./client main entry — no ./store subpath survives on either package (the web-react one is deleted, none is opened on runtime). - Store products are bare snapshot sources: useSelector leaves SnapshotStore/StoreInstance and Session; every hook is composed at the binding site in web-react's renderer (per-source cached uSES binding). The SlotRendererHost sessions face carries bare observables only. - SessionProvider becomes a standard-kit seat: an entry whose children declare a session-scope slot receives the framework component as a prop, retiring the last value import of web-react from plugin packages. UseSession and the session-area types now live in ui-slots. - web-react shrinks to the shell-only React glue (renderer, providers, uSES bridge); zustand/immer belong to runtime alone; the module-table seed and tsdown externals drop the web-react/store seat. - NODE_ENV replacement is defined once in the shared tsdown client preset (browser bundles inline the engine and lost vite's define); the 3-line process.env typecheck shim moves to runtime with the engine. - Stray tsc artifacts (.js/.d.ts/.d.ts.map beside sources under src/) swept repo-wide; they shadow real sources under vitest resolution. Verified: both aggregate typecheck programs at zero; 604 client tests green; repo-wide grep for web-react/store at zero; real-host playwright run 7/7 including persist round-trip. ci: fix test/docs
113 lines
4.7 KiB
TypeScript
113 lines
4.7 KiB
TypeScript
/**
|
|
* apply wiring on a real cordis Context + SlotsService (terminal register
|
|
* form): SidebarRoot registered into the layout-declared sidebar slot, the
|
|
* thin inject surface (three plain service callbacks closed over the plugin
|
|
* ctx — no hooks, no store lines), load-order fail-loud, and fiber-teardown
|
|
* unregistration. Component behavior is covered props-direct in
|
|
* sidebar-root.spec.tsx; no renderer machinery here.
|
|
*/
|
|
import { Context } from 'cordis'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import type { SessionId, SessionListState } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import { apply, inject } from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
import type { SidebarRootInjected } from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
// Type-only: ui-layout's SlotMap merge so the sidebar slot key typechecks.
|
|
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
|
|
const sid = (s: string) => s as SessionId
|
|
|
|
async function bench() {
|
|
const ctx = new Context()
|
|
await ctx.plugin(SlotsService).await()
|
|
const list = createSnapshotStore<SessionListState>({
|
|
ids: [sid('a')],
|
|
byId: { [sid('a')]: { id: sid('a'), title: 'alpha', cwd: '/proj', running: false, updatedAt: 1 } },
|
|
current: undefined,
|
|
})
|
|
const sessions = { list, create: vi.fn(async () => sid('minted')), open: vi.fn() }
|
|
const layout = { toggleSidebar: vi.fn() }
|
|
ctx.provide('sessions', sessions)
|
|
ctx.provide('layout', layout)
|
|
const slots = ctx.get('slots') as SlotsService
|
|
// Stand-in for ui-layout's root entry: the sidebar slot only exists while
|
|
// a live entry declares it in children (declaration account: design §2.2).
|
|
slots.register(
|
|
{ name: 'root', children: { 'sidebar': { kind: 'single', scope: 'root' } } } as never,
|
|
() => null,
|
|
)
|
|
return { ctx, slots, sessions, layout }
|
|
}
|
|
|
|
/** The sidebar entry's injected share, read off the stored entry. */
|
|
function injectedOf(slots: SlotsService): SidebarRootInjected {
|
|
const entries = slots.entries('sidebar')
|
|
expect(entries).toHaveLength(1)
|
|
// The typed StoredEntry.inject is declaration-derived ((...args: never[])
|
|
// shape); the sidebar factory is parameterless, so the call is safe here.
|
|
const inject = entries[0]!.inject as (() => SidebarRootInjected) | undefined
|
|
return inject!()
|
|
}
|
|
|
|
describe('apply', () => {
|
|
it('declares the services it binds', () => {
|
|
expect(inject).toEqual(['slots', 'layout', 'sessions'])
|
|
})
|
|
|
|
it('fails loud when mounted without the inject declaration', async () => {
|
|
// ctx.slots rides the cordis property proxy: reading it from a plugin
|
|
// that never declared the dependency throws instead of yielding undefined.
|
|
const ctx = new Context()
|
|
await ctx.plugin(SlotsService).await()
|
|
await expect(ctx.plugin({ apply })).rejects.toThrow(/without inject/)
|
|
})
|
|
|
|
it('fails loud when no live entry has declared the sidebar slot', async () => {
|
|
const ctx = new Context()
|
|
await ctx.plugin(SlotsService).await()
|
|
ctx.provide('sessions', {})
|
|
ctx.provide('layout', {})
|
|
await expect(ctx.plugin({ inject: [...inject], apply })).rejects.toThrow(/slot "sidebar" is not declared/)
|
|
})
|
|
|
|
it('registers SidebarRoot with the thin three-callback inject surface', async () => {
|
|
const { ctx, slots } = await bench()
|
|
await ctx.plugin({ inject: [...inject], apply }).await()
|
|
const injected = injectedOf(slots)
|
|
// The whole business face: three plain callbacks, no hooks, no store lines.
|
|
expect(Object.keys(injected).sort()).toEqual(['onCreate', 'onOpen', 'onToggleSidebar'])
|
|
})
|
|
|
|
it('routes the callbacks to the layout/sessions services', async () => {
|
|
const { ctx, slots, sessions, layout } = await bench()
|
|
await ctx.plugin({ inject: [...inject], apply }).await()
|
|
const injected = injectedOf(slots)
|
|
|
|
injected.onToggleSidebar()
|
|
expect(layout.toggleSidebar).toHaveBeenCalledOnce()
|
|
|
|
injected.onOpen(sid('a'))
|
|
expect(sessions.open).toHaveBeenCalledWith('a')
|
|
|
|
injected.onCreate()
|
|
expect(sessions.create).toHaveBeenCalledWith({})
|
|
// create-then-open lands after the create promise resolves.
|
|
await Promise.resolve()
|
|
await Promise.resolve()
|
|
expect(sessions.open).toHaveBeenCalledWith('minted')
|
|
|
|
injected.onCreate('/proj')
|
|
expect(sessions.create).toHaveBeenCalledWith({ cwd: '/proj' })
|
|
})
|
|
|
|
it('teardown unregisters the slot entry', async () => {
|
|
const { ctx, slots } = await bench()
|
|
const fiber = ctx.plugin({ inject: [...inject], apply })
|
|
await fiber.await()
|
|
expect(slots.entries('sidebar')).toHaveLength(1)
|
|
await fiber.dispose()
|
|
expect(slots.entries('sidebar')).toHaveLength(0)
|
|
})
|
|
})
|