
























@@ -15,6 +15,7 @@ const hoisted = vi.hoisted(() => ({
1515readAcpSessionEntryMock: vi.fn(),
1616resolveConversationBindingContextMock: vi.fn(),
1717resolveFocusTargetSessionMock: vi.fn(),
18+resolveStoredSubagentCapabilitiesMock: vi.fn(),
1819sessionBindingCapabilitiesMock: vi.fn(),
1920sessionBindingBindMock: vi.fn(),
2021sessionBindingResolveByConversationMock: vi.fn(),
@@ -102,6 +103,11 @@ vi.mock("../../infra/outbound/session-binding-service.js", () => ({
102103getSessionBindingService: () => buildFocusSessionBindingService(),
103104}));
104105106+vi.mock("../../agents/subagent-capabilities.js", () => ({
107+resolveStoredSubagentCapabilities: (sessionKey: string, options: unknown) =>
108+hoisted.resolveStoredSubagentCapabilitiesMock(sessionKey, options),
109+}));
110+105111vi.mock("./conversation-binding-input.js", () => ({
106112resolveConversationBindingContextFromAcpCommand: (params: unknown) =>
107113hoisted.resolveConversationBindingContextMock(params),
@@ -195,6 +201,7 @@ function buildFocusContext(params?: {
195201chatType?: string;
196202senderId?: string;
197203token?: string;
204+requesterKey?: string;
198205}) {
199206return {
200207params: buildCommandParams({
@@ -203,7 +210,7 @@ function buildFocusContext(params?: {
203210senderId: params?.senderId,
204211}),
205212handledPrefix: "/focus",
206-requesterKey: "agent:main:main",
213+requesterKey: params?.requesterKey ?? "agent:main:main",
207214runs: [],
208215restTokens: [params?.token ?? "codex-acp"],
209216} satisfies Parameters<typeof handleSubagentsFocusAction>[0];
@@ -224,6 +231,9 @@ function buildUnfocusContext(params?: { senderId?: string }) {
224231describe("focus actions", () => {
225232beforeEach(() => {
226233vi.clearAllMocks();
234+hoisted.resolveStoredSubagentCapabilitiesMock.mockReturnValue({
235+controlScope: "children",
236+});
227237hoisted.sessionBindingCapabilitiesMock.mockReturnValue(createSessionBindingCapabilities());
228238hoisted.sessionBindingResolveByConversationMock.mockReturnValue(null);
229239hoisted.resolveFocusTargetSessionMock.mockResolvedValue({
@@ -278,6 +288,11 @@ describe("focus actions", () => {
278288279289expect(result.reply?.text).toContain("bound this conversation");
280290expect(result.reply?.text).toContain("(acp)");
291+expect(hoisted.resolveFocusTargetSessionMock).toHaveBeenCalledWith(
292+expect.objectContaining({
293+requesterKey: "agent:main:main",
294+}),
295+);
281296expect(hoisted.sessionBindingBindMock).toHaveBeenCalledWith(
282297expect.objectContaining({
283298placement: "current",
@@ -291,6 +306,29 @@ describe("focus actions", () => {
291306);
292307});
293308309+it("rejects /focus from a leaf subagent", async () => {
310+hoisted.resolveStoredSubagentCapabilitiesMock.mockReturnValue({
311+controlScope: "none",
312+});
313+hoisted.resolveConversationBindingContextMock.mockReturnValue({
314+channel: THREAD_CHANNEL,
315+accountId: "default",
316+conversationId: "thread-1",
317+parentConversationId: "parent-1",
318+threadId: "thread-1",
319+});
320+321+const result = await handleSubagentsFocusAction(
322+buildFocusContext({
323+requesterKey: "agent:main:subagent:leaf-a",
324+}),
325+);
326+327+expect(result.reply?.text).toContain("Leaf subagents cannot control other sessions.");
328+expect(hoisted.resolveFocusTargetSessionMock).not.toHaveBeenCalled();
329+expect(hoisted.sessionBindingBindMock).not.toHaveBeenCalled();
330+});
331+294332it("binds topic-chat topics as current conversations", async () => {
295333hoisted.resolveConversationBindingContextMock.mockReturnValue({
296334channel: TOPIC_CHANNEL,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。