The aside button could hit `subagent/unauthorized: subagent mode does not match the supplied address`. The host's session-controller (dsh-api-session- controller validateAddress) compares the `mode` in the resource address against the child's durable descriptor mode and rejects any mismatch. The address `mode` came from the row, and on the degraded path the row's mode is a best-effort guess from the summary `running` flag (running => continuable) — so a child that is genuinely one-shot but still running was guessed as continuable and the very open it was meant to trigger 401'd. Two layers, so it holds on both the real-catalog and the degraded path: - subagentChatAddress: only pass a mode through when it is one of the two durable values (`one-shot` / `continuable`). Anything else is omitted, which the controller reads as `mode: "unknown"` and therefore skips the comparison — the safest form when the caller does not actually know. - openChildAside: prefer the host's real descriptor mode for the child (the `subagentsByParent` entry carries the durable mode) over the address mode, falling back to the address only when the real catalog never arrived. Gates: typecheck / lint / build / test 132 files 1392 passed.
74 lines
2.7 KiB
TypeScript
74 lines
2.7 KiB
TypeScript
/**
|
|
* The `subagentchat` resource address must stay byte-compatible with the host's
|
|
* own codec: `@deepseek-ai/dsh-client-ui-subagent` parses what we emit, so a
|
|
* different query order or an unescaped child id would silently open the wrong
|
|
* page (or none).
|
|
*/
|
|
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
SUBAGENT_CHAT_ADDRESS,
|
|
SUBAGENT_CHAT_KIND,
|
|
subagentChatAddress,
|
|
} from '../src/client/native/subagent-chat.ts'
|
|
|
|
describe('subagentChatAddress', () => {
|
|
it("emits the canonical form with parent and mode", () => {
|
|
expect(subagentChatAddress({
|
|
parentSessionId: 'parent-1',
|
|
childSessionId: 'child-2',
|
|
mode: 'one-shot',
|
|
})).toBe('dsh-resource://subagentchat/session/child-2?parent=parent-1&mode=one-shot')
|
|
})
|
|
|
|
it('keeps a continuable mode verbatim', () => {
|
|
expect(subagentChatAddress({
|
|
parentSessionId: 'p',
|
|
childSessionId: 'c',
|
|
mode: 'continuable',
|
|
})).toContain('mode=continuable')
|
|
})
|
|
|
|
it('percent-encodes a child id so the address stays one path segment', () => {
|
|
const address = subagentChatAddress({
|
|
parentSessionId: 'p',
|
|
childSessionId: 'child/with:odd chars',
|
|
mode: 'one-shot',
|
|
})
|
|
expect(address).toContain('/child%2Fwith%3Aodd%20chars?')
|
|
})
|
|
|
|
it('never leaks the parent into the path (it is a routing query only)', () => {
|
|
const address = subagentChatAddress({
|
|
parentSessionId: 'p secret',
|
|
childSessionId: 'c',
|
|
mode: 'one-shot',
|
|
})
|
|
expect(address.split('?')[0]).toBe(`${SUBAGENT_CHAT_ADDRESS}c`)
|
|
})
|
|
|
|
it('names the page kind the native Sidebar roots these pages under', () => {
|
|
expect(SUBAGENT_CHAT_KIND).toBe('subagentchat')
|
|
})
|
|
|
|
it('omits an unknown mode so the host treats it as "unknown" and skips the check', () => {
|
|
// The degraded synthesized catalog guesses mode from `running`; a wrong
|
|
// guess used to 401 the open (the controller compares address.mode against
|
|
// the child's durable descriptor). An unrecognized mode must be dropped,
|
|
// which the controller reads as `mode: "unknown"` and does not compare.
|
|
const address = subagentChatAddress({
|
|
parentSessionId: 'p',
|
|
childSessionId: 'c',
|
|
mode: 'unknown' as never,
|
|
})
|
|
expect(address).toBe('dsh-resource://subagentchat/session/c?parent=p')
|
|
expect(address).not.toContain('mode=')
|
|
})
|
|
|
|
it('still emits the mode when it is one of the two durable values', () => {
|
|
expect(subagentChatAddress({ parentSessionId: 'p', childSessionId: 'c', mode: 'one-shot' }))
|
|
.toContain('mode=one-shot')
|
|
expect(subagentChatAddress({ parentSessionId: 'p', childSessionId: 'c', mode: 'continuable' }))
|
|
.toContain('mode=continuable')
|
|
})
|
|
})
|