MUST-FIX - isSourceUniqueViolation read the wrong error field: kysely-postgres-js (postgres@3.4.8) puts the violated constraint on `constraint_name`, not node-postgres' `.constraint`, so a concurrent same-slug+language import's 23505 was never recognized as a source-collision and surfaced a false "name already exists" error. Now read `constraint_name` (with `.constraint` as a fallback for other drivers). Fix the faked test fixture (it built the error with the same wrong `.constraint` field, masking the bug): it now uses `constraint_name`, so the test genuinely exercises the skip path and FAILS against the unfixed code. - Extract the catalog modal's role-state computation into a pure `catalogRoleInstallState(role, workspaceRoles, language)` helper (mirrors role-launch.ts) and cover it with vitest: import / installed / update / same-slug-different-language. SUGGESTIONS - Restore IAiRoleUpdateFromCatalogResult as a discriminated union mirroring the server; narrow the consumer via `"reason" in result` (the boolean discriminant does not narrow under strictNullChecks:false). - README: add a "How it's served" section documenting AI_AGENT_ROLES_CATALOG_URL (remote http(s) base / local path / empty => in-repo folder). - check.mjs: drop the redundant `const key = slug` alias. - Cover the reason->message mapping in useUpdateAiRoleFromCatalogMutation (4 branches) via renderHook with a mocked service. - Cover importFromCatalog "bundle not in index" => BadGateway. - Cover updateFromCatalog "slug in index but missing in bundle file" => not-in-catalog. ARCHITECTURE - Extract the shared catalog read prefix: a private `loadBundleById` (fetchIndex -> meta -> fetchBundle -> versionMap) reused by getCatalogBundle and importFromCatalog, and a `catalogRoleContentFields` mapper shared by the import insert and update patch. The three orchestrations and their distinct write paths stay separate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
3.4 KiB
TypeScript
108 lines
3.4 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { catalogRoleInstallState } from "./catalog-role-install-state.ts";
|
|
import type { IAiRole } from "@/features/ai-chat/types/ai-chat.types.ts";
|
|
|
|
// Build a workspace role with a catalog source. Fields irrelevant to the
|
|
// install-state decision are filled with harmless defaults.
|
|
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,
|
|
};
|
|
}
|
|
|
|
const catalogRole = { slug: "writer", version: 3 };
|
|
|
|
// Mirrors the role-launch.ts precedent: the modal's role-state computation is a
|
|
// pure function so the import/installed/update decision is testable directly.
|
|
describe("catalogRoleInstallState", () => {
|
|
it("no matching installed role -> import", () => {
|
|
const result = catalogRoleInstallState(catalogRole, [], "en");
|
|
expect(result).toEqual({ state: "import" });
|
|
});
|
|
|
|
it("same slug + language, installed version > catalog -> installed", () => {
|
|
const installed = installedRole({
|
|
slug: "writer",
|
|
language: "en",
|
|
version: 5,
|
|
});
|
|
const result = catalogRoleInstallState(catalogRole, [installed], "en");
|
|
expect(result).toEqual({ state: "installed", installed });
|
|
});
|
|
|
|
it("same slug + language, installed version == catalog -> installed", () => {
|
|
const installed = installedRole({
|
|
slug: "writer",
|
|
language: "en",
|
|
version: 3,
|
|
});
|
|
const result = catalogRoleInstallState(catalogRole, [installed], "en");
|
|
expect(result).toEqual({ state: "installed", installed });
|
|
});
|
|
|
|
it("same slug + language, installed version < catalog -> update (from/to)", () => {
|
|
const installed = installedRole({
|
|
slug: "writer",
|
|
language: "en",
|
|
version: 1,
|
|
});
|
|
const result = catalogRoleInstallState(catalogRole, [installed], "en");
|
|
expect(result).toEqual({
|
|
state: "update",
|
|
installed,
|
|
fromVersion: 1,
|
|
toVersion: 3,
|
|
});
|
|
});
|
|
|
|
it("same slug but DIFFERENT language -> import (a separate install)", () => {
|
|
// 'writer' is installed in 'ru'; browsing the 'en' catalog must offer it as a
|
|
// fresh import, not treat the ru copy as already installed.
|
|
const installed = installedRole({
|
|
slug: "writer",
|
|
language: "ru",
|
|
version: 5,
|
|
});
|
|
const result = catalogRoleInstallState(catalogRole, [installed], "en");
|
|
expect(result).toEqual({ state: "import" });
|
|
});
|
|
|
|
it("matches the right language when the same slug is installed in several", () => {
|
|
const ru = installedRole(
|
|
{ slug: "writer", language: "ru", version: 5 },
|
|
{ id: "ru-role" },
|
|
);
|
|
const en = installedRole(
|
|
{ slug: "writer", language: "en", version: 1 },
|
|
{ id: "en-role" },
|
|
);
|
|
const result = catalogRoleInstallState(catalogRole, [ru, en], "en");
|
|
expect(result).toEqual({
|
|
state: "update",
|
|
installed: en,
|
|
fromVersion: 1,
|
|
toVersion: 3,
|
|
});
|
|
});
|
|
|
|
it("ignores manually-created roles (no source) sharing the name", () => {
|
|
const manual = installedRole(
|
|
{ slug: "writer", language: "en", version: 9 },
|
|
{ source: null },
|
|
);
|
|
const result = catalogRoleInstallState(catalogRole, [manual], "en");
|
|
expect(result).toEqual({ state: "import" });
|
|
});
|
|
});
|