Dernier lot de §17 : seuls les profils pilotables en mode structuré sont proposés à la sélection/création. - domain : AgentProfile::is_selectable() = structured_adapter.is_some(), source unique de vérité du prédicat. - infrastructure : AgentSessionFactory::supports délègue à is_selectable (supports et is_selectable ne peuvent plus diverger). - application : selectable_reference_profiles() = reference_profiles() filtré ; ReferenceProfiles et FirstRunState exposent la liste filtrée (Claude/Codex). reference_profiles() brut reste à 4 (data intacte) ⇒ un agent Gemini/Aider/custom legacy déjà configuré continue de tourner. - frontend : bloc AddCustomProfile retiré du wizard first-run, action addCustom retirée du viewmodel ; wizard n'affiche que la liste filtrée. Tests (QA) : is_selectable (table de vérité), cohérence stricte is_selectable<->supports, liste exposée=2 / data brute=4, garde anti-régression Vitest sur l'absence du bloc custom — validées par mutation. cargo test --workspace : 820 passed. npx vitest run : 344 passed. §17 COMPLET (D0->D7). Suivi restant : D6b (surfacer reply dans le writer wire .response.json pour la délégation par protocole fichier). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
/**
|
|
* L5 — the in-memory {@link MockProfileGateway}: first-run flag lifecycle,
|
|
* reference catalogue, simulated detection (only `claude` installed), and the
|
|
* save/list/delete/configure CRUD.
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import type { AgentProfile } from "@/domain";
|
|
import { MockProfileGateway, MOCK_REFERENCE_PROFILES } from "./index";
|
|
|
|
function customProfile(id: string, command: string): AgentProfile {
|
|
return {
|
|
id,
|
|
name: `Custom ${id}`,
|
|
command,
|
|
args: [],
|
|
contextInjection: { strategy: "stdin" },
|
|
detect: null,
|
|
cwdTemplate: "{projectRoot}",
|
|
};
|
|
}
|
|
|
|
describe("MockProfileGateway", () => {
|
|
it("firstRunState is first-run with only the selectable reference profiles", async () => {
|
|
// §17.3/D7: only structured-drivable profiles (Claude/Codex) are offered;
|
|
// Gemini/Aider are filtered out of the selection path server-side.
|
|
const gw = new MockProfileGateway();
|
|
const state = await gw.firstRunState();
|
|
expect(state.isFirstRun).toBe(true);
|
|
expect(state.referenceProfiles.map((p) => p.command)).toEqual([
|
|
"claude",
|
|
"codex",
|
|
]);
|
|
});
|
|
|
|
it("referenceProfiles returns a clone of the catalogue", async () => {
|
|
const gw = new MockProfileGateway();
|
|
const refs = await gw.referenceProfiles();
|
|
expect(refs).toEqual(MOCK_REFERENCE_PROFILES);
|
|
refs[0].command = "mutated";
|
|
const again = await gw.referenceProfiles();
|
|
expect(again[0].command).toBe("claude");
|
|
});
|
|
|
|
it("detectProfiles marks only claude as installed", async () => {
|
|
const gw = new MockProfileGateway();
|
|
const results = await gw.detectProfiles([...MOCK_REFERENCE_PROFILES]);
|
|
const byCommand = Object.fromEntries(
|
|
results.map((r) => [r.profile.command, r.available]),
|
|
);
|
|
expect(byCommand).toEqual({
|
|
claude: true,
|
|
codex: false,
|
|
});
|
|
});
|
|
|
|
it("saveProfile upserts and listProfiles reflects it", async () => {
|
|
const gw = new MockProfileGateway();
|
|
await gw.saveProfile(customProfile("c1", "foo"));
|
|
await gw.saveProfile(customProfile("c1", "bar")); // same id ⇒ replace
|
|
const list = await gw.listProfiles();
|
|
expect(list).toHaveLength(1);
|
|
expect(list[0].command).toBe("bar");
|
|
});
|
|
|
|
it("deleteProfile removes by id", async () => {
|
|
const gw = new MockProfileGateway();
|
|
await gw.saveProfile(customProfile("a", "a"));
|
|
await gw.saveProfile(customProfile("b", "b"));
|
|
await gw.deleteProfile("a");
|
|
const list = await gw.listProfiles();
|
|
expect(list.map((p) => p.id)).toEqual(["b"]);
|
|
});
|
|
|
|
it("configureProfiles persists the batch and closes the first run", async () => {
|
|
const gw = new MockProfileGateway();
|
|
expect((await gw.firstRunState()).isFirstRun).toBe(true);
|
|
|
|
const chosen = [customProfile("x", "x")];
|
|
const out = await gw.configureProfiles(chosen);
|
|
expect(out).toEqual(chosen);
|
|
|
|
expect((await gw.firstRunState()).isFirstRun).toBe(false);
|
|
expect(await gw.listProfiles()).toEqual(chosen);
|
|
});
|
|
|
|
it("configureProfiles with an empty list still closes the first run", async () => {
|
|
const gw = new MockProfileGateway();
|
|
await gw.configureProfiles([]);
|
|
expect((await gw.firstRunState()).isFirstRun).toBe(false);
|
|
});
|
|
});
|