import os from "node:os" ;
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest" ;
import {
createSubagentSpawnTestConfig,
installSessionStoreCaptureMock,
loadSubagentSpawnModuleForTest,
} from "./subagent-spawn.test-helpers.js" ;
import { installAcceptedSubagentGatewayMock } from "./test-helpers/subagent-gateway.js" ;
const hoisted = vi.hoisted(() => ({
callGatewayMock: vi.fn(),
updateSessionStoreMock: vi.fn(),
registerSubagentRunMock: vi.fn(),
emitSessionLifecycleEventMock: vi.fn(),
hookRunner: {
hasHooks: vi.fn(),
runSubagentSpawning: vi.fn(),
},
}));
describe("spawnSubagentDirect thread binding delivery" , () => {
type SpawnModule = Awaited<ReturnType<typeof loadSubagentSpawnModuleForTest>>;
type SessionBindingService = NonNullable<
Parameters<typeof loadSubagentSpawnModuleForTest>[0 ]["getSessionBindingService" ]
>;
type DeliveryTargetResolver = NonNullable<
Parameters<typeof loadSubagentSpawnModuleForTest>[0 ]["resolveConversationDeliveryTarget" ]
>;
let spawnSubagentDirect: SpawnModule["spawnSubagentDirect" ];
let currentConfig: Record<string, unknown>;
let currentSessionBindingService: ReturnType<SessionBindingService>;
let currentDeliveryTargetResolver: DeliveryTargetResolver;
beforeAll(async () => {
({ spawnSubagentDirect } = await loadSubagentSpawnModuleForTest({
callGatewayMock: hoisted.callGatewayMock,
loadConfig: () => currentConfig,
updateSessionStoreMock: hoisted.updateSessionStoreMock,
registerSubagentRunMock: hoisted.registerSubagentRunMock,
emitSessionLifecycleEventMock: hoisted.emitSessionLifecycleEventMock,
hookRunner: hoisted.hookRunner,
resolveSubagentSpawnModelSelection: () => "openai-codex/gpt-5.4" ,
resolveSandboxRuntimeStatus: () => ({ sandboxed: false }),
getSessionBindingService: () => currentSessionBindingService,
resolveConversationDeliveryTarget: (params) => currentDeliveryTargetResolver(params),
}));
});
beforeEach(() => {
currentConfig = createSubagentSpawnTestConfig(os.tmpdir(), {
agents: {
defaults: {
workspace: os.tmpdir(),
},
list: [{ id: "main" , workspace: "/tmp/workspace-main" }],
},
});
currentSessionBindingService = { listBySession: () => [] };
currentDeliveryTargetResolver = (params) => ({
to: params.conversationId ? `channel:${String(params.conversationId)}` : undefined,
});
hoisted.callGatewayMock.mockReset();
hoisted.updateSessionStoreMock.mockReset();
hoisted.registerSubagentRunMock.mockReset();
hoisted.emitSessionLifecycleEventMock.mockReset();
hoisted.hookRunner.hasHooks.mockReset();
hoisted.hookRunner.runSubagentSpawning.mockReset();
installAcceptedSubagentGatewayMock(hoisted.callGatewayMock);
installSessionStoreCaptureMock(hoisted.updateSessionStoreMock);
});
it("passes the target agent's bound account to thread binding hooks" , async () => {
const boundRoom = "!room:example.org" ;
let hookRequester:
| { channel?: string; accountId?: string; to?: string; threadId?: string | number }
| undefined;
hoisted.hookRunner.hasHooks.mockImplementation(
(hookName?: string) => hookName === "subagent_spawning" ,
);
hoisted.hookRunner.runSubagentSpawning.mockImplementation(async (event: unknown) => {
hookRequester = (
event as {
requester?: {
channel?: string;
accountId?: string;
to?: string;
threadId?: string | number;
};
}
).requester;
return {
status: "ok" ,
threadBindingReady: true ,
deliveryOrigin: {
channel: "matrix" ,
to: `room:${boundRoom}`,
threadId: "$thread-root" ,
},
};
});
currentConfig = createSubagentSpawnTestConfig(os.tmpdir(), {
agents: {
defaults: {
workspace: os.tmpdir(),
subagents: {
allowAgents: ["bot-alpha" ],
},
},
list: [
{ id: "main" , workspace: "/tmp/workspace-main" },
{ id: "bot-alpha" , workspace: "/tmp/workspace-bot-alpha" },
],
},
bindings: [
{
type: "route" ,
agentId: "bot-alpha" ,
match: {
channel: "matrix" ,
peer: {
kind: "channel" ,
id: boundRoom,
},
accountId: "bot-alpha" ,
},
},
],
});
const result = await spawnSubagentDirect(
{
task: "reply with a marker" ,
agentId: "bot-alpha" ,
thread : true ,
mode: "session" ,
},
{
agentSessionKey: "agent:main:main" ,
agentChannel: "matrix" ,
agentAccountId: "bot-beta" ,
agentTo: `room:${boundRoom}`,
},
);
expect(result.status).toBe("accepted" );
expect(hookRequester).toMatchObject({
channel: "matrix" ,
accountId: "bot-alpha" ,
to: `room:${boundRoom}`,
});
const agentCall = hoisted.callGatewayMock.mock.calls.find(
([call]) => (call as { method?: string }).method === "agent" ,
)?.[0 ] as { params?: Record<string, unknown> } | undefined;
expect(agentCall?.params).toMatchObject({
channel: "matrix" ,
accountId: "bot-alpha" ,
to: `room:${boundRoom}`,
threadId: "$thread-root" ,
deliver: true ,
});
expect(hoisted.registerSubagentRunMock).toHaveBeenCalledWith(
expect.objectContaining({
requesterOrigin: {
channel: "matrix" ,
accountId: "bot-alpha" ,
to: `room:${boundRoom}`,
threadId: "$thread-root" ,
},
expectsCompletionMessage: false ,
spawnMode: "session" ,
}),
);
});
it("keeps completion announcements when only a generic binding is available" , async () => {
hoisted.hookRunner.hasHooks.mockImplementation(
(hookName?: string) => hookName === "subagent_spawning" ,
);
hoisted.hookRunner.runSubagentSpawning.mockResolvedValue({
status: "ok" ,
threadBindingReady: true ,
});
currentSessionBindingService = {
listBySession: () => [
{
status: "active" ,
conversation: {
channel: "collabchat" ,
accountId: "work" ,
conversationId: "collab_dm_1" ,
},
},
],
};
currentDeliveryTargetResolver = () => ({
to: "channel:collab_dm_1" ,
});
const result = await spawnSubagentDirect(
{
task: "reply with a marker" ,
thread : true ,
mode: "session" ,
},
{
agentSessionKey: "agent:main:main" ,
agentChannel: "matrix" ,
agentAccountId: "sut" ,
agentTo: "room:!parent:example" ,
},
);
expect(result.status).toBe("accepted" );
const agentCall = hoisted.callGatewayMock.mock.calls.find(
([call]) => (call as { method?: string }).method === "agent" ,
)?.[0 ] as { params?: Record<string, unknown> } | undefined;
expect(agentCall?.params).toMatchObject({
channel: "matrix" ,
accountId: "sut" ,
to: "room:!parent:example" ,
deliver: false ,
});
expect(hoisted.registerSubagentRunMock).toHaveBeenCalledWith(
expect.objectContaining({
expectsCompletionMessage: true ,
requesterOrigin: {
channel: "matrix" ,
accountId: "sut" ,
to: "room:!parent:example" ,
},
}),
);
});
});
Messung V0.5 in Prozent C=99 H=97 G=97
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-05-26)
¤
*© Formatika GbR, Deutschland