@devness/coverit 0.1.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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/dist/agents/orchestrator.d.ts +21 -0
  4. package/dist/agents/orchestrator.d.ts.map +1 -0
  5. package/dist/agents/orchestrator.js +235 -0
  6. package/dist/agents/orchestrator.js.map +1 -0
  7. package/dist/agents/reporter.d.ts +13 -0
  8. package/dist/agents/reporter.d.ts.map +1 -0
  9. package/dist/agents/reporter.js +323 -0
  10. package/dist/agents/reporter.js.map +1 -0
  11. package/dist/ai/anthropic-provider.d.ts +19 -0
  12. package/dist/ai/anthropic-provider.d.ts.map +1 -0
  13. package/dist/ai/anthropic-provider.js +83 -0
  14. package/dist/ai/anthropic-provider.js.map +1 -0
  15. package/dist/ai/claude-cli-provider.d.ts +22 -0
  16. package/dist/ai/claude-cli-provider.d.ts.map +1 -0
  17. package/dist/ai/claude-cli-provider.js +197 -0
  18. package/dist/ai/claude-cli-provider.js.map +1 -0
  19. package/dist/ai/index.d.ts +15 -0
  20. package/dist/ai/index.d.ts.map +1 -0
  21. package/dist/ai/index.js +16 -0
  22. package/dist/ai/index.js.map +1 -0
  23. package/dist/ai/ollama-provider.d.ts +17 -0
  24. package/dist/ai/ollama-provider.d.ts.map +1 -0
  25. package/dist/ai/ollama-provider.js +88 -0
  26. package/dist/ai/ollama-provider.js.map +1 -0
  27. package/dist/ai/openai-provider.d.ts +20 -0
  28. package/dist/ai/openai-provider.d.ts.map +1 -0
  29. package/dist/ai/openai-provider.js +74 -0
  30. package/dist/ai/openai-provider.js.map +1 -0
  31. package/dist/ai/prompts.d.ts +36 -0
  32. package/dist/ai/prompts.d.ts.map +1 -0
  33. package/dist/ai/prompts.js +259 -0
  34. package/dist/ai/prompts.js.map +1 -0
  35. package/dist/ai/provider-factory.d.ts +26 -0
  36. package/dist/ai/provider-factory.d.ts.map +1 -0
  37. package/dist/ai/provider-factory.js +111 -0
  38. package/dist/ai/provider-factory.js.map +1 -0
  39. package/dist/ai/types.d.ts +37 -0
  40. package/dist/ai/types.d.ts.map +1 -0
  41. package/dist/ai/types.js +10 -0
  42. package/dist/ai/types.js.map +1 -0
  43. package/dist/analysis/code-scanner.d.ts +9 -0
  44. package/dist/analysis/code-scanner.d.ts.map +1 -0
  45. package/dist/analysis/code-scanner.js +409 -0
  46. package/dist/analysis/code-scanner.js.map +1 -0
  47. package/dist/analysis/dependency-graph.d.ts +9 -0
  48. package/dist/analysis/dependency-graph.d.ts.map +1 -0
  49. package/dist/analysis/dependency-graph.js +149 -0
  50. package/dist/analysis/dependency-graph.js.map +1 -0
  51. package/dist/analysis/diff-analyzer.d.ts +9 -0
  52. package/dist/analysis/diff-analyzer.d.ts.map +1 -0
  53. package/dist/analysis/diff-analyzer.js +232 -0
  54. package/dist/analysis/diff-analyzer.js.map +1 -0
  55. package/dist/analysis/index.d.ts +5 -0
  56. package/dist/analysis/index.d.ts.map +1 -0
  57. package/dist/analysis/index.js +5 -0
  58. package/dist/analysis/index.js.map +1 -0
  59. package/dist/analysis/strategy-planner.d.ts +11 -0
  60. package/dist/analysis/strategy-planner.d.ts.map +1 -0
  61. package/dist/analysis/strategy-planner.js +384 -0
  62. package/dist/analysis/strategy-planner.js.map +1 -0
  63. package/dist/cli/index.d.ts +9 -0
  64. package/dist/cli/index.d.ts.map +1 -0
  65. package/dist/cli/index.js +288 -0
  66. package/dist/cli/index.js.map +1 -0
  67. package/dist/executors/base-executor.d.ts +35 -0
  68. package/dist/executors/base-executor.d.ts.map +1 -0
  69. package/dist/executors/base-executor.js +138 -0
  70. package/dist/executors/base-executor.js.map +1 -0
  71. package/dist/executors/browser-runner.d.ts +24 -0
  72. package/dist/executors/browser-runner.d.ts.map +1 -0
  73. package/dist/executors/browser-runner.js +194 -0
  74. package/dist/executors/browser-runner.js.map +1 -0
  75. package/dist/executors/cloud-runner.d.ts +41 -0
  76. package/dist/executors/cloud-runner.d.ts.map +1 -0
  77. package/dist/executors/cloud-runner.js +153 -0
  78. package/dist/executors/cloud-runner.js.map +1 -0
  79. package/dist/executors/index.d.ts +12 -0
  80. package/dist/executors/index.d.ts.map +1 -0
  81. package/dist/executors/index.js +28 -0
  82. package/dist/executors/index.js.map +1 -0
  83. package/dist/executors/local-runner.d.ts +40 -0
  84. package/dist/executors/local-runner.d.ts.map +1 -0
  85. package/dist/executors/local-runner.js +395 -0
  86. package/dist/executors/local-runner.js.map +1 -0
  87. package/dist/executors/reporter.d.ts +6 -0
  88. package/dist/executors/reporter.d.ts.map +1 -0
  89. package/dist/executors/reporter.js +6 -0
  90. package/dist/executors/reporter.js.map +1 -0
  91. package/dist/executors/simulator-runner.d.ts +30 -0
  92. package/dist/executors/simulator-runner.d.ts.map +1 -0
  93. package/dist/executors/simulator-runner.js +339 -0
  94. package/dist/executors/simulator-runner.js.map +1 -0
  95. package/dist/generators/api-test.d.ts +22 -0
  96. package/dist/generators/api-test.d.ts.map +1 -0
  97. package/dist/generators/api-test.js +235 -0
  98. package/dist/generators/api-test.js.map +1 -0
  99. package/dist/generators/base-generator.d.ts +79 -0
  100. package/dist/generators/base-generator.d.ts.map +1 -0
  101. package/dist/generators/base-generator.js +234 -0
  102. package/dist/generators/base-generator.js.map +1 -0
  103. package/dist/generators/desktop-test.d.ts +22 -0
  104. package/dist/generators/desktop-test.d.ts.map +1 -0
  105. package/dist/generators/desktop-test.js +290 -0
  106. package/dist/generators/desktop-test.js.map +1 -0
  107. package/dist/generators/e2e-browser.d.ts +19 -0
  108. package/dist/generators/e2e-browser.d.ts.map +1 -0
  109. package/dist/generators/e2e-browser.js +233 -0
  110. package/dist/generators/e2e-browser.js.map +1 -0
  111. package/dist/generators/index.d.ts +21 -0
  112. package/dist/generators/index.d.ts.map +1 -0
  113. package/dist/generators/index.js +66 -0
  114. package/dist/generators/index.js.map +1 -0
  115. package/dist/generators/mobile-test.d.ts +22 -0
  116. package/dist/generators/mobile-test.d.ts.map +1 -0
  117. package/dist/generators/mobile-test.js +286 -0
  118. package/dist/generators/mobile-test.js.map +1 -0
  119. package/dist/generators/unit-test.d.ts +23 -0
  120. package/dist/generators/unit-test.d.ts.map +1 -0
  121. package/dist/generators/unit-test.js +282 -0
  122. package/dist/generators/unit-test.js.map +1 -0
  123. package/dist/index.d.ts +12 -0
  124. package/dist/index.d.ts.map +1 -0
  125. package/dist/index.js +13 -0
  126. package/dist/index.js.map +1 -0
  127. package/dist/mcp/server.d.ts +8 -0
  128. package/dist/mcp/server.d.ts.map +1 -0
  129. package/dist/mcp/server.js +217 -0
  130. package/dist/mcp/server.js.map +1 -0
  131. package/dist/types/index.d.ts +295 -0
  132. package/dist/types/index.d.ts.map +1 -0
  133. package/dist/types/index.js +8 -0
  134. package/dist/types/index.js.map +1 -0
  135. package/dist/utils/framework-detector.d.ts +28 -0
  136. package/dist/utils/framework-detector.d.ts.map +1 -0
  137. package/dist/utils/framework-detector.js +184 -0
  138. package/dist/utils/framework-detector.js.map +1 -0
  139. package/dist/utils/git.d.ts +33 -0
  140. package/dist/utils/git.d.ts.map +1 -0
  141. package/dist/utils/git.js +82 -0
  142. package/dist/utils/git.js.map +1 -0
  143. package/dist/utils/logger.d.ts +17 -0
  144. package/dist/utils/logger.d.ts.map +1 -0
  145. package/dist/utils/logger.js +47 -0
  146. package/dist/utils/logger.js.map +1 -0
  147. package/package.json +86 -0
@@ -0,0 +1,79 @@
1
+ import type { CodeScanResult, GeneratorContext, GeneratorResult, TestFramework, TestType } from "../types/index.js";
2
+ import type { AIProvider } from "../ai/types.js";
3
+ /**
4
+ * Abstract base class for all test generators.
5
+ * Provides shared helpers for building test file content strings,
6
+ * deduplicating against existing tests, and producing consistent output.
7
+ *
8
+ * When an AIProvider is attached, generators attempt LLM-powered test
9
+ * generation first, falling back to AST-based templates on failure or
10
+ * when no provider is available.
11
+ */
12
+ export declare abstract class BaseGenerator {
13
+ protected aiProvider: AIProvider | null;
14
+ abstract generate(context: GeneratorContext): Promise<GeneratorResult>;
15
+ setAIProvider(provider: AIProvider): void;
16
+ /**
17
+ * Attempts LLM-powered test generation using the attached AI provider.
18
+ * Returns the generated test file content as a string, or null if:
19
+ * - No AI provider is configured
20
+ * - The provider call fails for any reason
21
+ *
22
+ * Callers should fall back to template-based generation when this returns null.
23
+ */
24
+ protected generateWithAI(params: {
25
+ sourceCode: string;
26
+ scanResult: CodeScanResult;
27
+ testType: TestType;
28
+ framework: TestFramework;
29
+ existingTests?: string[];
30
+ }): Promise<string | null>;
31
+ /**
32
+ * Extracts raw code from an LLM response that may be wrapped in
33
+ * markdown code fences (```typescript ... ``` or ``` ... ```).
34
+ */
35
+ private extractCodeFromResponse;
36
+ /**
37
+ * Counts the number of test cases in generated code by looking for
38
+ * `it(`, `test(`, and `test.only(` invocations.
39
+ */
40
+ protected countTestCases(code: string): number;
41
+ protected fileHeader(): string;
42
+ protected buildImports(framework: TestFramework): string;
43
+ protected buildDescribeBlock(name: string, tests: string[]): string;
44
+ protected buildTestCase(name: string, body: string): string;
45
+ protected buildAsyncTestCase(name: string, body: string): string;
46
+ protected generateTestFileName(sourceFile: string, type: TestType): string;
47
+ /**
48
+ * Reads all test file contents from a directory so generators
49
+ * can skip creating tests that already exist.
50
+ */
51
+ protected readExistingTests(dir: string): Promise<string[]>;
52
+ /**
53
+ * Returns true if an existing test already covers the given target name
54
+ * (function, class, endpoint, etc.) by scanning existing test contents.
55
+ */
56
+ protected isAlreadyTested(targetName: string, existingTests: string[]): boolean;
57
+ protected indent(text: string, spaces: number): string;
58
+ protected escapeString(str: string): string;
59
+ private escapeRegExp;
60
+ /**
61
+ * Generates a mock variable name from a module path.
62
+ * e.g. "../services/user.service" -> "mockUserService"
63
+ */
64
+ protected mockNameFromModule(modulePath: string): string;
65
+ /**
66
+ * Generates a sample value for a given TypeScript type string.
67
+ * Used to create meaningful test data.
68
+ */
69
+ protected sampleValueForType(type: string | null): string;
70
+ /**
71
+ * Builds a full test file string from its parts.
72
+ */
73
+ protected assembleTestFile(parts: {
74
+ header?: string;
75
+ imports: string[];
76
+ body: string[];
77
+ }): string;
78
+ }
79
+ //# sourceMappingURL=base-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-generator.d.ts","sourceRoot":"","sources":["../../src/generators/base-generator.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,cAAc,EACd,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,QAAQ,EACT,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGjD;;;;;;;;GAQG;AACH,8BAAsB,aAAa;IACjC,SAAS,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAQ;IAE/C,QAAQ,CAAC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAEtE,aAAa,CAAC,QAAQ,EAAE,UAAU,GAAG,IAAI;IAIzC;;;;;;;OAOG;cACa,cAAc,CAAC,MAAM,EAAE;QACrC,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,cAAc,CAAC;QAC3B,QAAQ,EAAE,QAAQ,CAAC;QACnB,SAAS,EAAE,aAAa,CAAC;QACzB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;KAC1B,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA6B1B;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAO/B;;;OAGG;IACH,SAAS,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAQ9C,SAAS,CAAC,UAAU,IAAI,MAAM;IAM9B,SAAS,CAAC,YAAY,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM;IAqBxD,SAAS,CAAC,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM;IAKnE,SAAS,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAK3D,SAAS,CAAC,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAOhE,SAAS,CAAC,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,MAAM;IAsB1E;;;OAGG;cACa,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA2BjE;;;OAGG;IACH,SAAS,CAAC,eAAe,CACvB,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EAAE,GACtB,OAAO;IAUV,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAQtD,SAAS,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAI3C,OAAO,CAAC,YAAY;IAIpB;;;OAGG;IACH,SAAS,CAAC,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAWxD;;;OAGG;IACH,SAAS,CAAC,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM;IAuBzD;;OAEG;IACH,SAAS,CAAC,gBAAgB,CAAC,KAAK,EAAE;QAChC,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,IAAI,EAAE,MAAM,EAAE,CAAC;KAChB,GAAG,MAAM;CAWX"}
@@ -0,0 +1,234 @@
1
+ // Generated by coverit — https://coverit.dev
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import { basename, dirname, extname, join } from "node:path";
4
+ import { buildTestGenerationPrompt } from "../ai/prompts.js";
5
+ /**
6
+ * Abstract base class for all test generators.
7
+ * Provides shared helpers for building test file content strings,
8
+ * deduplicating against existing tests, and producing consistent output.
9
+ *
10
+ * When an AIProvider is attached, generators attempt LLM-powered test
11
+ * generation first, falling back to AST-based templates on failure or
12
+ * when no provider is available.
13
+ */
14
+ export class BaseGenerator {
15
+ aiProvider = null;
16
+ setAIProvider(provider) {
17
+ this.aiProvider = provider;
18
+ }
19
+ /**
20
+ * Attempts LLM-powered test generation using the attached AI provider.
21
+ * Returns the generated test file content as a string, or null if:
22
+ * - No AI provider is configured
23
+ * - The provider call fails for any reason
24
+ *
25
+ * Callers should fall back to template-based generation when this returns null.
26
+ */
27
+ async generateWithAI(params) {
28
+ if (!this.aiProvider)
29
+ return null;
30
+ try {
31
+ const messages = buildTestGenerationPrompt({
32
+ sourceCode: params.sourceCode,
33
+ scanResult: params.scanResult,
34
+ testType: params.testType,
35
+ framework: params.framework,
36
+ existingTests: params.existingTests,
37
+ });
38
+ const response = await this.aiProvider.generate(messages, {
39
+ temperature: 0.2,
40
+ maxTokens: 4096,
41
+ });
42
+ const content = response.content.trim();
43
+ if (!content)
44
+ return null;
45
+ // Strip markdown fences if the model wrapped its output in them
46
+ return this.extractCodeFromResponse(content);
47
+ }
48
+ catch (err) {
49
+ const msg = err instanceof Error ? err.message : String(err);
50
+ console.error(`[coverit] AI generation failed: ${msg}`);
51
+ return null;
52
+ }
53
+ }
54
+ /**
55
+ * Extracts raw code from an LLM response that may be wrapped in
56
+ * markdown code fences (```typescript ... ``` or ``` ... ```).
57
+ */
58
+ extractCodeFromResponse(raw) {
59
+ const fenceMatch = raw.match(/^```(?:typescript|ts|javascript|js|tsx|jsx)?\s*\n([\s\S]*?)\n```\s*$/);
60
+ return fenceMatch?.[1] ? fenceMatch[1].trim() : raw;
61
+ }
62
+ /**
63
+ * Counts the number of test cases in generated code by looking for
64
+ * `it(`, `test(`, and `test.only(` invocations.
65
+ */
66
+ countTestCases(code) {
67
+ const itCount = (code.match(/\bit\s*\(/g) || []).length;
68
+ const testCount = (code.match(/\btest\s*\(/g) || []).length;
69
+ return Math.max(itCount + testCount, 1);
70
+ }
71
+ // ── File header ────────────────────────────────────────────
72
+ fileHeader() {
73
+ return "// Generated by coverit — https://coverit.dev";
74
+ }
75
+ // ── Import builders ────────────────────────────────────────
76
+ buildImports(framework) {
77
+ switch (framework) {
78
+ case "vitest":
79
+ return "import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';";
80
+ case "jest":
81
+ // Jest globals are available without explicit import in most configs,
82
+ // but we import jest for mock utilities.
83
+ return "import { jest } from '@jest/globals';";
84
+ case "playwright":
85
+ return "import { test, expect } from '@playwright/test';";
86
+ case "detox":
87
+ return [
88
+ "import { device, element, by, expect as detoxExpect } from 'detox';",
89
+ ].join("\n");
90
+ default:
91
+ return "// No framework-specific imports";
92
+ }
93
+ }
94
+ // ── Block builders ─────────────────────────────────────────
95
+ buildDescribeBlock(name, tests) {
96
+ const body = tests.map((t) => this.indent(t, 2)).join("\n\n");
97
+ return `describe('${this.escapeString(name)}', () => {\n${body}\n});`;
98
+ }
99
+ buildTestCase(name, body) {
100
+ const indentedBody = this.indent(body, 2);
101
+ return `it('${this.escapeString(name)}', () => {\n${indentedBody}\n});`;
102
+ }
103
+ buildAsyncTestCase(name, body) {
104
+ const indentedBody = this.indent(body, 2);
105
+ return `it('${this.escapeString(name)}', async () => {\n${indentedBody}\n});`;
106
+ }
107
+ // ── File naming ────────────────────────────────────────────
108
+ generateTestFileName(sourceFile, type) {
109
+ const dir = dirname(sourceFile);
110
+ const ext = extname(sourceFile);
111
+ const name = basename(sourceFile, ext);
112
+ const suffixMap = {
113
+ unit: "test",
114
+ integration: "integration.test",
115
+ api: "api.test",
116
+ "e2e-browser": "e2e.test",
117
+ "e2e-mobile": "mobile.test",
118
+ "e2e-desktop": "desktop.test",
119
+ snapshot: "snap.test",
120
+ performance: "perf.test",
121
+ };
122
+ const suffix = suffixMap[type] ?? "test";
123
+ return join(dir, `${name}.${suffix}${ext}`);
124
+ }
125
+ // ── Existing test deduplication ────────────────────────────
126
+ /**
127
+ * Reads all test file contents from a directory so generators
128
+ * can skip creating tests that already exist.
129
+ */
130
+ async readExistingTests(dir) {
131
+ try {
132
+ const entries = await readdir(dir, { recursive: true });
133
+ const testFiles = entries.filter((e) => typeof e === "string" &&
134
+ (e.endsWith(".test.ts") ||
135
+ e.endsWith(".test.tsx") ||
136
+ e.endsWith(".spec.ts") ||
137
+ e.endsWith(".spec.tsx")));
138
+ const contents = [];
139
+ for (const file of testFiles) {
140
+ try {
141
+ const content = await readFile(join(dir, file), "utf-8");
142
+ contents.push(content);
143
+ }
144
+ catch {
145
+ // Silently skip unreadable files
146
+ }
147
+ }
148
+ return contents;
149
+ }
150
+ catch {
151
+ return [];
152
+ }
153
+ }
154
+ /**
155
+ * Returns true if an existing test already covers the given target name
156
+ * (function, class, endpoint, etc.) by scanning existing test contents.
157
+ */
158
+ isAlreadyTested(targetName, existingTests) {
159
+ const pattern = new RegExp(`(?:describe|it|test)\\s*\\(\\s*['"\`].*${this.escapeRegExp(targetName)}`, "i");
160
+ return existingTests.some((content) => pattern.test(content));
161
+ }
162
+ // ── Utility helpers ────────────────────────────────────────
163
+ indent(text, spaces) {
164
+ const pad = " ".repeat(spaces);
165
+ return text
166
+ .split("\n")
167
+ .map((line) => (line.trim() ? `${pad}${line}` : line))
168
+ .join("\n");
169
+ }
170
+ escapeString(str) {
171
+ return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
172
+ }
173
+ escapeRegExp(str) {
174
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
175
+ }
176
+ /**
177
+ * Generates a mock variable name from a module path.
178
+ * e.g. "../services/user.service" -> "mockUserService"
179
+ */
180
+ mockNameFromModule(modulePath) {
181
+ const name = basename(modulePath, extname(modulePath));
182
+ const camel = name
183
+ .split(/[-.]/)
184
+ .map((part, i) => i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1))
185
+ .join("");
186
+ return `mock${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
187
+ }
188
+ /**
189
+ * Generates a sample value for a given TypeScript type string.
190
+ * Used to create meaningful test data.
191
+ */
192
+ sampleValueForType(type) {
193
+ if (!type)
194
+ return "'test-value'";
195
+ const normalized = type.trim().toLowerCase();
196
+ if (normalized === "string")
197
+ return "'test-string'";
198
+ if (normalized === "number")
199
+ return "42";
200
+ if (normalized === "boolean")
201
+ return "true";
202
+ if (normalized === "void" || normalized === "undefined")
203
+ return "undefined";
204
+ if (normalized === "null")
205
+ return "null";
206
+ if (normalized.endsWith("[]"))
207
+ return "[]";
208
+ if (normalized.startsWith("promise<")) {
209
+ const inner = type.slice(8, -1);
210
+ return this.sampleValueForType(inner);
211
+ }
212
+ if (normalized === "date")
213
+ return "new Date('2025-01-01')";
214
+ if (normalized === "record<string, any>" || normalized === "object")
215
+ return "{}";
216
+ // Default to an empty object for complex types
217
+ return `{} as ${type}`;
218
+ }
219
+ /**
220
+ * Builds a full test file string from its parts.
221
+ */
222
+ assembleTestFile(parts) {
223
+ const sections = [
224
+ parts.header ?? this.fileHeader(),
225
+ "",
226
+ ...parts.imports,
227
+ "",
228
+ ...parts.body,
229
+ "", // trailing newline
230
+ ];
231
+ return sections.join("\n");
232
+ }
233
+ }
234
+ //# sourceMappingURL=base-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-generator.js","sourceRoot":"","sources":["../../src/generators/base-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { GeneratorContext, GeneratorResult } from "../types/index.js";
2
+ import { BaseGenerator } from "./base-generator.js";
3
+ /**
4
+ * Generates tests for Tauri desktop applications:
5
+ * - IPC command invoke tests (mocking @tauri-apps/api/core)
6
+ * - React component tests for Tauri frontends
7
+ */
8
+ export declare class DesktopTestGenerator extends BaseGenerator {
9
+ generate(context: GeneratorContext): Promise<GeneratorResult>;
10
+ private buildDesktopImports;
11
+ private generateCommandTests;
12
+ private generateTauriComponentTests;
13
+ private generateStandardComponentTests;
14
+ private detectTauriCommands;
15
+ private getTauriCommandFunctions;
16
+ private isTauriIntegrated;
17
+ private buildCommandPayload;
18
+ private buildDefaultProps;
19
+ private toSnakeCase;
20
+ private relativeImportPath;
21
+ }
22
+ //# sourceMappingURL=desktop-test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"desktop-test.d.ts","sourceRoot":"","sources":["../../src/generators/desktop-test.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAKV,gBAAgB,EAChB,eAAe,EAIhB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,aAAa;IAC/C,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAuInE,OAAO,CAAC,mBAAmB;IA2D3B,OAAO,CAAC,oBAAoB;IA0D5B,OAAO,CAAC,2BAA2B;IAqDnC,OAAO,CAAC,8BAA8B;IA+DtC,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,wBAAwB;IAMhC,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,iBAAiB;IAezB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,kBAAkB;CAK3B"}
@@ -0,0 +1,290 @@
1
+ // Generated by coverit — https://coverit.dev
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join, relative } from "node:path";
4
+ import { BaseGenerator } from "./base-generator.js";
5
+ /**
6
+ * Generates tests for Tauri desktop applications:
7
+ * - IPC command invoke tests (mocking @tauri-apps/api/core)
8
+ * - React component tests for Tauri frontends
9
+ */
10
+ export class DesktopTestGenerator extends BaseGenerator {
11
+ async generate(context) {
12
+ const { plan, project, scanResults, existingTests } = context;
13
+ const tests = [];
14
+ const warnings = [];
15
+ const skipped = [];
16
+ for (const scan of scanResults) {
17
+ if (!plan.target.files.includes(scan.file))
18
+ continue;
19
+ // ── AI-powered generation (preferred path) ──────────────
20
+ if (this.aiProvider) {
21
+ try {
22
+ const sourceCode = await readFile(join(project.root, scan.file), "utf-8");
23
+ const aiResult = await this.generateWithAI({
24
+ sourceCode,
25
+ scanResult: scan,
26
+ testType: "e2e-desktop",
27
+ framework: project.testFramework,
28
+ existingTests,
29
+ });
30
+ if (aiResult) {
31
+ const testFileName = this.generateTestFileName(scan.file, "e2e-desktop");
32
+ tests.push({
33
+ planId: plan.id,
34
+ filePath: testFileName,
35
+ content: aiResult,
36
+ testType: "e2e-desktop",
37
+ testCount: this.countTestCases(aiResult),
38
+ framework: project.testFramework === "playwright" ||
39
+ project.testFramework === "detox"
40
+ ? "vitest"
41
+ : project.testFramework,
42
+ });
43
+ continue; // Skip template fallback for this file
44
+ }
45
+ }
46
+ catch {
47
+ // Fall through to template-based generation
48
+ }
49
+ }
50
+ // ── Template-based generation (fallback) ────────────────
51
+ const hasComponents = scan.components.length > 0;
52
+ const hasTauriCommands = this.detectTauriCommands(scan);
53
+ if (!hasComponents && !hasTauriCommands) {
54
+ skipped.push({
55
+ target: scan.file,
56
+ reason: "No Tauri components or commands detected",
57
+ });
58
+ continue;
59
+ }
60
+ const testFileName = this.generateTestFileName(scan.file, "e2e-desktop");
61
+ const fw = project.testFramework;
62
+ const imports = this.buildDesktopImports(scan, fw, testFileName, hasTauriCommands);
63
+ const bodyBlocks = [];
64
+ let testCount = 0;
65
+ // Tauri IPC command tests
66
+ if (hasTauriCommands) {
67
+ const commandFns = this.getTauriCommandFunctions(scan);
68
+ for (const fn of commandFns) {
69
+ if (this.isAlreadyTested(`invoke:${fn.name}`, existingTests)) {
70
+ skipped.push({
71
+ target: `invoke:${fn.name}`,
72
+ reason: "Already has command tests",
73
+ });
74
+ continue;
75
+ }
76
+ const cases = this.generateCommandTests(fn, fw);
77
+ bodyBlocks.push(this.buildDescribeBlock(`Tauri command: ${fn.name}`, cases));
78
+ testCount += cases.length;
79
+ }
80
+ }
81
+ // React component tests
82
+ for (const component of scan.components) {
83
+ if (this.isAlreadyTested(component.name, existingTests)) {
84
+ skipped.push({
85
+ target: component.name,
86
+ reason: "Already has desktop component tests",
87
+ });
88
+ continue;
89
+ }
90
+ const isTauriComponent = this.isTauriIntegrated(component, scan);
91
+ const cases = isTauriComponent
92
+ ? this.generateTauriComponentTests(component, scan, fw)
93
+ : this.generateStandardComponentTests(component, fw);
94
+ bodyBlocks.push(this.buildDescribeBlock(component.name, cases));
95
+ testCount += cases.length;
96
+ }
97
+ if (testCount === 0)
98
+ continue;
99
+ const content = this.assembleTestFile({
100
+ imports,
101
+ body: bodyBlocks,
102
+ });
103
+ tests.push({
104
+ planId: plan.id,
105
+ filePath: testFileName,
106
+ content,
107
+ testType: "e2e-desktop",
108
+ testCount,
109
+ framework: fw === "playwright" || fw === "detox" ? "vitest" : fw,
110
+ });
111
+ }
112
+ return { tests, warnings, skipped };
113
+ }
114
+ // ── Import generation ──────────────────────────────────────
115
+ buildDesktopImports(scan, fw, testFilePath, hasTauriCommands) {
116
+ const lines = [this.buildImports(fw)];
117
+ const mockFn = fw === "vitest" ? "vi" : "jest";
118
+ // Mock Tauri core if IPC commands are used
119
+ if (hasTauriCommands) {
120
+ lines.push(`${mockFn === "vi" ? "vi.mock" : "jest.mock"}('@tauri-apps/api/core', () => ({`, ` invoke: ${mockFn}.fn(),`, `}));`, "", `import { invoke } from '@tauri-apps/api/core';`);
121
+ }
122
+ // Import testing library for React component tests
123
+ if (scan.components.length > 0) {
124
+ lines.push("import { render, screen, fireEvent, waitFor } from '@testing-library/react';");
125
+ }
126
+ // Import source components/functions
127
+ const sourcePath = this.relativeImportPath(testFilePath, scan.file);
128
+ const exportedNames = scan.exports
129
+ .filter((e) => e.kind === "function" || e.kind === "class")
130
+ .map((e) => e.name);
131
+ const componentNames = scan.components.map((c) => c.name);
132
+ const allNames = [...new Set([...exportedNames, ...componentNames])];
133
+ if (allNames.length > 0) {
134
+ const defaultExport = scan.exports.find((e) => e.isDefault);
135
+ const namedExports = allNames.filter((n) => n !== defaultExport?.name);
136
+ if (defaultExport && namedExports.length > 0) {
137
+ lines.push(`import ${defaultExport.name}, { ${namedExports.join(", ")} } from '${sourcePath}';`);
138
+ }
139
+ else if (defaultExport) {
140
+ lines.push(`import ${defaultExport.name} from '${sourcePath}';`);
141
+ }
142
+ else if (namedExports.length > 0) {
143
+ lines.push(`import { ${namedExports.join(", ")} } from '${sourcePath}';`);
144
+ }
145
+ }
146
+ return lines;
147
+ }
148
+ // ── Tauri command tests ────────────────────────────────────
149
+ generateCommandTests(fn, fw) {
150
+ const cases = [];
151
+ const mockFn = fw === "vitest" ? "vi" : "jest";
152
+ const mockedInvoke = `${mockFn === "vi" ? "vi.mocked" : "jest.mocked"}(invoke)`;
153
+ // Build sample payload from function params
154
+ const payload = this.buildCommandPayload(fn.params);
155
+ const payloadStr = payload ? `, ${payload}` : "";
156
+ const returnSample = this.sampleValueForType(fn.returnType);
157
+ // Success case
158
+ cases.push(this.buildAsyncTestCase("should invoke command successfully", [
159
+ `${mockedInvoke}.mockResolvedValueOnce(${returnSample});`,
160
+ `const result = await invoke('${this.toSnakeCase(fn.name)}'${payloadStr});`,
161
+ `expect(invoke).toHaveBeenCalledWith('${this.toSnakeCase(fn.name)}'${payloadStr});`,
162
+ `expect(result).toEqual(${returnSample});`,
163
+ ].join("\n")));
164
+ // Error case
165
+ cases.push(this.buildAsyncTestCase("should handle command error", [
166
+ `${mockedInvoke}.mockRejectedValueOnce(new Error('Command failed'));`,
167
+ `await expect(invoke('${this.toSnakeCase(fn.name)}'${payloadStr})).rejects.toThrow('Command failed');`,
168
+ ].join("\n")));
169
+ // Payload validation (if params exist)
170
+ if (fn.params.length > 0) {
171
+ cases.push(this.buildAsyncTestCase("should send correct payload", [
172
+ `${mockedInvoke}.mockResolvedValueOnce(undefined);`,
173
+ `await invoke('${this.toSnakeCase(fn.name)}'${payloadStr});`,
174
+ `expect(invoke).toHaveBeenCalledTimes(1);`,
175
+ `const calledPayload = ${mockedInvoke}.mock.calls[0][1];`,
176
+ `expect(calledPayload).toBeDefined();`,
177
+ ].join("\n")));
178
+ }
179
+ return cases;
180
+ }
181
+ // ── Tauri-integrated component tests ───────────────────────
182
+ generateTauriComponentTests(component, _scan, fw) {
183
+ const cases = [];
184
+ const mockFn = fw === "vitest" ? "vi" : "jest";
185
+ const mockedInvoke = `${mockFn === "vi" ? "vi.mocked" : "jest.mocked"}(invoke)`;
186
+ const propStr = this.buildDefaultProps(component.props, fw);
187
+ // Rendering test
188
+ cases.push(this.buildTestCase("should render without crashing", [
189
+ `const { container } = render(<${component.name}${propStr} />);`,
190
+ `expect(container).toBeTruthy();`,
191
+ ].join("\n")));
192
+ // Test that Tauri commands are invoked on mount/interaction
193
+ cases.push(this.buildAsyncTestCase("should invoke Tauri commands", [
194
+ `${mockedInvoke}.mockResolvedValue({});`,
195
+ `render(<${component.name}${propStr} />);`,
196
+ `await waitFor(() => {`,
197
+ ` expect(invoke).toHaveBeenCalled();`,
198
+ `});`,
199
+ ].join("\n")));
200
+ // Reset mocks
201
+ cases.push(this.buildTestCase("should clean up on unmount", [
202
+ `${mockedInvoke}.mockResolvedValue({});`,
203
+ `const { unmount } = render(<${component.name}${propStr} />);`,
204
+ `unmount();`,
205
+ `// Component should unmount without errors`,
206
+ ].join("\n")));
207
+ return cases;
208
+ }
209
+ // ── Standard React component tests ─────────────────────────
210
+ generateStandardComponentTests(component, fw) {
211
+ const cases = [];
212
+ const propStr = this.buildDefaultProps(component.props, fw);
213
+ // Basic render
214
+ cases.push(this.buildTestCase("should render without crashing", [
215
+ `const { container } = render(<${component.name}${propStr} />);`,
216
+ `expect(container).toBeTruthy();`,
217
+ ].join("\n")));
218
+ // Event handlers
219
+ const eventProps = component.props.filter((p) => /^on[A-Z]/.test(p.name));
220
+ for (const prop of eventProps) {
221
+ const mockName = `mock${prop.name.charAt(0).toUpperCase()}${prop.name.slice(1)}`;
222
+ const mockInit = fw === "vitest" ? "vi.fn()" : "jest.fn()";
223
+ cases.push(this.buildTestCase(`should handle ${prop.name}`, [
224
+ `const ${mockName} = ${mockInit};`,
225
+ `render(<${component.name}${propStr} ${prop.name}={${mockName}} />);`,
226
+ `const target = screen.getByRole('button');`,
227
+ `fireEvent.click(target);`,
228
+ `expect(${mockName}).toHaveBeenCalled();`,
229
+ ].join("\n")));
230
+ }
231
+ // Conditional rendering for boolean props
232
+ const boolProps = component.props.filter((p) => p.type?.toLowerCase() === "boolean");
233
+ for (const prop of boolProps) {
234
+ cases.push(this.buildTestCase(`should render correctly when ${prop.name} is false`, [
235
+ `const { container } = render(<${component.name}${propStr} ${prop.name}={false} />);`,
236
+ `expect(container).toBeTruthy();`,
237
+ ].join("\n")));
238
+ }
239
+ return cases;
240
+ }
241
+ // ── Detection helpers ──────────────────────────────────────
242
+ detectTauriCommands(scan) {
243
+ return scan.imports.some((m) => m.source === "@tauri-apps/api/core" ||
244
+ m.source === "@tauri-apps/api" ||
245
+ m.source.startsWith("@tauri-apps/"));
246
+ }
247
+ getTauriCommandFunctions(scan) {
248
+ // Functions that likely invoke Tauri commands are exported async functions
249
+ // in files that import from @tauri-apps
250
+ return scan.functions.filter((f) => f.isExported && f.isAsync);
251
+ }
252
+ isTauriIntegrated(_component, scan) {
253
+ return this.detectTauriCommands(scan);
254
+ }
255
+ // ── Utility helpers ────────────────────────────────────────
256
+ buildCommandPayload(params) {
257
+ if (params.length === 0)
258
+ return null;
259
+ const fields = params
260
+ .map((p) => `${p.name}: ${this.sampleValueForType(p.type)}`)
261
+ .join(", ");
262
+ return `{ ${fields} }`;
263
+ }
264
+ buildDefaultProps(props, fw) {
265
+ const required = props.filter((p) => !p.isOptional);
266
+ if (required.length === 0)
267
+ return "";
268
+ const mockInit = fw === "vitest" ? "vi.fn()" : "jest.fn()";
269
+ const assignments = required
270
+ .map((p) => {
271
+ if (/^on[A-Z]/.test(p.name))
272
+ return `${p.name}={${mockInit}}`;
273
+ return `${p.name}={${this.sampleValueForType(p.type)}}`;
274
+ })
275
+ .join(" ");
276
+ return ` ${assignments}`;
277
+ }
278
+ toSnakeCase(name) {
279
+ return name
280
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
281
+ .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
282
+ .toLowerCase();
283
+ }
284
+ relativeImportPath(from, to) {
285
+ const rel = relative(dirname(from), to);
286
+ const withoutExt = rel.replace(/\.(ts|tsx|js|jsx)$/, "");
287
+ return rel.startsWith(".") ? withoutExt : `./${withoutExt}`;
288
+ }
289
+ }
290
+ //# sourceMappingURL=desktop-test.js.map