@popoverai/dotrequirements 0.22.0 → 0.23.0
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/README.md +6 -3
- package/dist/cli.js +0 -1
- package/dist/commands/browsertest.d.ts +0 -1
- package/dist/commands/browsertest.js +24 -6
- package/dist/mcp/index.js +9 -11
- package/dist/schema/browser.d.ts +2 -0
- package/dist/schema/browser.js +2 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/scenario.d.ts +91 -0
- package/dist/schema/scenario.js +82 -0
- package/dist/utils/project-settings.d.ts +4 -3
- package/dist/utils/project-settings.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -182,18 +182,21 @@ dotreq browsertest LOGIN-1 --json
|
|
|
182
182
|
|
|
183
183
|
**Configuration required:**
|
|
184
184
|
|
|
185
|
-
Browser testing
|
|
185
|
+
Browser testing needs a Stagehand model and the API key for that model's provider in `project-settings.json`:
|
|
186
186
|
|
|
187
187
|
```json
|
|
188
188
|
{
|
|
189
189
|
"defaultURL": "https://your-app.com",
|
|
190
190
|
"browserTest": {
|
|
191
|
-
"
|
|
191
|
+
"modelName": "gateway/anthropic/claude-haiku-4-5",
|
|
192
|
+
"modelApiKey": "your-api-key"
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
```
|
|
195
196
|
|
|
196
|
-
|
|
197
|
+
Pick any Stagehand-supported model. Example values: `gateway/anthropic/claude-haiku-4-5` (Vercel AI Gateway key), `google/gemini-3-flash-preview` (Gemini key).
|
|
198
|
+
|
|
199
|
+
Optional settings: `vercelBypassSecret`, `browserbaseApiKey`, `browserbaseProjectId`. Setting both Browserbase fields switches runs from LOCAL (spawns Playwright on your machine) to BROWSERBASE (managed cloud browsers).
|
|
197
200
|
|
|
198
201
|
### `dotreq mcp-setup`
|
|
199
202
|
|
package/dist/cli.js
CHANGED
|
@@ -78,7 +78,6 @@ program
|
|
|
78
78
|
.command('browsertest <requirement-key> [url]')
|
|
79
79
|
.description('Run browser-based acceptance test for a requirement')
|
|
80
80
|
.option('--json', 'Output results as JSON')
|
|
81
|
-
.option('--useAgent', 'Encourage Claude to use the Agent tool for multi-step tasks')
|
|
82
81
|
.action(wrapCommand(browserTestCommand));
|
|
83
82
|
program
|
|
84
83
|
.command('prepare')
|
|
@@ -44,11 +44,22 @@ export async function browserTestCommand(requirementKey, url, options) {
|
|
|
44
44
|
console.log('Either provide a URL argument or add "defaultURL" to .requirements/project-settings.json');
|
|
45
45
|
process.exit(1);
|
|
46
46
|
}
|
|
47
|
-
// 3.
|
|
47
|
+
// 3. Resolve model config
|
|
48
|
+
const modelName = settings?.browserTest?.modelName;
|
|
49
|
+
const modelApiKey = settings?.browserTest?.modelApiKey;
|
|
48
50
|
const geminiApiKey = settings?.browserTest?.geminiApiKey;
|
|
49
|
-
if (!geminiApiKey) {
|
|
50
|
-
console.error('Error:
|
|
51
|
-
console.log('Add
|
|
51
|
+
if (!modelName && !modelApiKey && !geminiApiKey) {
|
|
52
|
+
console.error('Error: no browser-test model configured in project-settings.json.');
|
|
53
|
+
console.log('Add a block like:\n' +
|
|
54
|
+
' "browserTest": {\n' +
|
|
55
|
+
' "modelName": "gateway/anthropic/claude-haiku-4-5",\n' +
|
|
56
|
+
' "modelApiKey": "..."\n' +
|
|
57
|
+
' }\n' +
|
|
58
|
+
'to .requirements/project-settings.json. See the docs for supported models.');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
if ((modelName && !modelApiKey) || (modelApiKey && !modelName)) {
|
|
62
|
+
console.error('Error: browserTest.modelName and browserTest.modelApiKey must be set together.');
|
|
52
63
|
process.exit(1);
|
|
53
64
|
}
|
|
54
65
|
// 4. Load requirements
|
|
@@ -67,7 +78,13 @@ export async function browserTestCommand(requirementKey, url, options) {
|
|
|
67
78
|
const assertions = requirementTree.map((req) => formatRequirement(req));
|
|
68
79
|
// 7. Build environment with injected secrets
|
|
69
80
|
const env = { ...process.env };
|
|
70
|
-
env
|
|
81
|
+
// env var keeps the key out of process listings (not a CLI flag)
|
|
82
|
+
if (modelApiKey) {
|
|
83
|
+
env.MODEL_API_KEY = modelApiKey;
|
|
84
|
+
}
|
|
85
|
+
else if (geminiApiKey) {
|
|
86
|
+
env.GEMINI_API_KEY = geminiApiKey;
|
|
87
|
+
}
|
|
71
88
|
if (settings?.browserTest?.vercelBypassSecret) {
|
|
72
89
|
env.VERCEL_AUTOMATION_BYPASS_SECRET = settings.browserTest.vercelBypassSecret;
|
|
73
90
|
}
|
|
@@ -82,7 +99,8 @@ export async function browserTestCommand(requirementKey, url, options) {
|
|
|
82
99
|
console.log(`Testing ${requirementKey} against ${targetURL}...\n`);
|
|
83
100
|
}
|
|
84
101
|
try {
|
|
85
|
-
const
|
|
102
|
+
const modelFlags = modelName ? ['--modelName', modelName] : [];
|
|
103
|
+
const { stdout } = await execFileAsync('npx', ['@popoverai/browser-automation', 'test', ...modelFlags, targetURL, ...assertions], { env, maxBuffer: 10 * 1024 * 1024 });
|
|
86
104
|
// 9. Parse results
|
|
87
105
|
let results;
|
|
88
106
|
try {
|
package/dist/mcp/index.js
CHANGED
|
@@ -33,17 +33,16 @@ const PROJECT_PATHS = getProjectPathsFromEnv();
|
|
|
33
33
|
// Parse --auth-from-env flag for CI/CD environments
|
|
34
34
|
// When set, credentials are read from DOTREQ_PROJECT_ID and DOTREQ_PROJECT_SECRET env vars
|
|
35
35
|
const USE_ENV_AUTH = process.argv.includes('--auth-from-env');
|
|
36
|
-
// Cache for loaded requirements (refreshed on each tool call for now)
|
|
37
|
-
let cachedRequirements = new Map();
|
|
38
36
|
let cachedDiscoveryResult = null;
|
|
37
|
+
// Requirements are loaded fresh on every call. A long-lived in-memory cache
|
|
38
|
+
// silently served stale data when .requirements/*.md files were created or
|
|
39
|
+
// edited mid-session, which is the primary authoring workflow the MCP server
|
|
40
|
+
// is meant to support. If this ever becomes a measured performance concern,
|
|
41
|
+
// invalidate via directory mtime rather than reintroducing a lifetime cache.
|
|
39
42
|
async function getRequirements(projectId) {
|
|
40
43
|
const project = await getProjectFromDiscovery(projectId);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const { flattened } = await loadAllRequirements(project.path);
|
|
44
|
-
cachedRequirements.set(cacheKey, flattened);
|
|
45
|
-
}
|
|
46
|
-
return cachedRequirements.get(cacheKey);
|
|
44
|
+
const { flattened } = await loadAllRequirements(project.path);
|
|
45
|
+
return flattened;
|
|
47
46
|
}
|
|
48
47
|
async function getProjectFromDiscovery(projectId) {
|
|
49
48
|
const { isConfiguredProject } = await import('../utils/project-discovery.js');
|
|
@@ -92,10 +91,9 @@ async function getProjectFromDiscovery(projectId) {
|
|
|
92
91
|
}
|
|
93
92
|
return resolveProject(cachedDiscoveryResult, projectId);
|
|
94
93
|
}
|
|
95
|
-
// Invalidate
|
|
96
|
-
// Exported for testing
|
|
94
|
+
// Invalidate the project-discovery cache. Requirements are no longer cached,
|
|
95
|
+
// so this only resets discovery state. Exported for testing.
|
|
97
96
|
export function invalidateCache() {
|
|
98
|
-
cachedRequirements.clear();
|
|
99
97
|
cachedDiscoveryResult = null;
|
|
100
98
|
}
|
|
101
99
|
// Tool definitions
|
package/dist/schema/browser.d.ts
CHANGED
|
@@ -9,4 +9,6 @@ export { DEFAULT_DELIMITER, buildRequirementsMarkdown, buildRequirementMarkdown,
|
|
|
9
9
|
export type { ConvexRequirement, } from './conversions.js';
|
|
10
10
|
export { convexToRequirements, requirementsToConvex, buildMetadata, extractRequirementKeys, groupByRoot, constructKey, parseKey, } from './conversions.js';
|
|
11
11
|
export { DELIMITER_PATTERN, parseCriterionLine, parseRootLine, parseRequirementBlock, extractRequirementBlocks, parseRequirementBlocksFromMarkdown, flattenRequirementTree, findRequirementById, getAllRequirements, } from './parser-core.js';
|
|
12
|
+
export { buildScenarioFromRequirements, requirementTreeToScenario, } from './scenario.js';
|
|
13
|
+
export type { Scenario, ScenarioStep, ScenarioAssertionSource, BuildScenarioOptions, } from './scenario.js';
|
|
12
14
|
//# sourceMappingURL=browser.d.ts.map
|
package/dist/schema/browser.js
CHANGED
|
@@ -21,4 +21,6 @@ export { convexToRequirements, requirementsToConvex, buildMetadata, extractRequi
|
|
|
21
21
|
export { DELIMITER_PATTERN, parseCriterionLine, parseRootLine, parseRequirementBlock, extractRequirementBlocks, parseRequirementBlocksFromMarkdown, flattenRequirementTree, findRequirementById, getAllRequirements, } from './parser-core.js';
|
|
22
22
|
// NOTE: parser.ts and resolver.ts are excluded because they use Node.js 'fs' module.
|
|
23
23
|
// Use parser-core.ts functions above for browser/Convex environments.
|
|
24
|
+
// Scenario building (pure TypeScript - browser-safe, used by Convex Node actions)
|
|
25
|
+
export { buildScenarioFromRequirements, requirementTreeToScenario, } from './scenario.js';
|
|
24
26
|
//# sourceMappingURL=browser.js.map
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -11,4 +11,6 @@ export { buildRequirementsMarkdown, buildRequirementMarkdown, buildRequirementsF
|
|
|
11
11
|
export { parseRequirementPath, parsePathSegment, findChildrenByLabel, resolvePathSegment, resolveRequirementPath, resolveToNumericPath, getAllLabelPaths, checkPathAmbiguity, } from './resolver.js';
|
|
12
12
|
export type { ConvexRequirement, } from './conversions.js';
|
|
13
13
|
export { convexToRequirements, requirementsToConvex, buildMetadata, extractRequirementKeys, groupByRoot, constructKey, parseKey, } from './conversions.js';
|
|
14
|
+
export { buildScenarioFromRequirements, requirementTreeToScenario, } from './scenario.js';
|
|
15
|
+
export type { Scenario, ScenarioStep, ScenarioAssertionSource, BuildScenarioOptions, } from './scenario.js';
|
|
14
16
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/schema/index.js
CHANGED
|
@@ -21,4 +21,6 @@ export { buildRequirementsMarkdown, buildRequirementMarkdown, buildRequirementsF
|
|
|
21
21
|
// Path resolution
|
|
22
22
|
export { parseRequirementPath, parsePathSegment, findChildrenByLabel, resolvePathSegment, resolveRequirementPath, resolveToNumericPath, getAllLabelPaths, checkPathAmbiguity, } from './resolver.js';
|
|
23
23
|
export { convexToRequirements, requirementsToConvex, buildMetadata, extractRequirementKeys, groupByRoot, constructKey, parseKey, } from './conversions.js';
|
|
24
|
+
// Scenario building (for browser-automation / runScenario)
|
|
25
|
+
export { buildScenarioFromRequirements, requirementTreeToScenario, } from './scenario.js';
|
|
24
26
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenario builder — turns a requirement tree into a browser-automation Scenario.
|
|
3
|
+
*
|
|
4
|
+
* Used by both the CLI (via `dotreq browsertest`) and the Convex Node action
|
|
5
|
+
* (`testCoverage/browserRun:run`) so the two paths produce identical scenarios
|
|
6
|
+
* for the same input.
|
|
7
|
+
*
|
|
8
|
+
* Naive passthrough: every requirement node with non-empty content becomes
|
|
9
|
+
* exactly one assertion keyed by its numeric-path id (e.g. "LOGIN-1" for the
|
|
10
|
+
* root, "LOGIN-1.0" for the first criterion). No arrange/act steps are
|
|
11
|
+
* emitted; Stagehand's hybrid agent is expected to infer setup from the
|
|
12
|
+
* assertion list. See BROWSER-RUN-3 in
|
|
13
|
+
* .requirements/testing-tab-browser-run.requirements.md.
|
|
14
|
+
*
|
|
15
|
+
* Types here are defined locally to avoid adding `@popoverai/browser-automation`
|
|
16
|
+
* as a runtime dependency of this package. They are structurally compatible
|
|
17
|
+
* with browser-automation's exported `Scenario` / `Step` types and can be
|
|
18
|
+
* passed directly to `runScenario`.
|
|
19
|
+
*/
|
|
20
|
+
import type { RequirementNode } from "./schemas.js";
|
|
21
|
+
/**
|
|
22
|
+
* One step in a scenario. Structurally compatible with
|
|
23
|
+
* `@popoverai/browser-automation`'s `Step` type.
|
|
24
|
+
*/
|
|
25
|
+
export interface ScenarioStep {
|
|
26
|
+
step: "arrange" | "act" | "assert";
|
|
27
|
+
description: string;
|
|
28
|
+
url?: string;
|
|
29
|
+
key?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* One entry in the `Scenario.variables` map. Structurally compatible with
|
|
33
|
+
* `@popoverai/browser-automation`'s `Variable` type — the `value` is what
|
|
34
|
+
* gets substituted into the scenario, and the optional `description` gives
|
|
35
|
+
* the agent extra context about what the substitution represents.
|
|
36
|
+
*/
|
|
37
|
+
export interface ScenarioVariable {
|
|
38
|
+
value: string;
|
|
39
|
+
description?: string;
|
|
40
|
+
}
|
|
41
|
+
export type ScenarioVariables = Record<string, ScenarioVariable>;
|
|
42
|
+
/**
|
|
43
|
+
* A scenario passed to `runScenario`. Structurally compatible with
|
|
44
|
+
* `@popoverai/browser-automation`'s `Scenario` type.
|
|
45
|
+
*/
|
|
46
|
+
export interface Scenario {
|
|
47
|
+
baseUrl: string;
|
|
48
|
+
steps: ScenarioStep[];
|
|
49
|
+
variables?: ScenarioVariables;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* One requirement contributing an assertion to a scenario. The minimum shape
|
|
53
|
+
* the builder needs so callers sourcing requirements from different backends
|
|
54
|
+
* (Markdown files via `RequirementNode`, Convex rows, etc.) can all use it.
|
|
55
|
+
*/
|
|
56
|
+
export interface ScenarioAssertionSource {
|
|
57
|
+
/** Full requirement key including path, e.g. "LOGIN-1" or "LOGIN-1.0.1". */
|
|
58
|
+
id: string;
|
|
59
|
+
/** Requirement text; becomes the assertion's `description`. */
|
|
60
|
+
content: string;
|
|
61
|
+
}
|
|
62
|
+
export interface BuildScenarioOptions {
|
|
63
|
+
/**
|
|
64
|
+
* The requirements contributing assertions, in the order they should be
|
|
65
|
+
* evaluated. Every entry with non-empty `content` becomes one `assert` step.
|
|
66
|
+
*/
|
|
67
|
+
assertions: ScenarioAssertionSource[];
|
|
68
|
+
/** Target URL the scenario runs against; becomes `Scenario.baseUrl`. */
|
|
69
|
+
baseUrl: string;
|
|
70
|
+
/**
|
|
71
|
+
* Optional Stagehand variable substitutions. Used to inject secrets
|
|
72
|
+
* (test-user credentials, bypass tokens) into assertion descriptions
|
|
73
|
+
* without exposing them to the language model in plaintext.
|
|
74
|
+
*/
|
|
75
|
+
variables?: Record<string, string>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build a `Scenario` from a list of requirement assertion sources.
|
|
79
|
+
*
|
|
80
|
+
* Throws if the resulting scenario would have zero assertions
|
|
81
|
+
* (browser-automation rejects scenarios with no asserts).
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildScenarioFromRequirements(opts: BuildScenarioOptions): Scenario;
|
|
84
|
+
/**
|
|
85
|
+
* Convenience wrapper for callers that have a `RequirementNode` tree (root
|
|
86
|
+
* plus nested children). Flattens the tree to the order produced by
|
|
87
|
+
* `flattenRequirementTree` (pre-order: root first, then each subtree), then
|
|
88
|
+
* delegates to `buildScenarioFromRequirements`.
|
|
89
|
+
*/
|
|
90
|
+
export declare function requirementTreeToScenario(root: RequirementNode, opts: Omit<BuildScenarioOptions, "assertions">): Scenario;
|
|
91
|
+
//# sourceMappingURL=scenario.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenario builder — turns a requirement tree into a browser-automation Scenario.
|
|
3
|
+
*
|
|
4
|
+
* Used by both the CLI (via `dotreq browsertest`) and the Convex Node action
|
|
5
|
+
* (`testCoverage/browserRun:run`) so the two paths produce identical scenarios
|
|
6
|
+
* for the same input.
|
|
7
|
+
*
|
|
8
|
+
* Naive passthrough: every requirement node with non-empty content becomes
|
|
9
|
+
* exactly one assertion keyed by its numeric-path id (e.g. "LOGIN-1" for the
|
|
10
|
+
* root, "LOGIN-1.0" for the first criterion). No arrange/act steps are
|
|
11
|
+
* emitted; Stagehand's hybrid agent is expected to infer setup from the
|
|
12
|
+
* assertion list. See BROWSER-RUN-3 in
|
|
13
|
+
* .requirements/testing-tab-browser-run.requirements.md.
|
|
14
|
+
*
|
|
15
|
+
* Types here are defined locally to avoid adding `@popoverai/browser-automation`
|
|
16
|
+
* as a runtime dependency of this package. They are structurally compatible
|
|
17
|
+
* with browser-automation's exported `Scenario` / `Step` types and can be
|
|
18
|
+
* passed directly to `runScenario`.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Pre-order flatten: root first, then each subtree's nodes in order.
|
|
22
|
+
* Inlined here (rather than imported from `parser.ts`) so this module stays
|
|
23
|
+
* free of Node-only dependencies and can be re-exported from the browser
|
|
24
|
+
* entry point.
|
|
25
|
+
*/
|
|
26
|
+
function flattenTree(node) {
|
|
27
|
+
const result = [node];
|
|
28
|
+
for (const child of node.children) {
|
|
29
|
+
result.push(...flattenTree(child));
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build a `Scenario` from a list of requirement assertion sources.
|
|
35
|
+
*
|
|
36
|
+
* Throws if the resulting scenario would have zero assertions
|
|
37
|
+
* (browser-automation rejects scenarios with no asserts).
|
|
38
|
+
*/
|
|
39
|
+
export function buildScenarioFromRequirements(opts) {
|
|
40
|
+
const { assertions, baseUrl, variables } = opts;
|
|
41
|
+
if (!baseUrl || baseUrl.trim().length === 0) {
|
|
42
|
+
throw new Error("buildScenarioFromRequirements: baseUrl is required");
|
|
43
|
+
}
|
|
44
|
+
const steps = [];
|
|
45
|
+
for (const source of assertions) {
|
|
46
|
+
const description = source.content.trim();
|
|
47
|
+
if (description.length === 0)
|
|
48
|
+
continue;
|
|
49
|
+
steps.push({
|
|
50
|
+
step: "assert",
|
|
51
|
+
description,
|
|
52
|
+
key: source.id,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (steps.length === 0) {
|
|
56
|
+
throw new Error("buildScenarioFromRequirements: requirement tree has no nodes with content; cannot build a scenario");
|
|
57
|
+
}
|
|
58
|
+
const scenario = { baseUrl, steps };
|
|
59
|
+
if (variables && Object.keys(variables).length > 0) {
|
|
60
|
+
// Callers pass a flat Record<string, string> for ergonomics; wrap each
|
|
61
|
+
// entry as `{ value }` so the scenario matches runScenario's expected
|
|
62
|
+
// Variables shape. Callers with richer values can set variables on the
|
|
63
|
+
// returned scenario directly before invocation.
|
|
64
|
+
scenario.variables = Object.fromEntries(Object.entries(variables).map(([key, value]) => [key, { value }]));
|
|
65
|
+
}
|
|
66
|
+
return scenario;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Convenience wrapper for callers that have a `RequirementNode` tree (root
|
|
70
|
+
* plus nested children). Flattens the tree to the order produced by
|
|
71
|
+
* `flattenRequirementTree` (pre-order: root first, then each subtree), then
|
|
72
|
+
* delegates to `buildScenarioFromRequirements`.
|
|
73
|
+
*/
|
|
74
|
+
export function requirementTreeToScenario(root, opts) {
|
|
75
|
+
const flat = flattenTree(root);
|
|
76
|
+
const assertions = flat.map((node) => ({
|
|
77
|
+
id: node.id,
|
|
78
|
+
content: node.content,
|
|
79
|
+
}));
|
|
80
|
+
return buildScenarioFromRequirements({ ...opts, assertions });
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=scenario.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser test configuration
|
|
3
|
-
*/
|
|
1
|
+
/** Browser test configuration. */
|
|
4
2
|
export interface BrowserTestSettings {
|
|
3
|
+
modelName?: string;
|
|
4
|
+
modelApiKey?: string;
|
|
5
|
+
/** @deprecated Backward-compat fallback; use modelName + modelApiKey instead. */
|
|
5
6
|
geminiApiKey?: string;
|
|
6
7
|
vercelBypassSecret?: string;
|
|
7
8
|
browserbaseApiKey?: string;
|
|
@@ -80,6 +80,12 @@ export function readProjectSettings(projectRoot) {
|
|
|
80
80
|
if (typeof record.browserTest === 'object' && record.browserTest !== null) {
|
|
81
81
|
const bt = record.browserTest;
|
|
82
82
|
settings.browserTest = {};
|
|
83
|
+
if (typeof bt.modelName === 'string') {
|
|
84
|
+
settings.browserTest.modelName = bt.modelName;
|
|
85
|
+
}
|
|
86
|
+
if (typeof bt.modelApiKey === 'string') {
|
|
87
|
+
settings.browserTest.modelApiKey = bt.modelApiKey;
|
|
88
|
+
}
|
|
83
89
|
if (typeof bt.geminiApiKey === 'string') {
|
|
84
90
|
settings.browserTest.geminiApiKey = bt.geminiApiKey;
|
|
85
91
|
}
|