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>
50 lines
1.6 KiB
TypeScript
50 lines
1.6 KiB
TypeScript
import type {
|
|
IAiRole,
|
|
IAiRoleCatalogRole,
|
|
} from "@/features/ai-chat/types/ai-chat.types.ts";
|
|
|
|
/**
|
|
* The install state of a single catalog role relative to the workspace's
|
|
* existing roles. Extracted as a pure function so the catalog modal's role-state
|
|
* computation is unit-testable without mounting the component (mirrors the
|
|
* `roleLaunchMessage` precedent in role-launch.ts).
|
|
*
|
|
* A catalog role is matched to an installed role by BOTH `source.slug` and
|
|
* `source.language`: the same slug in a different language is a separate install
|
|
* (so it shows as "import", not "installed"). When matched, the installed source
|
|
* version decides the state:
|
|
* - no match -> "import"
|
|
* - matched & installed version >= catalog version -> "installed"
|
|
* - matched & installed version < catalog version -> "update" (from -> to)
|
|
*/
|
|
export type CatalogRoleInstallState =
|
|
| { state: "import" }
|
|
| { state: "installed"; installed: IAiRole }
|
|
| {
|
|
state: "update";
|
|
installed: IAiRole;
|
|
fromVersion: number;
|
|
toVersion: number;
|
|
};
|
|
|
|
export function catalogRoleInstallState(
|
|
role: Pick<IAiRoleCatalogRole, "slug" | "version">,
|
|
workspaceRoles: IAiRole[],
|
|
language: string,
|
|
): CatalogRoleInstallState {
|
|
const installed = workspaceRoles.find(
|
|
(r) => r.source?.slug === role.slug && r.source?.language === language,
|
|
);
|
|
if (!installed) return { state: "import" };
|
|
const fromVersion = installed.source?.version ?? 0;
|
|
if (fromVersion >= role.version) {
|
|
return { state: "installed", installed };
|
|
}
|
|
return {
|
|
state: "update",
|
|
installed,
|
|
fromVersion,
|
|
toVersion: role.version,
|
|
};
|
|
}
|