Merge branch 'main' into codex/assemble-runtime-context-budget
This commit is contained in:
commit
cee8c865f6
@ -152,6 +152,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram: stabilize pairing/session/forum routing and reply formatting tests (#50155) Thanks @joshavant.
|
||||
- Hardening: refresh stale device pairing requests and pending metadata (#50695) Thanks @smaeljaish771 and @joshavant.
|
||||
- Gateway: harden OpenResponses file-context escaping (#50782) Thanks @YLChen-007 and @joshavant.
|
||||
- LINE: harden Express webhook parsing to verified raw body (#51202) Thanks @gladiator9797 and @joshavant.
|
||||
- xAI/models: rename the bundled Grok 4.20 catalog entries to the GA IDs and normalize saved deprecated beta IDs at runtime so existing configs and sessions keep resolving. (#50772) thanks @Jaaneek
|
||||
|
||||
### Fixes
|
||||
|
||||
@ -51,6 +51,7 @@ If you need a custom path, set `channels.line.webhookPath` or
|
||||
Security note:
|
||||
|
||||
- LINE signature verification is body-dependent (HMAC over the raw body), so OpenClaw applies strict pre-auth body limits and timeout before verification.
|
||||
- OpenClaw processes webhook events from the verified raw request bytes. Upstream middleware-transformed `req.body` values are ignored for signature-integrity safety.
|
||||
|
||||
## Configure
|
||||
|
||||
|
||||
@ -195,6 +195,7 @@ function scanWebSearchRegistrySmells(sourceFile, filePath) {
|
||||
function shouldSkipFile(filePath) {
|
||||
const relativeFile = normalizePath(filePath);
|
||||
return (
|
||||
relativeFile === "src/plugins/bundled-web-search-registry.ts" ||
|
||||
relativeFile.startsWith("src/plugins/contracts/") ||
|
||||
/^src\/plugins\/runtime\/runtime-[^/]+-contract\.[cm]?[jt]s$/u.test(relativeFile)
|
||||
);
|
||||
|
||||
@ -297,7 +297,7 @@ const defaultHeavyUnitFileLimit =
|
||||
: isMacMiniProfile
|
||||
? 90
|
||||
: testProfile === "low"
|
||||
? 20
|
||||
? 32
|
||||
: highMemLocalHost
|
||||
? 80
|
||||
: 60;
|
||||
@ -307,7 +307,7 @@ const defaultHeavyUnitLaneCount =
|
||||
: isMacMiniProfile
|
||||
? 6
|
||||
: testProfile === "low"
|
||||
? 2
|
||||
? 3
|
||||
: highMemLocalHost
|
||||
? 5
|
||||
: 4;
|
||||
|
||||
@ -135,24 +135,16 @@ describe("registerQrCli", () => {
|
||||
};
|
||||
}
|
||||
|
||||
function expectLoggedSetupCode(
|
||||
url: string,
|
||||
auth?: {
|
||||
token?: string;
|
||||
password?: string;
|
||||
},
|
||||
) {
|
||||
function expectLoggedSetupCode(url: string) {
|
||||
const expected = encodePairingSetupCode({
|
||||
url,
|
||||
bootstrapToken: "bootstrap-123",
|
||||
...(auth?.token ? { token: auth.token } : {}),
|
||||
...(auth?.password ? { password: auth.password } : {}),
|
||||
});
|
||||
expect(runtime.log).toHaveBeenCalledWith(expected);
|
||||
}
|
||||
|
||||
function expectLoggedLocalSetupCode(auth?: { token?: string; password?: string }) {
|
||||
expectLoggedSetupCode("ws://gateway.local:18789", auth);
|
||||
function expectLoggedLocalSetupCode() {
|
||||
expectLoggedSetupCode("ws://gateway.local:18789");
|
||||
}
|
||||
|
||||
function mockTailscaleStatusLookup() {
|
||||
@ -189,7 +181,6 @@ describe("registerQrCli", () => {
|
||||
const expected = encodePairingSetupCode({
|
||||
url: "ws://gateway.local:18789",
|
||||
bootstrapToken: "bootstrap-123",
|
||||
token: "tok",
|
||||
});
|
||||
expect(runtime.log).toHaveBeenCalledWith(expected);
|
||||
expect(qrGenerate).not.toHaveBeenCalled();
|
||||
@ -225,7 +216,7 @@ describe("registerQrCli", () => {
|
||||
|
||||
await runQr(["--setup-code-only", "--token", "override-token"]);
|
||||
|
||||
expectLoggedLocalSetupCode({ token: "override-token" });
|
||||
expectLoggedLocalSetupCode();
|
||||
});
|
||||
|
||||
it("skips local password SecretRef resolution when --token override is provided", async () => {
|
||||
@ -237,7 +228,7 @@ describe("registerQrCli", () => {
|
||||
|
||||
await runQr(["--setup-code-only", "--token", "override-token"]);
|
||||
|
||||
expectLoggedLocalSetupCode({ token: "override-token" });
|
||||
expectLoggedLocalSetupCode();
|
||||
});
|
||||
|
||||
it("resolves local gateway auth password SecretRefs before setup code generation", async () => {
|
||||
@ -250,7 +241,7 @@ describe("registerQrCli", () => {
|
||||
|
||||
await runQr(["--setup-code-only"]);
|
||||
|
||||
expectLoggedLocalSetupCode({ password: "local-password-secret" });
|
||||
expectLoggedLocalSetupCode();
|
||||
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -264,7 +255,7 @@ describe("registerQrCli", () => {
|
||||
|
||||
await runQr(["--setup-code-only"]);
|
||||
|
||||
expectLoggedLocalSetupCode({ password: "password-from-env" });
|
||||
expectLoggedLocalSetupCode();
|
||||
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -279,7 +270,7 @@ describe("registerQrCli", () => {
|
||||
|
||||
await runQr(["--setup-code-only"]);
|
||||
|
||||
expectLoggedLocalSetupCode({ token: "token-123" });
|
||||
expectLoggedLocalSetupCode();
|
||||
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -293,7 +284,7 @@ describe("registerQrCli", () => {
|
||||
|
||||
await runQr(["--setup-code-only"]);
|
||||
|
||||
expectLoggedLocalSetupCode({ password: "inferred-password" });
|
||||
expectLoggedLocalSetupCode();
|
||||
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@ -342,7 +333,6 @@ describe("registerQrCli", () => {
|
||||
const expected = encodePairingSetupCode({
|
||||
url: "wss://remote.example.com:444",
|
||||
bootstrapToken: "bootstrap-123",
|
||||
token: "remote-tok",
|
||||
});
|
||||
expect(runtime.log).toHaveBeenCalledWith(expected);
|
||||
expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith(
|
||||
@ -386,7 +376,6 @@ describe("registerQrCli", () => {
|
||||
const expected = encodePairingSetupCode({
|
||||
url: "wss://remote.example.com:444",
|
||||
bootstrapToken: "bootstrap-123",
|
||||
token: "remote-tok",
|
||||
});
|
||||
expect(runtime.log).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
|
||||
@ -69,8 +69,6 @@ function createGatewayTokenRefFixture() {
|
||||
function decodeSetupCode(setupCode: string): {
|
||||
url?: string;
|
||||
bootstrapToken?: string;
|
||||
token?: string;
|
||||
password?: string;
|
||||
} {
|
||||
const padded = setupCode.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padLength = (4 - (padded.length % 4)) % 4;
|
||||
@ -79,8 +77,6 @@ function decodeSetupCode(setupCode: string): {
|
||||
return JSON.parse(json) as {
|
||||
url?: string;
|
||||
bootstrapToken?: string;
|
||||
token?: string;
|
||||
password?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -119,7 +115,7 @@ describe("cli integration: qr + dashboard token SecretRef", () => {
|
||||
delete process.env.SHARED_GATEWAY_TOKEN;
|
||||
});
|
||||
|
||||
it("uses the same resolved token SecretRef for both qr and dashboard commands", async () => {
|
||||
it("uses the same resolved token SecretRef for qr auth validation and dashboard commands", async () => {
|
||||
const fixture = createGatewayTokenRefFixture();
|
||||
process.env.SHARED_GATEWAY_TOKEN = "shared-token-123";
|
||||
loadConfigMock.mockReturnValue(fixture);
|
||||
@ -137,7 +133,6 @@ describe("cli integration: qr + dashboard token SecretRef", () => {
|
||||
const payload = decodeSetupCode(setupCode ?? "");
|
||||
expect(payload.url).toBe("ws://gateway.local:18789");
|
||||
expect(payload.bootstrapToken).toBeTruthy();
|
||||
expect(payload.token).toBe("shared-token-123");
|
||||
expect(runtimeErrors).toEqual([]);
|
||||
|
||||
runtimeLogs.length = 0;
|
||||
|
||||
@ -138,6 +138,92 @@ describe("createLineWebhookMiddleware", () => {
|
||||
expect(onEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the signed raw body instead of a pre-parsed req.body object", async () => {
|
||||
const onEvents = vi.fn(async (_body: WebhookRequestBody) => {});
|
||||
const rawBody = JSON.stringify({
|
||||
events: [{ type: "message", source: { userId: "signed-user" } }],
|
||||
});
|
||||
const reqBody = {
|
||||
events: [{ type: "message", source: { userId: "tampered-user" } }],
|
||||
};
|
||||
const middleware = createLineWebhookMiddleware({
|
||||
channelSecret: SECRET,
|
||||
onEvents,
|
||||
});
|
||||
|
||||
const req = {
|
||||
headers: { "x-line-signature": sign(rawBody, SECRET) },
|
||||
rawBody,
|
||||
body: reqBody,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
const res = createRes();
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await middleware(req, res, {} as any);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(onEvents).toHaveBeenCalledTimes(1);
|
||||
const processedBody = onEvents.mock.calls[0]?.[0] as WebhookRequestBody | undefined;
|
||||
expect(processedBody?.events?.[0]?.source?.userId).toBe("signed-user");
|
||||
expect(processedBody?.events?.[0]?.source?.userId).not.toBe("tampered-user");
|
||||
});
|
||||
|
||||
it("uses signed raw buffer body instead of a pre-parsed req.body object", async () => {
|
||||
const onEvents = vi.fn(async (_body: WebhookRequestBody) => {});
|
||||
const rawBodyText = JSON.stringify({
|
||||
events: [{ type: "message", source: { userId: "signed-buffer-user" } }],
|
||||
});
|
||||
const reqBody = {
|
||||
events: [{ type: "message", source: { userId: "tampered-user" } }],
|
||||
};
|
||||
const middleware = createLineWebhookMiddleware({
|
||||
channelSecret: SECRET,
|
||||
onEvents,
|
||||
});
|
||||
|
||||
const req = {
|
||||
headers: { "x-line-signature": sign(rawBodyText, SECRET) },
|
||||
rawBody: Buffer.from(rawBodyText, "utf-8"),
|
||||
body: reqBody,
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
const res = createRes();
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await middleware(req, res, {} as any);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(onEvents).toHaveBeenCalledTimes(1);
|
||||
const processedBody = onEvents.mock.calls[0]?.[0] as WebhookRequestBody | undefined;
|
||||
expect(processedBody?.events?.[0]?.source?.userId).toBe("signed-buffer-user");
|
||||
expect(processedBody?.events?.[0]?.source?.userId).not.toBe("tampered-user");
|
||||
});
|
||||
|
||||
it("rejects invalid signed raw JSON even when req.body is a valid object", async () => {
|
||||
const onEvents = vi.fn(async (_body: WebhookRequestBody) => {});
|
||||
const rawBody = "not-json";
|
||||
const middleware = createLineWebhookMiddleware({
|
||||
channelSecret: SECRET,
|
||||
onEvents,
|
||||
});
|
||||
|
||||
const req = {
|
||||
headers: { "x-line-signature": sign(rawBody, SECRET) },
|
||||
rawBody,
|
||||
body: { events: [{ type: "message" }] },
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
} as any;
|
||||
const res = createRes();
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await middleware(req, res, {} as any);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({ error: "Invalid webhook payload" });
|
||||
expect(onEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 500 when event processing fails and does not acknowledge with 200", async () => {
|
||||
const onEvents = vi.fn(async () => {
|
||||
throw new Error("boom");
|
||||
|
||||
@ -23,10 +23,7 @@ function readRawBody(req: Request): string | null {
|
||||
return Buffer.isBuffer(rawBody) ? rawBody.toString("utf-8") : rawBody;
|
||||
}
|
||||
|
||||
function parseWebhookBody(req: Request, rawBody?: string | null): WebhookRequestBody | null {
|
||||
if (req.body && typeof req.body === "object" && !Buffer.isBuffer(req.body)) {
|
||||
return req.body as WebhookRequestBody;
|
||||
}
|
||||
function parseWebhookBody(rawBody?: string | null): WebhookRequestBody | null {
|
||||
if (!rawBody) {
|
||||
return null;
|
||||
}
|
||||
@ -64,7 +61,8 @@ export function createLineWebhookMiddleware(
|
||||
return;
|
||||
}
|
||||
|
||||
const body = parseWebhookBody(req, rawBody);
|
||||
// Keep processing tied to the exact bytes that passed signature verification.
|
||||
const body = parseWebhookBody(rawBody);
|
||||
|
||||
if (!body) {
|
||||
res.status(400).json({ error: "Invalid webhook payload" });
|
||||
|
||||
@ -45,8 +45,6 @@ describe("pairing setup code", () => {
|
||||
authLabel: string;
|
||||
url?: string;
|
||||
urlSource?: string;
|
||||
token?: string;
|
||||
password?: string;
|
||||
},
|
||||
) {
|
||||
expect(resolved.ok).toBe(true);
|
||||
@ -55,8 +53,6 @@ describe("pairing setup code", () => {
|
||||
}
|
||||
expect(resolved.authLabel).toBe(params.authLabel);
|
||||
expect(resolved.payload.bootstrapToken).toBe("bootstrap-123");
|
||||
expect(resolved.payload.token).toBe(params.token);
|
||||
expect(resolved.payload.password).toBe(params.password);
|
||||
if (params.url) {
|
||||
expect(resolved.payload.url).toBe(params.url);
|
||||
}
|
||||
@ -117,7 +113,6 @@ describe("pairing setup code", () => {
|
||||
payload: {
|
||||
url: "ws://gateway.local:19001",
|
||||
bootstrapToken: "bootstrap-123",
|
||||
token: "tok_123",
|
||||
},
|
||||
authLabel: "token",
|
||||
urlSource: "gateway.bind=custom",
|
||||
@ -144,7 +139,7 @@ describe("pairing setup code", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expectResolvedSetupOk(resolved, { authLabel: "password", password: "resolved-password" });
|
||||
expectResolvedSetupOk(resolved, { authLabel: "password" });
|
||||
});
|
||||
|
||||
it("uses OPENCLAW_GATEWAY_PASSWORD without resolving configured password SecretRef", async () => {
|
||||
@ -167,7 +162,7 @@ describe("pairing setup code", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expectResolvedSetupOk(resolved, { authLabel: "password", password: "password-from-env" });
|
||||
expectResolvedSetupOk(resolved, { authLabel: "password" });
|
||||
});
|
||||
|
||||
it("does not resolve gateway.auth.password SecretRef in token mode", async () => {
|
||||
@ -189,7 +184,7 @@ describe("pairing setup code", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expectResolvedSetupOk(resolved, { authLabel: "token", token: "tok_123" });
|
||||
expectResolvedSetupOk(resolved, { authLabel: "token" });
|
||||
});
|
||||
|
||||
it("resolves gateway.auth.token SecretRef for pairing payload", async () => {
|
||||
@ -212,7 +207,7 @@ describe("pairing setup code", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expectResolvedSetupOk(resolved, { authLabel: "token", token: "resolved-token" });
|
||||
expectResolvedSetupOk(resolved, { authLabel: "token" });
|
||||
});
|
||||
|
||||
it("errors when gateway.auth.token SecretRef is unresolved in token mode", async () => {
|
||||
@ -261,13 +256,13 @@ describe("pairing setup code", () => {
|
||||
id: "MISSING_GW_TOKEN",
|
||||
});
|
||||
|
||||
expectResolvedSetupOk(resolved, { authLabel: "password", password: "password-from-env" });
|
||||
expectResolvedSetupOk(resolved, { authLabel: "password" });
|
||||
});
|
||||
|
||||
it("does not treat env-template token as plaintext in inferred mode", async () => {
|
||||
const resolved = await resolveInferredModeWithPasswordEnv("${MISSING_GW_TOKEN}");
|
||||
|
||||
expectResolvedSetupOk(resolved, { authLabel: "password", password: "password-from-env" });
|
||||
expectResolvedSetupOk(resolved, { authLabel: "password" });
|
||||
});
|
||||
|
||||
it("requires explicit auth mode when token and password are both configured", async () => {
|
||||
@ -333,7 +328,7 @@ describe("pairing setup code", () => {
|
||||
},
|
||||
);
|
||||
|
||||
expectResolvedSetupOk(resolved, { authLabel: "token", token: "new-token" });
|
||||
expectResolvedSetupOk(resolved, { authLabel: "token" });
|
||||
});
|
||||
|
||||
it("errors when gateway is loopback only", async () => {
|
||||
@ -367,7 +362,6 @@ describe("pairing setup code", () => {
|
||||
payload: {
|
||||
url: "wss://mb-server.tailnet.ts.net",
|
||||
bootstrapToken: "bootstrap-123",
|
||||
password: "secret",
|
||||
},
|
||||
authLabel: "password",
|
||||
urlSource: "gateway.tailscale.mode=serve",
|
||||
@ -396,7 +390,6 @@ describe("pairing setup code", () => {
|
||||
payload: {
|
||||
url: "wss://remote.example.com:444",
|
||||
bootstrapToken: "bootstrap-123",
|
||||
token: "tok_123",
|
||||
},
|
||||
authLabel: "token",
|
||||
urlSource: "gateway.remote.url",
|
||||
|
||||
@ -16,8 +16,6 @@ import { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js";
|
||||
export type PairingSetupPayload = {
|
||||
url: string;
|
||||
bootstrapToken: string;
|
||||
token?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
export type PairingSetupCommandResult = {
|
||||
@ -64,11 +62,6 @@ type ResolveAuthLabelResult = {
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type ResolveSharedAuthResult = {
|
||||
token?: string;
|
||||
password?: string;
|
||||
};
|
||||
|
||||
function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
@ -213,41 +206,6 @@ function resolvePairingSetupAuthLabel(
|
||||
return { error: "Gateway auth is not configured (no token or password)." };
|
||||
}
|
||||
|
||||
function resolvePairingSetupSharedAuth(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): ResolveSharedAuthResult {
|
||||
const defaults = cfg.secrets?.defaults;
|
||||
const tokenRef = resolveSecretInputRef({
|
||||
value: cfg.gateway?.auth?.token,
|
||||
defaults,
|
||||
}).ref;
|
||||
const passwordRef = resolveSecretInputRef({
|
||||
value: cfg.gateway?.auth?.password,
|
||||
defaults,
|
||||
}).ref;
|
||||
const token =
|
||||
resolveGatewayTokenFromEnv(env) ||
|
||||
(tokenRef ? undefined : normalizeSecretInputString(cfg.gateway?.auth?.token));
|
||||
const password =
|
||||
resolveGatewayPasswordFromEnv(env) ||
|
||||
(passwordRef ? undefined : normalizeSecretInputString(cfg.gateway?.auth?.password));
|
||||
const mode = cfg.gateway?.auth?.mode;
|
||||
if (mode === "token") {
|
||||
return { token };
|
||||
}
|
||||
if (mode === "password") {
|
||||
return { password };
|
||||
}
|
||||
if (token) {
|
||||
return { token };
|
||||
}
|
||||
if (password) {
|
||||
return { password };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async function resolveGatewayTokenSecretRef(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
@ -417,8 +375,6 @@ export async function resolvePairingSetupFromConfig(
|
||||
if (authLabel.error) {
|
||||
return { ok: false, error: authLabel.error };
|
||||
}
|
||||
const sharedAuth = resolvePairingSetupSharedAuth(cfgForAuth, env);
|
||||
|
||||
const urlResult = await resolveGatewayUrl(cfgForAuth, {
|
||||
env,
|
||||
publicUrl: options.publicUrl,
|
||||
@ -445,8 +401,6 @@ export async function resolvePairingSetupFromConfig(
|
||||
baseDir: options.pairingBaseDir,
|
||||
})
|
||||
).token,
|
||||
...(sharedAuth.token ? { token: sharedAuth.token } : {}),
|
||||
...(sharedAuth.password ? { password: sharedAuth.password } : {}),
|
||||
},
|
||||
authLabel: authLabel.label,
|
||||
urlSource: urlResult.source ?? "unknown",
|
||||
|
||||
@ -154,6 +154,21 @@ function createRuntime(): RuntimeEnv {
|
||||
};
|
||||
}
|
||||
|
||||
function createWebSearchProviderEntry(
|
||||
provider: Pick<
|
||||
PluginWebSearchProviderEntry,
|
||||
"id" | "label" | "hint" | "envVars" | "placeholder" | "signupUrl" | "credentialPath"
|
||||
>,
|
||||
): PluginWebSearchProviderEntry {
|
||||
return {
|
||||
pluginId: `plugin-${provider.id}`,
|
||||
getCredentialValue: () => undefined,
|
||||
setCredentialValue: () => {},
|
||||
createTool: () => null,
|
||||
...provider,
|
||||
};
|
||||
}
|
||||
|
||||
function expectFirstOnboardingInstallPlanCallOmitsToken() {
|
||||
const [firstArg] =
|
||||
(buildGatewayInstallPlan.mock.calls.at(0) as [Record<string, unknown>] | undefined) ?? [];
|
||||
@ -414,7 +429,7 @@ describe("finalizeSetupWizard", () => {
|
||||
|
||||
it("only reports legacy auto-detect for runtime-visible providers", async () => {
|
||||
listConfiguredWebSearchProviders.mockReturnValue([
|
||||
{
|
||||
createWebSearchProviderEntry({
|
||||
id: "perplexity",
|
||||
label: "Perplexity Search",
|
||||
hint: "Fast web answers",
|
||||
@ -422,7 +437,7 @@ describe("finalizeSetupWizard", () => {
|
||||
placeholder: "pplx-...",
|
||||
signupUrl: "https://www.perplexity.ai/",
|
||||
credentialPath: "plugins.entries.perplexity.config.webSearch.apiKey",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
hasExistingKey.mockImplementation((_config, provider) => provider === "perplexity");
|
||||
|
||||
@ -463,7 +478,7 @@ describe("finalizeSetupWizard", () => {
|
||||
|
||||
it("uses configured provider resolution instead of the active runtime registry", async () => {
|
||||
listConfiguredWebSearchProviders.mockReturnValue([
|
||||
{
|
||||
createWebSearchProviderEntry({
|
||||
id: "firecrawl",
|
||||
label: "Firecrawl Search",
|
||||
hint: "Structured results",
|
||||
@ -471,7 +486,7 @@ describe("finalizeSetupWizard", () => {
|
||||
placeholder: "fc-...",
|
||||
signupUrl: "https://www.firecrawl.dev/",
|
||||
credentialPath: "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
hasExistingKey.mockImplementation((_config, provider) => provider === "firecrawl");
|
||||
|
||||
|
||||
@ -21,12 +21,15 @@ function readBaseline() {
|
||||
}
|
||||
|
||||
describe("plugin extension import boundary inventory", () => {
|
||||
it("keeps web-search-providers out of the remaining inventory", async () => {
|
||||
it("keeps dedicated web-search registry shims out of the remaining inventory", async () => {
|
||||
const inventory = await collectPluginExtensionImportBoundaryInventory();
|
||||
|
||||
expect(inventory.some((entry) => entry.file === "src/plugins/web-search-providers.ts")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
inventory.some((entry) => entry.file === "src/plugins/bundled-web-search-registry.ts"),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores boundary shims by scope", async () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user