@itmetsam/samurai-framework 0.22.3 → 0.22.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/AGENT-GUIDE.md +50 -0
- package/bin/samurai.mjs +2 -0
- package/dist/agent/index.d.ts +11 -0
- package/dist/agent/index.js +12 -0
- package/dist/agent/instructions.d.ts +13 -0
- package/dist/agent/instructions.js +44 -0
- package/dist/agent/limits.d.ts +12 -0
- package/dist/agent/limits.js +18 -0
- package/dist/agent/loop.d.ts +32 -0
- package/dist/agent/loop.js +46 -0
- package/dist/agent/model.d.ts +9 -0
- package/dist/agent/model.js +14 -0
- package/dist/agent/prompt.d.ts +15 -0
- package/dist/agent/prompt.js +46 -0
- package/dist/agent/providers.d.ts +19 -0
- package/dist/agent/providers.js +95 -0
- package/dist/agent/recording.d.ts +41 -0
- package/dist/agent/recording.js +68 -0
- package/dist/agent/run.d.ts +11 -0
- package/dist/agent/run.js +209 -0
- package/dist/agent/sdk-tools.d.ts +15 -0
- package/dist/agent/sdk-tools.js +44 -0
- package/dist/agent/session.d.ts +62 -0
- package/dist/agent/session.js +196 -0
- package/dist/agent/snapshot-script.d.ts +18 -0
- package/dist/agent/snapshot-script.js +163 -0
- package/dist/agent/snapshot.d.ts +54 -0
- package/dist/agent/snapshot.js +95 -0
- package/dist/agent/take-over.d.ts +19 -0
- package/dist/agent/take-over.js +49 -0
- package/dist/agent/tools/act.d.ts +2 -0
- package/dist/agent/tools/act.js +124 -0
- package/dist/agent/tools/ask.d.ts +1 -0
- package/dist/agent/tools/ask.js +19 -0
- package/dist/agent/tools/context.d.ts +63 -0
- package/dist/agent/tools/context.js +42 -0
- package/dist/agent/tools/expect.d.ts +1 -0
- package/dist/agent/tools/expect.js +152 -0
- package/dist/agent/tools/finish.d.ts +1 -0
- package/dist/agent/tools/finish.js +12 -0
- package/dist/agent/tools/index.d.ts +3 -0
- package/dist/agent/tools/index.js +21 -0
- package/dist/agent/tools/navigate.d.ts +1 -0
- package/dist/agent/tools/navigate.js +53 -0
- package/dist/agent/tools/origin-guard.d.ts +6 -0
- package/dist/agent/tools/origin-guard.js +19 -0
- package/dist/agent/tools/origins.d.ts +11 -0
- package/dist/agent/tools/origins.js +22 -0
- package/dist/agent/tools/screenshot.d.ts +1 -0
- package/dist/agent/tools/screenshot.js +39 -0
- package/dist/agent/tools/snapshot.d.ts +1 -0
- package/dist/agent/tools/snapshot.js +12 -0
- package/dist/agent/tools/use-secret.d.ts +1 -0
- package/dist/agent/tools/use-secret.js +41 -0
- package/dist/agent/types.d.ts +104 -0
- package/dist/agent/types.js +1 -0
- package/dist/capabilities.d.ts +2 -0
- package/dist/capabilities.js +2 -0
- package/dist/cli/args.d.ts +5 -0
- package/dist/cli/args.js +5 -0
- package/dist/cli/generate.d.ts +10 -0
- package/dist/cli/generate.js +139 -0
- package/dist/cli/io.d.ts +2 -0
- package/dist/cli/main.js +3 -0
- package/dist/cli/prompt.d.ts +2 -0
- package/dist/cli/prompt.js +16 -0
- package/dist/cli/usage.d.ts +1 -1
- package/dist/cli/usage.js +18 -1
- package/dist/index.js +2 -0
- package/dist/recorder/normalise.d.ts +6 -0
- package/dist/recorder/normalise.js +2 -0
- package/dist/types/config.d.ts +5 -0
- package/package.json +19 -4
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { parseRunnerFlags, parseRunOverrides, } from "./args.js";
|
|
2
|
+
const USAGE_LINE = 'Usage: samurai generate "<goal>" --file <spec> [--new <title>] [--test <name|index>] [--url <url>] [--model <provider:name>] [--max-steps <n>] [--yes-origin <origin>] [--secret <NAME>] [--json]';
|
|
3
|
+
async function loadAgent() {
|
|
4
|
+
try {
|
|
5
|
+
return await import("../agent/index.js");
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error.code === "ERR_MODULE_NOT_FOUND")
|
|
9
|
+
throw new Error("samurai generate needs the packages ai and zod, and a provider package for your model; run: npm install ai zod @ai-sdk/anthropic (or @ai-sdk/openai, @ai-sdk/openai-compatible)");
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function origin(raw) {
|
|
14
|
+
try {
|
|
15
|
+
return new URL(raw).origin;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new Error(`--yes-origin: "${raw}" is not a URL such as https://shop.example`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** The model the run uses: `--model`, else `SAMURAI_AI_MODEL`, else the config's `ai.model` */
|
|
22
|
+
async function chosenModel(agent, flag, env) {
|
|
23
|
+
const spec = flag ?? (await agent.defaultModelSpec(env));
|
|
24
|
+
if (!spec)
|
|
25
|
+
throw new Error("No model: pass --model provider:name (anthropic, openai or openai-compatible), set SAMURAI_AI_MODEL, or set ai.model in samurai.config.ts");
|
|
26
|
+
return {
|
|
27
|
+
spec,
|
|
28
|
+
model: await agent.modelFromSpec(spec, { projectDir: process.cwd(), env }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** `samurai generate "<goal>"`: lets the AI agent write a test and prints what it does. Returns the exit code */
|
|
32
|
+
export async function generateCommand({ values, positionals }, io, deps = {}) {
|
|
33
|
+
const goal = positionals[0];
|
|
34
|
+
if (!goal)
|
|
35
|
+
throw new Error(`Which goal? ${USAGE_LINE}`);
|
|
36
|
+
const files = values.file ?? [];
|
|
37
|
+
if (files.length !== 1)
|
|
38
|
+
throw new Error(`Give exactly one --file <spec>. ${USAGE_LINE}`);
|
|
39
|
+
const maxSteps = values["max-steps"] === undefined ? undefined : Number(values["max-steps"]);
|
|
40
|
+
if (maxSteps !== undefined && (!Number.isInteger(maxSteps) || maxSteps < 1))
|
|
41
|
+
throw new Error(`--max-steps must be a whole number from 1, got "${values["max-steps"]}"`);
|
|
42
|
+
const flags = parseRunnerFlags(values);
|
|
43
|
+
if (flags.browsers && flags.browsers.length > 1)
|
|
44
|
+
throw new Error("--browser: the agent uses one browser");
|
|
45
|
+
const test = values.test === undefined
|
|
46
|
+
? undefined
|
|
47
|
+
: /^\d+$/.test(values.test)
|
|
48
|
+
? Number(values.test)
|
|
49
|
+
: values.test;
|
|
50
|
+
const allowedOrigins = (values["yes-origin"] ?? []).map(origin);
|
|
51
|
+
const agent = await (deps.loadAgent ?? loadAgent)();
|
|
52
|
+
const { spec, model } = await chosenModel(agent, values.model, io.env);
|
|
53
|
+
const json = values.json === true;
|
|
54
|
+
if (!json)
|
|
55
|
+
io.out(`Model ${spec.slice(0, spec.indexOf(":"))}: the pages the agent reads are sent to that provider.\n`);
|
|
56
|
+
const session = agent.runAgent({
|
|
57
|
+
...parseRunOverrides(values, io.env),
|
|
58
|
+
model,
|
|
59
|
+
goal,
|
|
60
|
+
file: files[0],
|
|
61
|
+
...(test !== undefined && { test }),
|
|
62
|
+
...(values.new !== undefined && { create: values.new }),
|
|
63
|
+
...(values.url !== undefined && { url: values.url }),
|
|
64
|
+
...(flags.browsers && { browser: flags.browsers[0] }),
|
|
65
|
+
...(flags.port !== undefined && { port: flags.port }),
|
|
66
|
+
...(flags.headless !== undefined && { headless: flags.headless }),
|
|
67
|
+
...(allowedOrigins.length > 0 && { allowedOrigins }),
|
|
68
|
+
...(values.secret && { secrets: values.secret }),
|
|
69
|
+
...(values["secret-field"] && { secretFields: values["secret-field"] }),
|
|
70
|
+
...(maxSteps !== undefined && { limits: { maxSteps } }),
|
|
71
|
+
...(io.signal && { signal: io.signal }),
|
|
72
|
+
});
|
|
73
|
+
let unanswerable;
|
|
74
|
+
for await (const event of session.events) {
|
|
75
|
+
if (json)
|
|
76
|
+
io.out(`${JSON.stringify(event)}\n`);
|
|
77
|
+
switch (event.type) {
|
|
78
|
+
case "plan":
|
|
79
|
+
if (!json)
|
|
80
|
+
io.out(`${event.text}\n`);
|
|
81
|
+
break;
|
|
82
|
+
case "tool-call":
|
|
83
|
+
if (!json)
|
|
84
|
+
io.out(` · ${event.tool} ${summaryOf(event.args)}\n`);
|
|
85
|
+
break;
|
|
86
|
+
case "step":
|
|
87
|
+
if (!json && event.code !== undefined)
|
|
88
|
+
io.out(` ${event.event.op === "insert" ? "+" : "~"} ${event.code}\n`);
|
|
89
|
+
break;
|
|
90
|
+
case "question": {
|
|
91
|
+
const originQuestion = event.options?.join("/") === "allow/deny";
|
|
92
|
+
if (io.prompt && !json) {
|
|
93
|
+
io.out(`? ${event.question}${event.options ? ` (${event.options.join("/")})` : ""}\n`);
|
|
94
|
+
void io
|
|
95
|
+
.prompt("> ")
|
|
96
|
+
.then((answer) => session.answer(event.id, answer))
|
|
97
|
+
.catch(() => undefined); // the run ended while the question was open
|
|
98
|
+
}
|
|
99
|
+
else if (originQuestion) {
|
|
100
|
+
session.answer(event.id, "deny");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
unanswerable = event.question;
|
|
104
|
+
session.stop();
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case "ended":
|
|
109
|
+
if (!json)
|
|
110
|
+
summarise(io, event.result);
|
|
111
|
+
break;
|
|
112
|
+
default:
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (unanswerable !== undefined)
|
|
117
|
+
io.err(`The agent asked "${unanswerable}" and nobody can answer: stdin is not a terminal${json ? " (or --json is set)" : ""}. Run it in a terminal, or put the answer in the goal.\n`);
|
|
118
|
+
return (await session.result).reason === "finished" ? 0 : 1;
|
|
119
|
+
}
|
|
120
|
+
function summaryOf(args) {
|
|
121
|
+
const text = JSON.stringify(args ?? {});
|
|
122
|
+
return text.length > 100 ? `${text.slice(0, 99)}…` : text;
|
|
123
|
+
}
|
|
124
|
+
const ENDINGS = {
|
|
125
|
+
stopped: "Stopped.",
|
|
126
|
+
"step-limit": "Stopped at the step limit (--max-steps).",
|
|
127
|
+
"token-limit": "Stopped at the token limit.",
|
|
128
|
+
"time-limit": "Stopped at the time limit.",
|
|
129
|
+
};
|
|
130
|
+
function summarise(io, result) {
|
|
131
|
+
const base = `"${result.test}" has ${result.steps} ${result.steps === 1 ? "step" : "steps"} in ${result.file}.`;
|
|
132
|
+
if (result.reason === "error")
|
|
133
|
+
io.err(`\nThe run failed: ${result.error ?? "unknown error"}\n${base}\n`);
|
|
134
|
+
else
|
|
135
|
+
io.out(`\n${result.reason === "finished" ? "Done" : (ENDINGS[result.reason] ?? result.reason)} ${base}\n${result.summary ? `${result.summary}\n` : ""}`);
|
|
136
|
+
if (result.secrets.length > 0)
|
|
137
|
+
io.out(`Set ${result.secrets.map((name) => `SAMURAI_SECRET_${name}`).join(", ")} (environment variable or .env.<environment>) before running it.\n`);
|
|
138
|
+
io.out(`${result.tokens} tokens used. Review the steps before you run the test.\n`);
|
|
139
|
+
}
|
package/dist/cli/io.d.ts
CHANGED
|
@@ -7,4 +7,6 @@ export interface Io {
|
|
|
7
7
|
env: Partial<NodeJS.ProcessEnv>;
|
|
8
8
|
/** Aborts a recording (Ctrl+C) */
|
|
9
9
|
signal?: AbortSignal;
|
|
10
|
+
/** Asks the person at the terminal; set when stdin is a terminal, so a command may ask questions */
|
|
11
|
+
prompt?: (question: string) => Promise<string>;
|
|
10
12
|
}
|
package/dist/cli/main.js
CHANGED
|
@@ -3,6 +3,7 @@ import { listTests, runTests } from "../runner/run.js";
|
|
|
3
3
|
import { ConsoleReporter } from "./console-reporter.js";
|
|
4
4
|
import { browsersCommand } from "./browsers.js";
|
|
5
5
|
import { flowCommand } from "./flow.js";
|
|
6
|
+
import { generateCommand } from "./generate.js";
|
|
6
7
|
import { initProject } from "./init.js";
|
|
7
8
|
import { recordCommand } from "./record.js";
|
|
8
9
|
import { USAGE } from "./usage.js";
|
|
@@ -36,6 +37,8 @@ export async function main(argv, io) {
|
|
|
36
37
|
io.out("\nNext: install the dependencies, then `samurai browsers install firefox`, then `samurai run`.\n");
|
|
37
38
|
return 0;
|
|
38
39
|
}
|
|
40
|
+
if (command === "generate")
|
|
41
|
+
return await generateCommand({ values, positionals }, io);
|
|
39
42
|
if (command === "record")
|
|
40
43
|
return await recordCommand({ values, positionals }, io);
|
|
41
44
|
if (command !== "run" && command !== "list") {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
/** Asks on the terminal when stdin is one; undefined otherwise (a pipe, CI), so a command knows it cannot ask */
|
|
3
|
+
export function terminalPrompt() {
|
|
4
|
+
if (!process.stdin.isTTY)
|
|
5
|
+
return undefined;
|
|
6
|
+
return (question) => new Promise((resolve) => {
|
|
7
|
+
const lines = createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout,
|
|
10
|
+
});
|
|
11
|
+
lines.question(question, (answer) => {
|
|
12
|
+
lines.close();
|
|
13
|
+
resolve(answer);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
package/dist/cli/usage.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const USAGE = "samurai: browser tests over WebDriver BiDi\n\nUsage:\n samurai run [options] Run the tests (the default command)\n samurai list [options] List the tests without running them\n samurai record <spec> Open a browser and record what you do into a spec file\n samurai init [folder] Scaffold a project: config, an example test, .env.example\n samurai flow <command> List, check and run the flows in ./flows (see below)\n samurai browsers [command] List, install and remove the browsers SAMURAI launches\n\nOptions for run and list:\n --env <name> Environment to run against (or SAMURAI_ENV)\n --file <spec> Only this spec file; repeatable\n --grep <text> Only tests whose full name contains the text\n --group <name> Only the tests of this group of the config's groups; repeat it for several\n --browser <name> Run in this browser (firefox or chrome) instead of the config's\n browsers; repeat it for more than one\n --json Machine-readable output: JSON lines for run, one array for list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless, --no-headless Run the browser without a window, or with one.\n Default: the config's use.headless, else headless when CI is set\n --port <n> Browser debugging port (default 9223)\n --workers <n|n%> Tests that run at the same time, each in a worker process: a number\n from 1, or a share of the CPUs like 50%. Default: the config's\n workers (or SAMURAI_WORKERS), else half the CPUs\n --retries <n> Run a failing test again up to n times, each with a fresh browser\n (or SAMURAI_RETRIES; default 0)\n --shard <current/total> Run one slice of the selected tests, to split a run over machines:\n --shard=2/4 runs the second of four. Whole files (and serial\n blocks) go to one shard; an empty shard succeeds\n\nOptions for record (also --env, --port, --json; always with a window):\n --browser <name> Record in this browser (default: the first of the config's browsers)\n --new <title> Add a test with this title (creates the spec file if needed)\n --test <name|index> Record into this test; not needed when the file has one\n --at <n> Insert from step n (default: after the last step)\n --url <url> Page to start on (default: the environment's baseURL)\n Stop with Ctrl+C or by closing the browser window.\n\n -h, --help Show this help\n -v, --version Show the version\n\nExit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.\n\nsamurai flow:\n list The flows in ./flows\n check [<flow>...] Check flows without running them\n run <flow>... | --all Run flows; each writes result/flows/<id>.json\n Options for run: --env, --headless, --timeout, --expect-timeout, --port, --json, --allow-unsupported\n Run `samurai flow` alone for the details.\n\nsamurai browsers:\n list The pinned browsers and every build in the cache (the default)\n install [firefox|chrome|all]\n Download the pinned builds (default: all)\n uninstall <name> [--build <id>] | --unused\n\nRun it in the project folder, the one holding samurai.config.ts.\n";
|
|
1
|
+
export declare const USAGE = "samurai: browser tests over WebDriver BiDi\n\nUsage:\n samurai run [options] Run the tests (the default command)\n samurai list [options] List the tests without running them\n samurai record <spec> Open a browser and record what you do into a spec file\n samurai generate \"<goal>\" Let an AI agent write a test into a spec file (needs ai, zod and a provider)\n samurai init [folder] Scaffold a project: config, an example test, .env.example\n samurai flow <command> List, check and run the flows in ./flows (see below)\n samurai browsers [command] List, install and remove the browsers SAMURAI launches\n\nOptions for run and list:\n --env <name> Environment to run against (or SAMURAI_ENV)\n --file <spec> Only this spec file; repeatable\n --grep <text> Only tests whose full name contains the text\n --group <name> Only the tests of this group of the config's groups; repeat it for several\n --browser <name> Run in this browser (firefox or chrome) instead of the config's\n browsers; repeat it for more than one\n --json Machine-readable output: JSON lines for run, one array for list\n\nOptions for run:\n --timeout <ms> Time one test may take\n --expect-timeout <ms> Time actions and assertions retry\n --headless, --no-headless Run the browser without a window, or with one.\n Default: the config's use.headless, else headless when CI is set\n --port <n> Browser debugging port (default 9223)\n --workers <n|n%> Tests that run at the same time, each in a worker process: a number\n from 1, or a share of the CPUs like 50%. Default: the config's\n workers (or SAMURAI_WORKERS), else half the CPUs\n --retries <n> Run a failing test again up to n times, each with a fresh browser\n (or SAMURAI_RETRIES; default 0)\n --shard <current/total> Run one slice of the selected tests, to split a run over machines:\n --shard=2/4 runs the second of four. Whole files (and serial\n blocks) go to one shard; an empty shard succeeds\n\nOptions for record (also --env, --port, --json; always with a window):\n --browser <name> Record in this browser (default: the first of the config's browsers)\n --new <title> Add a test with this title (creates the spec file if needed)\n --test <name|index> Record into this test; not needed when the file has one\n --at <n> Insert from step n (default: after the last step)\n --url <url> Page to start on (default: the environment's baseURL)\n Stop with Ctrl+C or by closing the browser window.\n\nOptions for generate (also --env, --browser, --port, --headless, --json):\n --file <spec> The spec file to write into (required, once)\n --new <title> Add a test with this title (creates the file if needed)\n --test <name|index> Write into this test; not needed when the file has one\n --url <url> Page to start on (default: the environment's baseURL)\n --model <provider:name> anthropic, openai or openai-compatible, for example\n anthropic:claude-sonnet-4-5. Default: SAMURAI_AI_MODEL, then the\n config's ai.model. Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY,\n SAMURAI_AI_API_KEY (+ SAMURAI_AI_BASE_URL for openai-compatible)\n --max-steps <n> Most model calls in the run (default 40)\n --yes-origin <origin> A site the agent may open without asking; repeatable\n --secret <NAME> A secret the agent may type (it never sees the value); repeatable\n --secret-field <css> A field whose value is never shown to the model; repeatable\n Questions are asked in the terminal; without one (or with --json) an unknown site is refused\n and any other question ends the run.\n\n -h, --help Show this help\n -v, --version Show the version\n\nExit code: 0 when every test passed (generate: the agent finished), 1 when a test failed (generate: it ended at a limit, was stopped or failed), 2 for a usage or configuration error.\n\nsamurai flow:\n list The flows in ./flows\n check [<flow>...] Check flows without running them\n run <flow>... | --all Run flows; each writes result/flows/<id>.json\n Options for run: --env, --headless, --timeout, --expect-timeout, --port, --json, --allow-unsupported\n Run `samurai flow` alone for the details.\n\nsamurai browsers:\n list The pinned browsers and every build in the cache (the default)\n install [firefox|chrome|all]\n Download the pinned builds (default: all)\n uninstall <name> [--build <id>] | --unused\n\nRun it in the project folder, the one holding samurai.config.ts.\n";
|
package/dist/cli/usage.js
CHANGED
|
@@ -5,6 +5,7 @@ Usage:
|
|
|
5
5
|
samurai run [options] Run the tests (the default command)
|
|
6
6
|
samurai list [options] List the tests without running them
|
|
7
7
|
samurai record <spec> Open a browser and record what you do into a spec file
|
|
8
|
+
samurai generate "<goal>" Let an AI agent write a test into a spec file (needs ai, zod and a provider)
|
|
8
9
|
samurai init [folder] Scaffold a project: config, an example test, .env.example
|
|
9
10
|
samurai flow <command> List, check and run the flows in ./flows (see below)
|
|
10
11
|
samurai browsers [command] List, install and remove the browsers SAMURAI launches
|
|
@@ -41,10 +42,26 @@ Options for record (also --env, --port, --json; always with a window):
|
|
|
41
42
|
--url <url> Page to start on (default: the environment's baseURL)
|
|
42
43
|
Stop with Ctrl+C or by closing the browser window.
|
|
43
44
|
|
|
45
|
+
Options for generate (also --env, --browser, --port, --headless, --json):
|
|
46
|
+
--file <spec> The spec file to write into (required, once)
|
|
47
|
+
--new <title> Add a test with this title (creates the file if needed)
|
|
48
|
+
--test <name|index> Write into this test; not needed when the file has one
|
|
49
|
+
--url <url> Page to start on (default: the environment's baseURL)
|
|
50
|
+
--model <provider:name> anthropic, openai or openai-compatible, for example
|
|
51
|
+
anthropic:claude-sonnet-4-5. Default: SAMURAI_AI_MODEL, then the
|
|
52
|
+
config's ai.model. Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY,
|
|
53
|
+
SAMURAI_AI_API_KEY (+ SAMURAI_AI_BASE_URL for openai-compatible)
|
|
54
|
+
--max-steps <n> Most model calls in the run (default 40)
|
|
55
|
+
--yes-origin <origin> A site the agent may open without asking; repeatable
|
|
56
|
+
--secret <NAME> A secret the agent may type (it never sees the value); repeatable
|
|
57
|
+
--secret-field <css> A field whose value is never shown to the model; repeatable
|
|
58
|
+
Questions are asked in the terminal; without one (or with --json) an unknown site is refused
|
|
59
|
+
and any other question ends the run.
|
|
60
|
+
|
|
44
61
|
-h, --help Show this help
|
|
45
62
|
-v, --version Show the version
|
|
46
63
|
|
|
47
|
-
Exit code: 0 when every test passed, 1 when a test failed, 2 for a usage or configuration error.
|
|
64
|
+
Exit code: 0 when every test passed (generate: the agent finished), 1 when a test failed (generate: it ended at a limit, was stopped or failed), 2 for a usage or configuration error.
|
|
48
65
|
|
|
49
66
|
samurai flow:
|
|
50
67
|
list The flows in ./flows
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { main } from "./cli/main.js";
|
|
2
|
+
import { terminalPrompt } from "./cli/prompt.js";
|
|
2
3
|
// Development entry (`npm run dev`, which runs `tsx src/index.ts`): the same as `samurai run`, straight from the sources
|
|
3
4
|
const interrupt = new AbortController();
|
|
4
5
|
process.on("SIGINT", () => {
|
|
@@ -12,4 +13,5 @@ process.exit(await main(process.argv.slice(2), {
|
|
|
12
13
|
color: Boolean(process.stdout.isTTY) && !process.env.NO_COLOR,
|
|
13
14
|
env: process.env,
|
|
14
15
|
signal: interrupt.signal,
|
|
16
|
+
...(terminalPrompt() && { prompt: terminalPrompt() }),
|
|
15
17
|
}));
|
|
@@ -64,6 +64,12 @@ export type Observation = {
|
|
|
64
64
|
flags: string;
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
+
/** An action with no argument that a page event does not tell apart: the AI agent's hover, double click and clear */
|
|
68
|
+
| {
|
|
69
|
+
type: "action";
|
|
70
|
+
kind: "hover" | "dblclick" | "clear";
|
|
71
|
+
locator: LocatorSpec;
|
|
72
|
+
}
|
|
67
73
|
/** The toolbar's Page panel: the page's URL or title */
|
|
68
74
|
| {
|
|
69
75
|
type: "expectPage";
|
|
@@ -71,6 +71,8 @@ export class Normaliser {
|
|
|
71
71
|
locator: observation.locator,
|
|
72
72
|
value: observation.value,
|
|
73
73
|
};
|
|
74
|
+
case "action":
|
|
75
|
+
return { kind: observation.kind, locator: observation.locator };
|
|
74
76
|
case "wait-idle":
|
|
75
77
|
return { kind: "waitForNetworkIdle" };
|
|
76
78
|
case "wait-time":
|
package/dist/types/config.d.ts
CHANGED
|
@@ -204,6 +204,11 @@ export interface SamuraiTestConfig {
|
|
|
204
204
|
* @default "files"
|
|
205
205
|
*/
|
|
206
206
|
filesDir?: string;
|
|
207
|
+
/**
|
|
208
|
+
* The AI agent (`samurai generate`). `model` is a `provider:name` pair, such as `anthropic:claude-sonnet-4-5`;
|
|
209
|
+
* never a key (keys come from the environment). The `--model` flag and `SAMURAI_AI_MODEL` win over it
|
|
210
|
+
*/
|
|
211
|
+
ai?: { model?: string };
|
|
207
212
|
/** How pages are opened, and what the browser pretends: the options of `test.use()` apply to every test */
|
|
208
213
|
use?: UseOptions &
|
|
209
214
|
FixtureUse & {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itmetsam/samurai-framework",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.4",
|
|
4
4
|
"description": "Browser test framework over WebDriver BiDi: auto-waiting locators, environments and secrets, a recorder, and a command line.",
|
|
5
5
|
"main": "dist/api.js",
|
|
6
6
|
"exports": {
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"types": "./dist/capabilities.d.ts",
|
|
53
53
|
"default": "./dist/capabilities.js"
|
|
54
54
|
},
|
|
55
|
+
"./agent": {
|
|
56
|
+
"types": "./dist/agent/index.d.ts",
|
|
57
|
+
"default": "./dist/agent/index.js"
|
|
58
|
+
},
|
|
55
59
|
"./package.json": "./package.json"
|
|
56
60
|
},
|
|
57
61
|
"scripts": {
|
|
@@ -80,10 +84,12 @@
|
|
|
80
84
|
"type": "module",
|
|
81
85
|
"devDependencies": {
|
|
82
86
|
"@types/node": "^25.6.0",
|
|
87
|
+
"ai": "^7.0.0",
|
|
83
88
|
"esbuild": "^0.28.2",
|
|
84
89
|
"knip": "^6.42.1",
|
|
85
90
|
"madge": "^8.0.0",
|
|
86
|
-
"tsx": "^4.23.15"
|
|
91
|
+
"tsx": "^4.23.15",
|
|
92
|
+
"zod": "^4.1.8"
|
|
87
93
|
},
|
|
88
94
|
"dependencies": {
|
|
89
95
|
"@puppeteer/browsers": "^3.2.4",
|
|
@@ -93,17 +99,26 @@
|
|
|
93
99
|
"types": "dist/api.d.ts",
|
|
94
100
|
"files": [
|
|
95
101
|
"dist",
|
|
96
|
-
"bin"
|
|
102
|
+
"bin",
|
|
103
|
+
"AGENT-GUIDE.md"
|
|
97
104
|
],
|
|
98
105
|
"bin": {
|
|
99
106
|
"samurai": "bin/samurai.mjs"
|
|
100
107
|
},
|
|
101
108
|
"peerDependencies": {
|
|
102
|
-
"
|
|
109
|
+
"ai": "^7.0.0",
|
|
110
|
+
"tsx": "^4.21.0",
|
|
111
|
+
"zod": "^3.25.76 || ^4.1.8"
|
|
103
112
|
},
|
|
104
113
|
"peerDependenciesMeta": {
|
|
114
|
+
"ai": {
|
|
115
|
+
"optional": true
|
|
116
|
+
},
|
|
105
117
|
"tsx": {
|
|
106
118
|
"optional": true
|
|
119
|
+
},
|
|
120
|
+
"zod": {
|
|
121
|
+
"optional": true
|
|
107
122
|
}
|
|
108
123
|
},
|
|
109
124
|
"repository": {
|