@agent-native/toolkit 0.227.0 → 0.228.0-nightly-20261111004142
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent-native.eject.json +8 -0
- package/dist/app/agent-page/AgentTabFrame.d.ts +2 -1
- package/dist/app/agent-page/AgentTabFrame.d.ts.map +1 -1
- package/dist/app/agent-page/AgentTabFrame.js +3 -2
- package/dist/app/agent-page/AgentTabFrame.js.map +1 -1
- package/dist/app/chat/AgentPanel.d.ts +15 -15
- package/dist/app/chat/AgentPanel.d.ts.map +1 -1
- package/dist/app/chat/AgentPanel.js.map +1 -1
- package/dist/app/chat/AgentSidebar.d.ts +7 -1
- package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
- package/dist/app/chat/AgentSidebar.js +47 -16
- package/dist/app/chat/AgentSidebar.js.map +1 -1
- package/dist/app/chat/chat/surface-types.d.ts +26 -26
- package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
- package/dist/app/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/app/extensions/EmbeddedExtension.js +6 -4
- package/dist/app/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/app/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/app/extensions/ExtensionViewer.js +6 -3
- package/dist/app/extensions/ExtensionViewer.js.map +1 -1
- package/dist/app/extensions/InlineExtensionFrame.d.ts.map +1 -1
- package/dist/app/extensions/InlineExtensionFrame.js +32 -28
- package/dist/app/extensions/InlineExtensionFrame.js.map +1 -1
- package/dist/app/extensions/use-extension-chat-submission.d.ts +14 -0
- package/dist/app/extensions/use-extension-chat-submission.d.ts.map +1 -0
- package/dist/app/extensions/use-extension-chat-submission.js +36 -0
- package/dist/app/extensions/use-extension-chat-submission.js.map +1 -0
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +16 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +12 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +12 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +12 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +13 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +13 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +12 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +12 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +12 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +13 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +12 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +12 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/app/org/OrgSwitcher.js +3 -2
- package/dist/app/org/OrgSwitcher.js.map +1 -1
- package/dist/app/resources/McpAccessSettings.d.ts +9 -2
- package/dist/app/resources/McpAccessSettings.d.ts.map +1 -1
- package/dist/app/resources/McpAccessSettings.js +264 -86
- package/dist/app/resources/McpAccessSettings.js.map +1 -1
- package/dist/app/settings/api-keys/ApiKeyDialogs.d.ts +6 -0
- package/dist/app/settings/api-keys/ApiKeyDialogs.d.ts.map +1 -1
- package/dist/app/settings/api-keys/ApiKeyDialogs.js +74 -2
- package/dist/app/settings/api-keys/ApiKeyDialogs.js.map +1 -1
- package/dist/app/settings/api-keys/ApiKeysSettingsPage.d.ts.map +1 -1
- package/dist/app/settings/api-keys/ApiKeysSettingsPage.js +23 -9
- package/dist/app/settings/api-keys/ApiKeysSettingsPage.js.map +1 -1
- package/dist/app/settings/api-keys/api-keys-client.d.ts +10 -1
- package/dist/app/settings/api-keys/api-keys-client.d.ts.map +1 -1
- package/dist/app/settings/api-keys/api-keys-client.js +19 -1
- package/dist/app/settings/api-keys/api-keys-client.js.map +1 -1
- package/dist/app/settings/shell/pages/mcp.d.ts +1 -1
- package/dist/app/settings/shell/pages/mcp.d.ts.map +1 -1
- package/dist/app/settings/shell/pages/mcp.js +26 -20
- package/dist/app/settings/shell/pages/mcp.js.map +1 -1
- package/dist/app/sharing/ShareButton.d.ts +2 -0
- package/dist/app/sharing/ShareButton.d.ts.map +1 -1
- package/dist/app/sharing/ShareButton.js +4 -3
- package/dist/app/sharing/ShareButton.js.map +1 -1
- package/dist/composer/TiptapComposer.js +3 -0
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/sharing/ShareControls.d.ts +2 -0
- package/dist/sharing/ShareControls.d.ts.map +1 -1
- package/dist/sharing/ShareControls.js +7 -2
- package/dist/sharing/ShareControls.js.map +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/numbered-steps.d.ts +14 -0
- package/dist/ui/numbered-steps.d.ts.map +1 -0
- package/dist/ui/numbered-steps.js +19 -0
- package/dist/ui/numbered-steps.js.map +1 -0
- package/package.json +4 -4
- package/src/app/agent-page/AgentTabFrame.tsx +21 -10
- package/src/app/chat/AgentPanel.header.spec.ts +4 -1
- package/src/app/chat/AgentPanel.tsx +19 -14
- package/src/app/chat/AgentSidebar.spec.tsx +167 -0
- package/src/app/chat/AgentSidebar.tsx +170 -101
- package/src/app/chat/chat/surface-types.ts +35 -31
- package/src/app/extensions/EmbeddedExtension.height.spec.tsx +45 -0
- package/src/app/extensions/EmbeddedExtension.tsx +6 -3
- package/src/app/extensions/ExtensionViewer.spec.tsx +28 -0
- package/src/app/extensions/ExtensionViewer.tsx +6 -2
- package/src/app/extensions/InlineExtensionFrame.spec.tsx +23 -0
- package/src/app/extensions/InlineExtensionFrame.tsx +23 -15
- package/src/app/extensions/use-extension-chat-submission.tsx +101 -0
- package/src/app/i18n/catalogs/ar-SA.ts +17 -0
- package/src/app/i18n/catalogs/de-DE.ts +15 -0
- package/src/app/i18n/catalogs/en-US.ts +13 -0
- package/src/app/i18n/catalogs/es-ES.ts +16 -0
- package/src/app/i18n/catalogs/fr-FR.ts +16 -0
- package/src/app/i18n/catalogs/hi-IN.ts +13 -0
- package/src/app/i18n/catalogs/ja-JP.ts +15 -0
- package/src/app/i18n/catalogs/ko-KR.ts +14 -0
- package/src/app/i18n/catalogs/pt-BR.ts +15 -0
- package/src/app/i18n/catalogs/zh-CN.ts +13 -0
- package/src/app/i18n/catalogs/zh-TW.ts +13 -0
- package/src/app/integrations/IntegrationsPanel.spec.tsx +1 -1
- package/src/app/org/OrgSwitcher.spec.tsx +43 -0
- package/src/app/org/OrgSwitcher.tsx +29 -24
- package/src/app/resources/McpAccessSettings.spec.tsx +354 -278
- package/src/app/resources/McpAccessSettings.tsx +1012 -323
- package/src/app/settings/api-keys/ApiKeyDialogs.tsx +148 -1
- package/src/app/settings/api-keys/ApiKeysSettingsPage.spec.tsx +190 -0
- package/src/app/settings/api-keys/ApiKeysSettingsPage.tsx +40 -1
- package/src/app/settings/api-keys/api-keys-client.spec.ts +49 -0
- package/src/app/settings/api-keys/api-keys-client.ts +32 -0
- package/src/app/settings/shell/pages/app-group-pages.spec.tsx +31 -21
- package/src/app/settings/shell/pages/mcp.tsx +50 -42
- package/src/app/sharing/ShareButton.tsx +10 -3
- package/src/composer/TiptapComposer.tsx +2 -0
- package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +16 -0
- package/src/sharing/ShareControls.tsx +44 -24
- package/src/ui/index.ts +1 -0
- package/src/ui/numbered-steps.tsx +104 -0
|
@@ -1,12 +1,41 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
|
-
|
|
2
|
+
// oracle-change: MCP setup now opens client detail pages from a list instead of switching tabs.
|
|
3
3
|
import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
|
|
4
|
+
import { queryByRole } from "@testing-library/react";
|
|
4
5
|
import React, { act } from "react";
|
|
5
6
|
import { createRoot, type Root } from "react-dom/client";
|
|
6
7
|
import { renderToString } from "react-dom/server";
|
|
7
8
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
9
|
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
McpAccessSettings,
|
|
12
|
+
type McpAccessSettingsProps,
|
|
13
|
+
} from "./McpAccessSettings.js";
|
|
14
|
+
|
|
15
|
+
const statusQuery = vi.hoisted(() => ({
|
|
16
|
+
data: {
|
|
17
|
+
connections: [
|
|
18
|
+
"claude",
|
|
19
|
+
"chatgpt",
|
|
20
|
+
"cursor",
|
|
21
|
+
"vscode",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"codex",
|
|
24
|
+
"grok",
|
|
25
|
+
"other",
|
|
26
|
+
].map((client) => ({
|
|
27
|
+
client,
|
|
28
|
+
status: "disconnected",
|
|
29
|
+
lastUsedAt: null as number | null,
|
|
30
|
+
})),
|
|
31
|
+
},
|
|
32
|
+
isPending: false,
|
|
33
|
+
isError: false,
|
|
34
|
+
refetch: vi.fn(),
|
|
35
|
+
}));
|
|
36
|
+
vi.mock("@agent-native/core/client/use-action", () => ({
|
|
37
|
+
useActionQuery: () => statusQuery,
|
|
38
|
+
}));
|
|
10
39
|
|
|
11
40
|
const IDENTITY = {
|
|
12
41
|
serverName: "beta-agent-native-content",
|
|
@@ -16,354 +45,401 @@ const IDENTITY = {
|
|
|
16
45
|
environment: "beta",
|
|
17
46
|
connect: true,
|
|
18
47
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
: { ok: false, status: 500, json: async () => ({}) };
|
|
26
|
-
}
|
|
27
|
-
return { ok: false, status: 404 };
|
|
28
|
-
});
|
|
48
|
+
function stubFetch(body: unknown = IDENTITY, ok = true) {
|
|
49
|
+
const fetchMock = vi.fn(async (input: RequestInfo | URL) =>
|
|
50
|
+
String(input).endsWith("/mcp/connect/identity")
|
|
51
|
+
? { ok, status: ok ? 200 : 500, json: async () => body }
|
|
52
|
+
: { ok: false, status: 404 },
|
|
53
|
+
);
|
|
29
54
|
vi.stubGlobal("fetch", fetchMock);
|
|
30
55
|
return fetchMock;
|
|
31
56
|
}
|
|
32
57
|
|
|
33
|
-
|
|
34
|
-
await vi.waitFor(() =>
|
|
35
|
-
expect(container.querySelector("#mcp-guide-tab-claude")).not.toBeNull(),
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Radix tabs activate on mousedown, not click.
|
|
40
|
-
function selectTab(tab: HTMLButtonElement | null) {
|
|
41
|
-
tab?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, button: 0 }));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describe("McpAccessSettings localization", () => {
|
|
58
|
+
describe("MCP client setup", () => {
|
|
45
59
|
let container: HTMLDivElement;
|
|
46
60
|
let root: Root;
|
|
47
|
-
|
|
48
61
|
beforeEach(() => {
|
|
49
62
|
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
50
|
-
stubFetch(
|
|
63
|
+
stubFetch();
|
|
64
|
+
statusQuery.isError = false;
|
|
65
|
+
statusQuery.isPending = false;
|
|
66
|
+
statusQuery.refetch.mockClear();
|
|
67
|
+
statusQuery.data.connections.forEach((row) => {
|
|
68
|
+
row.status = "disconnected";
|
|
69
|
+
row.lastUsedAt = null;
|
|
70
|
+
});
|
|
51
71
|
window.history.replaceState({}, "", "/settings/mcp");
|
|
52
72
|
container = document.createElement("div");
|
|
53
73
|
document.body.appendChild(container);
|
|
54
74
|
root = createRoot(container);
|
|
55
75
|
});
|
|
56
|
-
|
|
57
76
|
afterEach(() => {
|
|
58
77
|
act(() => root.unmount());
|
|
59
78
|
container.remove();
|
|
79
|
+
vi.restoreAllMocks();
|
|
60
80
|
vi.unstubAllGlobals();
|
|
61
81
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
await act(async () => {
|
|
82
|
+
async function render(props: McpAccessSettingsProps = {}, locale = "en-US") {
|
|
83
|
+
await act(async () =>
|
|
65
84
|
root.render(
|
|
66
85
|
<AgentNativeI18nProvider
|
|
67
|
-
initialLocale=
|
|
68
|
-
initialPreference=
|
|
86
|
+
initialLocale={locale}
|
|
87
|
+
initialPreference={locale}
|
|
69
88
|
persistPreference={false}
|
|
70
89
|
>
|
|
71
|
-
<McpAccessSettings
|
|
90
|
+
<McpAccessSettings hideHeader {...props} />
|
|
72
91
|
</AgentNativeI18nProvider>,
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
await vi.waitFor(() =>
|
|
76
|
-
expect(container.textContent).toContain(
|
|
77
|
-
"Conecta esta app con Claude, ChatGPT, Cursor, Codex u otro host MCP.",
|
|
78
92
|
),
|
|
79
93
|
);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"Conecta esta app con Claude, ChatGPT, Cursor, Codex u otro host MCP.",
|
|
94
|
+
await vi.waitFor(() =>
|
|
95
|
+
expect(container.querySelector("[aria-busy=true]")).toBeNull(),
|
|
83
96
|
);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
97
|
+
}
|
|
98
|
+
function link(name: string) {
|
|
99
|
+
return (
|
|
100
|
+
queryByRole<HTMLAnchorElement>(container, "link", {
|
|
101
|
+
name,
|
|
102
|
+
exact: true,
|
|
103
|
+
}) ?? undefined
|
|
91
104
|
);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
}
|
|
106
|
+
async function open(name: string) {
|
|
107
|
+
const target = link(name);
|
|
108
|
+
expect(target).toBeDefined();
|
|
109
|
+
await act(async () => target?.click());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
it("shows server-backed connection status and activity", async () => {
|
|
113
|
+
const row = statusQuery.data.connections.find(
|
|
114
|
+
(row) => row.client === "codex",
|
|
97
115
|
);
|
|
98
|
-
|
|
116
|
+
if (!row) throw new Error("Missing Codex fixture");
|
|
117
|
+
row.status = "connected";
|
|
118
|
+
row.lastUsedAt = 1_800_000_000_000;
|
|
119
|
+
await render({ guideId: "codex" });
|
|
120
|
+
expect(container.textContent).toContain("Connected");
|
|
121
|
+
expect(container.textContent).toContain("Last used");
|
|
122
|
+
expect(container.textContent).not.toContain("No activity recorded");
|
|
123
|
+
expect(
|
|
124
|
+
queryByRole<HTMLButtonElement>(container, "button", {
|
|
125
|
+
name: "Copy prompt",
|
|
126
|
+
exact: true,
|
|
127
|
+
})?.disabled,
|
|
128
|
+
).toBe(false);
|
|
99
129
|
});
|
|
100
130
|
|
|
101
|
-
it("
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await act(async () => {
|
|
108
|
-
root.render(
|
|
109
|
-
<AgentNativeI18nProvider
|
|
110
|
-
initialLocale="en-US"
|
|
111
|
-
initialPreference="en-US"
|
|
112
|
-
persistPreference={false}
|
|
113
|
-
>
|
|
114
|
-
<McpAccessSettings />
|
|
115
|
-
</AgentNativeI18nProvider>,
|
|
116
|
-
);
|
|
131
|
+
it("shows connection status directly in the client list", async () => {
|
|
132
|
+
statusQuery.data.connections[0].status = "connected";
|
|
133
|
+
statusQuery.data.connections[0].lastUsedAt = 1_800_000_000_000;
|
|
134
|
+
await render();
|
|
135
|
+
const claude = queryByRole<HTMLAnchorElement>(container, "link", {
|
|
136
|
+
name: /^Claude Last used/,
|
|
117
137
|
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const claudeTab = container.querySelector<HTMLButtonElement>(
|
|
122
|
-
"#mcp-guide-tab-claude",
|
|
123
|
-
);
|
|
124
|
-
expect(claudeTab).not.toBeNull();
|
|
125
|
-
await act(async () => selectTab(claudeTab));
|
|
126
|
-
expect(container.textContent).toContain("name it Mail");
|
|
127
|
-
} finally {
|
|
128
|
-
meta.remove();
|
|
129
|
-
}
|
|
138
|
+
expect(claude?.textContent).toContain("Connected");
|
|
139
|
+
expect(claude?.textContent).toContain("Last used");
|
|
140
|
+
expect(link("ChatGPT")).toBeDefined();
|
|
130
141
|
});
|
|
131
142
|
|
|
132
|
-
it("
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
await
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
persistPreference={false}
|
|
141
|
-
>
|
|
142
|
-
<McpAccessSettings appName="Content" />
|
|
143
|
-
</AgentNativeI18nProvider>,
|
|
144
|
-
);
|
|
143
|
+
it("reports a failed check instead of showing stale Connected metadata", async () => {
|
|
144
|
+
statusQuery.data.connections[0].status = "connected";
|
|
145
|
+
statusQuery.isError = true;
|
|
146
|
+
await render({ guideId: "claude" });
|
|
147
|
+
expect(container.textContent).toContain("Status unavailable");
|
|
148
|
+
expect(container.textContent).not.toContain("Connected");
|
|
149
|
+
const retry = queryByRole<HTMLButtonElement>(container, "button", {
|
|
150
|
+
name: "Try again",
|
|
145
151
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
expect(
|
|
149
|
-
container
|
|
150
|
-
.querySelector("#mcp-guide-tab-grok")
|
|
151
|
-
?.getAttribute("aria-selected"),
|
|
152
|
-
).toBe("true");
|
|
153
|
-
expect(container.textContent).toContain("grok.com/connectors");
|
|
152
|
+
expect(retry).not.toBeNull();
|
|
153
|
+
await act(async () => retry?.click());
|
|
154
|
+
expect(statusQuery.refetch).toHaveBeenCalledOnce();
|
|
154
155
|
});
|
|
155
156
|
|
|
156
|
-
it("
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
initialLocale="en-US"
|
|
163
|
-
initialPreference="en-US"
|
|
164
|
-
persistPreference={false}
|
|
165
|
-
>
|
|
166
|
-
<McpAccessSettings appName="Content" />
|
|
167
|
-
</AgentNativeI18nProvider>,
|
|
168
|
-
);
|
|
169
|
-
});
|
|
170
|
-
await waitForGuides(container);
|
|
171
|
-
|
|
172
|
-
await act(async () => {
|
|
173
|
-
window.history.pushState({}, "", "/settings/mcp?guide=cursor");
|
|
174
|
-
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
expect(
|
|
178
|
-
container
|
|
179
|
-
.querySelector("#mcp-guide-tab-cursor")
|
|
180
|
-
?.getAttribute("aria-selected"),
|
|
181
|
-
).toBe("true");
|
|
157
|
+
it("keeps unverifiable older credentials distinct from disconnected", async () => {
|
|
158
|
+
statusQuery.data.connections[0].status = "unknown";
|
|
159
|
+
await render({ guideId: "claude" });
|
|
160
|
+
expect(container.textContent).toContain("Status unavailable");
|
|
161
|
+
expect(container.textContent).toContain("Older credentials");
|
|
162
|
+
expect(container.textContent).not.toContain("Not connected");
|
|
182
163
|
});
|
|
183
164
|
|
|
184
|
-
it("
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
"",
|
|
188
|
-
"/settings/mcp?guide=grok§ion=access",
|
|
165
|
+
it("starts with a client list, a useful outcome, and no configuration form", async () => {
|
|
166
|
+
await render();
|
|
167
|
+
expect(container.textContent).toContain(
|
|
168
|
+
"Use Claude, ChatGPT, or another AI app to read and update data in Content.",
|
|
189
169
|
);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
);
|
|
170
|
+
for (const name of [
|
|
171
|
+
"Claude",
|
|
172
|
+
"ChatGPT",
|
|
173
|
+
"Cursor",
|
|
174
|
+
"VS Code",
|
|
175
|
+
"Claude Code",
|
|
176
|
+
"Codex CLI",
|
|
177
|
+
"Grok",
|
|
178
|
+
"Other",
|
|
179
|
+
])
|
|
180
|
+
expect(link(name)).toBeDefined();
|
|
181
|
+
expect(container.querySelector("[role=combobox]")).toBeNull();
|
|
182
|
+
expect(container.textContent).not.toContain("MCP server URL");
|
|
183
|
+
expect(container.textContent).not.toContain("Generate a static token");
|
|
184
|
+
expect(container.querySelectorAll("img")).toHaveLength(7);
|
|
185
|
+
expect(link("Cursor")?.querySelector("img")?.getAttribute("src")).toMatch(
|
|
186
|
+
/^data:image\/png;base64,/,
|
|
187
|
+
);
|
|
188
|
+
expect(link("Grok")?.querySelector("img")?.getAttribute("src")).toMatch(
|
|
189
|
+
/^data:image\/svg\+xml;base64,/,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
it("opens the chosen client while preserving unrelated query parameters", async () => {
|
|
193
|
+
window.history.replaceState({}, "", "/settings/mcp?section=access");
|
|
194
|
+
await render();
|
|
195
|
+
await open("Cursor");
|
|
196
|
+
expect(window.location.search).toContain("guide=cursor");
|
|
197
|
+
expect(window.location.search).toContain("section=access");
|
|
198
|
+
const install = link("Add to Cursor");
|
|
199
|
+
expect(install?.target).toBe("_blank");
|
|
200
|
+
const url = new URL(install?.href ?? "");
|
|
201
|
+
expect(url.origin + url.pathname).toBe("https://cursor.com/install-mcp");
|
|
202
|
+
expect(url.searchParams.get("name")).toBe(IDENTITY.serverName);
|
|
203
|
+
expect(JSON.parse(atob(url.searchParams.get("config") ?? ""))).toEqual({
|
|
204
|
+
url: IDENTITY.mcpUrl,
|
|
201
205
|
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
await act(async () =>
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
expect(container.textContent).not.toContain(IDENTITY.mcpUrl);
|
|
207
|
+
expect(container.textContent).not.toContain("mcp.json");
|
|
208
|
+
await act(async () =>
|
|
209
|
+
[...container.querySelectorAll("button")]
|
|
210
|
+
.find((item) => item.textContent === "Manual setup")
|
|
211
|
+
?.click(),
|
|
212
|
+
);
|
|
213
|
+
expect(container.textContent).toContain(IDENTITY.mcpUrl);
|
|
214
|
+
expect(container.textContent).toContain("mcp.json");
|
|
215
|
+
expect(container.textContent).toContain("approve the MCP scopes");
|
|
216
|
+
});
|
|
217
|
+
it("exposes real hrefs and preserves modified clicks", async () => {
|
|
218
|
+
const select = vi.fn();
|
|
219
|
+
await render({
|
|
220
|
+
guideId: null,
|
|
221
|
+
onSelectGuide: select,
|
|
222
|
+
guideHref: (id) => `/settings/mcp/${id}`,
|
|
208
223
|
});
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
const cursor = link("Cursor");
|
|
225
|
+
expect(cursor?.getAttribute("href")).toBe("/settings/mcp/cursor");
|
|
226
|
+
await act(async () =>
|
|
227
|
+
cursor?.dispatchEvent(
|
|
228
|
+
new MouseEvent("click", {
|
|
229
|
+
bubbles: true,
|
|
230
|
+
cancelable: true,
|
|
231
|
+
ctrlKey: true,
|
|
232
|
+
}),
|
|
233
|
+
),
|
|
211
234
|
);
|
|
212
|
-
expect(
|
|
213
|
-
|
|
235
|
+
expect(select).not.toHaveBeenCalled();
|
|
236
|
+
await act(async () =>
|
|
237
|
+
cursor?.dispatchEvent(
|
|
238
|
+
new MouseEvent("click", { bubbles: true, cancelable: true }),
|
|
239
|
+
),
|
|
214
240
|
);
|
|
215
|
-
|
|
241
|
+
expect(select).toHaveBeenCalledWith("cursor");
|
|
242
|
+
});
|
|
243
|
+
it("restores the chosen client on browser navigation", async () => {
|
|
244
|
+
await render();
|
|
245
|
+
await open("Cursor");
|
|
216
246
|
await act(async () => {
|
|
247
|
+
window.history.replaceState({}, "", "/settings/mcp?guide=grok");
|
|
217
248
|
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
218
249
|
});
|
|
219
|
-
expect(
|
|
220
|
-
|
|
221
|
-
.querySelector("#mcp-guide-tab-cursor")
|
|
222
|
-
?.getAttribute("aria-selected"),
|
|
223
|
-
).toBe("true");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("names the server and builds install links from the app's connect identity", async () => {
|
|
227
|
-
window.history.replaceState({}, "", "/settings/mcp?guide=claude-code");
|
|
228
|
-
|
|
250
|
+
expect(container.textContent).toContain("grok.com/connectors");
|
|
251
|
+
expect(link("Add to Cursor")).toBeUndefined();
|
|
229
252
|
await act(async () => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
initialLocale="en-US"
|
|
233
|
-
initialPreference="en-US"
|
|
234
|
-
persistPreference={false}
|
|
235
|
-
>
|
|
236
|
-
<McpAccessSettings appName="Content" />
|
|
237
|
-
</AgentNativeI18nProvider>,
|
|
238
|
-
);
|
|
253
|
+
window.history.replaceState({}, "", "/settings/mcp");
|
|
254
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
239
255
|
});
|
|
240
|
-
|
|
241
|
-
|
|
256
|
+
expect(link("Codex CLI")).toBeDefined();
|
|
257
|
+
expect(container.textContent).not.toContain("MCP server URL");
|
|
258
|
+
});
|
|
259
|
+
it("honors a direct CLI handoff and uses the server identity in the command", async () => {
|
|
260
|
+
window.history.replaceState({}, "", "/settings/mcp?guide=codex");
|
|
261
|
+
await render();
|
|
242
262
|
expect(container.textContent).toContain(
|
|
243
|
-
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
await act(async () => {
|
|
247
|
-
selectTab(
|
|
248
|
-
container.querySelector<HTMLButtonElement>("#mcp-guide-tab-cursor"),
|
|
249
|
-
);
|
|
250
|
-
});
|
|
251
|
-
const cursorLink = Array.from(container.querySelectorAll("a")).find(
|
|
252
|
-
(link) => link.textContent?.includes("Add to Cursor"),
|
|
253
|
-
);
|
|
254
|
-
const cursorHref = new URL(cursorLink?.getAttribute("href") ?? "");
|
|
255
|
-
expect(cursorHref.origin + cursorHref.pathname).toBe(
|
|
256
|
-
"https://cursor.com/install-mcp",
|
|
263
|
+
`codex mcp add ${IDENTITY.serverName} --url ${IDENTITY.mcpUrl}`,
|
|
257
264
|
);
|
|
258
|
-
expect(
|
|
259
|
-
|
|
265
|
+
expect(container.textContent).toContain(
|
|
266
|
+
`codex mcp login ${IDENTITY.serverName}`,
|
|
260
267
|
);
|
|
261
|
-
expect(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
expect(container.querySelector("h1")).toBeNull();
|
|
269
|
+
expect(container.textContent).not.toContain("Try it in Codex.");
|
|
270
|
+
});
|
|
271
|
+
it("opens the web client from its setup stage and copies the URL separately with failure feedback", async () => {
|
|
272
|
+
const write = vi
|
|
273
|
+
.spyOn(navigator.clipboard, "writeText")
|
|
274
|
+
.mockRejectedValueOnce(new Error("denied"))
|
|
275
|
+
.mockResolvedValue(undefined);
|
|
276
|
+
await render({ guideId: "chatgpt" });
|
|
277
|
+
const action = link("Open ChatGPT");
|
|
278
|
+
expect(action?.href).toBe("https://chatgpt.com/plugins");
|
|
279
|
+
expect(action?.target).toBe("_blank");
|
|
280
|
+
const copy = queryByRole<HTMLButtonElement>(container, "button", {
|
|
281
|
+
name: "Copy URL",
|
|
282
|
+
exact: true,
|
|
270
283
|
});
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
expect(
|
|
278
|
-
"
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
284
|
+
expect(copy).not.toBeNull();
|
|
285
|
+
await act(async () => copy?.click());
|
|
286
|
+
expect(write).toHaveBeenCalledWith(IDENTITY.mcpUrl);
|
|
287
|
+
expect(container.textContent).toContain("Copy failed");
|
|
288
|
+
await act(async () => copy?.click());
|
|
289
|
+
expect(container.textContent).not.toContain("Copy failed");
|
|
290
|
+
expect(
|
|
291
|
+
[...container.querySelectorAll("[role=status]")].some(
|
|
292
|
+
(item) => item.textContent === "Copied",
|
|
293
|
+
),
|
|
294
|
+
).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
it("places the install action in the opening setup stage", async () => {
|
|
297
|
+
await render({ guideId: "cursor" });
|
|
298
|
+
const install = link("Add to Cursor");
|
|
299
|
+
expect(install?.closest("li")?.textContent).toContain("Add the MCP server");
|
|
300
|
+
expect(
|
|
301
|
+
container.querySelectorAll('a[href^="https://cursor.com/install-mcp"]'),
|
|
302
|
+
).toHaveLength(1);
|
|
282
303
|
expect(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
304
|
+
queryByRole<HTMLButtonElement>(container, "button", {
|
|
305
|
+
name: "Copy prompt",
|
|
306
|
+
exact: true,
|
|
307
|
+
})?.disabled,
|
|
308
|
+
).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
it("localizes the client list and detail controls", async () => {
|
|
311
|
+
await render({}, "es-ES");
|
|
312
|
+
await vi.waitFor(() =>
|
|
313
|
+
expect(container.textContent).toContain(
|
|
314
|
+
"leer y actualizar datos en Content",
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
await open("Claude");
|
|
318
|
+
expect(container.textContent).toContain("herramientas de Agent-Native");
|
|
319
|
+
expect(container.textContent).toContain("URL del servidor");
|
|
320
|
+
expect(container.textContent).toContain(
|
|
321
|
+
"En Claude, ve a Customize → Connectors.",
|
|
322
|
+
);
|
|
323
|
+
expect(container.textContent).not.toContain("MCP server URL");
|
|
324
|
+
});
|
|
325
|
+
it("keeps the VS Code installation transport and environment-specific server name", async () => {
|
|
326
|
+
await render({ guideId: "vscode" });
|
|
327
|
+
const url = link("Add to VS Code")?.getAttribute("href") ?? "";
|
|
328
|
+
expect(url.startsWith("vscode:mcp/install?")).toBe(true);
|
|
329
|
+
expect(link("Add to VS Code")?.target).toBe("");
|
|
330
|
+
expect(JSON.parse(decodeURIComponent(url.split("?")[1]))).toEqual({
|
|
331
|
+
name: IDENTITY.serverName,
|
|
286
332
|
type: "http",
|
|
287
|
-
url:
|
|
333
|
+
url: IDENTITY.mcpUrl,
|
|
288
334
|
});
|
|
289
335
|
});
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
await act(async () => {
|
|
296
|
-
root.render(
|
|
297
|
-
<AgentNativeI18nProvider
|
|
298
|
-
initialLocale="en-US"
|
|
299
|
-
initialPreference="en-US"
|
|
300
|
-
persistPreference={false}
|
|
301
|
-
>
|
|
302
|
-
<McpAccessSettings appName="Content" />
|
|
303
|
-
</AgentNativeI18nProvider>,
|
|
304
|
-
);
|
|
305
|
-
});
|
|
306
|
-
await vi.waitFor(() =>
|
|
307
|
-
expect(container.textContent).toContain(IDENTITY.mcpUrl),
|
|
336
|
+
it("keeps manual JSON readable and token fallback behind advanced disclosure", async () => {
|
|
337
|
+
await render({ guideId: "other" });
|
|
338
|
+
expect(container.querySelector("pre")?.textContent).toContain(
|
|
339
|
+
'"mcpServers"',
|
|
308
340
|
);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
(
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
).
|
|
341
|
+
expect(container.textContent).not.toContain("Generate a static token");
|
|
342
|
+
await act(async () =>
|
|
343
|
+
[...container.querySelectorAll("button")]
|
|
344
|
+
.find((item) => item.textContent === "Advanced options")
|
|
345
|
+
?.click(),
|
|
346
|
+
);
|
|
347
|
+
const target = link("Open full connect page")?.getAttribute("href");
|
|
348
|
+
expect(target).toContain("guide=other");
|
|
349
|
+
expect(target).toContain("locale=en-US");
|
|
350
|
+
expect(container.textContent).toContain("Generate a static token");
|
|
318
351
|
});
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
<AgentNativeI18nProvider
|
|
326
|
-
initialLocale="en-US"
|
|
327
|
-
initialPreference="en-US"
|
|
328
|
-
persistPreference={false}
|
|
329
|
-
>
|
|
330
|
-
<McpAccessSettings appName="Content" />
|
|
331
|
-
</AgentNativeI18nProvider>,
|
|
332
|
-
);
|
|
352
|
+
it("does not offer setup routes when OAuth and token routes are unavailable", async () => {
|
|
353
|
+
stubFetch({ ...IDENTITY, connect: false });
|
|
354
|
+
await render();
|
|
355
|
+
const field = queryByRole<HTMLInputElement>(container, "textbox", {
|
|
356
|
+
name: "MCP server URL",
|
|
357
|
+
exact: true,
|
|
333
358
|
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
359
|
+
expect(field?.value).toBe(IDENTITY.mcpUrl);
|
|
360
|
+
expect(field?.readOnly).toBe(true);
|
|
361
|
+
expect(link("Claude")).toBeUndefined();
|
|
362
|
+
expect(link("Open full connect page")).toBeUndefined();
|
|
363
|
+
});
|
|
364
|
+
it("offers retry after a failed identity read without guessing the server URL", async () => {
|
|
365
|
+
stubFetch(undefined, false);
|
|
366
|
+
await render();
|
|
367
|
+
expect(container.querySelector("[role=alert]")?.textContent).toContain(
|
|
368
|
+
"Couldn't load this app's connection details.",
|
|
339
369
|
);
|
|
340
|
-
expect(container.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
370
|
+
expect(container.textContent).not.toContain(IDENTITY.mcpUrl);
|
|
371
|
+
const fetch = stubFetch();
|
|
372
|
+
await act(async () =>
|
|
373
|
+
[...container.querySelectorAll("button")]
|
|
374
|
+
.find((item) => item.textContent === "Try again")
|
|
375
|
+
?.click(),
|
|
345
376
|
);
|
|
346
|
-
await
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
377
|
+
await vi.waitFor(() => expect(link("Claude")).toBeDefined());
|
|
378
|
+
expect(fetch).toHaveBeenCalled();
|
|
379
|
+
});
|
|
380
|
+
it("announces new tabs and desktop launches without adding navigation arrows to copy actions", async () => {
|
|
381
|
+
await render({ guideId: "vscode" });
|
|
382
|
+
const install = link("Add to VS Code");
|
|
383
|
+
expect(install?.target).toBe("");
|
|
384
|
+
expect(
|
|
385
|
+
document.getElementById(install?.getAttribute("aria-describedby") ?? "")
|
|
386
|
+
?.textContent,
|
|
387
|
+
).toBe("Opens the desktop app");
|
|
388
|
+
expect(
|
|
389
|
+
install?.querySelector(".tabler-icon-device-desktop"),
|
|
390
|
+
).not.toBeNull();
|
|
391
|
+
expect(install?.querySelector(".tabler-icon-external-link")).toBeNull();
|
|
392
|
+
|
|
393
|
+
const docs = link("Setup guide");
|
|
394
|
+
expect(docs?.target).toBe("_blank");
|
|
395
|
+
expect(
|
|
396
|
+
document.getElementById(docs?.getAttribute("aria-describedby") ?? "")
|
|
397
|
+
?.textContent,
|
|
398
|
+
).toBe("Opens in a new tab");
|
|
399
|
+
expect(docs?.querySelector(".tabler-icon-external-link")).not.toBeNull();
|
|
400
|
+
|
|
401
|
+
await act(async () =>
|
|
402
|
+
[...container.querySelectorAll("button")]
|
|
403
|
+
.find((button) => button.textContent === "Manual setup")
|
|
404
|
+
?.click(),
|
|
351
405
|
);
|
|
406
|
+
const insiders = link("Add to VS Code Insiders");
|
|
407
|
+
expect(insiders?.target).toBe("");
|
|
408
|
+
expect(
|
|
409
|
+
document.getElementById(insiders?.getAttribute("aria-describedby") ?? "")
|
|
410
|
+
?.textContent,
|
|
411
|
+
).toBe("Opens the desktop app");
|
|
412
|
+
|
|
413
|
+
await render({ guideId: "cursor" });
|
|
414
|
+
const cursor = link("Add to Cursor");
|
|
415
|
+
expect(cursor?.target).toBe("_blank");
|
|
416
|
+
expect(
|
|
417
|
+
document.getElementById(cursor?.getAttribute("aria-describedby") ?? "")
|
|
418
|
+
?.textContent,
|
|
419
|
+
).toBe("Opens in a new tab");
|
|
420
|
+
expect(cursor?.querySelector(".tabler-icon-external-link")).not.toBeNull();
|
|
421
|
+
|
|
422
|
+
await render({ guideId: "codex" });
|
|
423
|
+
const copy = queryByRole(container, "button", {
|
|
424
|
+
name: "Copy command",
|
|
425
|
+
exact: true,
|
|
426
|
+
});
|
|
427
|
+
expect(copy).not.toBeNull();
|
|
428
|
+
expect(copy?.querySelector("svg")).toBeNull();
|
|
429
|
+
expect(copy?.hasAttribute("aria-describedby")).toBe(false);
|
|
352
430
|
});
|
|
353
431
|
|
|
354
|
-
it("renders
|
|
432
|
+
it("renders an impersonal loading shell without a browser", () => {
|
|
355
433
|
const browserWindow = window;
|
|
356
|
-
vi.stubGlobal("window", undefined);
|
|
357
|
-
|
|
358
434
|
try {
|
|
435
|
+
vi.stubGlobal("window", undefined);
|
|
359
436
|
expect(() =>
|
|
360
437
|
renderToString(
|
|
361
438
|
<AgentNativeI18nProvider
|
|
362
439
|
initialLocale="en-US"
|
|
363
|
-
initialPreference="en-US"
|
|
364
440
|
persistPreference={false}
|
|
365
441
|
>
|
|
366
|
-
<McpAccessSettings
|
|
442
|
+
<McpAccessSettings />
|
|
367
443
|
</AgentNativeI18nProvider>,
|
|
368
444
|
),
|
|
369
445
|
).not.toThrow();
|