Address PR #222 re-review: fix source-uniqueness detection + coverage/cleanups

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>
This commit is contained in:
claude code agent 227
2026-06-27 01:01:29 +03:00
parent 26ca19f89e
commit 2b7c861f78
10 changed files with 422 additions and 73 deletions

View File

@@ -27,6 +27,7 @@ import {
IAiRoleCatalogBundleSummary,
IAiRoleCatalogRole,
} from "@/features/ai-chat/types/ai-chat.types.ts";
import { catalogRoleInstallState } from "@/features/ai-chat/utils/catalog-role-install-state.ts";
interface AiAgentRolesCatalogModalProps {
opened: boolean;
@@ -230,19 +231,14 @@ function BundlePanel({
const updateMutation = useUpdateAiRoleFromCatalogMutation();
// Compute each catalog role's install state against the current workspace
// roles: an importable role matched by source.slug + source.language.
// roles (matched by source.slug + source.language). The decision lives in the
// pure `catalogRoleInstallState` helper so it is unit-tested directly.
const computed = useMemo(() => {
const list = bundleQuery.data?.roles ?? [];
return list.map((role) => {
const installed = roles.find(
(r) => r.source?.slug === role.slug && r.source?.language === language,
);
if (!installed) return { role, state: "import" as const };
if ((installed.source?.version ?? 0) >= role.version) {
return { role, state: "installed" as const, installed };
}
return { role, state: "update" as const, installed };
});
return list.map((role) => ({
role,
...catalogRoleInstallState(role, roles, language),
}));
}, [bundleQuery.data, roles, language]);
// Default-check every importable role once the bundle content arrives (unless
@@ -300,17 +296,23 @@ function BundlePanel({
{bundleQuery.data && (
<Stack gap="xs">
{computed.map(({ role, state, installed }) => (
{computed.map((entry) => (
<CatalogRoleRow
key={role.slug}
role={role}
state={state}
checked={state === "import" ? !!selected?.has(role.slug) : false}
onToggle={(checked) => onToggleSlug(role.slug, checked)}
fromVersion={installed?.source?.version}
key={entry.role.slug}
role={entry.role}
state={entry.state}
checked={
entry.state === "import"
? !!selected?.has(entry.role.slug)
: false
}
onToggle={(checked) => onToggleSlug(entry.role.slug, checked)}
fromVersion={
entry.state === "update" ? entry.fromVersion : undefined
}
onUpdate={
state === "update" && installed
? () => updateMutation.mutate(installed.id)
entry.state === "update"
? () => updateMutation.mutate(entry.installed.id)
: undefined
}
updating={updateMutation.isPending}