@equinor/fusion-framework-cli-plugin-copilot 1.0.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/esm/commands/app/command.js +176 -0
- package/dist/esm/commands/app/command.js.map +1 -0
- package/dist/esm/commands/app/eval.js +92 -0
- package/dist/esm/commands/app/eval.js.map +1 -0
- package/dist/esm/commands/app/format.js +93 -0
- package/dist/esm/commands/app/format.js.map +1 -0
- package/dist/esm/commands/app/login.js +71 -0
- package/dist/esm/commands/app/login.js.map +1 -0
- package/dist/esm/commands/app/prompts/execute-step.prompt.js +44 -0
- package/dist/esm/commands/app/prompts/execute-step.prompt.js.map +1 -0
- package/dist/esm/commands/app/prompts/judge.prompt.js +116 -0
- package/dist/esm/commands/app/prompts/judge.prompt.js.map +1 -0
- package/dist/esm/commands/app/prompts/plan.prompt.js +47 -0
- package/dist/esm/commands/app/prompts/plan.prompt.js.map +1 -0
- package/dist/esm/commands/app/prompts/step.prompt.js +42 -0
- package/dist/esm/commands/app/prompts/step.prompt.js.map +1 -0
- package/dist/esm/commands/app/prompts/system.dev-server.prompt.js +21 -0
- package/dist/esm/commands/app/prompts/system.dev-server.prompt.js.map +1 -0
- package/dist/esm/commands/app/prompts/system.prompt.js +77 -0
- package/dist/esm/commands/app/prompts/system.prompt.js.map +1 -0
- package/dist/esm/commands/app/server.js +83 -0
- package/dist/esm/commands/app/server.js.map +1 -0
- package/dist/esm/commands/app/session-logger.js +171 -0
- package/dist/esm/commands/app/session-logger.js.map +1 -0
- package/dist/esm/commands/app/tools/click.js +27 -0
- package/dist/esm/commands/app/tools/click.js.map +1 -0
- package/dist/esm/commands/app/tools/errors.js +24 -0
- package/dist/esm/commands/app/tools/errors.js.map +1 -0
- package/dist/esm/commands/app/tools/eval-js.js +36 -0
- package/dist/esm/commands/app/tools/eval-js.js.map +1 -0
- package/dist/esm/commands/app/tools/fill.js +28 -0
- package/dist/esm/commands/app/tools/fill.js.map +1 -0
- package/dist/esm/commands/app/tools/find.js +44 -0
- package/dist/esm/commands/app/tools/find.js.map +1 -0
- package/dist/esm/commands/app/tools/get-styles.js +36 -0
- package/dist/esm/commands/app/tools/get-styles.js.map +1 -0
- package/dist/esm/commands/app/tools/get-url.js +24 -0
- package/dist/esm/commands/app/tools/get-url.js.map +1 -0
- package/dist/esm/commands/app/tools/go-back.js +15 -0
- package/dist/esm/commands/app/tools/go-back.js.map +1 -0
- package/dist/esm/commands/app/tools/hover.js +24 -0
- package/dist/esm/commands/app/tools/hover.js.map +1 -0
- package/dist/esm/commands/app/tools/index.js +32 -0
- package/dist/esm/commands/app/tools/index.js.map +1 -0
- package/dist/esm/commands/app/tools/navigate.js +31 -0
- package/dist/esm/commands/app/tools/navigate.js.map +1 -0
- package/dist/esm/commands/app/tools/press-key.js +24 -0
- package/dist/esm/commands/app/tools/press-key.js.map +1 -0
- package/dist/esm/commands/app/tools/read-file.js +98 -0
- package/dist/esm/commands/app/tools/read-file.js.map +1 -0
- package/dist/esm/commands/app/tools/registry.js +34 -0
- package/dist/esm/commands/app/tools/registry.js.map +1 -0
- package/dist/esm/commands/app/tools/reload.js +15 -0
- package/dist/esm/commands/app/tools/reload.js.map +1 -0
- package/dist/esm/commands/app/tools/screenshot.js +133 -0
- package/dist/esm/commands/app/tools/screenshot.js.map +1 -0
- package/dist/esm/commands/app/tools/scroll.js +41 -0
- package/dist/esm/commands/app/tools/scroll.js.map +1 -0
- package/dist/esm/commands/app/tools/select.js +25 -0
- package/dist/esm/commands/app/tools/select.js.map +1 -0
- package/dist/esm/commands/app/tools/snapshot.js +24 -0
- package/dist/esm/commands/app/tools/snapshot.js.map +1 -0
- package/dist/esm/commands/app/tools/type-text.js +29 -0
- package/dist/esm/commands/app/tools/type-text.js.map +1 -0
- package/dist/esm/commands/app/tools/types.js +2 -0
- package/dist/esm/commands/app/tools/types.js.map +1 -0
- package/dist/esm/commands/app/tools/wait.js +39 -0
- package/dist/esm/commands/app/tools/wait.js.map +1 -0
- package/dist/esm/commands/app/tools/write-file.js +98 -0
- package/dist/esm/commands/app/tools/write-file.js.map +1 -0
- package/dist/esm/commands/app/types.js +2 -0
- package/dist/esm/commands/app/types.js.map +1 -0
- package/dist/esm/eval-resolve.js +83 -0
- package/dist/esm/eval-resolve.js.map +1 -0
- package/dist/esm/index.js +18 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/utils/agent-browser.js +67 -0
- package/dist/esm/utils/agent-browser.js.map +1 -0
- package/dist/esm/utils/browser-poll.js +26 -0
- package/dist/esm/utils/browser-poll.js.map +1 -0
- package/dist/esm/utils/daemon.js +83 -0
- package/dist/esm/utils/daemon.js.map +1 -0
- package/dist/esm/utils/index.js +6 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/dist/esm/utils/process.js +30 -0
- package/dist/esm/utils/process.js.map +1 -0
- package/dist/esm/utils/server.js +65 -0
- package/dist/esm/utils/server.js.map +1 -0
- package/dist/esm/version.js +3 -0
- package/dist/esm/version.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/commands/app/command.d.ts +7 -0
- package/dist/types/commands/app/eval.d.ts +9 -0
- package/dist/types/commands/app/format.d.ts +14 -0
- package/dist/types/commands/app/login.d.ts +14 -0
- package/dist/types/commands/app/prompts/execute-step.prompt.d.ts +13 -0
- package/dist/types/commands/app/prompts/judge.prompt.d.ts +15 -0
- package/dist/types/commands/app/prompts/plan.prompt.d.ts +13 -0
- package/dist/types/commands/app/prompts/step.prompt.d.ts +12 -0
- package/dist/types/commands/app/prompts/system.dev-server.prompt.d.ts +12 -0
- package/dist/types/commands/app/prompts/system.prompt.d.ts +12 -0
- package/dist/types/commands/app/server.d.ts +26 -0
- package/dist/types/commands/app/session-logger.d.ts +14 -0
- package/dist/types/commands/app/tools/click.d.ts +9 -0
- package/dist/types/commands/app/tools/errors.d.ts +9 -0
- package/dist/types/commands/app/tools/eval-js.d.ts +13 -0
- package/dist/types/commands/app/tools/fill.d.ts +9 -0
- package/dist/types/commands/app/tools/find.d.ts +9 -0
- package/dist/types/commands/app/tools/get-styles.d.ts +13 -0
- package/dist/types/commands/app/tools/get-url.d.ts +9 -0
- package/dist/types/commands/app/tools/go-back.d.ts +9 -0
- package/dist/types/commands/app/tools/hover.d.ts +9 -0
- package/dist/types/commands/app/tools/index.d.ts +9 -0
- package/dist/types/commands/app/tools/navigate.d.ts +9 -0
- package/dist/types/commands/app/tools/press-key.d.ts +9 -0
- package/dist/types/commands/app/tools/read-file.d.ts +13 -0
- package/dist/types/commands/app/tools/registry.d.ts +28 -0
- package/dist/types/commands/app/tools/reload.d.ts +9 -0
- package/dist/types/commands/app/tools/screenshot.d.ts +9 -0
- package/dist/types/commands/app/tools/scroll.d.ts +9 -0
- package/dist/types/commands/app/tools/select.d.ts +9 -0
- package/dist/types/commands/app/tools/snapshot.d.ts +9 -0
- package/dist/types/commands/app/tools/type-text.d.ts +9 -0
- package/dist/types/commands/app/tools/types.d.ts +12 -0
- package/dist/types/commands/app/tools/wait.d.ts +9 -0
- package/dist/types/commands/app/tools/write-file.d.ts +19 -0
- package/dist/types/commands/app/types.d.ts +68 -0
- package/dist/types/eval-resolve.d.ts +31 -0
- package/dist/types/index.d.ts +15 -0
- package/dist/types/utils/agent-browser.d.ts +26 -0
- package/dist/types/utils/browser-poll.d.ts +9 -0
- package/dist/types/utils/daemon.d.ts +16 -0
- package/dist/types/utils/index.d.ts +5 -0
- package/dist/types/utils/process.d.ts +21 -0
- package/dist/types/utils/server.d.ts +26 -0
- package/dist/types/version.d.ts +1 -0
- package/package.json +57 -0
- package/src/commands/app/command.ts +204 -0
- package/src/commands/app/eval.ts +114 -0
- package/src/commands/app/format.ts +110 -0
- package/src/commands/app/login.ts +92 -0
- package/src/commands/app/prompts/execute-step.prompt.ts +48 -0
- package/src/commands/app/prompts/judge.prompt.ts +121 -0
- package/src/commands/app/prompts/plan.prompt.ts +48 -0
- package/src/commands/app/prompts/step.prompt.ts +45 -0
- package/src/commands/app/prompts/system.dev-server.prompt.ts +22 -0
- package/src/commands/app/prompts/system.prompt.ts +78 -0
- package/src/commands/app/server.ts +105 -0
- package/src/commands/app/session-logger.ts +187 -0
- package/src/commands/app/tools/click.ts +28 -0
- package/src/commands/app/tools/errors.ts +26 -0
- package/src/commands/app/tools/eval-js.ts +40 -0
- package/src/commands/app/tools/fill.ts +29 -0
- package/src/commands/app/tools/find.ts +49 -0
- package/src/commands/app/tools/get-styles.ts +40 -0
- package/src/commands/app/tools/get-url.ts +26 -0
- package/src/commands/app/tools/go-back.ts +16 -0
- package/src/commands/app/tools/hover.ts +25 -0
- package/src/commands/app/tools/index.ts +57 -0
- package/src/commands/app/tools/navigate.ts +32 -0
- package/src/commands/app/tools/press-key.ts +25 -0
- package/src/commands/app/tools/read-file.ts +113 -0
- package/src/commands/app/tools/registry.ts +38 -0
- package/src/commands/app/tools/reload.ts +16 -0
- package/src/commands/app/tools/screenshot.ts +168 -0
- package/src/commands/app/tools/scroll.ts +45 -0
- package/src/commands/app/tools/select.ts +26 -0
- package/src/commands/app/tools/snapshot.ts +27 -0
- package/src/commands/app/tools/type-text.ts +30 -0
- package/src/commands/app/tools/types.ts +15 -0
- package/src/commands/app/tools/wait.ts +43 -0
- package/src/commands/app/tools/write-file.ts +104 -0
- package/src/commands/app/types.ts +76 -0
- package/src/eval-resolve.ts +113 -0
- package/src/index.ts +20 -0
- package/src/utils/agent-browser.ts +66 -0
- package/src/utils/browser-poll.ts +27 -0
- package/src/utils/daemon.ts +84 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/process.ts +33 -0
- package/src/utils/server.ts +66 -0
- package/src/version.ts +2 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { createCommand } from 'commander';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { resetDaemon, cleanup, createRunDir } from '../../utils/index.js';
|
|
5
|
+
import { runLogin } from './login.js';
|
|
6
|
+
import { resolveEvalCommandInput, resolveEvalFiles } from '../../eval-resolve.js';
|
|
7
|
+
import { createSession } from './eval.js';
|
|
8
|
+
import { startAppServer } from './server.js';
|
|
9
|
+
import { attachSessionLogger } from './session-logger.js';
|
|
10
|
+
import type { CopilotEvalOptions, Plan, Verdict } from './types.js';
|
|
11
|
+
import { formatVerdict } from './format.js';
|
|
12
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { createPlanPrompt } from './prompts/plan.prompt.js';
|
|
14
|
+
import { createStepPrompt } from './prompts/step.prompt.js';
|
|
15
|
+
import { createJudgePrompt } from './prompts/judge.prompt.js';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* CLI command: `copilot app eval <path-or-eval>`
|
|
20
|
+
*
|
|
21
|
+
* Evaluates a Fusion application using the GitHub Copilot SDK with
|
|
22
|
+
* `agent-browser` exposed as tools. The Copilot agent autonomously
|
|
23
|
+
* navigates, interacts, and judges the application against eval criteria.
|
|
24
|
+
*/
|
|
25
|
+
const _appEvalCommand = createCommand('eval')
|
|
26
|
+
.description('Evaluate a Fusion app using Copilot SDK with agent-browser tools')
|
|
27
|
+
.argument('<path-or-eval>', 'Path to the Fusion application directory or an eval markdown file')
|
|
28
|
+
.option('--eval <name-or-path>', 'Specific eval to run (name or file path)')
|
|
29
|
+
.option('--port <port>', 'Port for the app server', '3333')
|
|
30
|
+
.option('--host <host>', 'Host for the app server', '0.0.0.0')
|
|
31
|
+
.option('--url <url>', 'Skip server start, use an already-running URL')
|
|
32
|
+
.option('--verbose', 'Show detailed output', false)
|
|
33
|
+
.option('--logon, --login', 'Open a headed browser for interactive MSAL login', false)
|
|
34
|
+
.option('-m, --model <model>', 'LLM model to use (e.g. claude-sonnet-4)')
|
|
35
|
+
.option('-r, --reasoning <effort>', 'Reasoning effort: low, medium, high, xhigh')
|
|
36
|
+
.option('-o, --output <dir>', 'Output directory for run artifacts')
|
|
37
|
+
.addHelpText(
|
|
38
|
+
'after',
|
|
39
|
+
[
|
|
40
|
+
'',
|
|
41
|
+
'Uses the GitHub Copilot SDK to create an agentic session where the LLM',
|
|
42
|
+
'autonomously drives agent-browser to verify eval criteria.',
|
|
43
|
+
'',
|
|
44
|
+
'Examples:',
|
|
45
|
+
' $ ffc copilot app eval ./cookbooks/app-react',
|
|
46
|
+
' $ ffc copilot app eval . --eval smoke',
|
|
47
|
+
' $ ffc copilot app eval . --eval 06-people-api-page.md',
|
|
48
|
+
' $ ffc copilot app eval ./cookbooks/app-react-router/eval/06-people-api-page.md',
|
|
49
|
+
' $ ffc copilot app eval . --model claude-sonnet-4',
|
|
50
|
+
' $ ffc copilot app eval . --login',
|
|
51
|
+
' $ ffc copilot app eval . --logon',
|
|
52
|
+
' $ ffc copilot app eval . --url http://localhost:3333/apps/my-app',
|
|
53
|
+
].join('\n'),
|
|
54
|
+
)
|
|
55
|
+
.action(async (appOrEvalPath: string, options: CopilotEvalOptions) => {
|
|
56
|
+
const resolvedInput = resolveEvalCommandInput(appOrEvalPath);
|
|
57
|
+
const absAppPath = resolvedInput.appPath;
|
|
58
|
+
const port = parseInt(options.port, 10);
|
|
59
|
+
const verbose = options.verbose;
|
|
60
|
+
|
|
61
|
+
if (resolvedInput.evalFile && options.eval) {
|
|
62
|
+
console.error('❌ Do not combine a positional eval markdown file with --eval');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Login mode: open headed browser for interactive MSAL login ──
|
|
67
|
+
if (options.login) {
|
|
68
|
+
await runLogin(absAppPath, port, options.host);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Resolve eval files
|
|
73
|
+
const evalFiles = resolvedInput.evalFile
|
|
74
|
+
? [resolvedInput.evalFile]
|
|
75
|
+
: resolveEvalFiles(absAppPath, options.eval);
|
|
76
|
+
console.log(
|
|
77
|
+
`📋 Found ${evalFiles.length} eval(s): ${evalFiles.map((f) => basename(f)).join(', ')}`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Start the app dev server (unless --url provides an already-running URL)
|
|
81
|
+
const { serverProcess, appUrl } = await startAppServer(absAppPath, {
|
|
82
|
+
port,
|
|
83
|
+
host: options.host,
|
|
84
|
+
url: options.url,
|
|
85
|
+
verbose,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Kill any stale daemon and clean up profile locks before the run
|
|
89
|
+
resetDaemon();
|
|
90
|
+
|
|
91
|
+
// Track active session logger + session for SIGINT cleanup
|
|
92
|
+
let logger: ReturnType<typeof attachSessionLogger> | null = null;
|
|
93
|
+
let activeSession: Awaited<ReturnType<typeof createSession>> | null = null;
|
|
94
|
+
|
|
95
|
+
process.on('SIGINT', () => {
|
|
96
|
+
logger?.stop();
|
|
97
|
+
console.log('\n\n👋 Shutting down...');
|
|
98
|
+
activeSession?.disconnect();
|
|
99
|
+
resetDaemon();
|
|
100
|
+
cleanup(serverProcess);
|
|
101
|
+
process.exit(130);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let failures = 0;
|
|
105
|
+
try {
|
|
106
|
+
for (const evalFilePath of evalFiles) {
|
|
107
|
+
const evalName = basename(evalFilePath, '.md');
|
|
108
|
+
const query = readFileSync(evalFilePath, 'utf-8').trim();
|
|
109
|
+
console.log(`\n${'═'.repeat(60)}`);
|
|
110
|
+
console.log(`📄 Running eval: ${evalName}`);
|
|
111
|
+
console.log(`${'═'.repeat(60)}`);
|
|
112
|
+
|
|
113
|
+
const runDir = createRunDir(evalName, options.output);
|
|
114
|
+
|
|
115
|
+
const ctx = { url: appUrl, outDir: runDir };
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const sessionConfig = {
|
|
119
|
+
model: options.model,
|
|
120
|
+
...(options.reasoning ? { reasoningEffort: options.reasoning } : {}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Single session with all tools (browser + file)
|
|
124
|
+
const session = await createSession({ ctx, config: sessionConfig });
|
|
125
|
+
activeSession = session;
|
|
126
|
+
logger = attachSessionLogger(session, { requestedModel: options.model });
|
|
127
|
+
|
|
128
|
+
// ── Phase 1: Plan ──────────────────────────────────────────
|
|
129
|
+
await session.sendAndWait({ prompt: createPlanPrompt(query, ctx) }, 300_000);
|
|
130
|
+
|
|
131
|
+
const planPath = `${runDir}/plan.json`;
|
|
132
|
+
if (!existsSync(planPath)) {
|
|
133
|
+
console.error(
|
|
134
|
+
chalk.red(
|
|
135
|
+
`❌ Agent did not produce ${planPath}. The planning phase may have failed silently.`,
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
session.disconnect();
|
|
139
|
+
logger.stop();
|
|
140
|
+
failures++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const plan = JSON.parse(readFileSync(planPath, 'utf-8')) as Plan;
|
|
145
|
+
console.log(plan.summary);
|
|
146
|
+
|
|
147
|
+
// ── Phase 2: Execute + Evaluate each step ──────────────────
|
|
148
|
+
for (const step of plan.steps) {
|
|
149
|
+
console.log(chalk.cyan(`\n── Scenario: ${step.scenario}`));
|
|
150
|
+
for (const c of step.criteria) {
|
|
151
|
+
console.log(chalk.dim(` • ${c}`));
|
|
152
|
+
}
|
|
153
|
+
await session.sendAndWait({ prompt: createStepPrompt(step, ctx) }, 300_000);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Phase 3: Judge ─────────────────────────────────────────
|
|
157
|
+
console.log(chalk.cyan('\n── Judging results...'));
|
|
158
|
+
await session.sendAndWait({ prompt: createJudgePrompt(ctx) }, 300_000);
|
|
159
|
+
|
|
160
|
+
session.disconnect();
|
|
161
|
+
logger.stop();
|
|
162
|
+
|
|
163
|
+
// Print the verdict
|
|
164
|
+
const verdictPath = `${runDir}/verdict.json`;
|
|
165
|
+
if (existsSync(verdictPath)) {
|
|
166
|
+
const verdict = JSON.parse(readFileSync(verdictPath, 'utf-8')) as Verdict;
|
|
167
|
+
console.log(formatVerdict(verdict));
|
|
168
|
+
if (!verdict.pass) failures++;
|
|
169
|
+
} else {
|
|
170
|
+
console.error(chalk.red(`❌ Judge did not produce ${verdictPath}.`));
|
|
171
|
+
failures++;
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
logger?.stop();
|
|
175
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
176
|
+
console.error(chalk.red(`\n❌ Eval "${evalName}" failed: ${msg}`));
|
|
177
|
+
if (err instanceof Error && err.stack) {
|
|
178
|
+
console.error(chalk.dim(err.stack));
|
|
179
|
+
}
|
|
180
|
+
failures++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} finally {
|
|
184
|
+
logger?.stop();
|
|
185
|
+
resetDaemon();
|
|
186
|
+
cleanup(serverProcess);
|
|
187
|
+
process.exit(failures > 0 ? 1 : 0);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/** Intermediate `app` command group: `copilot app` */
|
|
192
|
+
const _appCommand = createCommand('app')
|
|
193
|
+
.description('Copilot-powered application commands')
|
|
194
|
+
.addCommand(_appEvalCommand);
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Top-level `copilot` command group.
|
|
198
|
+
*
|
|
199
|
+
* Subcommands:
|
|
200
|
+
* app eval — Evaluate a Fusion app with Copilot SDK + agent-browser tools
|
|
201
|
+
*/
|
|
202
|
+
export const copilotCommand = createCommand('copilot')
|
|
203
|
+
.description('GitHub Copilot SDK powered commands')
|
|
204
|
+
.addCommand(_appCommand);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { ab, abErrorMessage } from '../../utils/index.js';
|
|
5
|
+
import { createAgentBrowserTools } from './tools/index.js';
|
|
6
|
+
import { createWriteFileTool, createAppendFileTool } from './tools/write-file.js';
|
|
7
|
+
import { createReadFileTool } from './tools/read-file.js';
|
|
8
|
+
import type { RuntimeExecutionContext } from './types.js';
|
|
9
|
+
import type { CopilotSession, SessionConfig } from '@github/copilot-sdk';
|
|
10
|
+
import { createSystemPrompt } from './prompts/system.prompt.js';
|
|
11
|
+
import { createDevServerContext } from './prompts/system.dev-server.prompt.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lazily imports the Copilot SDK to avoid module resolution errors at CLI startup.
|
|
15
|
+
*
|
|
16
|
+
* The SDK depends on `vscode-jsonrpc/node` which may fail under certain Node versions
|
|
17
|
+
* when loaded eagerly via static imports.
|
|
18
|
+
*
|
|
19
|
+
* @returns The Copilot SDK module namespace
|
|
20
|
+
*/
|
|
21
|
+
async function loadCopilotSdk(): Promise<typeof import('@github/copilot-sdk')> {
|
|
22
|
+
return await import('@github/copilot-sdk');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type SessionOptions = {
|
|
26
|
+
ctx: RuntimeExecutionContext;
|
|
27
|
+
defaultToolTimeoutMs?: number;
|
|
28
|
+
config?: Omit<
|
|
29
|
+
SessionConfig,
|
|
30
|
+
'onPermissionRequest' | 'systemMessage' | 'availableTools' | 'excludedTools'
|
|
31
|
+
>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const createSession = async (options: SessionOptions): Promise<CopilotSession> => {
|
|
35
|
+
const { ctx, config, defaultToolTimeoutMs } = options;
|
|
36
|
+
|
|
37
|
+
const { CopilotClient, defineTool, approveAll } = await loadCopilotSdk();
|
|
38
|
+
const client = new CopilotClient({ autoStart: true });
|
|
39
|
+
await client.start();
|
|
40
|
+
|
|
41
|
+
const fileTools = [
|
|
42
|
+
createReadFileTool(ctx.outDir, defineTool),
|
|
43
|
+
createWriteFileTool(ctx.outDir, defineTool),
|
|
44
|
+
createAppendFileTool(ctx.outDir, defineTool),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const tools = [
|
|
48
|
+
...createBrowserTools(defineTool, {
|
|
49
|
+
cwd: ctx.outDir,
|
|
50
|
+
timeoutMs: defaultToolTimeoutMs,
|
|
51
|
+
}),
|
|
52
|
+
...fileTools,
|
|
53
|
+
...(config?.tools ?? []),
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Exclude dangerous built-in tools; names vary across model providers
|
|
57
|
+
// so a blocklist is more robust than an allowlist.
|
|
58
|
+
const excludedTools = [
|
|
59
|
+
'bash',
|
|
60
|
+
'list_bash',
|
|
61
|
+
'read_bash',
|
|
62
|
+
'write_bash',
|
|
63
|
+
'stop_bash',
|
|
64
|
+
'sql',
|
|
65
|
+
'web_fetch',
|
|
66
|
+
'skill',
|
|
67
|
+
'list_agents',
|
|
68
|
+
'read_agent',
|
|
69
|
+
'glob',
|
|
70
|
+
'rg',
|
|
71
|
+
'view',
|
|
72
|
+
'task',
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const session = await client.createSession({
|
|
76
|
+
...config,
|
|
77
|
+
tools,
|
|
78
|
+
excludedTools,
|
|
79
|
+
systemMessage: {
|
|
80
|
+
mode: 'replace',
|
|
81
|
+
content: `${createSystemPrompt(ctx)}\n\n## Dev Server${createDevServerContext(ctx)}`,
|
|
82
|
+
},
|
|
83
|
+
streaming: true,
|
|
84
|
+
onPermissionRequest: approveAll,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return session;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Creates the `agent-browser` tool definitions for the Copilot SDK session.
|
|
92
|
+
*
|
|
93
|
+
* Each tool wraps an `agent-browser` CLI subcommand so the Copilot agent can
|
|
94
|
+
* autonomously navigate and inspect a running Fusion application.
|
|
95
|
+
*
|
|
96
|
+
* @param defineTool - The `defineTool` helper from the Copilot SDK
|
|
97
|
+
* @returns Array of Copilot SDK tool definitions for browser interaction
|
|
98
|
+
*/
|
|
99
|
+
function createBrowserTools(
|
|
100
|
+
defineTool: Awaited<ReturnType<typeof loadCopilotSdk>>['defineTool'],
|
|
101
|
+
options?: { cwd?: string; timeoutMs?: number },
|
|
102
|
+
): ReturnType<typeof createAgentBrowserTools> {
|
|
103
|
+
const outDir = join(options?.cwd ?? process.cwd(), 'evidence');
|
|
104
|
+
mkdirSync(outDir, { recursive: true });
|
|
105
|
+
const invoke = (args: string[], timeoutMs = options?.timeoutMs): string => {
|
|
106
|
+
try {
|
|
107
|
+
return ab(args, timeoutMs);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
const msg = abErrorMessage(err);
|
|
110
|
+
return `Error: ${msg}`;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
return createAgentBrowserTools({ outDir, invoke }, defineTool);
|
|
114
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
import type { Plan, StepResult, Verdict } from './types.js';
|
|
4
|
+
|
|
5
|
+
// ── Plan ────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
/** Renders a structured plan as a human-readable summary. */
|
|
8
|
+
export function formatPlan(plan: Plan): string {
|
|
9
|
+
const lines = [
|
|
10
|
+
'',
|
|
11
|
+
chalk.bold(`📋 ${plan.summary}`),
|
|
12
|
+
'',
|
|
13
|
+
...plan.steps.flatMap((s, si) => [
|
|
14
|
+
` ${chalk.dim(`${si + 1}.`)} ${s.scenario}`,
|
|
15
|
+
...s.criteria.map((c) => chalk.dim(` • ${truncate(c, 70)}`)),
|
|
16
|
+
]),
|
|
17
|
+
'',
|
|
18
|
+
];
|
|
19
|
+
return lines.join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Step result (execution line) ────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/** Renders a single step execution result. */
|
|
25
|
+
export function formatStepResult(result: StepResult): string {
|
|
26
|
+
const icon = statusIcon(result.ok);
|
|
27
|
+
const files = result.evidence.length ? chalk.dim(` [${result.evidence.join(', ')}]`) : '';
|
|
28
|
+
return ` ${icon} ${result.criterion}\n${chalk.dim(` ${result.note}`)}${files}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Verdict ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** Renders the final verdict as a full-width coloured report. */
|
|
34
|
+
export function formatVerdict(verdict: Verdict): string {
|
|
35
|
+
const bar = '═'.repeat(60);
|
|
36
|
+
const banner = verdict.pass
|
|
37
|
+
? chalk.bgGreen.black.bold(' ✅ PASS ')
|
|
38
|
+
: chalk.bgRed.white.bold(' ❌ FAIL ');
|
|
39
|
+
|
|
40
|
+
const lines = [
|
|
41
|
+
'',
|
|
42
|
+
bar,
|
|
43
|
+
` ${banner}`,
|
|
44
|
+
bar,
|
|
45
|
+
'',
|
|
46
|
+
...verdict.steps.map(
|
|
47
|
+
(s) => ` ${statusIcon(s.ok)} ${s.criterion}\n${chalk.dim(` ${s.note}`)}`,
|
|
48
|
+
),
|
|
49
|
+
'',
|
|
50
|
+
chalk.dim(` ${verdict.reasoning}`),
|
|
51
|
+
'',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (verdict.ux && verdict.ux.length > 0) {
|
|
55
|
+
lines.push(chalk.bold(' 💡 UX Feedback'), '');
|
|
56
|
+
for (const hint of verdict.ux) {
|
|
57
|
+
lines.push(chalk.yellow(` • ${hint}`));
|
|
58
|
+
}
|
|
59
|
+
lines.push('');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return lines.join('\n');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Dispatcher ──────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Attempts to detect the JSON shape of an assistant message and format it.
|
|
69
|
+
*
|
|
70
|
+
* Returns `null` when the content is not a recognised structure, so the
|
|
71
|
+
* caller can fall back to plain-text rendering.
|
|
72
|
+
*/
|
|
73
|
+
export function tryFormatMessage(content: string): string | null {
|
|
74
|
+
let parsed: unknown;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(content.trim());
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (!parsed || typeof parsed !== 'object') return null;
|
|
81
|
+
|
|
82
|
+
// Verdict: { pass, reasoning, steps[] }
|
|
83
|
+
if ('pass' in parsed && 'steps' in parsed && Array.isArray((parsed as Verdict).steps)) {
|
|
84
|
+
return formatVerdict(parsed as Verdict);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Plan: { summary, steps[] }
|
|
88
|
+
if ('summary' in parsed && 'steps' in parsed && Array.isArray((parsed as Plan).steps)) {
|
|
89
|
+
return formatPlan(parsed as Plan);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Step result: { criterion, ok, note }
|
|
93
|
+
if ('criterion' in parsed && 'ok' in parsed && 'note' in parsed) {
|
|
94
|
+
return formatStepResult(parsed as StepResult);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
function statusIcon(ok: boolean | string): string {
|
|
103
|
+
if (ok === true) return chalk.green('✔');
|
|
104
|
+
if (ok === false) return chalk.red('✖');
|
|
105
|
+
return chalk.yellow('⚠'); // "blocked", "flaky", etc.
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function truncate(str: string, max: number): string {
|
|
109
|
+
return str.length > max ? `${str.slice(0, max - 1)}…` : str;
|
|
110
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ab,
|
|
7
|
+
resolveAppKey,
|
|
8
|
+
cleanup,
|
|
9
|
+
resetDaemon,
|
|
10
|
+
sleep,
|
|
11
|
+
stripAnsi,
|
|
12
|
+
waitForServer,
|
|
13
|
+
pollConsole,
|
|
14
|
+
} from '../../utils/index.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Interactive login mode for the `copilot app` command.
|
|
18
|
+
*
|
|
19
|
+
* Starts the application dev server, opens a headed browser window for
|
|
20
|
+
* manual MSAL authentication, and polls the browser console for the Fusion
|
|
21
|
+
* `"Done"` message that signals successful initialisation. The browser is
|
|
22
|
+
* left open after login so the user can finish any manual steps before
|
|
23
|
+
* pressing Ctrl+C.
|
|
24
|
+
*
|
|
25
|
+
* @param absAppPath - Absolute path to the Fusion application directory
|
|
26
|
+
* @param port - Port number for the dev server
|
|
27
|
+
* @param host - Host address for the dev server
|
|
28
|
+
*/
|
|
29
|
+
export async function runLogin(absAppPath: string, port: number, host: string): Promise<void> {
|
|
30
|
+
const pkgJsonPath = join(absAppPath, 'package.json');
|
|
31
|
+
if (!existsSync(pkgJsonPath)) {
|
|
32
|
+
console.error(`❌ No package.json found at ${absAppPath}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { name?: string };
|
|
36
|
+
if (!pkgJson.name) {
|
|
37
|
+
console.error(`❌ package.json at ${absAppPath} has no "name" field`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let serverProcess: ChildProcess | undefined;
|
|
42
|
+
|
|
43
|
+
console.log('📦 Resolving app manifest...');
|
|
44
|
+
const appKey = resolveAppKey(absAppPath);
|
|
45
|
+
console.log(`🚀 Starting server for ${appKey}...`);
|
|
46
|
+
serverProcess = spawn('ffc', ['app', 'dev', '--host', host, '--port', String(port)], {
|
|
47
|
+
cwd: absAppPath,
|
|
48
|
+
stdio: 'inherit',
|
|
49
|
+
detached: false,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const serverUrl = `http://localhost:${port}`;
|
|
53
|
+
const ready = await waitForServer(serverUrl, 60);
|
|
54
|
+
if (!ready) {
|
|
55
|
+
console.error('❌ Server failed to start within 60s');
|
|
56
|
+
cleanup(serverProcess);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const appUrl = `${serverUrl}/apps/${appKey}`;
|
|
61
|
+
console.log(`✅ Server ready at ${appUrl}`);
|
|
62
|
+
console.log('🌐 Opening headed browser — log in with your Equinor account...');
|
|
63
|
+
|
|
64
|
+
// Kill any stale daemon/Chrome so the new --headed --profile flags take effect
|
|
65
|
+
resetDaemon();
|
|
66
|
+
ab(['open', appUrl, '--headed'], 60_000);
|
|
67
|
+
|
|
68
|
+
// Ctrl+C handler: stop the dev server but leave the browser open
|
|
69
|
+
process.on('SIGINT', () => {
|
|
70
|
+
console.log('\n👋 Stopping server... (browser left open)');
|
|
71
|
+
cleanup(serverProcess);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Wait for the Fusion app to initialise (browser console emits "✓ Done" after auth)
|
|
76
|
+
console.log('⏳ Waiting for app to initialize after login... (Ctrl+C to finish early)');
|
|
77
|
+
const initialized = await pollConsole(
|
|
78
|
+
(logs: string) => stripAnsi(logs).includes('Done'),
|
|
79
|
+
300_000,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (initialized) {
|
|
83
|
+
await sleep(2000);
|
|
84
|
+
console.log('✅ Login complete — session saved.');
|
|
85
|
+
} else {
|
|
86
|
+
console.log('⚠ Timed out waiting for app init. Session may still be saved.');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Always wait for manual Ctrl+C so the user can finish what they're doing
|
|
90
|
+
console.log('🌐 Browser still open. Press Ctrl+C to exit.');
|
|
91
|
+
await new Promise(() => {}); // block until SIGINT
|
|
92
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { RuntimeExecutionContext } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates the execution prompt for a single test step.
|
|
5
|
+
*
|
|
6
|
+
* The agent receives one plan step and executes its actions against the
|
|
7
|
+
* running application using browser tools, collecting evidence artifacts
|
|
8
|
+
* into the evidence directory.
|
|
9
|
+
*
|
|
10
|
+
* @param actions - The action strings to execute in order
|
|
11
|
+
* @param ctx - Runtime context with output directory and app URL
|
|
12
|
+
* @returns The fully interpolated execution prompt string
|
|
13
|
+
*/
|
|
14
|
+
export const createExecuteStepPrompt = (
|
|
15
|
+
actions: string[],
|
|
16
|
+
ctx: RuntimeExecutionContext,
|
|
17
|
+
): string => {
|
|
18
|
+
return `
|
|
19
|
+
You are now EXECUTING one test step using real browser tools.
|
|
20
|
+
|
|
21
|
+
ACTIONS: ${JSON.stringify(actions, null, 2)}
|
|
22
|
+
|
|
23
|
+
APPLICATION URL: ${ctx.url}
|
|
24
|
+
EVIDENCE DIRECTORY: ${ctx.outDir}/evidence
|
|
25
|
+
|
|
26
|
+
Execute the "actions" array in order using the available browser tool calls.
|
|
27
|
+
Collect rich evidence at every stage — especially on failure.
|
|
28
|
+
|
|
29
|
+
Evidence rules (mandatory):
|
|
30
|
+
- Before important actions: screenshot --annotate evidence/step-{criterion-slug}-before.png
|
|
31
|
+
- After the step (always): screenshot --full --annotate evidence/step-{criterion-slug}-after.png
|
|
32
|
+
- On any failure or suspicion:
|
|
33
|
+
errors > evidence/step-{criterion-slug}-errors.log
|
|
34
|
+
console > evidence/step-{criterion-slug}-console.log
|
|
35
|
+
snapshot -i --json > evidence/step-{criterion-slug}-snapshot.json
|
|
36
|
+
- Use descriptive, unique filenames
|
|
37
|
+
Examples: step-login-button-missing-screenshot.png, step-dashboard-url.txt
|
|
38
|
+
|
|
39
|
+
Important patterns:
|
|
40
|
+
- Start most steps with: snapshot -i --annotate → use @eN refs for element stability
|
|
41
|
+
- After navigation: wait --load networkidle or wait --url "**/pattern*"
|
|
42
|
+
- On failure (non-zero exit, timeout, missing element, JS error): capture extra evidence
|
|
43
|
+
- If flaky (timing, network): take 2–3 extra screenshots ~1 s apart
|
|
44
|
+
|
|
45
|
+
When done, your work will be evaluated against the step's passEvidence and failEvidence.
|
|
46
|
+
Do NOT output markdown or explanations — only use browser tool calls and collect evidence.
|
|
47
|
+
`.trim();
|
|
48
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, extname } from 'node:path';
|
|
3
|
+
import type { RuntimeExecutionContext } from '../types.js';
|
|
4
|
+
|
|
5
|
+
/** Text file extensions that are safe to embed in the prompt. */
|
|
6
|
+
const TEXT_EXTENSIONS = new Set(['.txt', '.json', '.jsonl', '.log', '.html', '.csv']);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Reads `executions.jsonl` and all text evidence files from the run directory
|
|
10
|
+
* and returns them as a single string block suitable for prompt embedding.
|
|
11
|
+
*
|
|
12
|
+
* Binary files (screenshots) are listed by name but not inlined.
|
|
13
|
+
*
|
|
14
|
+
* @param outDir - The run output directory containing executions.jsonl and evidence/
|
|
15
|
+
* @returns Formatted string with execution records and text evidence
|
|
16
|
+
*/
|
|
17
|
+
function collectEvidence(outDir: string): string {
|
|
18
|
+
const parts: string[] = [];
|
|
19
|
+
|
|
20
|
+
// ── executions.jsonl ──
|
|
21
|
+
const execPath = join(outDir, 'executions.jsonl');
|
|
22
|
+
if (existsSync(execPath)) {
|
|
23
|
+
parts.push('### executions.jsonl', readFileSync(execPath, 'utf-8').trim());
|
|
24
|
+
} else {
|
|
25
|
+
parts.push('### executions.jsonl', '(file not found)');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── evidence/ directory ──
|
|
29
|
+
const evidenceDir = join(outDir, 'evidence');
|
|
30
|
+
if (existsSync(evidenceDir)) {
|
|
31
|
+
const files = readdirSync(evidenceDir).sort();
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const ext = extname(file).toLowerCase();
|
|
34
|
+
if (TEXT_EXTENSIONS.has(ext)) {
|
|
35
|
+
const content = readFileSync(join(evidenceDir, file), 'utf-8').trim();
|
|
36
|
+
parts.push(`### evidence/${file}`, content);
|
|
37
|
+
} else {
|
|
38
|
+
parts.push(`### evidence/${file}`, `(binary file — ${ext} screenshot)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return parts.join('\n\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates the final-verdict prompt for the judge phase.
|
|
48
|
+
*
|
|
49
|
+
* The judge reviews ALL step executions and evidence holistically, then writes
|
|
50
|
+
* a single `verdict.json` with an overall pass/fail decision. This differs
|
|
51
|
+
* from the per-step evaluate prompt which assesses one criterion at a time.
|
|
52
|
+
*
|
|
53
|
+
* Execution records and text evidence files are embedded directly in the prompt
|
|
54
|
+
* so the model does not need file-reading tools or SDK attachment resolution.
|
|
55
|
+
*
|
|
56
|
+
* @param ctx - Runtime context containing the output directory and app URL
|
|
57
|
+
* @returns The fully interpolated judge prompt string
|
|
58
|
+
*/
|
|
59
|
+
export const createJudgePrompt = (ctx: RuntimeExecutionContext): string => {
|
|
60
|
+
const evidence = collectEvidence(ctx.outDir);
|
|
61
|
+
|
|
62
|
+
return `
|
|
63
|
+
All test steps have been executed. You are now the JUDGE.
|
|
64
|
+
|
|
65
|
+
Review the evidence below and any screenshots using read_file, then produce a final verdict.
|
|
66
|
+
|
|
67
|
+
<evidence>
|
|
68
|
+
${evidence}
|
|
69
|
+
</evidence>
|
|
70
|
+
|
|
71
|
+
To inspect screenshots, use read_file with paths like "evidence/step-hello-fusion-header.jpg".
|
|
72
|
+
Images are returned as inline vision content you can see.
|
|
73
|
+
|
|
74
|
+
Save your verdict using write_file with path "verdict.json".
|
|
75
|
+
You MUST call write_file. Do NOT output the JSON as a message.
|
|
76
|
+
|
|
77
|
+
Evaluation rules — apply exactly:
|
|
78
|
+
1. Auto-fail a criterion if:
|
|
79
|
+
- Any "Uncaught", TypeError, or ReferenceError appears in console/errors during that step
|
|
80
|
+
- The page shows an error, HTTP 500, white screen, or spinner lasting >15 s
|
|
81
|
+
- The expected element, text, or URL never appeared
|
|
82
|
+
- An explicit assertion (is visible, get text, get count) returned a failing result
|
|
83
|
+
2. A criterion passes only when:
|
|
84
|
+
- All passEvidence conditions are clearly confirmed in screenshots (use read_file to view them), console, or URL
|
|
85
|
+
- No critical functional deviation is present
|
|
86
|
+
3. Be lenient on:
|
|
87
|
+
- Console warnings, deprecations, analytics 404s
|
|
88
|
+
- Cosmetic shifts <5 px or layout differences that do not hide content
|
|
89
|
+
- Minor visual differences without functional impact
|
|
90
|
+
|
|
91
|
+
Overall "pass" = true ONLY if every criterion is "ok": true.
|
|
92
|
+
|
|
93
|
+
Output ONLY this JSON — no markdown fences, no prose:
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
"pass": boolean,
|
|
97
|
+
"reasoning": "One-paragraph summary explaining the overall pass/fail outcome",
|
|
98
|
+
"steps": [
|
|
99
|
+
{
|
|
100
|
+
"criterion": "...",
|
|
101
|
+
"ok": true | false | "blocked" | "flaky",
|
|
102
|
+
"note": "Evidence-based verdict for this criterion"
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"ux": [
|
|
106
|
+
"Short UX observation or improvement suggestion based on what you saw in the screenshots and evidence"
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
The "ux" array is for observations about the user experience that go beyond
|
|
111
|
+
pass/fail — things a product owner or designer would want to know:
|
|
112
|
+
- Accessibility issues (contrast, missing labels, keyboard traps)
|
|
113
|
+
- Loading performance (slow renders, layout shifts, flash of unstyled content)
|
|
114
|
+
- Visual polish (alignment, spacing, truncation, responsive issues)
|
|
115
|
+
- Interaction hints (unclear affordances, missing hover states, confusing flow)
|
|
116
|
+
- Content quality (typos, unclear copy, missing help text)
|
|
117
|
+
Only include genuine observations from the evidence. Skip "ux" if nothing stands out.
|
|
118
|
+
|
|
119
|
+
If evidence for a criterion is missing or ambiguous, fail it.
|
|
120
|
+
`.trim();
|
|
121
|
+
};
|