Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4d0c7f9
fix(reference): decide browser setup from the selected artifact, not …
tnunamak Nov 5, 2026
4367603
fix(console): read the server's browser-setup verdict instead of deri…
tnunamak Nov 5, 2026
3afc5c6
refactor(reference): drop the roster exports nothing reads
tnunamak Nov 5, 2026
21c9b91
test(reference-implementation): give the ChatGPT shell test a runnabl…
tnunamak Nov 5, 2026
037bbbb
fix(scripts): stop requiring a roster predicate in the staged console
tnunamak Nov 5, 2026
cf5f238
test(console): pin the readiness note by rendered output
tnunamak Nov 5, 2026
c145446
fix(reference): judge browser enrollment by attended-setup needs and …
tnunamak Nov 5, 2026
2acf577
fix(console): trust the server verdict and show retry when it cannot …
tnunamak Nov 5, 2026
ea93119
fix(reference): withhold browser enrollment from a package that decla…
tnunamak Nov 6, 2026
2202884
fix(console): defer to a pre-readiness server only for browser-bound …
tnunamak Nov 6, 2026
345a1d1
fix(console): pick the readiness verdict by connector key
tnunamak Nov 6, 2026
c2aa299
fix(console): offer Install for a browser connector whose setup comes…
tnunamak Nov 6, 2026
b926675
fix(reference): gate UAT exposure of a browser-bound credential conne…
tnunamak Nov 6, 2026
1446e95
test(reference): import the console readiness vocabulary without type…
tnunamak Nov 6, 2026
823e30b
style: sort imports and drop non-null assertions in files this PR owns
tnunamak Nov 6, 2026
2223a67
fix(console): leave a connector that has a template but no verdict to…
tnunamak Nov 6, 2026
b020485
fix(console): keep the owner-token install client out of the connecto…
tnunamak Nov 6, 2026
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
131 changes: 122 additions & 9 deletions apps/console/src/app/(console)/components/source-setup-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const reddit: ConnectorCatalogEntry = {
displayName: "Reddit",
disposition: "browser_collector_manual",
externalDocs: [],
isKnownScaffold: false,
readiness: null,
listingNote: null,
modality: "browser_bound",
nextStepKind: "enroll_browser_collector",
Expand Down Expand Up @@ -87,6 +87,7 @@ const UNAVAILABLE_TRUE = /data-row-unavailable="true"/;
const UNAVAILABLE_DISCLOSURE = /data-testid="unavailable-connectors-disclosure"/;
const BROWSER_SETUP_LINK = /href="\/connect\/browser-session\//;
const INSTALL_OR_BROWSER_SETUP = />Install<\/button>|href="\/connect\/browser-session\//;
const NOT_INSTALLABLE = /Not installable: no setup path is available here\./;
const INSTALL_DISABLED = /data-testid="connector-install-disabled"/;
const HOST_BLOCK = /Host blocked: Browser unavailable here\./;
const CANNOT_ADD_HERE = /1 connector cannot be added here/;
Expand Down Expand Up @@ -131,19 +132,87 @@ test("an unregistered browser connector offers Install before account setup exis
assert.doesNotMatch(html, BROWSER_SETUP_LINK, "installation must not invent a working setup route");
});

test("known scaffolds still offer neither Install nor account setup", async () => {
const verdict = (
reason: NonNullable<ConnectorCatalogEntry["readiness"]>["reason"],
action: NonNullable<ConnectorCatalogEntry["readiness"]>["action"],
detail: string | null = null
): NonNullable<ConnectorCatalogEntry["readiness"]> => ({
action,
browserBound: true,
connectorKey: "new-browser",
detail,
reason,
});
const READINESS_NOTE = /data-testid="connector-readiness-note"/;
const NOT_BUILT_YET = /Not built yet/;

test("a published, uninstalled browser connector offers Install and is never 'Not built yet'", async () => {
// The defect this replaces: LinkedIn and Goodreads are published and
// installable, but a vendored roster made them read "Not built yet".
const html = await render(
false,
{ ...unregisteredBrowser, readiness: verdict("not_installed", "install") },
browserPackage
);
assert.match(html, INSTALL_BUTTON);
assert.doesNotMatch(html, NOT_BUILT_YET);
assert.doesNotMatch(html, READINESS_NOTE);
assert.match(html, UNAVAILABLE_FALSE);
});

// A browser connector's setup path comes from its package: before the reference
// has registered it there is no verdict, and installing is the step that lets
// the server decide. The page builds such entries from the manifest alone.
test("a manifest-only browser connector with a published package offers Install, not 'Not installable'", async () => {
const { registrationStatus: _omitted, ...manifestOnly } = unregisteredBrowser;
const entry = { ...manifestOnly, readiness: null };
const html = await render(false, entry, browserPackage);
assert.match(html, INSTALL_BUTTON);
assert.doesNotMatch(html, NOT_INSTALLABLE);
assert.match(html, UNAVAILABLE_FALSE);

// While the package catalog is busy the offer is unknown, which is not "cannot install".
const busy = await render(true, entry, browserPackage);
assert.doesNotMatch(busy, INSTALL_BUTTON);
assert.doesNotMatch(busy, NOT_INSTALLABLE);
assert.match(busy, UNKNOWN_PACKAGE);
});

test("installing is not offered when the verdict says installing cannot make the connector runnable", async () => {
for (const [reason, action] of [
["not_implemented", "unavailable"],
["host_requirement_unmet", "configure"],
["not_browser_bound", "unavailable"],
] as const) {
const html = await render(false, { ...unregisteredBrowser, readiness: verdict(reason, action) }, browserPackage);
assert.doesNotMatch(html, INSTALL_BUTTON, reason);
assert.match(html, NOT_INSTALLABLE, reason);
}
});

test("a development-tier manifest-only browser connector is not offered for install", async () => {
const { registrationStatus: _omitted, ...manifestOnly } = unregisteredBrowser;
const html = await render(false, { ...manifestOnly, publicTier: "development", readiness: null }, browserPackage);
assert.doesNotMatch(html, INSTALL_BUTTON);
});

test("a verdict that blocks setup shows its reason and offers no account setup", async () => {
const [installed] = parseConnectorInstallStatusResponse(connectorInstallStatusFixture).data;
assert.ok(installed);
assert.ok(browserPackage.catalog);
const html = await render(
false,
{
...unregisteredBrowser,
disposition: "browser_collector_manual",
isKnownScaffold: true,
ownerActionable: true,
readiness: verdict("host_requirement_unmet", "configure", "No browser surface is configured."),
},
browserPackage
{ catalog: { ...browserPackage.catalog, digest: installed.digest }, installed: { ...installed, connector_id: "new-browser" } }
);
assert.match(html, INSTALL_DISABLED);
assert.doesNotMatch(html, INSTALL_OR_BROWSER_SETUP);
assert.match(html, READINESS_NOTE);
assert.match(html, /This server is not set up to run this connector yet\./);
assert.doesNotMatch(html, /No browser surface is configured/, "the server's diagnostic detail is not shown");
assert.doesNotMatch(html, BROWSER_SETUP_LINK);
assert.doesNotMatch(html, NOT_BUILT_YET);
});

// Installing a package registers a connector but cannot create a setup path:
Expand All @@ -155,7 +224,6 @@ const DISPOSITIONS_WITHOUT_SETUP_PATH = [
"unknown_unsupported",
"browser_bound_runbook",
] as const;
const NOT_INSTALLABLE = /Not installable: no setup path is available here\./;

for (const disposition of DISPOSITIONS_WITHOUT_SETUP_PATH) {
test(`${disposition} never offers an install control and sorts into the collapsed footer`, async () => {
Expand All @@ -177,6 +245,51 @@ for (const disposition of DISPOSITIONS_WITHOUT_SETUP_PATH) {
});
}

test("a ready verdict offers account setup with no readiness note; a failed-verification verdict explains the repair", async () => {
const [installed] = parseConnectorInstallStatusResponse(connectorInstallStatusFixture).data;
assert.ok(installed);
assert.ok(browserPackage.catalog);
const lifecycle: ConnectorInstallLifecycle = {
catalog: { ...browserPackage.catalog, digest: installed.digest },
installed: { ...installed, connector_id: "new-browser" },
};
const setupReady = {
...unregisteredBrowser,
disposition: "browser_collector_manual" as const,
ownerActionable: true,
registrationStatus: "registered" as const,
};

const ready = await render(false, { ...setupReady, readiness: verdict("runnable", "connect") }, lifecycle);
assert.match(ready, NEW_BROWSER_SETUP);
assert.doesNotMatch(ready, READINESS_NOTE);

const needsRepair = await render(
false,
{ ...unregisteredBrowser, readiness: verdict("artifact_invalid", "repair", "Digest mismatch.") },
lifecycle
);
assert.match(needsRepair, READINESS_NOTE);
assert.match(needsRepair, /failed verification\. Reinstall it to repair it\./);
assert.doesNotMatch(needsRepair, /Digest mismatch/, "the server's diagnostic detail is not shown");
assert.doesNotMatch(needsRepair, BROWSER_SETUP_LINK);
});

test("a verified package that declares no collection shows the placeholder note and no account setup", async () => {
const [installed] = parseConnectorInstallStatusResponse(connectorInstallStatusFixture).data;
assert.ok(installed);
assert.ok(browserPackage.catalog);
const html = await render(
false,
{ ...unregisteredBrowser, readiness: verdict("not_implemented", "unavailable", "Declared by the package.") },
{ catalog: { ...browserPackage.catalog, digest: installed.digest }, installed: { ...installed, connector_id: "new-browser" } }
);
assert.match(html, READINESS_NOTE);
assert.match(html, /placeholder and does not collect data yet/);
assert.doesNotMatch(html, /Declared by the package/);
assert.doesNotMatch(html, BROWSER_SETUP_LINK);
});

test("an explicit host restriction is displayed without offering Install", async () => {
assert.ok(browserPackage.catalog);
const html = await render(false, setupCapable, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ConnectorMark } from "./connector-mark.tsx";
import { OpenExternalLink } from "./open-external-link.tsx";
import type { ConnectorAcquisitionPath, ConnectorCatalogEntry } from "../lib/connection-catalog.ts";
import type { ConnectorInstallLifecycle } from "../lib/connector-install-presentation.ts";
import { readinessBlockNote } from "../lib/connector-readiness-copy.ts";
import {
connectorLookupKey,
isUninstallableCatalogEntry,
Expand Down Expand Up @@ -144,16 +145,13 @@ function SourceAcquisitionPaths({ paths }: { paths: readonly ConnectorAcquisitio
* Development-tier method line, resolved before the `not_available_here`
* short-circuit below. `sourceSetupAvailability` reports `not_available_here`
* for every Development entry regardless of whether it renders an add
* action (see `sourceSetupAction`'s scaffold check), so that check alone
* action (see `sourceSetupAction`'s disposition check), so that check alone
* would print "No proven setup path is available" directly above a real
* "Add account" button for a self-testable entry -- contradicting the
* action right next to it.
*/
function developmentMethodLine(entry: ConnectorCatalogEntry): string {
if (entry.isKnownScaffold) {
return "Not built yet: nobody has written this connector's collection code.";
}
return "Setup path implemented, not yet proven against a live account.";
return readinessBlockNote(entry.readiness) ?? "Setup path implemented, not yet proven against a live account.";
}

function sourceMethodLine(entry: ConnectorCatalogEntry, existingSourceCount: number): string {
Expand Down Expand Up @@ -466,6 +464,7 @@ function SourceSetupCard({
installLifecycle: ConnectorInstallLifecycle | null;
}) {
const status = sourceSetupStatus(entry);
const readinessNote = readinessBlockNote(entry.readiness);
const {
installModel,
noSetupPath,
Expand Down Expand Up @@ -509,11 +508,12 @@ function SourceSetupCard({
</div>
<div className="border-t border-border/60 pt-3 sm:border-t-0 sm:border-l sm:pl-4" data-testid="source-setup-install">
<p className="pdpp-eyebrow mb-2 text-muted-foreground">Package</p>
{entry.isKnownScaffold ? (
<span className="pdpp-caption text-muted-foreground" data-testid="connector-install-disabled">
This connector's collection code is not built yet.
{readinessNote ? (
<span className="pdpp-caption mb-2 block text-muted-foreground" data-testid="connector-readiness-note">
{readinessNote}
</span>
) : noSetupPath ? (
) : null}
{noSetupPath ? (
<span className="pdpp-caption text-muted-foreground" data-testid="connector-install-disabled">
Not installable: no setup path is available here.
</span>
Expand Down
35 changes: 35 additions & 0 deletions apps/console/src/app/(console)/lib/browser-connect-readiness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright The PDP-Connect Contributors
// SPDX-License-Identifier: Apache-2.0

import { isBrowserBoundConnector } from "pdpp-reference-implementation/connection-setup-plan";
import type { ConnectorReadiness } from "./connector-readiness-contract.ts";

export interface BrowserConnectSupport {
/** The connector uses the browser-session flow. A bound connector keeps the routes for repair. */
readonly browserBound: boolean;
/** The server says the owner can add an account now. */
readonly canAddAccount: boolean;
}

/**
* Pure mapping from the server's verdict. The verdict decides both fields.
*
* Stopgap for mixed versions only: a server that predates the readiness
* endpoint (an older server next to a newer console) returns no verdict. That
* server applies its own rule when the enrollment route is called, so the
* console defers to it, and uses the bundled manifest to keep the routes
* reachable (and the add action open) only for connectors it marks
* browser-bound. Remove this branch, and the `isBrowserBoundConnector` import,
* once no supported server predates the endpoint.
*/
export function browserConnectSupportFromReadiness(
connectorId: string,
readiness: ConnectorReadiness | null,
serverPredatesReadiness = false
): BrowserConnectSupport {
const deferToOlderServer = serverPredatesReadiness && isBrowserBoundConnector(connectorId);
return {
browserBound: readiness ? readiness.browserBound : deferToOlderServer,
canAddAccount: readiness ? readiness.action === "connect" : deferToOlderServer,
};
}

This file was deleted.

87 changes: 0 additions & 87 deletions apps/console/src/app/(console)/lib/browser-connect-support.test.ts

This file was deleted.

Loading
Loading