fix(cli): store annonces inter-agent borné par projectId avec reset et réhydratation busy au changement de projet — #236

This commit is contained in:
2026-08-22 10:02:41 +02:00
parent 85f659c788
commit 8deaa1fd6c
7 changed files with 167 additions and 26 deletions

View File

@ -24,17 +24,19 @@ import { TargetAnnouncementsOverlay } from "./TargetAnnouncementsOverlay";
function setup(
node: React.ReactNode,
opts: { workState?: MockWorkStateGateway } = {},
opts: { projectId?: string | null; workState?: MockWorkStateGateway } = {},
) {
const system = new MockSystemGateway();
const workState = opts.workState;
const gateways = { system, workState } as unknown as Gateways;
render(
const result = render(
<DIProvider gateways={gateways}>
<AnnouncementsProvider>{node}</AnnouncementsProvider>
<AnnouncementsProvider projectId={opts.projectId ?? "p1"}>
{node}
</AnnouncementsProvider>
</DIProvider>,
);
return { system };
return { system, ...result };
}
type AnnouncementEvent = Extract<DomainEvent, { type: "agentAnnouncement" }>;
@ -84,6 +86,27 @@ describe("F3 — overlay lifecycle is the target's busy state", () => {
expect(screen.getByText("en cours")).not.toBeNull();
});
it("ignores streamed announcements from another project", async () => {
const { system } = setup(
<TargetAnnouncementsOverlay projectId="p1" agentId="agent-T" />,
);
await flush();
act(() => {
system.emit(busy("agent-T", true));
system.emit(
announcement({
projectId: "p2",
target: "agent-T",
text: "other-project",
}),
);
});
expect(overlay()).not.toBeNull();
expect(screen.queryByText("other-project")).toBeNull();
});
it("mounts on busy:true even before any announcement (banner only)", async () => {
const { system } = setup(
<TargetAnnouncementsOverlay projectId="p1" agentId="agent-T" />,
@ -171,6 +194,64 @@ describe("F3 — overlay lifecycle is the target's busy state", () => {
expect(overlay()).not.toBeNull();
});
it("resets and rehydrates when switching project", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState("p1", {
agents: [
{
agentId: "agent-T",
name: "Target",
profileId: "prof",
busy: { state: "idle" },
tickets: [],
},
],
conversations: [],
});
workState._setProjectWorkState("p2", {
agents: [
{
agentId: "agent-T",
name: "Target",
profileId: "prof",
busy: { state: "busy", ticket: "t-p2", sinceMs: 456 },
tickets: [],
},
],
conversations: [],
});
const system = new MockSystemGateway();
const gateways = { system, workState } as unknown as Gateways;
const { rerender } = render(
<DIProvider gateways={gateways}>
<AnnouncementsProvider projectId="p1">
<TargetAnnouncementsOverlay projectId="p1" agentId="agent-T" />
</AnnouncementsProvider>
</DIProvider>,
);
await flush();
act(() => {
system.emit(busy("agent-T", true));
system.emit(announcement({ projectId: "p1", text: "old-p1" }));
});
expect(screen.getByText("old-p1")).not.toBeNull();
rerender(
<DIProvider gateways={gateways}>
<AnnouncementsProvider projectId="p2">
<TargetAnnouncementsOverlay projectId="p2" agentId="agent-T" />
</AnnouncementsProvider>
</DIProvider>,
);
await flush();
expect(overlay()).not.toBeNull();
expect(screen.queryByText("old-p1")).toBeNull();
expect(screen.getByText("Travaille sur une demande en cours.")).not.toBeNull();
});
it("stays down when the read-model reports the agent idle", async () => {
const workState = new MockWorkStateGateway();
workState._setProjectWorkState("p1", {