@agentxjs/devtools 1.9.5-dev → 2.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/README.md +284 -0
- package/dist/bdd/cli.d.ts +1 -0
- package/dist/bdd/cli.js +117 -0
- package/dist/bdd/cli.js.map +1 -0
- package/dist/bdd/index.d.ts +202 -0
- package/dist/bdd/index.js +381 -0
- package/dist/bdd/index.js.map +1 -0
- package/dist/chunk-6OHXS7LW.js +297 -0
- package/dist/chunk-6OHXS7LW.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-DR45HEV4.js +152 -0
- package/dist/chunk-DR45HEV4.js.map +1 -0
- package/dist/chunk-J6L73HM5.js +301 -0
- package/dist/chunk-J6L73HM5.js.map +1 -0
- package/dist/chunk-S7J75AXG.js +64 -0
- package/dist/chunk-S7J75AXG.js.map +1 -0
- package/dist/fixtures/index.d.ts +49 -0
- package/dist/fixtures/index.js +22 -0
- package/dist/fixtures/index.js.map +1 -0
- package/dist/index.d.ts +240 -0
- package/dist/index.js +269 -0
- package/dist/index.js.map +1 -0
- package/dist/mock/index.d.ts +115 -0
- package/dist/mock/index.js +11 -0
- package/dist/mock/index.js.map +1 -0
- package/dist/recorder/index.d.ts +120 -0
- package/dist/recorder/index.js +10 -0
- package/dist/recorder/index.js.map +1 -0
- package/dist/types-C6Lf3vz2.d.ts +78 -0
- package/package.json +63 -8
- package/src/Devtools.ts +11 -14
- package/src/bdd/agent-doc-tester.ts +130 -0
- package/src/bdd/agent-ui-tester.ts +88 -0
- package/src/bdd/cli.ts +166 -0
- package/src/bdd/cucumber.config.ts +40 -0
- package/src/bdd/dev-server.ts +82 -0
- package/src/bdd/index.ts +41 -0
- package/src/bdd/paths.ts +140 -0
- package/src/bdd/playwright.ts +110 -0
- package/src/env.ts +97 -0
- package/src/index.ts +6 -1
- package/src/mock/MockDriver.ts +21 -12
- package/src/recorder/RecordingDriver.ts +1 -5
- package/scripts/record-fixture.ts +0 -148
- package/tsconfig.json +0 -10
package/src/bdd/cli.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BDD CLI wrapper for cucumber-js
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* bdd # Run all tests
|
|
7
|
+
* bdd path/to/file.feature # Run specific feature file
|
|
8
|
+
* bdd path/to/file.feature:10 # Run specific scenario by line
|
|
9
|
+
* bdd --tags @contributor # Run specific tags
|
|
10
|
+
* bdd --tags "@dev and not @slow" # Tag expression
|
|
11
|
+
* bdd --name "token usage" # Filter by scenario name (regex)
|
|
12
|
+
* bdd --dry-run # Validate without executing
|
|
13
|
+
* bdd --config path # Custom config (default: bdd/cucumber.js)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { resolve, dirname, relative } from "node:path";
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
|
|
23
|
+
// Load .env files (like dotenv but zero dependencies)
|
|
24
|
+
function loadEnvFile(filePath: string) {
|
|
25
|
+
if (!existsSync(filePath)) return;
|
|
26
|
+
const content = readFileSync(filePath, "utf-8");
|
|
27
|
+
for (const line of content.split("\n")) {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
30
|
+
const eqIndex = trimmed.indexOf("=");
|
|
31
|
+
if (eqIndex === -1) continue;
|
|
32
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
33
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
34
|
+
if (
|
|
35
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
36
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
37
|
+
) {
|
|
38
|
+
value = value.slice(1, -1);
|
|
39
|
+
}
|
|
40
|
+
if (process.env[key] === undefined) {
|
|
41
|
+
process.env[key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Find monorepo root by walking up to find the root package.json with workspaces
|
|
47
|
+
function findMonorepoRoot(startDir: string): string | null {
|
|
48
|
+
let dir = startDir;
|
|
49
|
+
while (true) {
|
|
50
|
+
const pkgPath = resolve(dir, "package.json");
|
|
51
|
+
if (existsSync(pkgPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
54
|
+
if (pkg.workspaces) return dir;
|
|
55
|
+
} catch {
|
|
56
|
+
// ignore parse errors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const parent = dirname(dir);
|
|
60
|
+
if (parent === dir) return null;
|
|
61
|
+
dir = parent;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const cwd = process.cwd();
|
|
66
|
+
|
|
67
|
+
// Load .env files from cwd first, then monorepo root
|
|
68
|
+
loadEnvFile(resolve(cwd, ".env"));
|
|
69
|
+
loadEnvFile(resolve(cwd, ".env.local"));
|
|
70
|
+
|
|
71
|
+
const monorepoRoot = findMonorepoRoot(cwd);
|
|
72
|
+
if (monorepoRoot && monorepoRoot !== cwd) {
|
|
73
|
+
loadEnvFile(resolve(monorepoRoot, ".env"));
|
|
74
|
+
loadEnvFile(resolve(monorepoRoot, ".env.local"));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const args = process.argv.slice(2);
|
|
78
|
+
|
|
79
|
+
// Extract --config
|
|
80
|
+
let configPath = "bdd/cucumber.js";
|
|
81
|
+
const configIndex = args.indexOf("--config");
|
|
82
|
+
if (configIndex !== -1 && args[configIndex + 1]) {
|
|
83
|
+
configPath = args[configIndex + 1];
|
|
84
|
+
args.splice(configIndex, 2);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check if config exists
|
|
88
|
+
const fullConfigPath = resolve(cwd, configPath);
|
|
89
|
+
if (!existsSync(fullConfigPath)) {
|
|
90
|
+
console.error(`Config not found: ${fullConfigPath}`);
|
|
91
|
+
console.error("Create bdd/cucumber.js or specify --config path");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Separate positional args (feature files/lines) from flags
|
|
96
|
+
const featurePaths: string[] = [];
|
|
97
|
+
const flags: string[] = [];
|
|
98
|
+
|
|
99
|
+
for (const arg of args) {
|
|
100
|
+
if (arg.startsWith("-")) {
|
|
101
|
+
flags.push(arg);
|
|
102
|
+
} else if (arg.endsWith(".feature") || arg.includes(".feature:")) {
|
|
103
|
+
featurePaths.push(arg);
|
|
104
|
+
} else {
|
|
105
|
+
// Could be a flag value (e.g. after --tags), keep as-is
|
|
106
|
+
flags.push(arg);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Find cucumber-js binary
|
|
111
|
+
const cucumberPaths = [
|
|
112
|
+
resolve(cwd, "node_modules/.bin/cucumber-js"),
|
|
113
|
+
resolve(__dirname, "../../../.bin/cucumber-js"),
|
|
114
|
+
"cucumber-js",
|
|
115
|
+
];
|
|
116
|
+
const cucumberBin =
|
|
117
|
+
cucumberPaths.find((p) => p === "cucumber-js" || existsSync(p)) || "cucumber-js";
|
|
118
|
+
|
|
119
|
+
const rootNodeModules = resolve(cwd, "node_modules");
|
|
120
|
+
|
|
121
|
+
// When feature paths are specified, generate a temp config that overrides
|
|
122
|
+
// the original config's `paths` — cucumber-js config.paths takes precedence
|
|
123
|
+
// over positional args, so we must override it in the config itself.
|
|
124
|
+
let effectiveConfig = configPath;
|
|
125
|
+
let tempConfigPath: string | null = null;
|
|
126
|
+
|
|
127
|
+
if (featurePaths.length > 0) {
|
|
128
|
+
const configRelPath = relative(
|
|
129
|
+
dirname(resolve(cwd, "bdd/.tmp-cucumber.js")),
|
|
130
|
+
fullConfigPath
|
|
131
|
+
).replace(/\\/g, "/");
|
|
132
|
+
const pathsJson = JSON.stringify(featurePaths);
|
|
133
|
+
const tempContent = [
|
|
134
|
+
`import config from "./${configRelPath}";`,
|
|
135
|
+
`export default { ...config.default ?? config, paths: ${pathsJson} };`,
|
|
136
|
+
"",
|
|
137
|
+
].join("\n");
|
|
138
|
+
|
|
139
|
+
tempConfigPath = resolve(cwd, "bdd/.tmp-cucumber.js");
|
|
140
|
+
writeFileSync(tempConfigPath, tempContent);
|
|
141
|
+
effectiveConfig = "bdd/.tmp-cucumber.js";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build cucumber args
|
|
145
|
+
const cucumberArgs = ["--config", effectiveConfig, ...flags];
|
|
146
|
+
|
|
147
|
+
const child = spawn(cucumberBin, cucumberArgs, {
|
|
148
|
+
stdio: "inherit",
|
|
149
|
+
env: {
|
|
150
|
+
...process.env,
|
|
151
|
+
NODE_OPTIONS: "--import tsx",
|
|
152
|
+
NODE_PATH: rootNodeModules,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
child.on("close", (code) => {
|
|
157
|
+
// Clean up temp config
|
|
158
|
+
if (tempConfigPath && existsSync(tempConfigPath)) {
|
|
159
|
+
try {
|
|
160
|
+
unlinkSync(tempConfigPath);
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore cleanup errors
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
process.exit(code ?? 0);
|
|
166
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Cucumber configuration for BDD tests
|
|
3
|
+
*
|
|
4
|
+
* Usage in project's cucumber.js:
|
|
5
|
+
*
|
|
6
|
+
* ```js
|
|
7
|
+
* import { createCucumberConfig } from "@agentxjs/devtools/bdd";
|
|
8
|
+
*
|
|
9
|
+
* export default createCucumberConfig({
|
|
10
|
+
* paths: ["bdd/journeys/** /*.feature"],
|
|
11
|
+
* import: ["bdd/steps/** /*.ts"],
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface CucumberConfigOptions {
|
|
17
|
+
/** Feature file paths */
|
|
18
|
+
paths: string[];
|
|
19
|
+
/** Step definition paths */
|
|
20
|
+
import: string[];
|
|
21
|
+
/** Tags to filter (default: exclude @pending and @skip) */
|
|
22
|
+
tags?: string;
|
|
23
|
+
/** Default timeout in ms (default: 30000) */
|
|
24
|
+
timeout?: number;
|
|
25
|
+
/** Format output (default: progress) */
|
|
26
|
+
format?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createCucumberConfig(options: CucumberConfigOptions) {
|
|
30
|
+
return {
|
|
31
|
+
format: options.format ?? ["progress"],
|
|
32
|
+
formatOptions: { snippetInterface: "async-await" },
|
|
33
|
+
import: options.import,
|
|
34
|
+
paths: options.paths,
|
|
35
|
+
tags: options.tags ?? "not @pending and not @skip",
|
|
36
|
+
worldParameters: {
|
|
37
|
+
defaultTimeout: options.timeout ?? 30000,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev server utilities for BDD testing
|
|
3
|
+
*
|
|
4
|
+
* Start and stop dev servers during test runs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn, ChildProcess } from "node:child_process";
|
|
8
|
+
import { waitForUrl } from "./playwright";
|
|
9
|
+
|
|
10
|
+
export interface DevServerOptions {
|
|
11
|
+
/** Working directory */
|
|
12
|
+
cwd: string;
|
|
13
|
+
/** Command to run (default: "bun") */
|
|
14
|
+
command?: string;
|
|
15
|
+
/** Command arguments (default: ["run", "dev"]) */
|
|
16
|
+
args?: string[];
|
|
17
|
+
/** Port to wait for */
|
|
18
|
+
port: number;
|
|
19
|
+
/** Startup timeout in ms (default: 30000) */
|
|
20
|
+
timeout?: number;
|
|
21
|
+
/** Show server output (default: false, or true if DEBUG env is set) */
|
|
22
|
+
debug?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let devServer: ChildProcess | null = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start a dev server and wait for it to be ready
|
|
29
|
+
*/
|
|
30
|
+
export async function startDevServer(options: DevServerOptions): Promise<void> {
|
|
31
|
+
if (devServer) return;
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
cwd,
|
|
35
|
+
command = "bun",
|
|
36
|
+
args = ["run", "dev"],
|
|
37
|
+
port,
|
|
38
|
+
timeout = 30000,
|
|
39
|
+
debug = !!process.env.DEBUG,
|
|
40
|
+
} = options;
|
|
41
|
+
|
|
42
|
+
devServer = spawn(command, args, {
|
|
43
|
+
cwd,
|
|
44
|
+
stdio: "pipe",
|
|
45
|
+
detached: false,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (debug) {
|
|
49
|
+
devServer.stdout?.on("data", (data) => {
|
|
50
|
+
console.log("[dev-server]", data.toString());
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
devServer.stderr?.on("data", (data) => {
|
|
54
|
+
console.error("[dev-server error]", data.toString());
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const url = `http://localhost:${port}`;
|
|
59
|
+
const ready = await waitForUrl(url, timeout);
|
|
60
|
+
|
|
61
|
+
if (!ready) {
|
|
62
|
+
stopDevServer();
|
|
63
|
+
throw new Error(`Dev server failed to start on port ${port}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Stop the dev server
|
|
69
|
+
*/
|
|
70
|
+
export function stopDevServer(): void {
|
|
71
|
+
if (devServer) {
|
|
72
|
+
devServer.kill("SIGTERM");
|
|
73
|
+
devServer = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the dev server process (for advanced use)
|
|
79
|
+
*/
|
|
80
|
+
export function getDevServer(): ChildProcess | null {
|
|
81
|
+
return devServer;
|
|
82
|
+
}
|
package/src/bdd/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BDD utilities for testing AgentX projects
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import {
|
|
7
|
+
* createCucumberConfig,
|
|
8
|
+
* launchBrowser,
|
|
9
|
+
* startDevServer,
|
|
10
|
+
* } from "@agentxjs/devtools/bdd";
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { createCucumberConfig, type CucumberConfigOptions } from "./cucumber.config";
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
launchBrowser,
|
|
18
|
+
getPage,
|
|
19
|
+
resetPage,
|
|
20
|
+
closePage,
|
|
21
|
+
closeBrowser,
|
|
22
|
+
waitForUrl,
|
|
23
|
+
type BrowserOptions,
|
|
24
|
+
} from "./playwright";
|
|
25
|
+
|
|
26
|
+
export { startDevServer, stopDevServer, getDevServer, type DevServerOptions } from "./dev-server";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
paths,
|
|
30
|
+
getMonorepoPath,
|
|
31
|
+
getPackagePath,
|
|
32
|
+
getBddPath,
|
|
33
|
+
getFixturesPath,
|
|
34
|
+
getTempPath,
|
|
35
|
+
ensureDir,
|
|
36
|
+
resetPaths,
|
|
37
|
+
} from "./paths";
|
|
38
|
+
|
|
39
|
+
export { agentUiTester, type UiTestResult, type UiTesterOptions } from "./agent-ui-tester";
|
|
40
|
+
|
|
41
|
+
export { agentDocTester, type DocTestResult, type DocTesterOptions } from "./agent-doc-tester";
|
package/src/bdd/paths.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified path utilities for BDD testing
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent path resolution across all packages.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { resolve, dirname } from "node:path";
|
|
8
|
+
import { existsSync, mkdtempSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Path Resolution
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find the monorepo root by looking for root package.json with workspaces
|
|
17
|
+
*/
|
|
18
|
+
export function findMonorepoRoot(startDir: string = process.cwd()): string {
|
|
19
|
+
let dir = startDir;
|
|
20
|
+
while (dir !== "/") {
|
|
21
|
+
const pkgPath = resolve(dir, "package.json");
|
|
22
|
+
if (existsSync(pkgPath)) {
|
|
23
|
+
try {
|
|
24
|
+
const pkg = require(pkgPath);
|
|
25
|
+
if (pkg.workspaces || pkg.private === true) {
|
|
26
|
+
// Check if it looks like a monorepo root
|
|
27
|
+
const hasPackages = existsSync(resolve(dir, "packages"));
|
|
28
|
+
const hasApps = existsSync(resolve(dir, "apps"));
|
|
29
|
+
if (hasPackages || hasApps) {
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Ignore errors
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
dir = dirname(dir);
|
|
38
|
+
}
|
|
39
|
+
return startDir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the current package root (where package.json is)
|
|
44
|
+
*/
|
|
45
|
+
export function getPackageRoot(startDir: string = process.cwd()): string {
|
|
46
|
+
let dir = startDir;
|
|
47
|
+
while (dir !== "/") {
|
|
48
|
+
if (existsSync(resolve(dir, "package.json"))) {
|
|
49
|
+
return dir;
|
|
50
|
+
}
|
|
51
|
+
dir = dirname(dir);
|
|
52
|
+
}
|
|
53
|
+
return startDir;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Standard Paths
|
|
58
|
+
// ============================================================================
|
|
59
|
+
|
|
60
|
+
let _monorepoRoot: string | null = null;
|
|
61
|
+
let _packageRoot: string | null = null;
|
|
62
|
+
let _tempDir: string | null = null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Monorepo root directory
|
|
66
|
+
*/
|
|
67
|
+
export function getMonorepoPath(): string {
|
|
68
|
+
if (!_monorepoRoot) {
|
|
69
|
+
_monorepoRoot = findMonorepoRoot();
|
|
70
|
+
}
|
|
71
|
+
return _monorepoRoot;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Current package root directory
|
|
76
|
+
*/
|
|
77
|
+
export function getPackagePath(): string {
|
|
78
|
+
if (!_packageRoot) {
|
|
79
|
+
_packageRoot = getPackageRoot();
|
|
80
|
+
}
|
|
81
|
+
return _packageRoot;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* BDD directory for current package
|
|
86
|
+
*/
|
|
87
|
+
export function getBddPath(): string {
|
|
88
|
+
return resolve(getPackagePath(), "bdd");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Fixtures directory for current package's BDD tests
|
|
93
|
+
*/
|
|
94
|
+
export function getFixturesPath(subdir?: string): string {
|
|
95
|
+
const base = resolve(getBddPath(), "fixtures");
|
|
96
|
+
return subdir ? resolve(base, subdir) : base;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get or create a temporary directory for tests
|
|
101
|
+
*/
|
|
102
|
+
export function getTempPath(prefix: string = "bdd-"): string {
|
|
103
|
+
if (!_tempDir) {
|
|
104
|
+
_tempDir = mkdtempSync(resolve(tmpdir(), prefix));
|
|
105
|
+
}
|
|
106
|
+
return _tempDir;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Ensure a directory exists, creating it if necessary
|
|
111
|
+
*/
|
|
112
|
+
export function ensureDir(path: string): string {
|
|
113
|
+
if (!existsSync(path)) {
|
|
114
|
+
mkdirSync(path, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
return path;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reset cached paths (useful for testing)
|
|
121
|
+
*/
|
|
122
|
+
export function resetPaths(): void {
|
|
123
|
+
_monorepoRoot = null;
|
|
124
|
+
_packageRoot = null;
|
|
125
|
+
_tempDir = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Convenience exports
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
export const paths = {
|
|
133
|
+
monorepo: getMonorepoPath,
|
|
134
|
+
package: getPackagePath,
|
|
135
|
+
bdd: getBddPath,
|
|
136
|
+
fixtures: getFixturesPath,
|
|
137
|
+
temp: getTempPath,
|
|
138
|
+
ensure: ensureDir,
|
|
139
|
+
reset: resetPaths,
|
|
140
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright utilities for BDD testing
|
|
3
|
+
*
|
|
4
|
+
* Uses system Chrome to avoid downloading Chromium.
|
|
5
|
+
* Install: PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 bun add -d @playwright/test
|
|
6
|
+
*
|
|
7
|
+
* Browser lifecycle:
|
|
8
|
+
* - Single browser instance for all tests
|
|
9
|
+
* - Single page (tab) reused across scenarios
|
|
10
|
+
* - resetPage() clears state between scenarios
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { chromium, Browser, Page } from "@playwright/test";
|
|
14
|
+
|
|
15
|
+
export interface BrowserOptions {
|
|
16
|
+
headless?: boolean;
|
|
17
|
+
slowMo?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let browser: Browser | null = null;
|
|
21
|
+
let page: Page | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Launch browser using system Chrome (singleton)
|
|
25
|
+
*/
|
|
26
|
+
export async function launchBrowser(options: BrowserOptions = {}): Promise<Browser> {
|
|
27
|
+
if (browser) return browser;
|
|
28
|
+
|
|
29
|
+
const headless = options.headless ?? process.env.CI === "true";
|
|
30
|
+
|
|
31
|
+
browser = await chromium.launch({
|
|
32
|
+
channel: "chrome",
|
|
33
|
+
headless,
|
|
34
|
+
slowMo: options.slowMo,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return browser;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get or create a page (singleton, reused across scenarios)
|
|
42
|
+
*/
|
|
43
|
+
export async function getPage(): Promise<Page> {
|
|
44
|
+
if (page && !page.isClosed()) return page;
|
|
45
|
+
|
|
46
|
+
const b = await launchBrowser();
|
|
47
|
+
page = await b.newPage();
|
|
48
|
+
return page;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Reset page state between scenarios (without closing)
|
|
53
|
+
* Use this instead of closePage() for faster tests
|
|
54
|
+
*/
|
|
55
|
+
export async function resetPage(): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
if (page && !page.isClosed()) {
|
|
58
|
+
const context = page.context();
|
|
59
|
+
await context.clearCookies();
|
|
60
|
+
await page.goto("about:blank");
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Browser may have crashed, will recreate on next getPage()
|
|
64
|
+
page = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Close current page
|
|
70
|
+
* @deprecated Use resetPage() for faster tests. Only use closePage() if you need full isolation.
|
|
71
|
+
*/
|
|
72
|
+
export async function closePage(): Promise<void> {
|
|
73
|
+
if (page) {
|
|
74
|
+
await page.close();
|
|
75
|
+
page = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Close browser and cleanup
|
|
81
|
+
*/
|
|
82
|
+
export async function closeBrowser(): Promise<void> {
|
|
83
|
+
if (page && !page.isClosed()) {
|
|
84
|
+
await page.close();
|
|
85
|
+
page = null;
|
|
86
|
+
}
|
|
87
|
+
if (browser) {
|
|
88
|
+
await browser.close();
|
|
89
|
+
browser = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Wait for a URL to be accessible
|
|
95
|
+
*/
|
|
96
|
+
export async function waitForUrl(url: string, timeout = 30000): Promise<boolean> {
|
|
97
|
+
const start = Date.now();
|
|
98
|
+
while (Date.now() - start < timeout) {
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(url);
|
|
101
|
+
if (response.ok) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Not ready yet
|
|
106
|
+
}
|
|
107
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
package/src/env.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified environment configuration for devtools
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for API credentials and model settings.
|
|
5
|
+
* Automatically loads .env / .env.local from monorepo root on import.
|
|
6
|
+
*
|
|
7
|
+
* All devtools modules (VCR, BDD, Devtools SDK) should use this
|
|
8
|
+
* instead of reading process.env directly.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { env } from "@agentxjs/devtools";
|
|
13
|
+
*
|
|
14
|
+
* const driver = createMonoDriver({
|
|
15
|
+
* apiKey: env.apiKey!,
|
|
16
|
+
* baseUrl: env.baseUrl,
|
|
17
|
+
* model: env.model,
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import { resolve, dirname } from "node:path";
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Auto-load .env files from monorepo root
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
function loadEnvFile(filePath: string): void {
|
|
30
|
+
if (!existsSync(filePath)) return;
|
|
31
|
+
const content = readFileSync(filePath, "utf-8");
|
|
32
|
+
for (const line of content.split("\n")) {
|
|
33
|
+
const trimmed = line.trim();
|
|
34
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
35
|
+
const eqIndex = trimmed.indexOf("=");
|
|
36
|
+
if (eqIndex === -1) continue;
|
|
37
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
38
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
39
|
+
if (
|
|
40
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
41
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
42
|
+
) {
|
|
43
|
+
value = value.slice(1, -1);
|
|
44
|
+
}
|
|
45
|
+
if (process.env[key] === undefined) {
|
|
46
|
+
process.env[key] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findMonorepoRoot(): string | null {
|
|
52
|
+
let dir = process.cwd();
|
|
53
|
+
while (true) {
|
|
54
|
+
const pkgPath = resolve(dir, "package.json");
|
|
55
|
+
if (existsSync(pkgPath)) {
|
|
56
|
+
try {
|
|
57
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
58
|
+
if (pkg.workspaces) return dir;
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
const parent = dirname(dir);
|
|
62
|
+
if (parent === dir) return null;
|
|
63
|
+
dir = parent;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Load on import — cwd first, then monorepo root
|
|
68
|
+
const cwd = process.cwd();
|
|
69
|
+
loadEnvFile(resolve(cwd, ".env"));
|
|
70
|
+
loadEnvFile(resolve(cwd, ".env.local"));
|
|
71
|
+
|
|
72
|
+
const monorepoRoot = findMonorepoRoot();
|
|
73
|
+
if (monorepoRoot && monorepoRoot !== cwd) {
|
|
74
|
+
loadEnvFile(resolve(monorepoRoot, ".env"));
|
|
75
|
+
loadEnvFile(resolve(monorepoRoot, ".env.local"));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Public API
|
|
80
|
+
// ============================================================================
|
|
81
|
+
|
|
82
|
+
export const env = {
|
|
83
|
+
/** Deepractice API key */
|
|
84
|
+
get apiKey(): string | undefined {
|
|
85
|
+
return process.env.DEEPRACTICE_API_KEY;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/** Deepractice API base URL */
|
|
89
|
+
get baseUrl(): string | undefined {
|
|
90
|
+
return process.env.DEEPRACTICE_BASE_URL;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/** Model identifier */
|
|
94
|
+
get model(): string {
|
|
95
|
+
return process.env.DEEPRACTICE_MODEL || "claude-haiku-4-5-20251001";
|
|
96
|
+
},
|
|
97
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
* ```typescript
|
|
9
9
|
* import { createDevtools } from "@agentxjs/devtools";
|
|
10
10
|
*
|
|
11
|
+
* import { env } from "@agentxjs/devtools";
|
|
12
|
+
*
|
|
11
13
|
* const devtools = createDevtools({
|
|
12
14
|
* fixturesDir: "./fixtures",
|
|
13
|
-
* apiKey:
|
|
15
|
+
* apiKey: env.apiKey,
|
|
14
16
|
* });
|
|
15
17
|
*
|
|
16
18
|
* // Has fixture → playback (MockDriver)
|
|
@@ -58,6 +60,9 @@ export {
|
|
|
58
60
|
type RecordingDriverOptions,
|
|
59
61
|
} from "./recorder/RecordingDriver";
|
|
60
62
|
|
|
63
|
+
// Environment
|
|
64
|
+
export { env } from "./env";
|
|
65
|
+
|
|
61
66
|
// Types
|
|
62
67
|
export type { Fixture, FixtureEvent, MockDriverOptions } from "./types";
|
|
63
68
|
|