@agentxjs/devtools 1.9.6-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/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
+ }
@@ -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";
@@ -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: process.env.DEEPRACTICE_API_KEY,
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