fix(agents): unwrap read_agent_context DTO content (#140)
Le contexte projet d'un agent s'affichait "[object Object]" et se
vidait à la réouverture du panneau : read_agent_context renvoie déjà
{content} côté backend, mais les gateways front traitaient la réponse
comme une string brute. Les deux gateways (Tauri + HTTP) unwrap
maintenant .content ; type AgentContextDocument ajouté au domaine.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
6
.ideai/tickets/140/carnet.md
Normal file
6
.ideai/tickets/140/carnet.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
issueRef: "#140"
|
||||
version: 3
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1785760014957
|
||||
---
|
||||
17
.ideai/tickets/140/issue.md
Normal file
17
.ideai/tickets/140/issue.md
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
id: "353aa2ae-cf98-4a63-b4fb-a15fdb801a0a"
|
||||
number: 140
|
||||
title: "[Bug] je ne peux pas editer le context projet d'un agent a la main"
|
||||
status: "open"
|
||||
priority: "medium"
|
||||
sprint: null
|
||||
links: []
|
||||
agentRefs: [{"agentId":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d","role":"assigned"}]
|
||||
attachments: []
|
||||
createdBy: {"kind":"user"}
|
||||
updatedBy: {"kind":"user"}
|
||||
createdAt: 1785759952774
|
||||
updatedAt: 1785760014957
|
||||
version: 3
|
||||
---
|
||||
Quand je suis dans le panneau des agents, que je selectionne un agent, le context projet de l'agent s'affiche mal (il n'affiche que [object Object]) et si je l'edit, que je save, et que je le réouvre il estd e nouveau vide
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"nextNumber": 140
|
||||
"nextNumber": 141
|
||||
}
|
||||
@ -1807,6 +1807,21 @@
|
||||
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
||||
},
|
||||
"updatedAt": 1785709504027
|
||||
},
|
||||
{
|
||||
"issueRef": "#140",
|
||||
"path": "140",
|
||||
"title": "[Bug] je ne peux pas editer le context projet d'un agent a la main",
|
||||
"status": "open",
|
||||
"priority": "medium",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
|
||||
],
|
||||
"createdBy": {
|
||||
"kind": "user"
|
||||
},
|
||||
"updatedAt": 1785760014957
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -42,12 +42,13 @@ describe("TauriAgentGateway invoke payloads", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("list_agents / read / delete pass top-level args (no request wrapper)", async () => {
|
||||
it("list_agents / read / delete pass top-level args and unwrap read context DTO", async () => {
|
||||
const gw = new TauriAgentGateway();
|
||||
await gw.listAgents("p");
|
||||
expect(invoke).toHaveBeenCalledWith("list_agents", { projectId: "p" });
|
||||
|
||||
await gw.readContext("p", "a");
|
||||
invoke.mockResolvedValueOnce({ content: "# context" });
|
||||
await expect(gw.readContext("p", "a")).resolves.toBe("# context");
|
||||
expect(invoke).toHaveBeenCalledWith("read_agent_context", {
|
||||
projectId: "p",
|
||||
agentId: "a",
|
||||
|
||||
@ -16,6 +16,7 @@ import { Channel, invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import type {
|
||||
Agent,
|
||||
AgentContextDocument,
|
||||
EffortSelection,
|
||||
ResumableAgent,
|
||||
TerminalSession,
|
||||
@ -117,7 +118,10 @@ export class TauriAgentGateway implements AgentGateway {
|
||||
}
|
||||
|
||||
readContext(projectId: string, agentId: string): Promise<string> {
|
||||
return invoke<string>("read_agent_context", { projectId, agentId });
|
||||
return invoke<AgentContextDocument>("read_agent_context", {
|
||||
projectId,
|
||||
agentId,
|
||||
}).then((res) => res.content);
|
||||
}
|
||||
|
||||
async updateContext(
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
import { HttpAgentGateway } from "./streamGateways";
|
||||
import { HttpInvoker } from "./httpInvoker";
|
||||
import { HttpInvoker, type FetchLike } from "./httpInvoker";
|
||||
import { WsLiveClient, type WebSocketLike } from "./wsLiveClient";
|
||||
import { bytesToBase64 } from "./frames";
|
||||
|
||||
@ -45,6 +45,27 @@ function gateway(): { gw: HttpAgentGateway; sockets: FakeSocket[] } {
|
||||
return { gw, sockets };
|
||||
}
|
||||
|
||||
function httpAgentGateway(
|
||||
fetchImpl: FetchLike,
|
||||
): { gw: HttpAgentGateway; calls: { url: string; init: unknown }[] } {
|
||||
const calls: { url: string; init: unknown }[] = [];
|
||||
const recordingFetch: FetchLike = async (url, init) => {
|
||||
calls.push({ url, init });
|
||||
return fetchImpl(url, init);
|
||||
};
|
||||
const ws = new WsLiveClient({
|
||||
wsUrl: "wss://host",
|
||||
socketFactory: () => new FakeSocket(),
|
||||
});
|
||||
return {
|
||||
gw: new HttpAgentGateway(
|
||||
new HttpInvoker({ baseUrl: "https://host", fetchImpl: recordingFetch }),
|
||||
ws,
|
||||
),
|
||||
calls,
|
||||
};
|
||||
}
|
||||
|
||||
async function replyToLast(
|
||||
socket: FakeSocket,
|
||||
index: number,
|
||||
@ -78,6 +99,26 @@ function attachedAck(
|
||||
const OPTS = { cwd: "/srv/app", rows: 24, cols: 80, nodeId: "node-1" };
|
||||
|
||||
describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
|
||||
it("readContext unwraps the read_agent_context DTO content over HTTP", async () => {
|
||||
const { gw, calls } = httpAgentGateway(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ content: "## Restored context" }),
|
||||
text: async () => JSON.stringify({ content: "## Restored context" }),
|
||||
}));
|
||||
|
||||
await expect(gw.readContext("proj-1", "agent-1")).resolves.toBe(
|
||||
"## Restored context",
|
||||
);
|
||||
|
||||
expect(calls).toHaveLength(1);
|
||||
const init = calls[0].init as { body: string };
|
||||
expect(JSON.parse(init.body)).toEqual({
|
||||
command: "read_agent_context",
|
||||
args: { projectId: "proj-1", agentId: "agent-1" },
|
||||
});
|
||||
});
|
||||
|
||||
it("launch → attached: conforming agent.launch frame, assignedConversationId consumed", async () => {
|
||||
const { gw, sockets } = gateway();
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
|
||||
import type {
|
||||
Agent,
|
||||
AgentContextDocument,
|
||||
AppExitWorkGuardState,
|
||||
DomainEvent,
|
||||
EffortSelection,
|
||||
@ -234,7 +235,9 @@ export class HttpAgentGateway implements AgentGateway {
|
||||
});
|
||||
}
|
||||
readContext(projectId: string, agentId: string): Promise<string> {
|
||||
return this.http.invoke<string>("read_agent_context", { projectId, agentId });
|
||||
return this.http
|
||||
.invoke<AgentContextDocument>("read_agent_context", { projectId, agentId })
|
||||
.then((res) => res.content);
|
||||
}
|
||||
async updateContext(projectId: string, agentId: string, content: string): Promise<void> {
|
||||
await this.http.invoke("update_agent_context", { request: { projectId, agentId, content } });
|
||||
|
||||
@ -1277,6 +1277,11 @@ export interface Agent {
|
||||
effort?: EffortSelection;
|
||||
}
|
||||
|
||||
/** Response DTO returned by `read_agent_context`; adapters unwrap `content`. */
|
||||
export interface AgentContextDocument {
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A terminal/PTY session as returned by the backend (mirror of
|
||||
* `TerminalSessionDto`, camelCase wire format). Surfaced by `changeAgentProfile`
|
||||
|
||||
@ -241,6 +241,43 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("reopening the agents panel reloads the saved context as textarea text", async () => {
|
||||
const agent = new MockAgentGateway();
|
||||
await agent.createAgent(PROJECT_ID, {
|
||||
name: "Reopen",
|
||||
profileId: "p1",
|
||||
initialContent: "initial",
|
||||
});
|
||||
const firstRender = renderPanel(agent);
|
||||
await waitForIdle();
|
||||
|
||||
let buttons = screen.getAllByRole("button", { name: /reopen/i });
|
||||
let rowBtn = buttons.find((b) => b.hasAttribute("aria-pressed"))!;
|
||||
fireEvent.click(rowBtn);
|
||||
|
||||
let textarea = await screen.findByLabelText("agent context");
|
||||
fireEvent.change(textarea, { target: { value: "persisted after reopen" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save" }));
|
||||
|
||||
await waitFor(async () => {
|
||||
const agents = await agent.listAgents(PROJECT_ID);
|
||||
await expect(agent.readContext(PROJECT_ID, agents[0].id)).resolves.toBe(
|
||||
"persisted after reopen",
|
||||
);
|
||||
});
|
||||
|
||||
firstRender.unmount();
|
||||
renderPanel(agent);
|
||||
await waitForIdle();
|
||||
|
||||
buttons = screen.getAllByRole("button", { name: /reopen/i });
|
||||
rowBtn = buttons.find((b) => b.hasAttribute("aria-pressed"))!;
|
||||
fireEvent.click(rowBtn);
|
||||
|
||||
textarea = await screen.findByLabelText("agent context");
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("persisted after reopen");
|
||||
});
|
||||
|
||||
it("deleting an agent removes it from the list", async () => {
|
||||
const agent = new MockAgentGateway();
|
||||
await agent.createAgent(PROJECT_ID, {
|
||||
|
||||
Reference in New Issue
Block a user