feat(FN-3969): restore room responder fallback in chat
Restores the room responder fallback mechanism in the chat module (`chat.ts`), with corresponding tests in `chat-manager.test.ts` and `chat.rooms.test.ts` verifying the behavior. Fusion-Task-Id: FN-3969
This commit is contained in:
@@ -1954,6 +1954,7 @@ describe("ChatManager generation isolation", () => {
|
||||
mockAgentStore.listAgents.mockResolvedValue([
|
||||
{ id: "agent-001", name: "Avery", role: "executor", state: "idle" },
|
||||
]);
|
||||
mockAgentStore.getAgent.mockResolvedValue({ id: "agent-001", name: "Avery", role: "executor", state: "idle" });
|
||||
|
||||
__setCreateResolvedAgentSession(async () => ({
|
||||
session: {
|
||||
@@ -1980,4 +1981,43 @@ describe("ChatManager generation isolation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("sendRoomMessage still resolves room member responders when listAgents is unavailable", async () => {
|
||||
(mockChatStore as any).getRoom = vi.fn().mockReturnValue({ id: "room-1", name: "team" });
|
||||
(mockChatStore as any).listRoomMembers = vi.fn().mockReturnValue([
|
||||
{ roomId: "room-1", agentId: "agent-001", role: "member", addedAt: "2026-01-01" },
|
||||
]);
|
||||
(mockChatStore as any).addRoomMessage = vi.fn().mockImplementation((_roomId: string, input: any) => ({
|
||||
id: "room-msg",
|
||||
roomId: "room-1",
|
||||
...input,
|
||||
}));
|
||||
|
||||
mockAgentStore.listAgents.mockRejectedValue(new Error("agent listing offline"));
|
||||
mockAgentStore.getAgent.mockResolvedValue({ id: "agent-001", name: "Avery", role: "executor", state: "idle" });
|
||||
|
||||
__setCreateResolvedAgentSession(async () => ({
|
||||
session: {
|
||||
prompt: vi.fn().mockResolvedValue(undefined),
|
||||
dispose: vi.fn(),
|
||||
state: { messages: [{ role: "assistant", content: "Recovered room answer" }] },
|
||||
},
|
||||
provider: "test",
|
||||
model: "test",
|
||||
fallbackInfo: undefined,
|
||||
} as any));
|
||||
|
||||
const chatManager = createChatManager();
|
||||
await chatManager.sendRoomMessage("room-1", "hello @Avery");
|
||||
|
||||
const assistant = (mockChatStore as any).addRoomMessage.mock.calls
|
||||
.map((call: any[]) => call[1])
|
||||
.find((entry: any) => entry.role === "assistant");
|
||||
|
||||
expect(assistant).toMatchObject({
|
||||
role: "assistant",
|
||||
senderAgentId: "agent-001",
|
||||
content: "Recovered room answer",
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ const mockChatStore = {
|
||||
|
||||
const mockAgentStore = {
|
||||
init: vi.fn(),
|
||||
getAgent: vi.fn(),
|
||||
listAgents: vi.fn(),
|
||||
};
|
||||
|
||||
@@ -59,6 +60,7 @@ describe("Chat orchestration — rooms (FN-3805..FN-3811 contract)", () => {
|
||||
{ roomId: "room-1", agentId: "agent-a", role: "member", addedAt: "2026-01-01" },
|
||||
]);
|
||||
mockAgentStore.listAgents.mockResolvedValue([{ id: "agent-a", name: "Alpha", role: "executor" }]);
|
||||
mockAgentStore.getAgent.mockResolvedValue({ id: "agent-a", name: "Alpha", role: "executor" });
|
||||
|
||||
__setCreateResolvedAgentSession(async () => ({
|
||||
session: {
|
||||
@@ -85,10 +87,38 @@ describe("Chat orchestration — rooms (FN-3805..FN-3811 contract)", () => {
|
||||
expect(assistantWrite).toMatchObject({ role: "assistant", senderAgentId: "agent-a", content: "Room reply" });
|
||||
});
|
||||
|
||||
it("records non-member mentions and emits explanatory assistant note", async () => {
|
||||
it("falls back to room-member getAgent lookup when listAgents fails", async () => {
|
||||
mockChatStore.listRoomMembers.mockReturnValue([
|
||||
{ roomId: "room-1", agentId: "agent-a", role: "member", addedAt: "2026-01-01" },
|
||||
]);
|
||||
mockAgentStore.listAgents.mockRejectedValue(new Error("list failed"));
|
||||
mockAgentStore.getAgent.mockResolvedValue({ id: "agent-a", name: "Alpha", role: "executor" });
|
||||
|
||||
__setCreateResolvedAgentSession(async () => ({
|
||||
session: {
|
||||
prompt: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
state: {
|
||||
messages: [{ role: "assistant", content: "Room reply" }],
|
||||
},
|
||||
},
|
||||
provider: "test",
|
||||
model: "test",
|
||||
fallbackInfo: undefined,
|
||||
} as any));
|
||||
|
||||
const manager = new ChatManager(mockChatStore as any, "/tmp", mockAgentStore as any);
|
||||
const result = await manager.sendRoomMessage("room-1", "hello");
|
||||
|
||||
expect(result.responders).toEqual(["agent-a"]);
|
||||
|
||||
const assistantWrite = mockChatStore.addRoomMessage.mock.calls[1]?.[1];
|
||||
expect(assistantWrite).toMatchObject({ role: "assistant", senderAgentId: "agent-a", content: "Room reply" });
|
||||
});
|
||||
|
||||
it("records non-member mentions and emits explanatory assistant note", async () => { mockChatStore.listRoomMembers.mockReturnValue([
|
||||
{ roomId: "room-1", agentId: "agent-a", role: "member", addedAt: "2026-01-01" },
|
||||
]);
|
||||
mockAgentStore.listAgents.mockResolvedValue([
|
||||
{ id: "agent-a", name: "Alpha", role: "executor" },
|
||||
{ id: "agent-z", name: "Zeta", role: "executor" },
|
||||
|
||||
@@ -676,6 +676,23 @@ export class ChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async getAgentById(agentId: string): Promise<Agent | null> {
|
||||
if (!this.agentStore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.agentStoreReady ??= this.agentStore.init();
|
||||
await this.agentStoreReady;
|
||||
const agent = await this.agentStore.getAgent(agentId);
|
||||
return agent ?? null;
|
||||
} catch (agentLookupError) {
|
||||
const message = agentLookupError instanceof Error ? agentLookupError.message : String(agentLookupError);
|
||||
diagnostics.warn(`Failed to resolve room member agent ${agentId}: ${message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** A parsed @ mention of an agent in a chat message */
|
||||
private async parseMentions(content: string, agents?: Agent[]): Promise<ChatMention[]> {
|
||||
if (!this.agentStore) {
|
||||
@@ -830,6 +847,20 @@ export class ChatManager {
|
||||
const trimmedContent = content.trim();
|
||||
const hasMentionCandidates = /@[\w-]+/.test(trimmedContent);
|
||||
const availableAgents = await this.listAgentsForMentions();
|
||||
const availableAgentsById = new Map(availableAgents.map((agent) => [agent.id, agent]));
|
||||
|
||||
for (const member of this.chatStore.listRoomMembers(roomId)) {
|
||||
if (availableAgentsById.has(member.agentId)) {
|
||||
continue;
|
||||
}
|
||||
const memberAgent = await this.getAgentById(member.agentId);
|
||||
if (!memberAgent) {
|
||||
continue;
|
||||
}
|
||||
availableAgentsById.set(memberAgent.id, memberAgent);
|
||||
availableAgents.push(memberAgent);
|
||||
}
|
||||
|
||||
const mentions = hasMentionCandidates ? await this.parseMentions(trimmedContent, availableAgents) : [];
|
||||
|
||||
const responderPlan = this.resolveRoomResponders(
|
||||
|
||||
Reference in New Issue
Block a user