Merge remote-tracking branch 'origin/master' into mergebot/pr1667

This commit is contained in:
imccyu
2026-08-06 11:53:33 +08:00
100 changed files with 1065 additions and 837 deletions
@@ -124,11 +124,13 @@ describe('AskQuestionRow', () => {
expect(screen.getByRole('button', { expanded: true })).toBeTruthy()
})
it('askQuestionToolview is a plain registrant riding the conversation load-order seam', () => {
it('askQuestionToolview injects the toolview declaration directly', () => {
expect(askQuestionToolview.name).toBe('ask-question-toolview')
expect(askQuestionToolview.inject).toEqual(['slots', 'conversation'])
const register = vi.fn()
askQuestionToolview.apply({ slots: { register } } as never)
expect(askQuestionToolview.inject).toEqual(['slots'])
const register = vi.fn(() => () => undefined)
const inject = vi.fn((_name: string, callback: () => () => void) => callback())
askQuestionToolview.apply({ slots: { inject, register } } as never)
expect(inject).toHaveBeenCalledWith('conversation.chat.toolview', expect.any(Function))
expect(register).toHaveBeenCalledWith(
{ name: 'conversation.chat.toolview', key: 'ask_user_question', locale: 'conversation' },
AskQuestionRow,
@@ -3,8 +3,8 @@
// as the first 'conversation.view' ring entry declaring the keyed toolview
// hole, the slot registrations land against a root entry's children
// declarations (the AppFrame role), the shared store handle rides all strict
// session entries, and the bash sample + todo row mount through the
// load-order seam as keyed entries. Full-chain rendering belongs to the
// session entries, and the bash sample + todo row mount through declaration
// injection as keyed entries. Full-chain rendering belongs to the
// machinery spec (chat-toolview-slot.spec.tsx) and the shell e2e; this spec
// stops at the assembly surface.
@@ -90,10 +90,9 @@ describe('apply wiring', () => {
await b.runtime.dispose()
})
it('mounts the bash sample, the read row, the file-mutation rows, the search rows (grep + glob), the web rows, and the product rows as keyed entries through the load-order seam', async () => {
it('mounts the tool rows as keyed entries through declaration injection', async () => {
const b = await bench()
// Every registrant plugin's inject: ['slots', 'conversation'] resolved — the
// service being present implies the chat entry declared the hole first. The
// The actual toolview declaration activates every registrant. The
// file-mutation registrant claims both write and edit for the diff card; the
// one search row registers under both grep and glob; the web rows register
// one component under both web tool names.
@@ -6,9 +6,8 @@
// entryKey (the bash sample lands through its plugin), unregistered tools
// fall back to GenericToolCard at the render site, live registration/unload
// flips rows in place, duplicate keys fail loud, the inject channel feeds
// (sessionId) => I into row components, and a registrant's
// inject: ['slots', 'conversation'] load-order seam suspends on real fiber
// semantics until the service (and with it the hole declaration) is present.
// (sessionId) => I into row components, and a registrant can activate before
// the declaration then land through slots.inject when the chat entry appears.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent } from '@testing-library/react'
@@ -191,8 +190,8 @@ describe('keyed toolview hole through the real machinery', () => {
})
})
describe('registrant load-order seam', () => {
it("suspends a registrant on inject: ['slots', 'conversation'] until the service (and the hole) exists", async () => {
describe('registrant declaration injection', () => {
it('runs the plugin before ui-conversation and waits on the actual toolview declaration', async () => {
const runtime = await SlotTestRuntime.create()
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
const locale = new LocaleService(runtime.ctx)
@@ -200,31 +199,26 @@ describe('registrant load-order seam', () => {
runtime.slots.installLocale(locale)
await runtime.root.declare(LAYOUT_CHILDREN, AppRoot)
// Third-party posture, mounted BEFORE ui-conversation: real fiber inject
// semantics hold it — apply must not run while 'conversation' is absent.
// Uses ctx.plugin directly (the deliberate-suspension escape hatch; mount()
// would fail loud on the missing service). (Plain arrow, not vi.fn: mock
// functions carry a prototype and trip the fiber's isConstructor branch.)
// Third-party posture, mounted BEFORE ui-conversation. Plugin apply runs,
// while slots.inject waits for the declaration itself.
let applyRuns = 0
const registrantApply = (registrantCtx: typeof runtime.ctx): void => {
applyRuns += 1
registrantCtx.slots.register(
{ name: 'conversation.chat.toolview', key: 'late' }, () => null)
registrantCtx.slots.inject('conversation.chat.toolview', () => registrantCtx.slots.register(
{ name: 'conversation.chat.toolview', key: 'late' }, () => null))
}
const late = runtime.ctx.plugin({
name: 'late-registrant',
inject: ['slots', 'conversation'],
inject: ['slots'],
apply: registrantApply,
})
await Promise.resolve()
expect(applyRuns).toBe(0)
// Mounting the package resolves the seam: service present ⟹ the chat
// entry (and its hole declaration) is already on the ledger, so the
// suspended registrant lands without an undeclared-slot throw.
await runtime.mount({ inject: [...inject], apply })
await late.await()
expect(applyRuns).toBe(1)
expect(runtime.slots.entries('conversation.chat.toolview')).toHaveLength(0)
// Mounting the package declares the slot and activates the waiting entry.
await runtime.mount({ inject: [...inject], apply })
expect(runtime.slots.entries('conversation.chat.toolview').map(e => e.options.key))
.toEqual(expect.arrayContaining(['bash', 'late']))
await runtime.dispose()
@@ -274,8 +274,14 @@ describe('fileMutationToolview registration', () => {
it('registers one component under both edit and write, and each disposes', () => {
const registered: { key: string; locale: unknown; disposed: boolean }[] = []
const disposers: (() => void)[] = []
let disposeInjection = (): void => {}
const ctx = {
slots: {
inject: (_name: string, callback: () => Iterable<() => void>) => {
const active = [...callback()]
disposeInjection = () => { for (const dispose of active.reverse()) dispose() }
return disposeInjection
},
register: ({ key, locale }: { name: string; key: string; locale?: string }) => {
const entry = { key, locale, disposed: false }
registered.push(entry)
@@ -289,10 +295,9 @@ describe('fileMutationToolview registration', () => {
expect(registered.map(r => r.key).sort()).toEqual(['edit', 'write'])
// Both keys claim the conversation locale seat ToolRow's body copy needs.
expect(registered.map(r => r.locale)).toEqual(['conversation', 'conversation'])
// The registrant's inject seam is the load-order contract the row relies on.
expect(fileMutationToolview.inject).toEqual(['slots', 'conversation'])
expect(fileMutationToolview.inject).toEqual(['slots'])
// Disposal removes each contribution (packages/AGENTS.md registry contract).
for (const dispose of disposers) dispose()
disposeInjection()
expect(registered.every(r => r.disposed)).toBe(true)
})
})
@@ -375,8 +375,10 @@ describe('QueueDock', () => {
it('registers as the terminal composer-context entry', () => {
expect(queueDockEntry.name).toBe('conversation-queue-dock')
expect(queueDockEntry.inject).toEqual(['slots', 'conversation', 'sessions'])
const register = vi.fn()
queueDockEntry.apply({ slots: { register } } as never)
const register = vi.fn(() => () => undefined)
const inject = vi.fn((_name: string, callback: () => () => void) => callback())
queueDockEntry.apply({ slots: { inject, register } } as never)
expect(inject).toHaveBeenCalledWith('conversation.input.dock', expect.any(Function))
expect(register).toHaveBeenCalledWith(
expect.objectContaining({ name: 'conversation.input.dock', id: 'queue', order: 20 }),
QueueDock,
@@ -237,11 +237,14 @@ describe('ReadRow keyed toolview', () => {
it('registers under the read key of the keyed toolview slot', () => {
const registered: { name: unknown; key?: unknown }[] = []
const ctx = { slots: { register: (options: { name: unknown; key?: unknown }) => { registered.push(options) } } } as unknown as Context
const ctx = { slots: {
inject: (_name: string, callback: () => () => void) => callback(),
register: (options: { name: unknown; key?: unknown }) => { registered.push(options); return () => undefined },
} } as unknown as Context
readToolview.apply(ctx)
// The row composes ToolRow, so it declares its locale namespace at the seat.
expect(registered).toEqual([{ name: 'conversation.chat.toolview', key: 'read', locale: 'conversation' }])
expect(readToolview.inject).toContain('conversation')
expect(readToolview.inject).toEqual(['slots'])
})
})
@@ -349,8 +349,13 @@ describe('SearchRow keyed card', () => {
const registered: { key: unknown; locale: unknown; component: unknown }[] = []
const ctx = {
slots: {
inject: (_name: string, callback: () => Iterable<() => void>) => {
for (const _dispose of callback()) { /* exhaust transactional setup */ }
return () => undefined
},
register: (options: { name: string; key: string; locale?: string }, component: unknown) => {
registered.push({ key: options.key, locale: options.locale, component })
return () => undefined
},
},
} as never
@@ -361,7 +366,7 @@ describe('SearchRow keyed card', () => {
// One component, two keys.
expect(registered[0]!.component).toBe(SearchRow)
expect(registered[1]!.component).toBe(SearchRow)
expect(searchToolview.inject).toEqual(['slots', 'conversation'])
expect(searchToolview.inject).toEqual(['slots'])
})
})
@@ -111,9 +111,11 @@ describe('TodoDock', () => {
it('registers before the goal and queue entries', () => {
expect(todoDockEntry.name).toBe('conversation-todo-dock')
expect(todoDockEntry.inject).toEqual(['slots', 'conversation'])
const register = vi.fn()
todoDockEntry.apply({ slots: { register } } as never)
expect(todoDockEntry.inject).toEqual(['slots'])
const register = vi.fn(() => () => undefined)
const inject = vi.fn((_name: string, callback: () => () => void) => callback())
todoDockEntry.apply({ slots: { inject, register } } as never)
expect(inject).toHaveBeenCalledWith('conversation.input.dock', expect.any(Function))
expect(register).toHaveBeenCalledWith({ name: 'conversation.input.dock', id: 'todo', order: 0, locale: NS }, TodoDock)
})
})
@@ -196,11 +198,13 @@ describe('TodoRow', () => {
expect(screen.getByText('todo_write · c1')).toBeTruthy()
})
it('todoToolview is a plain registrant riding the conversation load-order seam', () => {
it('todoToolview injects the toolview declaration directly', () => {
expect(todoToolview.name).toBe('todo-toolview')
expect(todoToolview.inject).toEqual(['slots', 'conversation'])
const register = vi.fn()
todoToolview.apply({ slots: { register } } as never)
expect(todoToolview.inject).toEqual(['slots'])
const register = vi.fn(() => () => undefined)
const inject = vi.fn((_name: string, callback: () => () => void) => callback())
todoToolview.apply({ slots: { inject, register } } as never)
expect(inject).toHaveBeenCalledWith('conversation.chat.toolview', expect.any(Function))
expect(register).toHaveBeenCalledWith({ name: 'conversation.chat.toolview', key: 'todo_write', locale: NS }, TodoRow)
})
})
@@ -272,6 +272,10 @@ describe('web toolview registration', () => {
const registered: { key: string; locale: unknown; component: unknown }[] = []
const ctx = {
slots: {
inject: (_name: string, callback: () => Iterable<() => void>) => {
for (const _dispose of callback()) { /* exhaust transactional setup */ }
return () => undefined
},
register: (options: { name: string; key: string; locale?: string }, component: unknown) => {
registered.push({ key: options.key, locale: options.locale, component })
return () => {}
@@ -285,7 +289,6 @@ describe('web toolview registration', () => {
// One component under both keys, not two thin rows.
expect(registered[0]?.component).toBe(WebRow)
expect(registered[1]?.component).toBe(WebRow)
// The load-order seam the render site depends on.
expect(webToolview.inject).toEqual(['slots', 'conversation'])
expect(webToolview.inject).toEqual(['slots'])
})
})