mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Wave 2 features: A2A live view (CEO chime-in + reply budget) and prompter memory (#297)
* feat(a2a): live view — watch fleet conversations, CEO chime-in, reply budget A2A_MESSAGE_SENT published from A2AService.send (excerpt-capped) and fanned through the existing /ws/system bridge; CEO-only admin REST for conversations/messages + a reply route on the publish-bearing send path; panel /a2a page with live transcript and a composer gated on task-linked conversations. The matrix gains its one asymmetric rule: CEO may message anyone, nobody may target the CEO — and agent replies inside a CEO-opened conversation are hard-budgeted to one per CEO message (per conversation, per agent), rejected with wait-don't-retry guidance. Built subagent-driven (Sonnet 5), reviewed; v1 seams documented in the map delta. * feat(prompter): intake remembers the task history Intake spawns now carry a per-project chronological digest of recent tasks (capped: 15 lines/project, 4000 chars total — ~300-1000 tokens) merged into the ambient layer, and the interviewer gets a bounded search_past_tasks tool (one shared implementation behind the grok MCP tool and the Claude SDK in-process tool) to check precedent mid-conversation. Informational memory only — the sequencing analyzer keeps ownership of ordering. Built subagent-driven (Sonnet 5), reviewed; pre-existing conventions-ambient MegaTask-scope gap flagged, untouched. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import type { A2AChatMessage } from "@/lib/api/a2a";
|
||||
|
||||
// react-markdown is heavyweight and irrelevant here — render bodies as-is.
|
||||
vi.mock("@/components/ui/markdown", () => ({
|
||||
Markdown: ({ children }: { children: string }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
import { A2ATranscript } from "../a2a-transcript";
|
||||
|
||||
function buildMessage(overrides: Partial<A2AChatMessage>): A2AChatMessage {
|
||||
return {
|
||||
id: "m1",
|
||||
conversation_id: "conv-1",
|
||||
from_agent: "be-dev-1",
|
||||
content: "hello",
|
||||
message_kind: "text",
|
||||
response_to_id: null,
|
||||
requires_response: false,
|
||||
read_at: null,
|
||||
created_at: "2026-07-02T10:00:00Z",
|
||||
edited_at: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("A2ATranscript", () => {
|
||||
it("renders messages chronologically with sender names and timestamps", () => {
|
||||
// Deliberately unordered payload: the later message first.
|
||||
const { container } = render(
|
||||
<A2ATranscript
|
||||
messages={[
|
||||
buildMessage({
|
||||
id: "m2",
|
||||
from_agent: "be-qa",
|
||||
content: "second message body",
|
||||
created_at: "2026-07-02T10:05:00Z",
|
||||
}),
|
||||
buildMessage({
|
||||
id: "m1",
|
||||
from_agent: "be-dev-1",
|
||||
content: "first message body",
|
||||
created_at: "2026-07-02T10:00:00Z",
|
||||
}),
|
||||
]}
|
||||
isLoading={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Backend Dev 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Backend QA")).toBeInTheDocument();
|
||||
// Every message carries a relative timestamp.
|
||||
expect(screen.getAllByText(/ago$/)).toHaveLength(2);
|
||||
// Chronological order: oldest first regardless of payload order.
|
||||
const text = container.textContent ?? "";
|
||||
expect(text.indexOf("first message body")).toBeLessThan(
|
||||
text.indexOf("second message body"),
|
||||
);
|
||||
});
|
||||
|
||||
it("shows the message kind as an outline badge when present", () => {
|
||||
render(
|
||||
<A2ATranscript
|
||||
messages={[buildMessage({ message_kind: "escalation" })]}
|
||||
isLoading={false}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("escalation")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the empty state when there are no messages", () => {
|
||||
render(<A2ATranscript messages={[]} isLoading={false} />);
|
||||
expect(
|
||||
screen.getByText(/No messages in this conversation yet/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user