Files
apes-sidebar/tests/subagent-chat-aside.spec.ts
T
figmar 061964e3e8
CI / ci (pull_request) Canceled after 0s
CI / ci-windows (pull_request) Canceled after 0s
CI / plugin-mount (pull_request) Canceled after 0s
fix(subagent): stop 401-ing the aside-open with a guessed child mode
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.
2026-10-14 10:18:58 +08:00

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')
})
})