fix(web): stop the remaining consumers calling a landed write a failure

Copilot review r3. Three findings, all the same root: a
ServerListReloadError rejects after the write succeeded, so any consumer
still reading "rejection" as "the write failed" now says something false.

- `useImportClientConfig.applyEntry` gave a successfully imported server a
  red "Failed" badge. It now reports the intended status and puts the
  reload message in `detail`, which renders in red beneath it either way —
  so the failure stays visible without claiming the row wasn't created.
- The settings-draft toast said "Failed to save settings". It now branches
  its title; this toast is the only signal there, since the modal has
  usually closed by the time a flush rejects.
- An edit modal left open by a failed reload kept targeting the old id.
  When the row landed under the new one, `configModalTarget` went
  undefined and ServerConfigModal's own reset blanked the open form and
  cleared the error it was showing. An edit/clone modal whose target has
  left the list now closes — adjusted during render via `useValueChange`,
  so no frame paints the blanked form. That also covers the ordinary case
  of an external mcp.json edit removing the server being edited.

The reachability here is not remote: the write is what triggers the
backend's change broadcast, so the SSE-driven refresh that lands the
renamed row is the likely next event.

Also adds the regression test r3 asked for on the add path — the
highlight batch has to be marked from the catch for the same reason the
rename is. Both new App tests and the import one fail without their fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-23 10:14:22 -04:00
commit b49d4f20f5
2 changed files with 134 additions and 22 deletions

View file

@ -6,6 +6,7 @@ import {
} from "./useImportClientConfig";
import type { ImportSourceResult } from "@inspector/core/mcp/import/index.js";
import type { MCPConfig } from "@inspector/core/mcp/types.js";
import { ServerListReloadError } from "@inspector/core/react/useServers.js";
function config(...ids: string[]): MCPConfig {
const mcpServers: MCPConfig["mcpServers"] = {};
@ -207,6 +208,31 @@ describe("useImportClientConfig", () => {
expect(outcome.detail).toBe("add failed");
});
it("reports a reload failure as imported-with-a-warning, not failed (#1914)", async () => {
// A `ServerListReloadError` rejects *after* the write landed, so the entry
// really was added. A red "Failed" badge would contradict the error's own
// message and invite a retry that then collides with the row it claims
// wasn't created. The reason still shows, as `detail`.
const onAddServer = vi.fn(async () => {
throw new ServerListReloadError(
"The server was added, but the server list could not be reloaded: disk full",
);
});
const { result } = setup({
onFetchSource: vi.fn(async () => foundSource(config("alpha"))),
onAddServer,
});
await act(async () => {
await result.current.pickSource("claude");
});
await act(async () => {
await result.current.runImport();
});
const outcome = result.current.outcomes[0];
expect(outcome.status).toBe("added");
expect(outcome.detail).toMatch(/could not be reloaded: disk full/);
});
it("resolves conflicts by overwrite, skip, and rename", async () => {
const onAddServer = vi.fn(async () => {});
const onUpdateServer = vi.fn(async () => {});

View file

@ -8,6 +8,7 @@ import {
type ImportSourceResult,
} from "@inspector/core/mcp/import/index.js";
import { validateStoreId } from "@inspector/core/storage/store-id.js";
import { ServerListReloadError } from "@inspector/core/react/useServers.js";
import type { MCPConfig, MCPServerConfig } from "@inspector/core/mcp/types.js";
export type ImportPhase = "select" | "loading" | "review" | "summary";
@ -198,11 +199,17 @@ export function useImportClientConfig({
await write();
return { id, status: outcome };
} catch (err) {
return {
id,
status: "failed",
detail: err instanceof Error ? err.message : String(err),
};
const detail = err instanceof Error ? err.message : String(err);
// A `ServerListReloadError` means the write landed and only reading the
// list back failed, so this entry really was imported. Reporting it as
// `failed` would contradict the error's own message and invite a retry
// that then collides with the row it says wasn't created (#1914 r3).
// The status is the truth about the write; `detail` renders in red
// beneath it either way, so the reload failure is still visible.
if (err instanceof ServerListReloadError) {
return { id, status: outcome, detail };
}
return { id, status: "failed", detail };
}
}