Files
gitmost/apps/client/src/features/ai-chat/utils/catalog-bundle-model.test.ts
T
agent_coder b8cce4f814 fix(#371): skipped role is not 'allInstalled', test the reason->action branch (review round 1)
F1 [WARNING] bundlePhase returned 'allInstalled' when a bundle's only
non-installed role was skipped (0 installed for it), so the collapsed green 'All
installed · up to date' header contradicted the open 'Installed 0 · 1 skipped'
plaque. It now returns 'mixed' whenever a skipped role is present. Fixed the test
that encoded the wrong behavior.

F2 [WARNING] The reason->action branch (name-conflict -> transient overlay +
'Rename & install'; already-installed -> informational, no button) lived only in
the component, untested. Extracted the two decisions into pure, unit-tested
helpers nameConflictSlugs() and partialOffersRename() and wired them into the
modal; both reason values are now covered.

F3 [low] Removed the unused useRef import (client eslint no-unused-vars is off, so
it shipped silently).

F4 [low] Extracted bundleCounts() as the single tally pass; bundlePhase and the
panel both derive from it instead of rescanning the roles array ~5x per render
(the same model<->component consolidation this PR is about).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 06:09:58 +03:00

235 lines
7.1 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
bundleCounts,
bundlePhase,
installedLangForRole,
mapBundleRolesToView,
mapCatalogRoleToView,
nameConflictSlugs,
partialOffersRename,
type CatalogViewRole,
} from "./catalog-bundle-model.ts";
import type {
IAiRole,
IAiRoleCatalogRole,
} from "@/features/ai-chat/types/ai-chat.types.ts";
function installedRole(
source: { slug: string; language: string; version: number },
overrides: Partial<IAiRole> = {},
): IAiRole {
return {
id: `role-${source.slug}-${source.language}`,
name: source.slug,
emoji: null,
description: null,
enabled: true,
autoStart: true,
launchMessage: null,
source,
...overrides,
};
}
function catalogRole(
overrides: Partial<IAiRoleCatalogRole> = {},
): IAiRoleCatalogRole {
return {
slug: "writer",
emoji: "✍️",
name: "Writer",
description: "Drafts copy.",
instructions: "be a writer",
autoStart: true,
launchMessage: null,
version: 3,
...overrides,
};
}
// Build a minimal view role for bundlePhase tests.
function viewRole(status: CatalogViewRole["status"]): CatalogViewRole {
return { slug: `s-${status}`, name: status, description: "", version: 1, status };
}
describe("bundlePhase", () => {
it("empty bundle -> empty", () => {
expect(bundlePhase([])).toBe("empty");
});
it("all importable, none installed -> allNew", () => {
expect(bundlePhase([viewRole("import"), viewRole("import")])).toBe(
"allNew",
);
});
it("nothing to import or update -> allInstalled", () => {
expect(bundlePhase([viewRole("installed"), viewRole("installed")])).toBe(
"allInstalled",
);
});
it("updates present, nothing to import -> updates", () => {
expect(bundlePhase([viewRole("update"), viewRole("installed")])).toBe(
"updates",
);
});
it("import + installed (no updates) -> mixed", () => {
expect(bundlePhase([viewRole("import"), viewRole("installed")])).toBe(
"mixed",
);
});
it("import + update -> mixed", () => {
expect(bundlePhase([viewRole("import"), viewRole("update")])).toBe("mixed");
});
it("a skipped role with nothing installed -> mixed (NOT allInstalled)", () => {
// F1: a bundle whose only non-installed role was skipped has 0 installed for
// it, so the collapsed 'All installed · up to date' header would contradict
// the open 'Installed 0 · 1 skipped' plaque. It must be mixed until resolved.
expect(bundlePhase([viewRole("skipped")])).toBe("mixed");
});
it("installed + a skipped role -> mixed (partial success is not allInstalled)", () => {
expect(bundlePhase([viewRole("installed"), viewRole("skipped")])).toBe(
"mixed",
);
});
});
describe("bundleCounts", () => {
it("tallies each status once", () => {
expect(
bundleCounts([
viewRole("import"),
viewRole("import"),
viewRole("installed"),
viewRole("update"),
viewRole("skipped"),
]),
).toEqual({ importable: 2, installed: 1, update: 1, skipped: 1 });
});
});
describe("nameConflictSlugs / partialOffersRename (reason -> action)", () => {
it("only name-conflict skips become the transient overlay / offer rename", () => {
const skipped = [
{ slug: "writer", name: "Writer", reason: "name-conflict" as const },
{ slug: "editor", name: "Editor", reason: "already-installed" as const },
];
expect(nameConflictSlugs(skipped)).toEqual(["writer"]);
expect(partialOffersRename(skipped)).toBe(true);
});
it("an already-installed-only skip is informational: no overlay, no rename", () => {
const skipped = [
{ slug: "editor", name: "Editor", reason: "already-installed" as const },
];
expect(nameConflictSlugs(skipped)).toEqual([]);
expect(partialOffersRename(skipped)).toBe(false);
});
});
describe("installedLangForRole", () => {
it("returns the other language when the same slug is installed elsewhere", () => {
const roles = [installedRole({ slug: "writer", language: "ru", version: 2 })];
expect(installedLangForRole("writer", roles, "en")).toBe("ru");
});
it("returns undefined when the same slug is installed in the SAME language", () => {
const roles = [installedRole({ slug: "writer", language: "en", version: 2 })];
expect(installedLangForRole("writer", roles, "en")).toBeUndefined();
});
it("returns undefined when no install of the slug exists", () => {
expect(installedLangForRole("writer", [], "en")).toBeUndefined();
});
it("ignores manually-created roles (no source)", () => {
const roles = [
installedRole({ slug: "writer", language: "ru", version: 2 }, {
source: null,
}),
];
expect(installedLangForRole("writer", roles, "en")).toBeUndefined();
});
});
describe("mapCatalogRoleToView", () => {
it("no install -> import status, catalog version, emoji preserved", () => {
const view = mapCatalogRoleToView(catalogRole(), [], "en");
expect(view).toMatchObject({
slug: "writer",
emoji: "✍️",
name: "Writer",
description: "Drafts copy.",
status: "import",
version: 3,
});
expect(view.installedRoleId).toBeUndefined();
expect(view.installedLang).toBeUndefined();
});
it("import with the slug installed in another language -> installedLang set", () => {
const roles = [installedRole({ slug: "writer", language: "ru", version: 9 })];
const view = mapCatalogRoleToView(catalogRole(), roles, "en");
expect(view.status).toBe("import");
expect(view.installedLang).toBe("ru");
});
it("installed (up to date) -> installed status, catalog version, installedRoleId", () => {
const installed = installedRole({
slug: "writer",
language: "en",
version: 3,
});
const view = mapCatalogRoleToView(catalogRole(), [installed], "en");
expect(view).toMatchObject({
status: "installed",
version: 3,
installedRoleId: installed.id,
});
});
it("update -> version=from, newVersion=to, installedRoleId", () => {
const installed = installedRole({
slug: "writer",
language: "en",
version: 1,
});
const view = mapCatalogRoleToView(catalogRole(), [installed], "en");
expect(view).toMatchObject({
status: "update",
version: 1,
newVersion: 3,
installedRoleId: installed.id,
});
});
it("missing emoji -> emoji undefined; null description -> empty string", () => {
const view = mapCatalogRoleToView(
catalogRole({ emoji: null, description: null }),
[],
"en",
);
expect(view.emoji).toBeUndefined();
expect(view.description).toBe("");
});
});
describe("mapBundleRolesToView", () => {
it("maps a bundle's roles preserving order", () => {
const roles = [
catalogRole({ slug: "a", name: "A", version: 1 }),
catalogRole({ slug: "b", name: "B", version: 1 }),
];
const installed = [installedRole({ slug: "a", language: "en", version: 1 })];
const view = mapBundleRolesToView(roles, installed, "en");
expect(view.map((r) => r.slug)).toEqual(["a", "b"]);
expect(view[0].status).toBe("installed");
expect(view[1].status).toBe("import");
});
});