Files
openclaw/src/secrets/runtime.loadable-plugin-origins.test.ts
Hiroshi Tanaka 52f412bf17 fix(browser): tab creation steals window focus during agent automation (#105356)
* fix(browser): tab creation steals window focus during agent automation

Agent-created tabs inherited CDP's foreground default: direct CDP
Target.createTarget omitted the background flag, and the extension
relay's createTab defaulted to active:true, so every agent tab open
activated the new tab (and, on the extension driver, focused the
window), interrupting whatever the human was doing in that browser.

Direct CDP tab creation now requests background:true (agent tab
ownership/selection is target-id based and never depended on
activation), and the extension relay defaults an omitted background
to true while preserving an explicit background:false, matching the
Codex/Claude-in-Chrome model the extension driver mirrors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(browser): keep focus fix LOC-neutral

Preserve background tab creation while keeping the oversized CDP and relay modules within the current LOC ratchet.\n\nCodex-Session: 019f5e93-780a-7350-88f9-1986cdb64914

* fix(browser): honor explicit CDP focus requests

Keep background-by-default automation while treating Target.createTarget focus=true as an explicit foreground request in the extension relay.

Codex-Session: 019f5e93-780a-7350-88f9-1986cdb64914

* fix(browser): preserve explicit CDP focus semantics

Apply the background-by-default automation policy only when focus is omitted, preserving focus=false foreground-tab requests as well as focus=true.

Codex-Session: 019f5e93-780a-7350-88f9-1986cdb64914

* fix(browser): preserve create target window focus

Carry the resolved CDP focus intent through the extension relay and explicitly focus the containing Chrome window when requested.\n\nCodex-Session: 019f5e93-780a-7350-88f9-1986cdb64914

* style(browser): refresh relay import order

* test(secrets): use secure node exec fixtures

* test(doctor): secure exec secret fixture

* test(doctor): retain narrowed temp path

* test(secrets): secure remaining exec fixtures

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-21 21:35:40 -07:00

211 lines
6.7 KiB
TypeScript

/** Tests secrets runtime loadable plugin origin detection. */
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { PluginManifestRecord } from "../plugins/manifest-registry.js";
import { asConfig, setupSecretsRuntimeSnapshotTestHooks } from "./runtime.test-support.ts";
import { withSecureTestNodeExecPath } from "./test-node-command.test-support.js";
const manifestMocks = vi.hoisted(() => ({
listPluginOriginsFromMetadataSnapshot: vi.fn(
(snapshot: { plugins: Array<{ id: string; origin: string }> }) =>
new Map(snapshot.plugins.map((record) => [record.id, record.origin])),
),
loadPluginMetadataSnapshot: vi.fn<() => { plugins: Array<{ id: string; origin: string }> }>(
() => ({
plugins: [],
}),
),
}));
vi.mock("./runtime-manifest.runtime.js", () => ({
listPluginOriginsFromMetadataSnapshot: manifestMocks.listPluginOriginsFromMetadataSnapshot,
loadPluginMetadataSnapshot: manifestMocks.loadPluginMetadataSnapshot,
}));
const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks();
describe("prepareSecretsRuntimeSnapshot loadable plugin origins", () => {
afterEach(() => {
manifestMocks.listPluginOriginsFromMetadataSnapshot.mockClear();
manifestMocks.loadPluginMetadataSnapshot.mockReset();
manifestMocks.loadPluginMetadataSnapshot.mockReturnValue({ plugins: [] });
});
it("skips metadata snapshot loading when plugin entries are absent", async () => {
await prepareSecretsRuntimeSnapshot({
config: asConfig({
models: {
providers: {
openai: {
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
models: [{ id: "gpt-5.4", name: "gpt-5.4" }],
},
},
},
}),
env: { OPENAI_API_KEY: "sk-test" },
includeAuthStoreRefs: false,
});
expect(manifestMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
expect(manifestMocks.listPluginOriginsFromMetadataSnapshot).not.toHaveBeenCalled();
});
it("derives loadable plugin origins from the shared metadata snapshot", async () => {
const snapshot = {
plugins: [{ id: "demo", origin: "workspace" }],
};
manifestMocks.loadPluginMetadataSnapshot.mockReturnValue(snapshot);
await prepareSecretsRuntimeSnapshot({
config: asConfig({
plugins: {
entries: {
demo: {
config: {
apiKey: { source: "env", provider: "default", id: "DEMO_API_KEY" },
},
},
},
},
}),
env: { HOME: "/home/demo", DEMO_API_KEY: "sk-demo" },
includeAuthStoreRefs: false,
});
const snapshotCalls = manifestMocks.loadPluginMetadataSnapshot.mock.calls as unknown as Array<
[
{
config: {
plugins?: unknown;
};
workspaceDir: unknown;
env: Record<string, unknown>;
},
]
>;
const snapshotParams = snapshotCalls[0]?.[0];
expect(snapshotParams?.config.plugins).toStrictEqual({
entries: {
demo: {
config: {
apiKey: { source: "env", provider: "default", id: "DEMO_API_KEY" },
},
},
},
});
expect(typeof snapshotParams?.workspaceDir).toBe("string");
expect(snapshotParams?.env.HOME).toBe("/home/demo");
expect(snapshotParams?.env.DEMO_API_KEY).toBe("sk-demo");
expect(manifestMocks.listPluginOriginsFromMetadataSnapshot).toHaveBeenCalledWith(snapshot);
});
it("keeps full plugin policy while projecting provider-auth assignments", async () => {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "oc-runtime-secret-provider-"));
fs.chmodSync(rootDir, 0o700);
fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {};\n", "utf8");
const resolverPath = path.join(rootDir, "resolve.mjs");
fs.writeFileSync(
resolverPath,
[
"import process from 'node:process';",
"let input = '';",
"process.stdin.setEncoding('utf8');",
"process.stdin.on('data', (chunk) => input += chunk);",
"process.stdin.on('end', () => {",
" const request = JSON.parse(input);",
" process.stdout.write(JSON.stringify({ protocolVersion: 1, values: Object.fromEntries(request.ids.map((id) => [id, `value:${id}`])) }));",
"});",
"",
].join("\n"),
"utf8",
);
fs.chmodSync(resolverPath, 0o600);
const plugin: PluginManifestRecord = {
id: "vault-secrets",
rootDir,
source: path.join(rootDir, "index.ts"),
manifestPath: path.join(rootDir, "openclaw.plugin.json"),
origin: "global",
channels: [],
providers: [],
cliBackends: [],
skills: [],
hooks: [],
secretProviderIntegrations: {
vault: {
providerAlias: "vault",
source: "exec",
command: "${node}",
args: ["./resolve.mjs"],
},
},
};
const pluginMetadataSnapshot = {
plugins: [plugin],
manifestRegistry: {
plugins: [plugin],
diagnostics: [],
},
};
try {
const config = asConfig({
plugins: {
entries: {
"vault-secrets": { enabled: true },
},
},
gateway: {
auth: {
mode: "token",
token: { source: "exec", provider: "vault", id: "gateway/token" },
},
},
models: {
providers: {
openai: {
apiKey: { source: "exec", provider: "vault", id: "models/openai" },
models: [],
},
},
},
secrets: {
providers: {
vault: {
source: "exec",
pluginIntegration: {
pluginId: "vault-secrets",
integrationId: "vault",
},
},
},
},
});
const snapshot = await withSecureTestNodeExecPath(async () =>
prepareSecretsRuntimeSnapshot({
config,
assignmentConfig: asConfig({
models: config.models,
secrets: config.secrets,
}),
env: { HOME: rootDir },
includeAuthStoreRefs: false,
pluginMetadataSnapshot,
}),
);
expect(snapshot.config.gateway).toBeUndefined();
expect(snapshot.config.models?.providers?.openai?.apiKey).toBe("value:models/openai");
expect(manifestMocks.loadPluginMetadataSnapshot).not.toHaveBeenCalled();
expect(manifestMocks.listPluginOriginsFromMetadataSnapshot).toHaveBeenCalledWith(
pluginMetadataSnapshot,
);
} finally {
fs.rmSync(rootDir, { recursive: true, force: true });
}
});
});