@capagents/forgeqe 1.1.2 → 1.1.4
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/HOW_TO_USE.md +5 -5
- package/README.md +8 -4
- package/browserstack.example.yaml +14 -0
- package/docs/security-data-flow.html +154 -86
- package/docs/user-guide.html +305 -131
- package/forgeqe.example.yaml +12 -6
- package/package.json +4 -2
- package/py/forgeqe_engine/browser.py +78 -11
- package/py/forgeqe_engine/service.py +13 -0
- package/src/auth/signin.ts +2 -2
- package/src/bi.ts +338 -243
- package/src/capabilities/guide.ts +322 -140
- package/src/capabilities/registry.ts +3 -3
- package/src/cli/help.ts +16 -7
- package/src/cli/index.ts +51 -9
- package/src/config.ts +115 -12
- package/src/heal/loop.ts +3 -1
- package/src/init.ts +18 -0
- package/src/mcp/tools.ts +26 -4
- package/src/monitor/run.ts +3 -1
- package/src/run.ts +9 -2
- package/src/suite/run.ts +3 -1
- package/testmu.example.yaml +13 -0
|
@@ -61,11 +61,11 @@ export const CAPABILITIES: Capability[] = [
|
|
|
61
61
|
area: "Browser",
|
|
62
62
|
title: "Run any goal in a real browser",
|
|
63
63
|
summary:
|
|
64
|
-
"Carries out a plain-language goal in
|
|
64
|
+
"Carries out a plain-language goal in a real browser one step at a time and verifies the outcome, with screenshots, a report, and the steps it took. The browser comes from forgeqe.yaml: local Chrome or Chromium by default, or BrowserStack or TestMu AI/LambdaTest when configured. Page memory replays steps that already worked on a site, so repeat runs are faster and cheaper.",
|
|
65
65
|
tools: ["browser_run", "run_details", "page_memory"],
|
|
66
66
|
examples: ["Go to booking.com and search hotels in Mumbai for next weekend", "What does ForgeQE know about the checkout page?"],
|
|
67
67
|
requires: [{ kind: "live_model" }],
|
|
68
|
-
limits: ["Web applications
|
|
68
|
+
limits: ["Web applications only: local Chrome or Chromium, or configured remote web grids such as BrowserStack and TestMu AI/LambdaTest; not native mobile or desktop applications."],
|
|
69
69
|
docs: [
|
|
70
70
|
{ doc: "HOW_TO_USE.md", heading: "4. Without the shell" },
|
|
71
71
|
{ doc: "HOW_TO_USE.md", heading: "5. Page memory" },
|
|
@@ -345,7 +345,7 @@ export const CAPABILITIES: Capability[] = [
|
|
|
345
345
|
* Only add what is certain.
|
|
346
346
|
*/
|
|
347
347
|
export const OUT_OF_SCOPE: { what: string; instead?: string }[] = [
|
|
348
|
-
{ what: "Native mobile apps (iOS, Android) and desktop applications", instead: "web applications in Chrome or Chromium" },
|
|
348
|
+
{ what: "Native mobile apps (iOS, Android) and desktop applications", instead: "web applications in local Chrome or Chromium, or configured remote web grids such as BrowserStack and TestMu AI/LambdaTest" },
|
|
349
349
|
{ what: "Load, stress, or soak testing with many simultaneous users", instead: "per-page Core Web Vitals with budgets" },
|
|
350
350
|
{ what: "Penetration testing or dynamic security scanning", instead: "security findings in pull request reviews" },
|
|
351
351
|
]
|
package/src/cli/help.ts
CHANGED
|
@@ -17,26 +17,32 @@ Usage:
|
|
|
17
17
|
[--repo DIR] [--no-code]
|
|
18
18
|
forgeqe run "<request>" [--profile name] [--url URL] [--headed] [--plain] [--max-steps N]
|
|
19
19
|
[--repo DIR] [--no-code] [--replay MODE] [--no-memory] [--attempts N] [--code-model provider/model]
|
|
20
|
+
[--browser-provider local|browserstack|testmu]
|
|
20
21
|
ForgeQE's agent works out what the request needs: a browser goal,
|
|
21
22
|
test cases from Jira, Azure DevOps, Xray or files, a test, a pipeline
|
|
22
|
-
forgeqe run --direct "<goal>" [--out DIR] [--skill NAME] ... one browser goal, no agent
|
|
23
|
+
forgeqe run --direct "<goal>" [--out DIR] [--skill NAME] [--browser-provider local|browserstack|testmu] ... one browser goal, no agent
|
|
23
24
|
forgeqe suite <source...> [--parallel N] [--repo DIR] [--no-code] [--no-write-back] [--out DIR]
|
|
24
25
|
[--profile name] [--url URL] [--headed] [--plain] [--attempts N] [--skill NAME]
|
|
26
|
+
[--browser-provider local|browserstack|testmu]
|
|
25
27
|
run test cases the same way every time (what pipelines call)
|
|
26
28
|
forgeqe cases <source...> [--goal] list the test cases a source gives (--goal: the goal each runs)
|
|
27
29
|
forgeqe code [run-dir] [--repo DIR] [--attempts N] [--code-model provider/model]
|
|
28
30
|
write a test for a run (default: the latest) with Scribe
|
|
29
31
|
forgeqe heal ["<test command>"] [--repo DIR] [--attempts N] [--code-model provider/model] [--headed] [--plain]
|
|
32
|
+
[--browser-provider local|browserstack|testmu]
|
|
30
33
|
run the repo's tests and heal the ones the app has outgrown
|
|
31
34
|
forgeqe monitor [source...] [--every 15m] [--once] [--replay MODE] [--parallel N] [--out DIR] [--verbose] [--skill NAME]
|
|
35
|
+
[--browser-provider local|browserstack|testmu]
|
|
32
36
|
check flows on a schedule and alert Teams or email when one breaks
|
|
33
37
|
forgeqe monitor --test-alerts send a sample alert to check the Teams and email settings
|
|
34
|
-
forgeqe mcp [--repo DIR] [--profile name] [--url URL] [--headed]
|
|
38
|
+
forgeqe mcp [--repo DIR] [--profile name] [--url URL] [--headed] [--browser-provider local|browserstack|testmu]
|
|
35
39
|
ForgeQE's tools as an MCP server on stdio, for other agents
|
|
36
40
|
forgeqe mcp list the Azure DevOps, Jira and other MCP servers ForgeQE's agent gets
|
|
37
|
-
forgeqe bi [build] [--evidence-dir DIR] [--data-dir DIR] [--build-dir DIR]
|
|
38
|
-
export ForgeQE + ADO/Jira datasets into an Evidence project and build static
|
|
39
|
-
|
|
41
|
+
forgeqe bi [build] [--evidence-dir DIR] [--data-dir DIR] [--build-dir DIR] [--static | --evidence]
|
|
42
|
+
export ForgeQE + ADO/Jira datasets into an Evidence project and build a static
|
|
43
|
+
site; without Node.js or when Evidence fails, ForgeQE's own page (--static: always,
|
|
44
|
+
--evidence: fail instead)
|
|
45
|
+
forgeqe bi init scaffold the Evidence project and install its packages
|
|
40
46
|
forgeqe bi sync refresh the CSV datasets and generated Evidence pages, without building
|
|
41
47
|
forgeqe pr-tests [pr] [--base BRANCH] [--agent] [--run] [--post]
|
|
42
48
|
the tests a pull request (or the commits since BRANCH) can break; --run runs
|
|
@@ -52,6 +58,7 @@ Usage:
|
|
|
52
58
|
forgeqe visual approve [run-dir] [--page a,b] [--yes]
|
|
53
59
|
make the run's screenshots the new baselines (an intended change)
|
|
54
60
|
forgeqe explore <story> [--url URL] [--charters N] [--roles a,b] [--focus TEXT] [--yes] [--file-bugs]
|
|
61
|
+
[--browser-provider local|browserstack|testmu]
|
|
55
62
|
exploratory testing of a story (ado:123, SHOP-12, a link, or its text):
|
|
56
63
|
charters beyond the scripted cases, defects confirmed by a second run,
|
|
57
64
|
proposed test cases, bugs for confirmed defects; exits 1 on a defect
|
|
@@ -104,6 +111,7 @@ page with ranked locators and a confidence %, plus the steps of each goal's last
|
|
|
104
111
|
--replay verify replay a known goal without the LLM, then one LLM step checks the result (default)
|
|
105
112
|
--replay trust replay and finish without the LLM when every step works
|
|
106
113
|
--replay off never replay; still record pages
|
|
114
|
+
--browser-provider pick which configured browser provider runs this command's browser work
|
|
107
115
|
|
|
108
116
|
ForgeQE's agent (forgeqe start, forgeqe run) runs on the agent runtime with the active profile's model (or code.model) and
|
|
109
117
|
calls ForgeQE's tools: browser runs, reading and running test cases, writing tests and running them, page memory, pushing
|
|
@@ -132,8 +140,9 @@ The report is report.html (step verdicts, timeline with screenshots, timings, en
|
|
|
132
140
|
out/suites/<time>/; every run folder also has its own report.html.
|
|
133
141
|
|
|
134
142
|
Static BI (bi: in forgeqe.yaml): \`forgeqe bi\` writes curated CSV datasets from ForgeQE's own suite and readiness
|
|
135
|
-
reports plus optional Azure DevOps WIQL and Jira JQL datasets, scaffolds an Evidence project
|
|
136
|
-
\`evidence build\` to produce static
|
|
143
|
+
reports plus optional Azure DevOps WIQL and Jira JQL datasets, scaffolds an Evidence project (@evidence-dev/evidence
|
|
144
|
+
40.x, needs Node.js), and runs \`evidence sources\` and \`evidence build\` to produce a static site in the build directory;
|
|
145
|
+
when that cannot run, it writes ForgeQE's own self-contained page instead. No server stays up between runs.
|
|
137
146
|
|
|
138
147
|
Healing (forgeqe heal): ForgeQE runs the tests (the command given, code.test_command, or one the agent finds).
|
|
139
148
|
For each failing test, Scribe re-runs its flow in the real browser: when the flow still works, the test is out
|
package/src/cli/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { goalFor, loadCases } from "../cases/index.ts"
|
|
|
10
10
|
import { codeSkipReason, latestRunDir, runCodeAgent } from "../code/index.ts"
|
|
11
11
|
import { detailFrom } from "../detail.ts"
|
|
12
12
|
import { ensureOpencode, INSTALL_HINT } from "../code/runtime.ts"
|
|
13
|
-
import { loadConfig, parseDuration, replayMode, selectProfile, userConfigDir, type AppConfig } from "../config.ts"
|
|
13
|
+
import { browserProvider, loadConfig, parseDuration, replayMode, selectProfile, userConfigDir, type AppConfig, type BrowserProvider } from "../config.ts"
|
|
14
14
|
import { runHeal } from "../heal/loop.ts"
|
|
15
15
|
import { formatHeal } from "../heal/report.ts"
|
|
16
16
|
import { backgroundMonitors, stopBackgroundMonitors } from "../monitor/background.ts"
|
|
@@ -62,6 +62,11 @@ const LOGGED_COMMANDS = new Set([...AGENT_COMMANDS, "mcp", "login", "setup", "bi
|
|
|
62
62
|
/** How much of the agents' work to print: brief unless --verbose. */
|
|
63
63
|
const detailOf = (flags: Flags) => detailFrom(boolFlag(flags, "verbose"))
|
|
64
64
|
|
|
65
|
+
function browserProviderFlag(flags: Flags): BrowserProvider | undefined {
|
|
66
|
+
const value = strFlag(flags, "browser-provider")
|
|
67
|
+
return value ? browserProvider(value) : undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
function debugRequested(flags: Flags): boolean {
|
|
66
71
|
return boolFlag(flags, "debug") || /^(1|true|yes|on)$/i.test(process.env.FORGEQE_DEBUG || "")
|
|
67
72
|
}
|
|
@@ -454,7 +459,17 @@ async function cmdExplore(args: string[], flags: Flags): Promise<number> {
|
|
|
454
459
|
process.once("SIGINT", onSigint)
|
|
455
460
|
try {
|
|
456
461
|
const ask = process.stdin.isTTY && !boolFlag(flags, "yes") ? terminalAsk(controller.signal) : undefined
|
|
457
|
-
const ctx: ToolContext = {
|
|
462
|
+
const ctx: ToolContext = {
|
|
463
|
+
config: cfg,
|
|
464
|
+
cwd: process.cwd(),
|
|
465
|
+
session: "once",
|
|
466
|
+
signal: controller.signal,
|
|
467
|
+
headed: boolFlag(flags, "headed"),
|
|
468
|
+
browserProvider: browserProviderFlag(flags),
|
|
469
|
+
maxSteps: maxStepsFlag(flags),
|
|
470
|
+
emit: toolPrinter(color, (line) => console.log(line), detailOf(flags)),
|
|
471
|
+
...(ask ? { ask } : {}),
|
|
472
|
+
}
|
|
458
473
|
const roles = (strFlag(flags, "roles") || "").split(",").map((s) => s.trim()).filter(Boolean)
|
|
459
474
|
const out = (await TOOLS.find((t) => t.name === "explore_story")!.run(
|
|
460
475
|
{ story, url: strFlag(flags, "url"), charters: Number(strFlag(flags, "charters")) || undefined, ...(roles.length ? { roles } : {}), focus: strFlag(flags, "focus"), file_bugs: boolFlag(flags, "file-bugs") },
|
|
@@ -474,11 +489,16 @@ async function cmdBi(args: string[], flags: Flags): Promise<number> {
|
|
|
474
489
|
dataDir: strFlag(flags, "data-dir"),
|
|
475
490
|
buildDir: strFlag(flags, "build-dir") || strFlag(flags, "out"),
|
|
476
491
|
}
|
|
492
|
+
const shown = (file: string) => path.relative(process.cwd(), file) || file
|
|
493
|
+
const printNotes = (notes: string[]) => {
|
|
494
|
+
for (const note of notes) console.log(`note ${note}`)
|
|
495
|
+
}
|
|
477
496
|
switch (action) {
|
|
478
497
|
case "init": {
|
|
479
498
|
const out = await initBiProject(cfg, overrides)
|
|
480
499
|
console.log(`Evidence project ${out.evidenceDir}`)
|
|
481
|
-
for (const file of out.files) console.log(`generated ${
|
|
500
|
+
for (const file of out.files) console.log(`generated ${shown(file)}`)
|
|
501
|
+
printNotes(out.notes)
|
|
482
502
|
return 0
|
|
483
503
|
}
|
|
484
504
|
case "sync": {
|
|
@@ -490,23 +510,30 @@ async function cmdBi(args: string[], flags: Flags): Promise<number> {
|
|
|
490
510
|
console.log(`Case results ${out.caseResults}`)
|
|
491
511
|
console.log(`Readiness reports ${out.readinessReports}`)
|
|
492
512
|
console.log(`External datasets ${out.externalDatasets}`)
|
|
493
|
-
console.log(`Summary ${
|
|
513
|
+
console.log(`Summary ${shown(out.summaryFile)}`)
|
|
514
|
+
printNotes(out.notes)
|
|
494
515
|
return 0
|
|
495
516
|
}
|
|
496
517
|
case "build": {
|
|
497
|
-
const out = await buildBi(cfg, overrides)
|
|
518
|
+
const out = await buildBi(cfg, overrides, boolFlag(flags, "static") ? "static" : boolFlag(flags, "evidence") ? "evidence" : "auto")
|
|
498
519
|
console.log(`Evidence project ${out.evidenceDir}`)
|
|
499
520
|
console.log(`Static site ${out.buildDir}`)
|
|
500
|
-
console.log(`Index ${
|
|
501
|
-
console.log(`Summary ${
|
|
521
|
+
console.log(`Index ${shown(out.indexFile)}`)
|
|
522
|
+
console.log(`Summary ${shown(out.summaryFile)}`)
|
|
502
523
|
console.log(`Suite runs ${out.suites}`)
|
|
503
524
|
console.log(`Case results ${out.caseResults}`)
|
|
504
525
|
console.log(`Readiness reports ${out.readinessReports}`)
|
|
505
526
|
console.log(`External datasets ${out.externalDatasets}`)
|
|
527
|
+
if (out.engine === "evidence") {
|
|
528
|
+
console.log("Built with Evidence. Serve the folder to view it (npx serve, or evidence preview): opened from disk it cannot load its data.")
|
|
529
|
+
} else {
|
|
530
|
+
console.log(`Built with ForgeQE's own page, without Evidence (${out.fallbackReason}). It opens straight from disk.`)
|
|
531
|
+
}
|
|
532
|
+
printNotes(out.notes)
|
|
506
533
|
return 0
|
|
507
534
|
}
|
|
508
535
|
default:
|
|
509
|
-
throw new Error("Usage: forgeqe bi [init|sync|build] [--evidence-dir DIR] [--data-dir DIR] [--build-dir DIR]")
|
|
536
|
+
throw new Error("Usage: forgeqe bi [init|sync|build] [--evidence-dir DIR] [--data-dir DIR] [--build-dir DIR] [--static | --evidence]")
|
|
510
537
|
}
|
|
511
538
|
}
|
|
512
539
|
|
|
@@ -913,6 +940,7 @@ async function runAgent(cfg: AppConfig, flags: Flags, text: string, maxSteps: nu
|
|
|
913
940
|
signal: controller.signal,
|
|
914
941
|
context: () => ({
|
|
915
942
|
headed: boolFlag(flags, "headed"),
|
|
943
|
+
browserProvider: browserProviderFlag(flags),
|
|
916
944
|
maxSteps,
|
|
917
945
|
url: strFlag(flags, "url"),
|
|
918
946
|
...memoryFlags(flags),
|
|
@@ -973,7 +1001,17 @@ async function cmdMcp(args: string[], flags: Flags): Promise<number> {
|
|
|
973
1001
|
const repo = repoFlag(flags)
|
|
974
1002
|
const cwd = repo || process.cwd()
|
|
975
1003
|
const log = toolPrinter(false, (line) => process.stderr.write(`${line}\n`), detailOf(flags))
|
|
976
|
-
const context = (): ToolContext => ({
|
|
1004
|
+
const context = (): ToolContext => ({
|
|
1005
|
+
config: cfg,
|
|
1006
|
+
cwd,
|
|
1007
|
+
headed: boolFlag(flags, "headed"),
|
|
1008
|
+
browserProvider: browserProviderFlag(flags),
|
|
1009
|
+
maxSteps: maxStepsFlag(flags),
|
|
1010
|
+
url: strFlag(flags, "url"),
|
|
1011
|
+
...memoryFlags(flags),
|
|
1012
|
+
emit: log,
|
|
1013
|
+
session: "server",
|
|
1014
|
+
})
|
|
977
1015
|
const server = createMcpServer({ tools: TOOLS, context, instructions: () => environmentNotes(context()), version: packageVersion() })
|
|
978
1016
|
process.stderr.write(`ForgeQE MCP server on stdio (${TOOLS.length} tools, working in ${cwd})\n`)
|
|
979
1017
|
await serveStdio(server)
|
|
@@ -988,6 +1026,7 @@ async function runGoal(cfg: AppConfig, flags: Flags, goal: string, maxSteps: num
|
|
|
988
1026
|
const { result, files } = await executeRun({
|
|
989
1027
|
config: cfg,
|
|
990
1028
|
goal,
|
|
1029
|
+
browserProvider: browserProviderFlag(flags),
|
|
991
1030
|
url: strFlag(flags, "url"),
|
|
992
1031
|
headed: boolFlag(flags, "headed"),
|
|
993
1032
|
maxSteps,
|
|
@@ -1129,6 +1168,7 @@ async function cmdHeal(args: string[], flags: Flags): Promise<number> {
|
|
|
1129
1168
|
repo,
|
|
1130
1169
|
command,
|
|
1131
1170
|
outDir: strFlag(flags, "out"),
|
|
1171
|
+
browserProvider: browserProviderFlag(flags),
|
|
1132
1172
|
headed: boolFlag(flags, "headed"),
|
|
1133
1173
|
maxSteps: maxStepsFlag(flags),
|
|
1134
1174
|
url: strFlag(flags, "url"),
|
|
@@ -1200,6 +1240,7 @@ async function cmdMonitor(refs: string[], flags: Flags): Promise<number> {
|
|
|
1200
1240
|
config: cfg,
|
|
1201
1241
|
checks,
|
|
1202
1242
|
once,
|
|
1243
|
+
browserProvider: browserProviderFlag(flags),
|
|
1203
1244
|
headed: boolFlag(flags, "headed"),
|
|
1204
1245
|
url: strFlag(flags, "url"),
|
|
1205
1246
|
skills: skillFlags(flags),
|
|
@@ -1238,6 +1279,7 @@ async function cmdSuite(refs: string[], flags: Flags): Promise<number> {
|
|
|
1238
1279
|
config: cfg,
|
|
1239
1280
|
cases,
|
|
1240
1281
|
repo,
|
|
1282
|
+
browserProvider: browserProviderFlag(flags),
|
|
1241
1283
|
parallel,
|
|
1242
1284
|
outDir: strFlag(flags, "out"),
|
|
1243
1285
|
writeBack: boolFlag(flags, "no-write-back") ? false : undefined,
|
package/src/config.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { ragConfig, type RagConfig } from "./cases/rag.ts"
|
|
|
18
18
|
|
|
19
19
|
export type BrowserProvider = "local" | "browserstack" | "testmu"
|
|
20
20
|
export type RemoteBrowserProvider = Exclude<BrowserProvider, "local">
|
|
21
|
+
export type BrowserRemoteConfigs = Partial<Record<RemoteBrowserProvider, BrowserRemoteConfig>>
|
|
22
|
+
export type BrowserProviderFiles = Partial<Record<RemoteBrowserProvider, string>>
|
|
21
23
|
|
|
22
24
|
export type BrowserRemoteConfig = {
|
|
23
25
|
provider: RemoteBrowserProvider
|
|
@@ -47,6 +49,8 @@ export type BrowserConfig = {
|
|
|
47
49
|
allowedHosts: string[]
|
|
48
50
|
startUrl?: string
|
|
49
51
|
remote?: BrowserRemoteConfig
|
|
52
|
+
remotes: BrowserRemoteConfigs
|
|
53
|
+
providerFiles: BrowserProviderFiles
|
|
50
54
|
/** ForgeQE overlay in a visible browser: edge glow, a bar with the current step, the person's input locked. */
|
|
51
55
|
overlay: boolean
|
|
52
56
|
/** A second model call checks the finished run against the goal, and its verdict can fail a run the agent completed. */
|
|
@@ -435,43 +439,104 @@ const REMOTE_BROWSER_DEFAULTS: Record<RemoteBrowserProvider, Pick<BrowserRemoteC
|
|
|
435
439
|
browserstack: {
|
|
436
440
|
usernameEnv: "BROWSERSTACK_USERNAME",
|
|
437
441
|
accessKeyEnv: "BROWSERSTACK_ACCESS_KEY",
|
|
438
|
-
endpoint: "wss://cdp.browserstack.com/
|
|
442
|
+
endpoint: "wss://cdp.browserstack.com/puppeteer",
|
|
439
443
|
},
|
|
440
444
|
testmu: {
|
|
441
445
|
usernameEnv: "LT_USERNAME",
|
|
442
446
|
accessKeyEnv: "LT_ACCESS_KEY",
|
|
443
|
-
endpoint: "wss://cdp.lambdatest.com/
|
|
447
|
+
endpoint: "wss://cdp.lambdatest.com/puppeteer",
|
|
444
448
|
},
|
|
445
449
|
}
|
|
446
450
|
|
|
451
|
+
const REMOTE_BROWSER_PROVIDERS = Object.keys(REMOTE_BROWSER_DEFAULTS) as RemoteBrowserProvider[]
|
|
452
|
+
const REMOTE_BROWSER_KEYS = new Set([
|
|
453
|
+
"browser",
|
|
454
|
+
"browser_version",
|
|
455
|
+
"browserVersion",
|
|
456
|
+
"os",
|
|
457
|
+
"os_version",
|
|
458
|
+
"osVersion",
|
|
459
|
+
"platform",
|
|
460
|
+
"username_env",
|
|
461
|
+
"usernameEnv",
|
|
462
|
+
"access_key_env",
|
|
463
|
+
"accessKeyEnv",
|
|
464
|
+
"endpoint",
|
|
465
|
+
"project",
|
|
466
|
+
"build",
|
|
467
|
+
"name",
|
|
468
|
+
"local",
|
|
469
|
+
"local_testing",
|
|
470
|
+
"extra",
|
|
471
|
+
"capabilities",
|
|
472
|
+
])
|
|
473
|
+
|
|
447
474
|
function objectMap(raw: unknown, where: string): Record<string, unknown> | undefined {
|
|
448
475
|
if (raw === undefined || raw === null) return undefined
|
|
449
476
|
if (typeof raw !== "object" || Array.isArray(raw)) throw new Error(`${where} must be a map of names to values.`)
|
|
450
477
|
return { ...(raw as Record<string, unknown>) }
|
|
451
478
|
}
|
|
452
479
|
|
|
453
|
-
function
|
|
480
|
+
function browserProviderFiles(raw: Record<string, unknown>, base: string): BrowserProviderFiles {
|
|
481
|
+
const files = objectMap(raw.providers, "browser.providers")
|
|
482
|
+
const out: BrowserProviderFiles = {}
|
|
483
|
+
for (const provider of REMOTE_BROWSER_PROVIDERS) {
|
|
484
|
+
const file = files?.[provider]
|
|
485
|
+
if (file === undefined || file === null || file === false || String(file).trim() === "") continue
|
|
486
|
+
out[provider] = path.resolve(base, String(file))
|
|
487
|
+
}
|
|
488
|
+
return out
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function browserProviderFileSection(file: string, provider: RemoteBrowserProvider): Record<string, unknown> {
|
|
492
|
+
if (!existsSync(file)) throw new Error(`browser.providers.${provider} not found: ${file}`)
|
|
493
|
+
const doc = readYaml(file)
|
|
494
|
+
const section = Object.keys(asRecord(doc.browser)).length ? { ...asRecord(doc.browser) } : { ...doc }
|
|
495
|
+
if (!section.provider && doc.provider !== undefined) section.provider = doc.provider
|
|
496
|
+
if (!Object.keys(asRecord(section.remote)).length) {
|
|
497
|
+
const rootRemote = asRecord(doc.remote)
|
|
498
|
+
if (Object.keys(rootRemote).length) section.remote = rootRemote
|
|
499
|
+
else {
|
|
500
|
+
const directRemote = Object.fromEntries(Object.entries(section).filter(([k]) => REMOTE_BROWSER_KEYS.has(k)))
|
|
501
|
+
if (Object.keys(directRemote).length) section.remote = directRemote
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (section.provider !== undefined && browserProvider(section.provider) !== provider) {
|
|
505
|
+
throw new Error(`browser.providers.${provider} points to ${file}, but that file declares provider ${String(section.provider)}.`)
|
|
506
|
+
}
|
|
507
|
+
return section
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function browserRemoteConfig(raw: Record<string, unknown>, provider: BrowserProvider, where = "browser"): BrowserRemoteConfig | undefined {
|
|
454
511
|
if (provider === "local") return undefined
|
|
455
512
|
const remote = asRecord(raw.remote)
|
|
456
513
|
const defaults = REMOTE_BROWSER_DEFAULTS[provider]
|
|
457
514
|
const browser = String(remote.browser || "").trim()
|
|
458
|
-
if (!browser) throw new Error(
|
|
515
|
+
if (!browser) throw new Error(`${where}.remote.browser is required when browser.provider is ${provider}.`)
|
|
516
|
+
// The engine drives the browser over the Chrome DevTools Protocol, so the grid must hand back a Chromium browser.
|
|
517
|
+
if (!/chrome|edge|chromium/i.test(browser) || /^(playwright|pw)-/i.test(browser)) {
|
|
518
|
+
throw new Error(`${where}.remote.browser is "${browser}": ForgeQE drives remote browsers over the Chrome DevTools Protocol, so use chrome or edge.`)
|
|
519
|
+
}
|
|
459
520
|
const browserVersion = String(remote.browser_version || remote.browserVersion || "latest").trim() || "latest"
|
|
460
521
|
const os = String(remote.os || "").trim() || undefined
|
|
461
522
|
const osVersion = String(remote.os_version || remote.osVersion || "").trim() || undefined
|
|
462
523
|
const platform = String(remote.platform || "").trim() || undefined
|
|
463
524
|
if (provider === "browserstack" && (!os || !osVersion)) {
|
|
464
|
-
throw new Error(
|
|
525
|
+
throw new Error(`${where}.remote.os and ${where}.remote.os_version are required when browser.provider is browserstack.`)
|
|
465
526
|
}
|
|
466
527
|
if (provider === "testmu" && !platform && !os) {
|
|
467
|
-
throw new Error(
|
|
528
|
+
throw new Error(`${where}.remote.platform is required when browser.provider is testmu, or set ${where}.remote.os (and optionally os_version).`)
|
|
529
|
+
}
|
|
530
|
+
const endpoint = String(remote.endpoint || defaults.endpoint).trim().replace(/\/+$/, "")
|
|
531
|
+
if (/\/playwright$/i.test(endpoint)) {
|
|
532
|
+
throw new Error(`${where}.remote.endpoint ${endpoint} speaks Playwright's own protocol; ForgeQE needs the CDP endpoint (${defaults.endpoint}).`)
|
|
468
533
|
}
|
|
469
|
-
const extra = objectMap(remote.extra ?? remote.capabilities,
|
|
534
|
+
const extra = objectMap(remote.extra ?? remote.capabilities, `${where}.remote.extra`)
|
|
470
535
|
return {
|
|
471
536
|
provider,
|
|
472
537
|
usernameEnv: String(remote.username_env || remote.usernameEnv || defaults.usernameEnv),
|
|
473
538
|
accessKeyEnv: String(remote.access_key_env || remote.accessKeyEnv || defaults.accessKeyEnv),
|
|
474
|
-
endpoint
|
|
539
|
+
endpoint,
|
|
475
540
|
browser,
|
|
476
541
|
browserVersion,
|
|
477
542
|
...(os ? { os } : {}),
|
|
@@ -485,6 +550,35 @@ function browserRemoteConfig(raw: Record<string, unknown>, provider: BrowserProv
|
|
|
485
550
|
}
|
|
486
551
|
}
|
|
487
552
|
|
|
553
|
+
function browserRemoteConfigs(browser: Record<string, unknown>, provider: BrowserProvider, base: string): { remotes: BrowserRemoteConfigs; providerFiles: BrowserProviderFiles } {
|
|
554
|
+
const providerFiles = browserProviderFiles(browser, base)
|
|
555
|
+
const remotes: BrowserRemoteConfigs = {}
|
|
556
|
+
for (const remoteProvider of REMOTE_BROWSER_PROVIDERS) {
|
|
557
|
+
const fromFile = providerFiles[remoteProvider] ? browserProviderFileSection(providerFiles[remoteProvider]!, remoteProvider) : undefined
|
|
558
|
+
const inline = provider === remoteProvider ? browser : undefined
|
|
559
|
+
if (!fromFile && !inline) continue
|
|
560
|
+
const where = fromFile ? `browser.providers.${remoteProvider}` : "browser"
|
|
561
|
+
remotes[remoteProvider] = browserRemoteConfig(
|
|
562
|
+
{
|
|
563
|
+
...(fromFile || {}),
|
|
564
|
+
...(inline || {}),
|
|
565
|
+
remote: { ...asRecord(fromFile?.remote), ...asRecord(inline?.remote) },
|
|
566
|
+
},
|
|
567
|
+
remoteProvider,
|
|
568
|
+
where,
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
return { remotes, providerFiles }
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function browserConfigFor(config: Pick<AppConfig, "browser">, provider = config.browser.provider): Pick<BrowserConfig, "provider" | "remote"> {
|
|
575
|
+
const chosen = provider || config.browser.provider || "local"
|
|
576
|
+
if (chosen === "local") return { provider: chosen }
|
|
577
|
+
const remote = config.browser.remotes?.[chosen] || ((config.browser.provider === chosen || !config.browser.provider) ? config.browser.remote : undefined)
|
|
578
|
+
if (remote) return { provider: chosen, remote }
|
|
579
|
+
throw new Error(`Browser provider ${chosen} is not configured. Add browser.providers.${chosen} to forgeqe.yaml, or set browser.provider: ${chosen} with browser.remote.`)
|
|
580
|
+
}
|
|
581
|
+
|
|
488
582
|
function profileFromRaw(name: string, raw: Record<string, unknown>): LlmProfile {
|
|
489
583
|
const provider = String(raw.provider || "openai") as LlmProfile["provider"]
|
|
490
584
|
return {
|
|
@@ -656,6 +750,8 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
656
750
|
: []
|
|
657
751
|
|
|
658
752
|
const sessionsDir = configPath ? path.join(projectDir(base), "sessions") : path.join(forgeqeHome(), "sessions")
|
|
753
|
+
const provider = browserProvider(browser.provider)
|
|
754
|
+
const { remotes, providerFiles } = browserRemoteConfigs(browser, provider, base)
|
|
659
755
|
const config: AppConfig = {
|
|
660
756
|
configPath,
|
|
661
757
|
mcp: extraMcpServers(yaml.mcp, base),
|
|
@@ -663,7 +759,7 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
663
759
|
profiles,
|
|
664
760
|
llm: profiles[activeProfile] || { ...NO_PROFILE },
|
|
665
761
|
browser: {
|
|
666
|
-
provider
|
|
762
|
+
provider,
|
|
667
763
|
headless: browser.headless !== false,
|
|
668
764
|
width: Number(viewport.width || 1280),
|
|
669
765
|
height: Number(viewport.height || 800),
|
|
@@ -672,7 +768,9 @@ export function loadConfig(opts?: { configPath?: string; profile?: string }): Ap
|
|
|
672
768
|
visionDetail: visionDetail(browser.vision_detail),
|
|
673
769
|
allowedHosts: hosts,
|
|
674
770
|
startUrl: browser.start_url ? String(browser.start_url) : undefined,
|
|
675
|
-
remote:
|
|
771
|
+
remote: provider === "local" ? undefined : remotes[provider],
|
|
772
|
+
remotes,
|
|
773
|
+
providerFiles,
|
|
676
774
|
overlay: browser.overlay !== false,
|
|
677
775
|
judge: browser.judge === true,
|
|
678
776
|
},
|
|
@@ -953,9 +1051,12 @@ function apiConfig(a: Record<string, unknown>): ApiConfig {
|
|
|
953
1051
|
}
|
|
954
1052
|
}
|
|
955
1053
|
|
|
1054
|
+
/** The tables forgeqe bi always exports; a dataset id may not reuse one. */
|
|
1055
|
+
export const BI_BUILTIN_TABLES = ["suites", "case_results", "step_results", "writebacks", "readiness", "readiness_stories", "readiness_cases", "external_datasets"]
|
|
1056
|
+
|
|
956
1057
|
function biConfig(b: Record<string, unknown>, base: string, outDir: string, configPath: string | undefined): BiConfig {
|
|
957
1058
|
const evidenceDir = path.resolve(base, String(b.evidence_dir || path.join(".", "evidence")))
|
|
958
|
-
const defaultDataDir = path.join(evidenceDir, "sources", "forgeqe"
|
|
1059
|
+
const defaultDataDir = path.join(evidenceDir, "sources", "forgeqe")
|
|
959
1060
|
const title = String(b.title || "ForgeQE BI").trim() || "ForgeQE BI"
|
|
960
1061
|
const datasets = Object.entries(asRecord(b.datasets)).map(([id, value]) => biDataset(id, asRecord(value)))
|
|
961
1062
|
return {
|
|
@@ -968,7 +1069,9 @@ function biConfig(b: Record<string, unknown>, base: string, outDir: string, conf
|
|
|
968
1069
|
}
|
|
969
1070
|
|
|
970
1071
|
function biDataset(id: string, raw: Record<string, unknown>): BiDatasetConfig {
|
|
971
|
-
|
|
1072
|
+
// The id is the dataset's table name in Evidence's SQL (forgeqe.<id>), so it must be a plain identifier.
|
|
1073
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(id)) throw new Error(`bi.datasets.${id}: ids may only use letters, digits and _, and must not start with a digit.`)
|
|
1074
|
+
if (BI_BUILTIN_TABLES.includes(id)) throw new Error(`bi.datasets.${id}: ${id} is one of ForgeQE's own BI tables; choose another id.`)
|
|
972
1075
|
const title = String(raw.title || id.replace(/[-_]+/g, " ").trim()).trim()
|
|
973
1076
|
const kind = String(raw.kind || "").trim().toLowerCase()
|
|
974
1077
|
if (kind === "ado_wiql") {
|
package/src/heal/loop.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { addUsage, costOf, meterSince, meterSnapshot, type Cost } from "../usage
|
|
|
7
7
|
import { openSession, type OpencodeSession } from "../code/runtime.ts"
|
|
8
8
|
import { runTestCommand, type TestRun } from "../code/testrun.ts"
|
|
9
9
|
import { changedFiles, ownPaths, protectPatterns, restoreRemoved, snapshot } from "../code/worktree.ts"
|
|
10
|
-
import type { AppConfig } from "../config.ts"
|
|
10
|
+
import type { AppConfig, BrowserProvider } from "../config.ts"
|
|
11
11
|
import { MCP_NAME, TOOL_TIMEOUT_MS, localToolServer } from "../mcp/server.ts"
|
|
12
12
|
import { TOOLS, environmentNotes, type ToolContext, type ToolEvent } from "../mcp/tools.ts"
|
|
13
13
|
import { runDirectory } from "../run.ts"
|
|
@@ -79,6 +79,7 @@ export type HealOptions = {
|
|
|
79
79
|
/** The repo's test command; default code.test_command, else the agent finds it. */
|
|
80
80
|
command?: string
|
|
81
81
|
outDir?: string
|
|
82
|
+
browserProvider?: BrowserProvider
|
|
82
83
|
headed?: boolean
|
|
83
84
|
maxSteps?: number
|
|
84
85
|
url?: string
|
|
@@ -149,6 +150,7 @@ export async function runHeal(opts: HealOptions): Promise<HealResult> {
|
|
|
149
150
|
config: runsConfig,
|
|
150
151
|
cwd: repo,
|
|
151
152
|
headed: opts.headed,
|
|
153
|
+
browserProvider: opts.browserProvider,
|
|
152
154
|
maxSteps: opts.maxSteps,
|
|
153
155
|
url: opts.url,
|
|
154
156
|
signal: opts.signal,
|
package/src/init.ts
CHANGED
|
@@ -64,6 +64,24 @@ export function initWorkspace(
|
|
|
64
64
|
// Prompts, skills, and product knowledge templates, to read and change: .forgeqe/ in a project, ~/.forgeqe for the user.
|
|
65
65
|
const root = home ? forgeqeHome() : projectDir(directory)
|
|
66
66
|
const shown = (p: string) => (home ? p : path.relative(directory, p) || p)
|
|
67
|
+
for (const [srcName, destName] of [
|
|
68
|
+
["browserstack.example.yaml", "browserstack.yaml"],
|
|
69
|
+
["testmu.example.yaml", "testmu.yaml"],
|
|
70
|
+
] as const) {
|
|
71
|
+
const src = path.join(ROOT, srcName)
|
|
72
|
+
const dest = path.join(root, destName)
|
|
73
|
+
if (!existsSync(src)) {
|
|
74
|
+
notes.push(`Template missing: ${src}`)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
if (existsSync(dest) && !opts.force) {
|
|
78
|
+
notes.push(`Kept existing ${shown(dest)}`)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
mkdirSync(path.dirname(dest), { recursive: true })
|
|
82
|
+
writeFileSync(dest, readFileSync(src, "utf8"), "utf8")
|
|
83
|
+
created.push(dest)
|
|
84
|
+
}
|
|
67
85
|
for (const { what, src, dest, keys } of [
|
|
68
86
|
{ what: "prompts", src: PROMPTS_DIR, dest: path.join(root, "prompts"), keys: entries(PROMPTS_DIR, (e) => e.endsWith(".md")) },
|
|
69
87
|
{ what: "skills", src: PREBUILT_DIR, dest: path.join(root, "skills"), keys: entries(PREBUILT_DIR, (e) => existsSync(path.join(PREBUILT_DIR, e, "SKILL.md"))) },
|
package/src/mcp/tools.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { buildHandoff } from "../code/handoff.ts"
|
|
|
15
15
|
import { latestRunDir, runCodeAgent } from "../code/index.ts"
|
|
16
16
|
import type { OpencodeSession } from "../code/runtime.ts"
|
|
17
17
|
import { runTestCommand } from "../code/testrun.ts"
|
|
18
|
-
import type
|
|
18
|
+
import { browserProvider, type AppConfig } from "../config.ts"
|
|
19
19
|
import { clearMemory, memoryFilesFor, memoryFlows, memoryOverview, memoryShow } from "../memory.ts"
|
|
20
20
|
import { executeRun, runDirectory } from "../run.ts"
|
|
21
21
|
import { counts } from "../suite/report.ts"
|
|
@@ -123,6 +123,7 @@ export type ToolContext = {
|
|
|
123
123
|
/** The repository or folder the agent works in. */
|
|
124
124
|
cwd: string
|
|
125
125
|
headed?: boolean
|
|
126
|
+
browserProvider?: AppConfig["browser"]["provider"]
|
|
126
127
|
maxSteps?: number
|
|
127
128
|
replay?: ReplayMode
|
|
128
129
|
memory?: boolean
|
|
@@ -256,7 +257,7 @@ export const TOOLS: McpTool[] = [
|
|
|
256
257
|
{
|
|
257
258
|
name: "browser_run",
|
|
258
259
|
description:
|
|
259
|
-
"Carry out a goal in a real browser with ForgeQE's browser agent and verify the outcome. The agent picks the site from the goal unless start_url is given. Returns pass/fail, the agent's summary, the steps, and the run folder (run.json, workflow.yaml with ranked locators, trace.md, steps and screenshots, report.html). Takes minutes.",
|
|
260
|
+
"Carry out a goal in a real browser with ForgeQE's browser agent and verify the outcome. The browser comes from forgeqe.yaml: local Chrome or Chromium by default, or BrowserStack or TestMu AI/LambdaTest when browser.provider selects one. The agent picks the site from the goal unless start_url is given. Returns pass/fail, the agent's summary, the steps, and the run folder (run.json, workflow.yaml with ranked locators, trace.md, steps and screenshots, report.html). Takes minutes.",
|
|
260
261
|
inputSchema: {
|
|
261
262
|
type: "object",
|
|
262
263
|
properties: {
|
|
@@ -266,6 +267,12 @@ export const TOOLS: McpTool[] = [
|
|
|
266
267
|
"The final instructions for the browser agent: what to do and what to check, in plain language, with every value it needs. Only the decided task; never your own questions, alternatives, or reasoning about it.",
|
|
267
268
|
},
|
|
268
269
|
start_url: { type: "string", description: "Page to open first, only when the user named one" },
|
|
270
|
+
browser_provider: {
|
|
271
|
+
type: "string",
|
|
272
|
+
enum: ["local", "browserstack", "testmu"],
|
|
273
|
+
description:
|
|
274
|
+
"Override the browser provider for this run when the user asked for one specifically. local uses the machine's own browser; browserstack and testmu use their configured YAML or browser.remote settings.",
|
|
275
|
+
},
|
|
269
276
|
resume_from: {
|
|
270
277
|
type: "string",
|
|
271
278
|
description:
|
|
@@ -287,6 +294,7 @@ export const TOOLS: McpTool[] = [
|
|
|
287
294
|
const from = args.resume_from ? String(args.resume_from) : ""
|
|
288
295
|
const resumeFrom = from ? (existsSync(path.resolve(ctx.cwd, from)) ? path.resolve(ctx.cwd, from) : from) : undefined
|
|
289
296
|
const url = args.start_url ? String(args.start_url) : ctx.url
|
|
297
|
+
const selectedBrowser = args.browser_provider ? browserProvider(args.browser_provider) : ctx.browserProvider
|
|
290
298
|
const runDir = runDirectory(ctx.config.export.outDir)
|
|
291
299
|
const emit = ctx.emit || (() => {})
|
|
292
300
|
const t0 = Date.now()
|
|
@@ -295,6 +303,7 @@ export const TOOLS: McpTool[] = [
|
|
|
295
303
|
const { result, files } = await (ctx.execute || executeRun)({
|
|
296
304
|
config: ctx.config,
|
|
297
305
|
goal,
|
|
306
|
+
browserProvider: selectedBrowser,
|
|
298
307
|
url,
|
|
299
308
|
headed: ctx.headed,
|
|
300
309
|
maxSteps: ctx.maxSteps,
|
|
@@ -410,13 +419,19 @@ export const TOOLS: McpTool[] = [
|
|
|
410
419
|
{
|
|
411
420
|
name: "run_test_cases",
|
|
412
421
|
description:
|
|
413
|
-
"Run test cases in real browsers: each case is one browser goal whose steps are done in order and whose expected results are checked, with a verdict for every step. Writes report.html (steps, screenshots, timings) and junit.xml, posts results back to Azure DevOps, Jira, Xray, TestRail, Zephyr Scale, or qTest unless write_back is false, and files or updates bugs for failures when bugs are set up. In a session that writes tests, each passed case is handed to ForgeQE's coding agent as soon as its browser run ends, and its test is written and run before the next case starts. Returns each case's result, step verdicts, run folder, and test (file, command, status, or why none was written). Takes minutes.",
|
|
422
|
+
"Run test cases in real browsers: each case is one browser goal whose steps are done in order and whose expected results are checked, with a verdict for every step. The browser comes from forgeqe.yaml: local Chrome or Chromium by default, or BrowserStack or TestMu AI/LambdaTest when browser.provider selects one. Writes report.html (steps, screenshots, timings) and junit.xml, posts results back to Azure DevOps, Jira, Xray, TestRail, Zephyr Scale, or qTest unless write_back is false, and files or updates bugs for failures when bugs are set up. In a session that writes tests, each passed case is handed to ForgeQE's coding agent as soon as its browser run ends, and its test is written and run before the next case starts. Returns each case's result, step verdicts, run folder, and test (file, command, status, or why none was written). Takes minutes.",
|
|
414
423
|
inputSchema: {
|
|
415
424
|
type: "object",
|
|
416
425
|
properties: {
|
|
417
426
|
sources: SOURCES_SCHEMA,
|
|
418
427
|
parallel: { type: "integer", minimum: 1, maximum: 8, description: "Browsers at once (default from config)" },
|
|
419
428
|
write_back: { type: "boolean", description: "Post results to the source system (default from config)" },
|
|
429
|
+
browser_provider: {
|
|
430
|
+
type: "string",
|
|
431
|
+
enum: ["local", "browserstack", "testmu"],
|
|
432
|
+
description:
|
|
433
|
+
"Override the browser provider for every case in this suite when the user asked for one specifically.",
|
|
434
|
+
},
|
|
420
435
|
skills: SKILLS_SCHEMA,
|
|
421
436
|
},
|
|
422
437
|
required: ["sources"],
|
|
@@ -429,6 +444,7 @@ export const TOOLS: McpTool[] = [
|
|
|
429
444
|
if (!cases.length) return { ran: false, reason: "No case can run as written (suite.case_check: block).", check: checkResult(checks) }
|
|
430
445
|
if (!cases.length) throw new Error("No test cases found.")
|
|
431
446
|
const parallel = Math.min(Number(args.parallel) || ctx.config.suite.parallel, cases.length)
|
|
447
|
+
const selectedBrowser = args.browser_provider ? browserProvider(args.browser_provider) : ctx.browserProvider
|
|
432
448
|
const emit = ctx.emit || (() => {})
|
|
433
449
|
emit({ type: "suite-start", cases, parallel })
|
|
434
450
|
// In a session that writes tests, each passed case goes to the coding agent right after its own browser run,
|
|
@@ -455,6 +471,7 @@ export const TOOLS: McpTool[] = [
|
|
|
455
471
|
config: ctx.writeTests ? { ...ctx.config, suite: { ...ctx.config.suite, code: true } } : ctx.config,
|
|
456
472
|
cases,
|
|
457
473
|
...(ctx.writeTests ? { repo: ctx.cwd, coder } : {}),
|
|
474
|
+
browserProvider: selectedBrowser,
|
|
458
475
|
parallel,
|
|
459
476
|
writeBack: typeof args.write_back === "boolean" ? args.write_back : undefined,
|
|
460
477
|
headed: ctx.headed,
|
|
@@ -765,7 +782,7 @@ function knowledgeNotes(cfg: AppConfig): string {
|
|
|
765
782
|
: `No product knowledge yet: the team can describe the product, roles, rules, and test data in ${cfg.knowledge.dir} (forgeqe init adds templates).`
|
|
766
783
|
}
|
|
767
784
|
|
|
768
|
-
export function environmentNotes(ctx: Pick<ToolContext, "config" | "cwd" | "url">): string {
|
|
785
|
+
export function environmentNotes(ctx: Pick<ToolContext, "config" | "cwd" | "url" | "browserProvider">): string {
|
|
769
786
|
const { config: cfg } = ctx
|
|
770
787
|
const s = cfg.sources
|
|
771
788
|
const systems = [
|
|
@@ -802,13 +819,17 @@ export function environmentNotes(ctx: Pick<ToolContext, "config" | "cwd" | "url"
|
|
|
802
819
|
const monitor = m.checks.length || channels.length
|
|
803
820
|
? `Monitoring (monitor: in forgeqe.yaml): ${m.checks.length ? `checks ${m.checks.join(" ")}` : "no checks yet"}, every ${Math.round(m.everyMs / 60_000)} min, alerts to ${channels.length ? channels.join(" and ") : "nobody yet"}.`
|
|
804
821
|
: ""
|
|
822
|
+
const browser = cfg.browser || {}
|
|
805
823
|
const browserSkills = skillsFor(loadSkills(cfg.skills, ctx.cwd), "browser")
|
|
824
|
+
const browserProvider = browser.provider || "local"
|
|
825
|
+
const remoteProviders = (["browserstack", "testmu"] as const).filter((name) => browser.remotes?.[name])
|
|
806
826
|
const skills = browserSkills.length
|
|
807
827
|
? [`Browser skills (pass the ones a goal needs as skills to browser_run or run_test_cases):`, ...catalogLines(browserSkills)].join("\n")
|
|
808
828
|
: ""
|
|
809
829
|
return [
|
|
810
830
|
`Working directory: ${ctx.cwd}`,
|
|
811
831
|
`Browser runs are saved under ${cfg.export.outDir}; page memory is in ${cfg.memory.dir}.`,
|
|
832
|
+
`Browser provider default: ${browserProvider}.${remoteProviders.length ? ` Also configured: ${remoteProviders.join(", ")}. Pass browser_provider to browser_run or run_test_cases when the user asks for one of them.` : ""}`,
|
|
812
833
|
`Test case systems configured: ${systems.length ? systems.join("; ") : "none (files and text still work)"}.`,
|
|
813
834
|
s.confluence ? `Confluence: ${s.confluence.url}${s.confluence.space ? `, space ${s.confluence.space}` : ""} (read_confluence).` : "",
|
|
814
835
|
`Sprints (list_sprints, find_tests sprint): ${[s.ado ? "Azure DevOps iterations" : "", s.jira ? "Jira scrum boards" : ""].filter(Boolean).join(" and ") || "none configured; ask the user for dates"}.`,
|
|
@@ -831,6 +852,7 @@ export function environmentNotes(ctx: Pick<ToolContext, "config" | "cwd" | "url"
|
|
|
831
852
|
`Browser model profile: ${cfg.activeProfile} (${cfg.llm.provider}/${cfg.llm.deployment || cfg.llm.model}).`,
|
|
832
853
|
keys.length ? `Set in ForgeQE's environment (names only, usable with set_ci_secret): ${keys.join(", ")}.` : "",
|
|
833
854
|
ctx.url ? `The user pinned this start URL for browser runs: ${ctx.url}` : "",
|
|
855
|
+
ctx.browserProvider ? `The user pinned this browser provider for runs in this session: ${ctx.browserProvider}.` : "",
|
|
834
856
|
skills,
|
|
835
857
|
domainNotes(cfg),
|
|
836
858
|
knowledgeNotes(cfg),
|