Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/settings-ui/src/connections-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -883,13 +883,17 @@ export function ConnectorCredentialDialog({
)}
</DialogTitle>
<DialogDescription>
{SETTINGS_STRINGS.connectionsDialogDescription}
{isUrlField
? SETTINGS_STRINGS.connectionsDialogUrlDescription
: SETTINGS_STRINGS.connectionsDialogDescription}
</DialogDescription>
</DialogHeader>
<DialogBody className="settings-form-stack">
<div className="settings-form-field">
<span>
{isUrlField ? "URL" : SETTINGS_STRINGS.connectionsKeyLabel}
{isUrlField
? SETTINGS_STRINGS.connectionsUrlLabel
: SETTINGS_STRINGS.connectionsKeyLabel}
</span>
{isUrlField ? (
<Input
Expand Down
3 changes: 3 additions & 0 deletions packages/settings-ui/src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,10 @@ export const SETTINGS_STRINGS = {
connectionsDialogReconnectTitle: (name: string) => `Reconnect ${name}`,
connectionsDialogDescription:
"Sealed on save — this key is never shown again after create. A bad key never gets saved; connecting surfaces the problem right here.",
connectionsDialogUrlDescription:
"The base address of the instance to connect. A bad address never gets saved; connecting surfaces the problem right here.",
connectionsKeyLabel: "API key",
connectionsUrlLabel: "Base URL",
connectionsConnectDialogAction: "Connect",
connectionsConnecting: "Connecting…",
connectionsSaving: "Saving…",
Expand Down
63 changes: 61 additions & 2 deletions packages/settings-ui/test/connector-credential-dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
// client-driven "test" round-trip before it: `/complete` itself proves
// the key against the connector's own probe and only stores it once that
// probe accepts, so a rejected key never gets sealed.
//
// CL-6793: Ollama's field is a base URL, not a sealed secret — the same
// dialog swaps copy when `credentialInputKind` is `"url"`.

import { afterEach, describe, expect, test } from "bun:test";
import { act } from "react";
Expand All @@ -11,6 +14,7 @@ import type { Root } from "react-dom/client";
import type { ConnectorDescriptor } from "@workbench/connections/registry";

import { ConnectorCredentialDialog } from "../src/connections-section";
import { SETTINGS_STRINGS } from "../src/strings";

const realFetch = globalThis.fetch;
afterEach(() => {
Expand All @@ -26,6 +30,17 @@ const descriptor: ConnectorDescriptor = {
feedsTools: ["linear"],
};

const ollamaDescriptor: ConnectorDescriptor = {
id: "ollama",
displayName: "Ollama",
authKind: "api-key",
docsUrl: "https://example.com/docs",
credentialPlugin: "http",
feedsTools: [],
credentialInputKind: "url",
credentialPlaceholder: "http://localhost:11434",
};

const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value",
Expand All @@ -39,7 +54,10 @@ function typeInto(input: HTMLInputElement, value: string) {
input.dispatchEvent(new Event("input", { bubbles: true }));
}

function mount(onConnected: () => void = () => undefined): {
function mount(
onConnected: () => void = () => undefined,
next: ConnectorDescriptor = descriptor,
): {
container: HTMLDivElement;
root: Root;
} {
Expand All @@ -49,7 +67,7 @@ function mount(onConnected: () => void = () => undefined): {
act(() => {
root.render(
<ConnectorCredentialDialog
descriptor={descriptor}
descriptor={next}
mode="connect"
tenantId="ten_1"
onClose={() => undefined}
Expand Down Expand Up @@ -150,4 +168,45 @@ describe("ConnectorCredentialDialog", () => {
container.remove();
}
});

test("Ollama connect dialog copy matches a URL / base-address field, not a sealed secret key", () => {
const { container, root } = mount(() => undefined, ollamaDescriptor);
try {
const body = document.body.textContent ?? "";
expect(body).toContain(SETTINGS_STRINGS.connectionsUrlLabel);
expect(body).toContain(SETTINGS_STRINGS.connectionsDialogUrlDescription);
expect(body).not.toContain(SETTINGS_STRINGS.connectionsDialogDescription);
expect(body).not.toContain(SETTINGS_STRINGS.connectionsKeyLabel);
expect(body).not.toContain("Sealed on save");
expect(body).not.toContain("this key is never shown again");
expect(body).not.toContain("A bad key never gets saved");
expect(document.body.querySelector("input[type=password]")).toBeNull();
expect(
(document.body.querySelector("input[type=text]") as HTMLInputElement)
.placeholder,
).toBe("http://localhost:11434");
} finally {
act(() => root.unmount());
container.remove();
}
});

test("Sealed-key wording is reserved for actual secret fields", () => {
const { container, root } = mount();
try {
const body = document.body.textContent ?? "";
expect(body).toContain(SETTINGS_STRINGS.connectionsDialogDescription);
expect(body).toContain(SETTINGS_STRINGS.connectionsKeyLabel);
expect(body).toContain("Sealed on save");
expect(body).not.toContain(
SETTINGS_STRINGS.connectionsDialogUrlDescription,
);
expect(
document.body.querySelector("input[type=password]"),
).not.toBeNull();
} finally {
act(() => root.unmount());
container.remove();
}
});
});
Loading