fix(mcp): keep manual database scope entries saved and visible

This commit is contained in:
Conastin authored and GitHub committed 2026-09-11 17:25:19 +08:00
1 parent 84b02d6960
commit 1ceeaad8a4
2 files changed
+74 -3

No files matched your search

@@ -52,7 +52,9 @@ const selectedDatabases = computed(() => selectedPolicy.value?.allowedDatabases
const selectedDatabasePolicies = computed(() => selectedPolicy.value?.databasePolicies ?? []);
const displayedDatabases = computed(() => {
const query = search.value.trim().toLocaleLowerCase();
return (databasesByConnection.value[selectedConnectionId.value] ?? []).filter((database) => !query || database.toLocaleLowerCase().includes(query));
// 加载缓存是会话级的:把已持久化的库名并入列表,重开对话框后授权项仍完整可见。
const loaded = databasesByConnection.value[selectedConnectionId.value] ?? [];
return [...new Set([...selectedDatabases.value, ...loaded])].sort((left, right) => left.localeCompare(right)).filter((database) => !query || database.toLocaleLowerCase().includes(query));
});
const connectionPageCount = computed(() => Math.max(1, Math.ceil(scopedConnections.value.length / pageSize.value)));
const databasePageCount = computed(() => Math.max(1, Math.ceil(displayedDatabases.value.length / pageSize.value)));
@@ -151,6 +153,8 @@ function databasePolicyMode(database: string): ExecutionMode | "inherit" {
}
function addManualDatabase() {
// busy 期间 updateSelectedPolicy 会被丢弃;提前拦截,避免库名进了本地列表却没进策略。
if (props.disabled || props.busy) return;
const database = manualDatabase.value.trim();
if (!database) return;
const loaded = databasesByConnection.value[selectedConnectionId.value] ?? [];
@@ -322,8 +326,8 @@ watch(databasePageCount, () => setDatabasePage(databasePage.value));
<p class="text-xs text-muted-foreground">{{ t("settings.mcpDatabaseScopeSelectedSummary", { count: selectedDatabases.length }) }}</p>
<div class="flex gap-2">
<div class="relative min-w-0 flex-1"><Search class="absolute left-2.5 top-2.5 h-3.5 w-3.5 text-muted-foreground" /><Input v-model="search" class="h-8 pl-8 text-xs" :placeholder="t('settings.mcpDatabaseSearchPlaceholder')" /></div>
<Input v-model="manualDatabase" class="h-8 w-40 text-xs" :placeholder="t('settings.mcpDatabaseManualPlaceholder')" @keydown.enter.prevent="addManualDatabase" />
<Button type="button" variant="outline" size="sm" :disabled="disabled || !manualDatabase.trim()" @click="addManualDatabase"><Plus class="h-3.5 w-3.5" /></Button>
<Input v-model="manualDatabase" class="h-8 w-40 text-xs" :placeholder="t('settings.mcpDatabaseManualPlaceholder')" :disabled="disabled || busy" @keydown.enter.prevent="addManualDatabase" />
<Button type="button" variant="outline" size="sm" :disabled="disabled || busy || !manualDatabase.trim()" @click="addManualDatabase"><Plus class="h-3.5 w-3.5" /></Button>
</div>
<p v-if="loadError" class="rounded border border-destructive/30 bg-destructive/5 px-2 py-1.5 text-xs text-destructive">{{ t("settings.mcpDatabaseLoadFailed", { error: loadError }) }}</p>
<div v-if="displayedDatabases.length" class="rounded border">
@@ -1,6 +1,9 @@
// @vitest-environment happy-dom
// MCP 数据库范围选择器回归测试:
// 加载资源列表前先 ensureConnected(修复未连接的 SQL 连接报 Connection not found)。
// 1) 正常流程:选择“仅指定数据库”→ 手工添加库名 → emit 携带 allowedDatabases。
// 2) busy/disabled 期间手工添加被整体拦截,库名不再进入本地列表(修复静默丢失)。
// 3) 重开对话框后已持久化的 allowedDatabases 重新显示并勾选(修复不回显)。
import { createApp, nextTick, type App } from "vue";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -182,4 +185,68 @@ describe("McpDatabaseScopePicker", () => {
expect(second.root.textContent).toContain("ORA-12170");
expect(mocks.listDatabases).not.toHaveBeenCalled();
});
it("正常流程:选择 selected 后手工添加库名应 emit 携带 allowedDatabases", async () => {
// 阶段一:初始无策略,点击“仅指定数据库”
const first = mountHarness([]);
await nextTick();
clickScopeRadio(first.root, "selected");
await nextTick();
expect(first.emitted.policies.length).toBe(1);
expect(first.emitted.policies[0][0].databaseScope).toBe("selected");
first.app.unmount();
harness = null;
// 阶段二:模拟父组件 store 已回写 scope=selected,再手工添加库名
const second = mountHarness([emptyPolicy("oracle-1", "selected")]);
await nextTick();
await addManualDatabase(second.root, "mydb");
expect(second.emitted.policies.length).toBe(1);
const policy = second.emitted.policies[0][0];
expect(policy.databaseScope).toBe("selected");
expect(policy.allowedDatabases).toEqual(["mydb"]);
});
it("竞态场景:disabled 期间手工添加被整体拦截,库名不再进入本地列表", async () => {
const { emitted, root } = mountHarness([emptyPolicy("oracle-1", "selected")], true);
await nextTick();
await addManualDatabase(root, "mydb");
expect(emitted.policies.length).toBe(0);
expect(root.textContent).not.toContain("mydb");
// 手工输入框本身也应被禁用
expect(manualInput(root).hasAttribute("disabled")).toBe(true);
});
it("重开对话框场景:已持久化的 allowedDatabases 重新显示在列表中并处于勾选状态", async () => {
// 模拟:策略已保存 scope=selected + allowedDatabases=["SAVEDDB"],加载缓存为空(新会话)
const policy: McpConnectionPolicy = { ...emptyPolicy("oracle-1", "selected"), allowedDatabases: ["SAVEDDB"] };
const { root } = mountHarness([policy]);
await nextTick();
expect(root.textContent).toContain("SAVEDDB");
expect(root.textContent).not.toContain("settings.mcpDatabaseEmptyHint");
const checkbox = Array.from(root.querySelectorAll('input[type="checkbox"]')).find((i) => i.value === "SAVEDDB" || i.closest("label")?.textContent?.includes("SAVEDDB"));
expect(checkbox?.checked).toBe(true);
});
it("加载列表与已持久化库名取并集:手工添加过但服务器上不存在的库名仍可见", async () => {
mocks.listDatabases.mockResolvedValue([{ name: "SERVER_DB" }]);
const first = mountHarness([]);
await nextTick();
clickScopeRadio(first.root, "selected");
await nextTick();
first.app.unmount();
harness = null;
const second = mountHarness([{ ...emptyPolicy("oracle-1", "selected"), allowedDatabases: ["MANUAL_DB"] }]);
await nextTick();
loadButton(second.root).click();
await new Promise((r) => setTimeout(r, 10));
await nextTick();
expect(second.root.textContent).toContain("SERVER_DB");
expect(second.root.textContent).toContain("MANUAL_DB");
});
});