@untestutils/ai 0.5.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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/run.d.mts +4 -0
  3. package/dist/agent/run.d.ts +19 -0
  4. package/dist/agent/run.d.ts.map +1 -0
  5. package/dist/agent/run.mjs +140 -0
  6. package/dist/agent/types.d.mts +33 -0
  7. package/dist/agent/types.d.ts +34 -0
  8. package/dist/agent/types.d.ts.map +1 -0
  9. package/dist/agent/types.mjs +22 -0
  10. package/dist/index.d.mts +19 -0
  11. package/dist/index.d.ts +20 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.mjs +14 -0
  14. package/dist/model.d.mts +1 -0
  15. package/dist/model.d.ts +14 -0
  16. package/dist/model.d.ts.map +1 -0
  17. package/dist/model.mjs +29 -0
  18. package/dist/prompts/convert-v1.md +28 -0
  19. package/dist/prompts/cover-v1.md +36 -0
  20. package/dist/prompts/fix-v1.md +20 -0
  21. package/dist/prompts/v1.md +37 -0
  22. package/dist/prompts.d.ts +14 -0
  23. package/dist/prompts.d.ts.map +1 -0
  24. package/dist/prompts.mjs +31 -0
  25. package/dist/tools/browser.d.mts +9 -0
  26. package/dist/tools/browser.d.ts +10 -0
  27. package/dist/tools/browser.d.ts.map +1 -0
  28. package/dist/tools/browser.mjs +146 -0
  29. package/dist/tools/fs.d.ts +16 -0
  30. package/dist/tools/fs.d.ts.map +1 -0
  31. package/dist/tools/fs.mjs +132 -0
  32. package/dist/tools/registry.d.mts +15 -0
  33. package/dist/tools/registry.d.ts +16 -0
  34. package/dist/tools/registry.d.ts.map +1 -0
  35. package/dist/tools/registry.mjs +22 -0
  36. package/dist/tree.d.ts +18 -0
  37. package/dist/tree.d.ts.map +1 -0
  38. package/dist/tree.mjs +70 -0
  39. package/dist/workflows/cover.d.mts +23 -0
  40. package/dist/workflows/cover.d.ts +24 -0
  41. package/dist/workflows/cover.d.ts.map +1 -0
  42. package/dist/workflows/cover.mjs +95 -0
  43. package/dist/workflows/fix.d.mts +17 -0
  44. package/dist/workflows/fix.d.ts +18 -0
  45. package/dist/workflows/fix.d.ts.map +1 -0
  46. package/dist/workflows/fix.mjs +88 -0
  47. package/dist/workflows/generate.d.ts +29 -0
  48. package/dist/workflows/generate.d.ts.map +1 -0
  49. package/dist/workflows/generate.mjs +167 -0
  50. package/dist/workflows/types.d.mts +6 -0
  51. package/dist/workflows/types.d.ts +7 -0
  52. package/dist/workflows/types.d.ts.map +1 -0
  53. package/dist/workflows/types.mjs +1 -0
  54. package/package.json +75 -0
@@ -0,0 +1,146 @@
1
+
2
+ export function createBrowserTools(opts = {}) {
3
+ let browser;
4
+ let page;
5
+ async function ensurePage() {
6
+ if (page) return page;
7
+ const { chromium } = await import("playwright-core");
8
+ browser = await chromium.launch({ headless: opts.headless ?? true });
9
+ const context = await browser.newContext(opts.baseURL ? { baseURL: opts.baseURL } : undefined);
10
+ page = await context.newPage();
11
+ return page;
12
+ }
13
+ async function dispose() {
14
+ await page?.context().close().catch(() => {});
15
+ await browser?.close().catch(() => {});
16
+ page = undefined;
17
+ browser = undefined;
18
+ }
19
+ return {
20
+ readPaths: () => [],
21
+ dispose,
22
+ tools: [
23
+ {
24
+ name: "browser_goto",
25
+ description: "Navigate the Playwright page to a path or absolute URL. Uses baseURL when path is relative.",
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: { url: {
29
+ type: "string",
30
+ description: "Path or absolute URL"
31
+ } },
32
+ required: ["url"]
33
+ },
34
+ execute: async (args) => {
35
+ const p = await ensurePage();
36
+ const res = await p.goto(String(args.url), { waitUntil: "domcontentloaded" });
37
+ return {
38
+ ok: Boolean(res && res.ok()),
39
+ status: res?.status() ?? null,
40
+ url: p.url(),
41
+ title: await p.title()
42
+ };
43
+ }
44
+ },
45
+ {
46
+ name: "browser_snapshot",
47
+ description: "Accessibility snapshot of the current page (roles, names, text) — preferred over raw HTML for test authoring.",
48
+ inputSchema: {
49
+ type: "object",
50
+ properties: {}
51
+ },
52
+ execute: async () => {
53
+ const p = await ensurePage();
54
+ const snapshot = await p.locator("body").ariaSnapshot().catch(async () => {
55
+ const text = await p.innerText("body").catch(() => "");
56
+ return text.slice(0, 8e3);
57
+ });
58
+ return {
59
+ url: p.url(),
60
+ title: await p.title(),
61
+ snapshot
62
+ };
63
+ }
64
+ },
65
+ {
66
+ name: "browser_content",
67
+ description: "Return truncated HTML or visible text of the current page.",
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ mode: {
72
+ type: "string",
73
+ description: "\"text\" (default) or \"html\""
74
+ },
75
+ maxChars: {
76
+ type: "string",
77
+ description: "Max characters (default 12000)"
78
+ }
79
+ }
80
+ },
81
+ execute: async (args) => {
82
+ const p = await ensurePage();
83
+ const max = Number(args.maxChars ?? 12e3);
84
+ const mode = String(args.mode ?? "text");
85
+ const raw = mode === "html" ? await p.content() : await p.innerText("body").catch(() => "");
86
+ return {
87
+ url: p.url(),
88
+ content: raw.slice(0, max)
89
+ };
90
+ }
91
+ },
92
+ {
93
+ name: "browser_click",
94
+ description: "Click an element by CSS selector or role=name (e.g. role=button[name=Save]).",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: {
98
+ selector: {
99
+ type: "string",
100
+ description: "CSS selector"
101
+ },
102
+ role: {
103
+ type: "string",
104
+ description: "ARIA role"
105
+ },
106
+ name: {
107
+ type: "string",
108
+ description: "Accessible name when using role"
109
+ }
110
+ }
111
+ },
112
+ execute: async (args) => {
113
+ const p = await ensurePage();
114
+ if (args.role) {
115
+ await p.getByRole(String(args.role), { name: args.name ? String(args.name) : undefined }).click();
116
+ } else if (args.selector) {
117
+ await p.locator(String(args.selector)).click();
118
+ } else {
119
+ throw new Error("Provide selector or role");
120
+ }
121
+ return {
122
+ ok: true,
123
+ url: p.url()
124
+ };
125
+ }
126
+ },
127
+ {
128
+ name: "browser_eval",
129
+ description: "Evaluate a small JS expression in the page (return JSON-serializable value).",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: { expression: {
133
+ type: "string",
134
+ description: "JS expression"
135
+ } },
136
+ required: ["expression"]
137
+ },
138
+ execute: async (args) => {
139
+ const p = await ensurePage();
140
+ const value = await p.evaluate(String(args.expression));
141
+ return { value };
142
+ }
143
+ }
144
+ ]
145
+ };
146
+ }
@@ -0,0 +1,16 @@
1
+ import type { AgentToolBag } from '../agent/types';
2
+ export declare function createFsTools(root: string, limits: {
3
+ maxFilesRead: number;
4
+ maxBytesTotal: number;
5
+ }): AgentToolBag;
6
+ /** Back-compat wrapper used by older call sites */
7
+ export declare function createAiTools(root: string, limits: {
8
+ maxFilesRead: number;
9
+ maxBytesTotal: number;
10
+ }): {
11
+ readPaths: () => string[];
12
+ list_dir: (path?: string) => Promise<unknown>;
13
+ read_file: (path: string) => Promise<unknown>;
14
+ grep: (pattern: string, path?: string) => Promise<unknown>;
15
+ };
16
+ //# sourceMappingURL=fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/tools/fs.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAQnD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GACtD,YAAY,CAmGd;AAED,mDAAmD;AACnD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE;;sBAMnC,MAAM;sBACN,MAAM;oBACR,MAAM,SAAS,MAAM;EAExC"}
@@ -0,0 +1,132 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join, relative, resolve, sep } from "pathe";
3
+ import { IGNORE } from "../tree.mjs";
4
+ function redact(content) {
5
+ return content.replace(/(api[_-]?key|token|password|secret)\s*[:=]\s*['"][^'"]+['"]/gi, "$1: \"***\"").replace(/Bearer\s+[A-Za-z0-9._-]+/g, "Bearer ***");
6
+ }
7
+ export function createFsTools(root, limits) {
8
+ const absRoot = resolve(root);
9
+ let filesRead = 0;
10
+ let bytes = 0;
11
+ const readPaths = [];
12
+ function assertInside(rel) {
13
+ const full = resolve(absRoot, rel);
14
+ if (!full.startsWith(absRoot + sep) && full !== absRoot) {
15
+ throw new Error(`path escapes root: ${rel}`);
16
+ }
17
+ return full;
18
+ }
19
+ const list_dir = async (path = ".") => {
20
+ const full = assertInside(path);
21
+ const entries = await readdir(full, { withFileTypes: true });
22
+ return entries.filter((e) => !IGNORE.has(e.name)).map((e) => ({
23
+ name: e.name,
24
+ type: e.isDirectory() ? "dir" : "file"
25
+ }));
26
+ };
27
+ const read_file = async (path) => {
28
+ if (filesRead >= limits.maxFilesRead) throw new Error("maxFilesRead exceeded");
29
+ const full = assertInside(path);
30
+ if (full.includes(`${sep}.env`) || /secret|credential/i.test(full)) {
31
+ throw new Error("denied path");
32
+ }
33
+ const content = await readFile(full, "utf8");
34
+ if (bytes + content.length > limits.maxBytesTotal) throw new Error("maxBytesTotal exceeded");
35
+ filesRead++;
36
+ bytes += content.length;
37
+ readPaths.push(path);
38
+ return redact(content);
39
+ };
40
+ const grep = async (pattern, path = ".") => {
41
+ const full = assertInside(path);
42
+ const re = new RegExp(pattern);
43
+ const hits = [];
44
+ async function walk(dir) {
45
+ if (hits.length >= 50) return;
46
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
47
+ for (const e of entries) {
48
+ if (IGNORE.has(e.name)) continue;
49
+ const p = join(dir, e.name);
50
+ if (e.isDirectory()) await walk(p);
51
+ else if (e.isFile()) {
52
+ const text = await readFile(p, "utf8").catch(() => "");
53
+ const lines = text.split("\n");
54
+ lines.forEach((line, i) => {
55
+ if (hits.length < 50 && re.test(line)) {
56
+ hits.push({
57
+ file: relative(absRoot, p),
58
+ line: i + 1,
59
+ text: line.slice(0, 200)
60
+ });
61
+ }
62
+ });
63
+ }
64
+ }
65
+ }
66
+ await walk(full);
67
+ return hits;
68
+ };
69
+ return {
70
+ readPaths: () => readPaths,
71
+ tools: [
72
+ {
73
+ name: "list_dir",
74
+ description: "List directory under project root",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: { path: {
78
+ type: "string",
79
+ description: "Relative directory"
80
+ } }
81
+ },
82
+ execute: async (args) => list_dir(String(args.path ?? "."))
83
+ },
84
+ {
85
+ name: "read_file",
86
+ description: "Read a file under project root",
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: { path: {
90
+ type: "string",
91
+ description: "Relative file path"
92
+ } },
93
+ required: ["path"]
94
+ },
95
+ execute: async (args) => read_file(String(args.path))
96
+ },
97
+ {
98
+ name: "grep",
99
+ description: "Search files for a regex pattern",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ pattern: {
104
+ type: "string",
105
+ description: "Regex"
106
+ },
107
+ path: {
108
+ type: "string",
109
+ description: "Relative start path"
110
+ }
111
+ },
112
+ required: ["pattern"]
113
+ },
114
+ execute: async (args) => grep(String(args.pattern), args.path ? String(args.path) : ".")
115
+ }
116
+ ]
117
+ };
118
+ }
119
+
120
+ export function createAiTools(root, limits) {
121
+ const bag = createFsTools(root, limits);
122
+ const byName = Object.fromEntries(bag.tools.map((t) => [t.name, t]));
123
+ return {
124
+ readPaths: bag.readPaths,
125
+ list_dir: (path) => byName.list_dir.execute({ path }),
126
+ read_file: (path) => byName.read_file.execute({ path }),
127
+ grep: (pattern, path) => byName.grep.execute({
128
+ pattern,
129
+ path
130
+ })
131
+ };
132
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentToolBag } from "../agent/types.mjs";
2
+ export interface CreateAgentToolkitOptions {
3
+ root: string;
4
+ maxFilesRead?: number;
5
+ maxBytesTotal?: number;
6
+ /** Enable Playwright page tools */
7
+ browser?: boolean | {
8
+ baseURL?: string;
9
+ headless?: boolean;
10
+ };
11
+ }
12
+ /**
13
+ * Shared toolkit used by AI workflows (generate / convert / fix / cover).
14
+ */
15
+ export declare function createAgentToolkit(opts: CreateAgentToolkitOptions): AgentToolBag;
@@ -0,0 +1,16 @@
1
+ import type { AgentToolBag } from '../agent/types';
2
+ export interface CreateAgentToolkitOptions {
3
+ root: string;
4
+ maxFilesRead?: number;
5
+ maxBytesTotal?: number;
6
+ /** Enable Playwright page tools */
7
+ browser?: boolean | {
8
+ baseURL?: string;
9
+ headless?: boolean;
10
+ };
11
+ }
12
+ /**
13
+ * Shared toolkit used by AI workflows (generate / convert / fix / cover).
14
+ */
15
+ export declare function createAgentToolkit(opts: CreateAgentToolkitOptions): AgentToolBag;
16
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/tools/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAa,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAI9D,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mCAAmC;IACnC,OAAO,CAAC,EAAE,OAAO,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,yBAAyB,GAAG,YAAY,CAoBhF"}
@@ -0,0 +1,22 @@
1
+ import { createFsTools } from "./fs.mjs";
2
+ import { createBrowserTools } from "./browser.mjs";
3
+
4
+ export function createAgentToolkit(opts) {
5
+ const fsBag = createFsTools(opts.root, {
6
+ maxFilesRead: opts.maxFilesRead ?? 30,
7
+ maxBytesTotal: opts.maxBytesTotal ?? 3e5
8
+ });
9
+ const bags = [fsBag];
10
+ if (opts.browser) {
11
+ const browserOpts = typeof opts.browser === "object" ? opts.browser : {};
12
+ bags.push(createBrowserTools(browserOpts));
13
+ }
14
+ const tools = bags.flatMap((b) => b.tools);
15
+ return {
16
+ tools,
17
+ readPaths: () => bags.flatMap((b) => b.readPaths()),
18
+ dispose: async () => {
19
+ for (const b of bags) await b.dispose?.();
20
+ }
21
+ };
22
+ }
package/dist/tree.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ export declare const IGNORE: Set<string>;
3
+ export interface FileTreeEntry {
4
+ path: string;
5
+ size: number;
6
+ }
7
+ export declare function buildFileTree(root: string, maxEntries?: number): Promise<FileTreeEntry[]>;
8
+ export declare function treeSignature(tree: FileTreeEntry[]): string;
9
+ export declare function fingerprint(partsOrOpts: string[] | {
10
+ prompt: string;
11
+ root: string;
12
+ recipes: string[];
13
+ focus: string[];
14
+ treeSig: string;
15
+ }): string;
16
+ export declare function listTestFiles(root: string): Promise<string[]>;
17
+ export { readdir };
18
+ //# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../src/tree.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAQ,MAAM,kBAAkB,CAAC;AAIjD,eAAO,MAAM,MAAM,aAUjB,CAAC;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,SAAO,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAwB7F;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,MAAM,CAI3D;AAED,wBAAgB,WAAW,CACzB,WAAW,EACP,MAAM,EAAE,GACR;IACE,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,GACJ,MAAM,CAiBR;AAED,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAQnE;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
package/dist/tree.mjs ADDED
@@ -0,0 +1,70 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readdir, stat } from "node:fs/promises";
3
+ import { join, resolve } from "pathe";
4
+ import { glob } from "tinyglobby";
5
+ export const IGNORE = new Set([
6
+ "node_modules",
7
+ ".git",
8
+ "dist",
9
+ ".output",
10
+ ".nuxt",
11
+ ".untestutils",
12
+ "coverage",
13
+ ".next",
14
+ ".turbo"
15
+ ]);
16
+ export async function buildFileTree(root, maxEntries = 2e3) {
17
+ const abs = resolve(root);
18
+ if (maxEntries <= 0) return [];
19
+ let paths = [];
20
+ try {
21
+ paths = await glob(["**/*"], {
22
+ cwd: abs,
23
+ onlyFiles: true,
24
+ ignore: [...IGNORE].map((d) => `**/${d}/**`).concat(["**/.env*", "**/.env"]),
25
+ absolute: false
26
+ });
27
+ } catch {
28
+ return [];
29
+ }
30
+ const out = [];
31
+ for (const rel of paths) {
32
+ if (out.length >= maxEntries) break;
33
+ if (rel.split(/[/\\]/).some((p) => IGNORE.has(p) || p.startsWith(".env"))) continue;
34
+ const full = join(abs, rel);
35
+ const s = await stat(full).catch(() => null);
36
+ if (!s?.isFile()) continue;
37
+ out.push({
38
+ path: rel.replace(/\\/g, "/"),
39
+ size: s.size
40
+ });
41
+ }
42
+ return out.sort((a, b) => a.path.localeCompare(b.path));
43
+ }
44
+ export function treeSignature(tree) {
45
+ return createHash("sha1").update(tree.map((t) => `${t.path}|${t.size}`).join("\n")).digest("hex");
46
+ }
47
+ export function fingerprint(partsOrOpts) {
48
+ if (Array.isArray(partsOrOpts)) {
49
+ return createHash("sha1").update(partsOrOpts.join("|")).digest("hex");
50
+ }
51
+ const opts = partsOrOpts;
52
+ return createHash("sha1").update([
53
+ "v1",
54
+ opts.prompt.trim(),
55
+ resolve(opts.root),
56
+ opts.recipes.join(","),
57
+ opts.focus.join(","),
58
+ opts.treeSig
59
+ ].join("|")).digest("hex");
60
+ }
61
+ export async function listTestFiles(root) {
62
+ const abs = resolve(root);
63
+ const paths = await glob(["**/*.{test,spec}.{ts,tsx,js,mjs,cjs}", "**/e2e/**/*.{ts,js}"], {
64
+ cwd: abs,
65
+ onlyFiles: true,
66
+ ignore: [...IGNORE].map((d) => `**/${d}/**`)
67
+ });
68
+ return paths.map((p) => p.replace(/\\/g, "/")).sort();
69
+ }
70
+ export { readdir };
@@ -0,0 +1,23 @@
1
+ export interface CoverTestsOptions {
2
+ root: string;
3
+ recipes?: string[];
4
+ baseURL?: string;
5
+ browser?: boolean;
6
+ /** Directory to write new tests into */
7
+ outDir?: string;
8
+ focus?: string[];
9
+ maxExploreSteps?: number;
10
+ maxFilesRead?: number;
11
+ maxBytesTotal?: number;
12
+ write?: boolean;
13
+ }
14
+ export interface CoverResult {
15
+ files: {
16
+ path: string;
17
+ code: string;
18
+ }[];
19
+ fingerprint: string;
20
+ readPaths: string[];
21
+ genDir: string;
22
+ }
23
+ export declare function coverMissingTests(opts: CoverTestsOptions): Promise<CoverResult>;
@@ -0,0 +1,24 @@
1
+ export interface CoverTestsOptions {
2
+ root: string;
3
+ recipes?: string[];
4
+ baseURL?: string;
5
+ browser?: boolean;
6
+ /** Directory to write new tests into */
7
+ outDir?: string;
8
+ focus?: string[];
9
+ maxExploreSteps?: number;
10
+ maxFilesRead?: number;
11
+ maxBytesTotal?: number;
12
+ write?: boolean;
13
+ }
14
+ export interface CoverResult {
15
+ files: {
16
+ path: string;
17
+ code: string;
18
+ }[];
19
+ fingerprint: string;
20
+ readPaths: string[];
21
+ genDir: string;
22
+ }
23
+ export declare function coverMissingTests(opts: CoverTestsOptions): Promise<CoverResult>;
24
+ //# sourceMappingURL=cover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cover.d.ts","sourceRoot":"","sources":["../../src/workflows/cover.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAsGrF"}
@@ -0,0 +1,95 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { dirname, join, relative, resolve } from "pathe";
5
+ import { resolveArtifactsRoot, debug, envFlag, log } from "@untestutils/core";
6
+ import { extractLabeledTsFiles } from "../agent/types.mjs";
7
+ import { runAgentRaw } from "../agent/run.mjs";
8
+ import { loadCoverPrompt } from "../prompts.mjs";
9
+ import { createAgentToolkit } from "../tools/registry.mjs";
10
+ import { buildFileTree, fingerprint, listTestFiles, treeSignature } from "../tree.mjs";
11
+ export async function coverMissingTests(opts) {
12
+ const root = resolve(opts.root);
13
+ const existing = await listTestFiles(root);
14
+ const tree = await buildFileTree(root);
15
+ const treeSig = treeSignature(tree);
16
+ const id = `cover-${createHash("sha1").update(root + existing.join(",")).digest("hex").slice(0, 10)}`;
17
+ const fp = fingerprint([
18
+ "cover-v1",
19
+ resolve(root),
20
+ (opts.recipes ?? []).join(","),
21
+ (opts.focus ?? []).join(","),
22
+ existing.join("\n"),
23
+ treeSig
24
+ ]);
25
+ const artifactsRoot = resolveArtifactsRoot();
26
+ const genDir = join(artifactsRoot, "ai", id);
27
+ await mkdir(genDir, { recursive: true });
28
+ const metaPath = join(genDir, `${fp.slice(0, 8)}.meta.json`);
29
+ if (existsSync(metaPath) && !opts.write) {
30
+ const meta = JSON.parse(await readFile(metaPath, "utf8"));
31
+ debug("ai", `cover cache hit ${id}`);
32
+ return meta;
33
+ }
34
+ if (!envFlag("UNTESTUTILS_AI", false) && process.env.UNTESTUTILS_AI_MOCK !== "1") {
35
+ throw new Error(`[untestutils/ai] no cached cover for "${root}". Run with UNTESTUTILS_AI=1 (genDir would be ${genDir}).`);
36
+ }
37
+ const toolkit = createAgentToolkit({
38
+ root,
39
+ maxFilesRead: opts.maxFilesRead ?? 40,
40
+ maxBytesTotal: opts.maxBytesTotal ?? 4e5,
41
+ browser: opts.browser || opts.baseURL ? {
42
+ baseURL: opts.baseURL,
43
+ headless: true
44
+ } : false
45
+ });
46
+ try {
47
+ const system = await loadCoverPrompt();
48
+ const raw = await runAgentRaw({
49
+ system,
50
+ mockKind: "cover",
51
+ mockRecipe: opts.recipes?.[0] ?? "basic",
52
+ maxSteps: opts.maxExploreSteps ?? 20,
53
+ tools: toolkit.tools,
54
+ prompt: [
55
+ `Project root: ${root}`,
56
+ `Recipes: ${(opts.recipes ?? []).join(", ") || "(none)"}`,
57
+ `Focus: ${(opts.focus ?? []).join(", ") || "(none)"}`,
58
+ opts.baseURL ? `Base URL: ${opts.baseURL}` : "Base URL: (none)",
59
+ `Existing test files:\n${existing.slice(0, 200).join("\n") || "(none)"}`,
60
+ `File tree (truncated):\n${tree.slice(0, 500).map((t) => t.path).join("\n")}`,
61
+ `Write missing high-value e2e tests. Prefer outDir ${opts.outDir ?? "tests/e2e"}.`
62
+ ].join("\n\n")
63
+ });
64
+ const blocks = extractLabeledTsFiles(raw);
65
+ const outDir = resolve(root, opts.outDir ?? "tests/e2e");
66
+ const files = [];
67
+ for (let i = 0; i < blocks.length; i++) {
68
+ const b = blocks[i];
69
+ const rel = b.path || relative(root, join(outDir, blocks.length === 1 ? "generated-cover.test.ts" : `generated-cover-${i + 1}.test.ts`));
70
+ const abs = resolve(root, rel);
71
+ files.push({
72
+ path: abs,
73
+ code: b.code
74
+ });
75
+ await mkdir(dirname(abs), { recursive: true });
76
+ const cacheCopy = join(genDir, rel.replace(/[\\/]/g, "__"));
77
+ await mkdir(dirname(cacheCopy), { recursive: true });
78
+ await writeFile(cacheCopy, b.code, "utf8");
79
+ if (opts.write) {
80
+ await writeFile(abs, b.code, "utf8");
81
+ log("ai", `cover wrote ${abs}`);
82
+ }
83
+ }
84
+ const result = {
85
+ files,
86
+ fingerprint: fp,
87
+ readPaths: toolkit.readPaths(),
88
+ genDir
89
+ };
90
+ await writeFile(metaPath, JSON.stringify(result, null, 2), "utf8");
91
+ return result;
92
+ } finally {
93
+ await toolkit.dispose?.();
94
+ }
95
+ }
@@ -0,0 +1,17 @@
1
+ import type { GenerateResult } from "./types.mjs";
2
+ export interface FixTestsOptions {
3
+ /** Failing test file */
4
+ path: string;
5
+ root: string;
6
+ /** Vitest/Playwright failure log */
7
+ failureLog: string;
8
+ recipes?: string[];
9
+ baseURL?: string;
10
+ browser?: boolean;
11
+ maxExploreSteps?: number;
12
+ maxFilesRead?: number;
13
+ maxBytesTotal?: number;
14
+ /** Write fixed content back to path */
15
+ write?: boolean;
16
+ }
17
+ export declare function fixBrokenTests(opts: FixTestsOptions): Promise<GenerateResult>;
@@ -0,0 +1,18 @@
1
+ import type { GenerateResult } from '../workflows/types';
2
+ export interface FixTestsOptions {
3
+ /** Failing test file */
4
+ path: string;
5
+ root: string;
6
+ /** Vitest/Playwright failure log */
7
+ failureLog: string;
8
+ recipes?: string[];
9
+ baseURL?: string;
10
+ browser?: boolean;
11
+ maxExploreSteps?: number;
12
+ maxFilesRead?: number;
13
+ maxBytesTotal?: number;
14
+ /** Write fixed content back to path */
15
+ write?: boolean;
16
+ }
17
+ export declare function fixBrokenTests(opts: FixTestsOptions): Promise<GenerateResult>;
18
+ //# sourceMappingURL=fix.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fix.d.ts","sourceRoot":"","sources":["../../src/workflows/fix.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAgFnF"}