chantier: durcissement reprise headless inter-projets + persistance profils Opencode
- Implémentation reprise de session Opencode headless (conversational_recovery) - Persistance des profils IA Opencode dans .ideai/memory avec JSON Schema - Gestion des permissions MCP pour agents externes - Tests QA verts : structured_launch_d3, conversation_log, tickets_missing_carnet, agents, ProfilesSettings
This commit is contained in:
@ -364,6 +364,21 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
const options = Array.from(select.options).map((o) => o.text);
|
||||
expect(options).toContain("Claude Code");
|
||||
});
|
||||
|
||||
it("shows persisted OpenCode profiles in the agent profile dropdown", async () => {
|
||||
const profile = new MockProfileGateway();
|
||||
const openCode = await profile.cloneOpenCodeProfileFromSeed({
|
||||
name: "OpenCode Local Durable",
|
||||
});
|
||||
await profile.saveProfile(openCode);
|
||||
|
||||
renderPanel(new MockAgentGateway(), profile);
|
||||
await waitForIdle();
|
||||
|
||||
const select = screen.getByLabelText("agent profile") as HTMLSelectElement;
|
||||
const options = Array.from(select.options).map((o) => o.text);
|
||||
expect(options.some((text) => text.includes("OpenCode Local Durable"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
|
||||
import { DIProvider } from "@/app/di";
|
||||
@ -221,4 +221,70 @@ describe("ProfilesSettings", () => {
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("creates OpenCode profiles through the OpenCode clone flow and persists them for agent pickers", async () => {
|
||||
const profile = new MockProfileGateway();
|
||||
const cloneOpenCodeSpy = vi.spyOn(profile, "cloneOpenCodeProfileFromSeed");
|
||||
const cloneGenericSpy = vi.spyOn(profile, "cloneProfileFromSeed");
|
||||
renderSettings(profile);
|
||||
await waitReady();
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: "OpenCode-local" }));
|
||||
await waitReady();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Creer un profil" }));
|
||||
|
||||
const name = await screen.findByDisplayValue("OpenCode + llama.cpp copy");
|
||||
expect(name).toBeTruthy();
|
||||
await waitFor(async () => {
|
||||
const saved = await profile.listProfiles();
|
||||
expect(saved.map((p) => p.name)).toContain("OpenCode + llama.cpp copy");
|
||||
expect(saved.some((p) => p.structuredAdapter === "openCode")).toBe(true);
|
||||
});
|
||||
expect(cloneOpenCodeSpy).toHaveBeenCalledWith({
|
||||
name: "OpenCode + llama.cpp copy",
|
||||
opencode: expect.objectContaining({
|
||||
baseURL: "http://localhost:8080/v1",
|
||||
model: "qwen3-coder-30b",
|
||||
}),
|
||||
});
|
||||
expect(cloneGenericSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("duplicates OpenCode profiles through the OpenCode clone flow and preserves their local config", async () => {
|
||||
const profile = new MockProfileGateway();
|
||||
renderSettings(profile);
|
||||
await waitReady();
|
||||
|
||||
fireEvent.click(screen.getByRole("tab", { name: "OpenCode-local" }));
|
||||
await waitReady();
|
||||
await createProfile();
|
||||
|
||||
const row = screen.getAllByRole("listitem")[0];
|
||||
fireEvent.change(within(row).getByLabelText("OpenCode + llama.cpp copy base url"), {
|
||||
target: { value: "http://localhost:9090/v1" },
|
||||
});
|
||||
fireEvent.change(within(row).getByLabelText("OpenCode + llama.cpp copy model"), {
|
||||
target: { value: "local-special" },
|
||||
});
|
||||
fireEvent.click(within(row).getByRole("button", { name: "Enregistrer" }));
|
||||
|
||||
await waitFor(async () => {
|
||||
const saved = await profile.listProfiles();
|
||||
expect(saved[0]?.opencode?.baseURL).toBe("http://localhost:9090/v1");
|
||||
});
|
||||
|
||||
fireEvent.click(within(screen.getAllByRole("listitem")[0]).getByRole("button", {
|
||||
name: "Dupliquer",
|
||||
}));
|
||||
|
||||
await waitFor(async () => {
|
||||
const saved = await profile.listProfiles();
|
||||
const copies = saved.filter((p) => p.structuredAdapter === "openCode");
|
||||
expect(copies).toHaveLength(2);
|
||||
expect(copies[1]?.name).toBe("OpenCode + llama.cpp copy copy");
|
||||
expect(copies[1]?.opencode?.baseURL).toBe("http://localhost:9090/v1");
|
||||
expect(copies[1]?.opencode?.model).toBe("local-special");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -56,7 +56,10 @@ function describe(e: unknown): string {
|
||||
function tabFor(profile: AgentProfile): ProfileTab | null {
|
||||
if (profile.structuredAdapter === "codex") return "codex";
|
||||
if (profile.structuredAdapter === "claude") return "claude";
|
||||
if (profile.structuredAdapter === "openCode" && profile.opencode) {
|
||||
if (
|
||||
profile.structuredAdapter === "openCode" &&
|
||||
(profile.opencode || profile.opencodeProvider)
|
||||
) {
|
||||
return "openCode";
|
||||
}
|
||||
return null;
|
||||
@ -352,6 +355,16 @@ export function ProfilesSettings() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
if (activeTab === "openCode") {
|
||||
const created = await profile.cloneOpenCodeProfileFromSeed({
|
||||
name: `${seed.name} copy`,
|
||||
opencode: seed.opencode,
|
||||
});
|
||||
await profile.saveProfile(created);
|
||||
await refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const models =
|
||||
activeTab === "codex" || activeTab === "claude"
|
||||
? catalogue[activeTab].models
|
||||
@ -404,6 +417,16 @@ export function ProfilesSettings() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
if (source.structuredAdapter === "openCode") {
|
||||
const created = await profile.cloneOpenCodeProfileFromSeed({
|
||||
name: `${source.name} copy`,
|
||||
opencode: source.opencode,
|
||||
});
|
||||
await profile.saveProfile(created);
|
||||
await refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
await profile.cloneProfileFromSeed({
|
||||
seedProfileId: source.id,
|
||||
name: `${source.name} copy`,
|
||||
|
||||
Reference in New Issue
Block a user