fix(mcp): keep manual database scope entries saved and visible
This commit is contained in:
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">
|
||||
|
||||
+67
@@ -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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user