lacuna-cli 0.1.1
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 +451 -0
- package/bin/run.js +5 -0
- package/dist/agent/context.d.ts +25 -0
- package/dist/agent/context.d.ts.map +1 -0
- package/dist/agent/context.js +366 -0
- package/dist/agent/context.js.map +1 -0
- package/dist/agent/fix-loop.d.ts +20 -0
- package/dist/agent/fix-loop.d.ts.map +1 -0
- package/dist/agent/fix-loop.js +466 -0
- package/dist/agent/fix-loop.js.map +1 -0
- package/dist/agent/generator.d.ts +35 -0
- package/dist/agent/generator.d.ts.map +1 -0
- package/dist/agent/generator.js +220 -0
- package/dist/agent/generator.js.map +1 -0
- package/dist/agent/loop.d.ts +23 -0
- package/dist/agent/loop.d.ts.map +1 -0
- package/dist/agent/loop.js +394 -0
- package/dist/agent/loop.js.map +1 -0
- package/dist/agent/project-memory.d.ts +10 -0
- package/dist/agent/project-memory.d.ts.map +1 -0
- package/dist/agent/project-memory.js +57 -0
- package/dist/agent/project-memory.js.map +1 -0
- package/dist/agent/prompts.d.ts +44 -0
- package/dist/agent/prompts.d.ts.map +1 -0
- package/dist/agent/prompts.js +377 -0
- package/dist/agent/prompts.js.map +1 -0
- package/dist/ci/comment.d.ts +2 -0
- package/dist/ci/comment.d.ts.map +1 -0
- package/dist/ci/comment.js +97 -0
- package/dist/ci/comment.js.map +1 -0
- package/dist/ci/parse-outputs.d.ts +2 -0
- package/dist/ci/parse-outputs.d.ts.map +1 -0
- package/dist/ci/parse-outputs.js +30 -0
- package/dist/ci/parse-outputs.js.map +1 -0
- package/dist/commands/analyze.d.ts +13 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +151 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/fix.d.ts +15 -0
- package/dist/commands/fix.d.ts.map +1 -0
- package/dist/commands/fix.js +106 -0
- package/dist/commands/fix.js.map +1 -0
- package/dist/commands/generate.d.ts +18 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +129 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +131 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/run.d.ts +10 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +45 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/lib/config.d.ts +58 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +68 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/coverage/gaps.d.ts +12 -0
- package/dist/lib/coverage/gaps.d.ts.map +1 -0
- package/dist/lib/coverage/gaps.js +186 -0
- package/dist/lib/coverage/gaps.js.map +1 -0
- package/dist/lib/coverage/index.d.ts +7 -0
- package/dist/lib/coverage/index.d.ts.map +1 -0
- package/dist/lib/coverage/index.js +24 -0
- package/dist/lib/coverage/index.js.map +1 -0
- package/dist/lib/coverage/json.d.ts +3 -0
- package/dist/lib/coverage/json.d.ts.map +1 -0
- package/dist/lib/coverage/json.js +24 -0
- package/dist/lib/coverage/json.js.map +1 -0
- package/dist/lib/coverage/lcov.d.ts +3 -0
- package/dist/lib/coverage/lcov.d.ts.map +1 -0
- package/dist/lib/coverage/lcov.js +58 -0
- package/dist/lib/coverage/lcov.js.map +1 -0
- package/dist/lib/coverage/types.d.ts +27 -0
- package/dist/lib/coverage/types.d.ts.map +1 -0
- package/dist/lib/coverage/types.js +2 -0
- package/dist/lib/coverage/types.js.map +1 -0
- package/dist/lib/coverage-spinner.d.ts +6 -0
- package/dist/lib/coverage-spinner.d.ts.map +1 -0
- package/dist/lib/coverage-spinner.js +101 -0
- package/dist/lib/coverage-spinner.js.map +1 -0
- package/dist/lib/detector.d.ts +13 -0
- package/dist/lib/detector.d.ts.map +1 -0
- package/dist/lib/detector.js +106 -0
- package/dist/lib/detector.js.map +1 -0
- package/dist/lib/extract-error.d.ts +2 -0
- package/dist/lib/extract-error.d.ts.map +1 -0
- package/dist/lib/extract-error.js +116 -0
- package/dist/lib/extract-error.js.map +1 -0
- package/dist/lib/providers/anthropic.d.ts +8 -0
- package/dist/lib/providers/anthropic.d.ts.map +1 -0
- package/dist/lib/providers/anthropic.js +38 -0
- package/dist/lib/providers/anthropic.js.map +1 -0
- package/dist/lib/providers/index.d.ts +6 -0
- package/dist/lib/providers/index.d.ts.map +1 -0
- package/dist/lib/providers/index.js +27 -0
- package/dist/lib/providers/index.js.map +1 -0
- package/dist/lib/providers/openai-compatible.d.ts +11 -0
- package/dist/lib/providers/openai-compatible.d.ts.map +1 -0
- package/dist/lib/providers/openai-compatible.js +93 -0
- package/dist/lib/providers/openai-compatible.js.map +1 -0
- package/dist/lib/providers/types.d.ts +17 -0
- package/dist/lib/providers/types.d.ts.map +1 -0
- package/dist/lib/providers/types.js +97 -0
- package/dist/lib/providers/types.js.map +1 -0
- package/dist/lib/report-upload.d.ts +3 -0
- package/dist/lib/report-upload.d.ts.map +1 -0
- package/dist/lib/report-upload.js +15 -0
- package/dist/lib/report-upload.js.map +1 -0
- package/dist/lib/reporter.d.ts +51 -0
- package/dist/lib/reporter.d.ts.map +1 -0
- package/dist/lib/reporter.js +172 -0
- package/dist/lib/reporter.js.map +1 -0
- package/dist/lib/runner.d.ts +9 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +50 -0
- package/dist/lib/runner.js.map +1 -0
- package/dist/lib/skeleton.d.ts +8 -0
- package/dist/lib/skeleton.d.ts.map +1 -0
- package/dist/lib/skeleton.js +122 -0
- package/dist/lib/skeleton.js.map +1 -0
- package/dist/lib/streaming-viewer.d.ts +14 -0
- package/dist/lib/streaming-viewer.d.ts.map +1 -0
- package/dist/lib/streaming-viewer.js +80 -0
- package/dist/lib/streaming-viewer.js.map +1 -0
- package/dist/lib/tips.d.ts +16 -0
- package/dist/lib/tips.d.ts.map +1 -0
- package/dist/lib/tips.js +76 -0
- package/dist/lib/tips.js.map +1 -0
- package/dist/lib/typecheck.d.ts +3 -0
- package/dist/lib/typecheck.d.ts.map +1 -0
- package/dist/lib/typecheck.js +28 -0
- package/dist/lib/typecheck.js.map +1 -0
- package/dist/lib/validate.d.ts +7 -0
- package/dist/lib/validate.d.ts.map +1 -0
- package/dist/lib/validate.js +82 -0
- package/dist/lib/validate.js.map +1 -0
- package/dist/lib/worker-display.d.ts +45 -0
- package/dist/lib/worker-display.d.ts.map +1 -0
- package/dist/lib/worker-display.js +168 -0
- package/dist/lib/worker-display.js.map +1 -0
- package/oclif.manifest.json +295 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# lacuna
|
|
2
|
+
|
|
3
|
+
**Agentic test coverage — finds gaps, writes tests, verifies they pass.**
|
|
4
|
+
|
|
5
|
+
Lacuna is a CLI tool that uses AI to analyze your codebase, identify untested code, generate meaningful tests, run them, and retry if they fail — all in one command.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
lacuna generate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
lacuna analyze / lacuna generate lacuna fix
|
|
17
|
+
│ │
|
|
18
|
+
├── 1. Collect coverage ├── 1. Find failing files
|
|
19
|
+
│ ├── If report is < 10 min old: │ ├── --file: run that file only (fast)
|
|
20
|
+
│ │ reuse cached report │ ├── No --file + cache < 5 min old: use cache
|
|
21
|
+
│ └── Otherwise: run full suite │ └── Otherwise: run full suite
|
|
22
|
+
├── 2. Find files below threshold │
|
|
23
|
+
│ └── For each failing test file:
|
|
24
|
+
└── For each gap: (generate only) ├── Runs file alone → captures error output
|
|
25
|
+
├── Reads source + existing tests ├── Reads the test file + its source file
|
|
26
|
+
├── Reads imported type definitions ├── Reads imported type definitions
|
|
27
|
+
├── Reads tsconfig paths, deps, ├── Reads tsconfig paths, deps, setup file
|
|
28
|
+
│ and test setup file ├── Detects network mocking issues
|
|
29
|
+
├── Sends full context to AI model ├── AI reasons in <thinking>, writes fix
|
|
30
|
+
├── AI reasons then writes tests ├── Writes the fixed file
|
|
31
|
+
├── Runs the tests ├── ✅ Pass → next file
|
|
32
|
+
├── ✅ Pass → next file └── ❌ Fail → records what failed,
|
|
33
|
+
└── ❌ Fail → records what failed, detects oscillation (stops early),
|
|
34
|
+
detects oscillation (stops early), retries with negative constraints
|
|
35
|
+
retries with negative constraints restores original on final failure
|
|
36
|
+
restores original on final failure
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g lacuna-cli
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cd your-project
|
|
53
|
+
lacuna init # interactive setup wizard
|
|
54
|
+
lacuna analyze # see what's uncovered (read-only)
|
|
55
|
+
lacuna generate # AI fills the gaps
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
### `lacuna init`
|
|
63
|
+
Interactive setup wizard. Configures your model, test runner, coverage threshold, and mock file.
|
|
64
|
+
Creates `.lacuna.json` in your project root.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
lacuna init
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `lacuna analyze`
|
|
71
|
+
Runs your test suite, collects coverage, and prints which files and functions are below threshold. **Does not write any files.**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
lacuna analyze
|
|
75
|
+
lacuna analyze --threshold 90
|
|
76
|
+
lacuna analyze --format json --output report.json
|
|
77
|
+
lacuna analyze --format markdown
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `lacuna generate`
|
|
81
|
+
The main command. Runs the full agent loop — analyzes gaps, writes tests, runs them, retries failures.
|
|
82
|
+
|
|
83
|
+
When `--file` is given, lacuna skips the coverage suite entirely and goes straight to the AI — no waiting for a full suite run. The generated tests are verified by running just that file. Use this to increase coverage on a specific file without touching the rest of the project.
|
|
84
|
+
|
|
85
|
+
If you ran `lacuna analyze` recently (within 10 minutes), `generate` will reuse the existing coverage report instead of running the suite again. Use `--fresh` to force a new run.
|
|
86
|
+
|
|
87
|
+
If all retries fail, the original test file is restored — your workspace is never left with a half-written file. If the model oscillates (produces the same code twice), the retry loop stops early rather than burning remaining iterations.
|
|
88
|
+
|
|
89
|
+
If a fix attempt breaks an import and causes the test runner to collect 0 tests, lacuna detects this and sends the model the original error alongside an explicit warning — so it knows it over-reached and what it was actually supposed to fix. The same applies if a fix reduces the number of passing tests: the model is told it caused a regression and shown what the baseline was.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
lacuna generate
|
|
93
|
+
lacuna generate --file src/utils/math.ts # target one file
|
|
94
|
+
lacuna generate --dry-run # preview without writing
|
|
95
|
+
lacuna generate --verbose # live code panel as model writes each file
|
|
96
|
+
lacuna generate --workers 4 # run 4 files in parallel
|
|
97
|
+
lacuna generate --fresh # force a new coverage run
|
|
98
|
+
lacuna generate --format json --output report.json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `lacuna fix`
|
|
102
|
+
Finds all failing tests and repairs them using AI — without rewriting them from scratch. Sends each failing file along with its error output and source code to the model, which surgically fixes what's broken and retries until it passes.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
lacuna fix
|
|
106
|
+
lacuna fix --workers 4 # fix 4 files in parallel
|
|
107
|
+
lacuna fix --file src/utils/math.test.ts # fix a single test file (skips full suite run)
|
|
108
|
+
lacuna fix --dry-run # preview fixes without writing
|
|
109
|
+
lacuna fix --verbose # live code panel as model writes each fix
|
|
110
|
+
lacuna fix --fresh # re-run the suite even if cache is recent
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Unlike `lacuna generate`, which creates new tests, `lacuna fix` operates on existing failing tests. It preserves all test logic and only changes what is necessary to make the suite pass.
|
|
114
|
+
|
|
115
|
+
If all retries fail or the model oscillates (identical output detected), the original file is restored automatically. Your test suite is always left in a coherent state.
|
|
116
|
+
|
|
117
|
+
If a fix attempt breaks an import (causing 0 tests to be collected) or reduces the number of passing tests, lacuna detects the regression and tells the model exactly what the original failure was — so it doesn't waste further iterations trying to recover from the wrong problem.
|
|
118
|
+
|
|
119
|
+
When `--file` is given, lacuna skips the full suite and runs only the target file — much faster for iterating on a single broken test. Without `--file`, the failing-files list is cached for 30 minutes. After a fix run, the cache is updated to contain only the files that are still failing — so re-running `lacuna fix` immediately picks up exactly where the last run left off. Once all files are fixed, the cache is cleared so the next run does a clean suite scan.
|
|
120
|
+
|
|
121
|
+
### `lacuna run`
|
|
122
|
+
Runs your test suite and reports coverage. No AI involved.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
lacuna run
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Configuration — `.lacuna.json`
|
|
131
|
+
|
|
132
|
+
Created by `lacuna init`. All fields are optional with sensible defaults.
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"provider": "anthropic",
|
|
137
|
+
"model": "claude-sonnet-4-6",
|
|
138
|
+
"apiKeyEnv": "ANTHROPIC_API_KEY",
|
|
139
|
+
"testRunner": "jest",
|
|
140
|
+
"coverageFormat": "lcov",
|
|
141
|
+
"coverageDir": "coverage",
|
|
142
|
+
"sourceDir": "src",
|
|
143
|
+
"threshold": 80,
|
|
144
|
+
"maxIterations": 3,
|
|
145
|
+
"mocksFile": "src/test/mocks.ts",
|
|
146
|
+
"setupFile": "src/test/setup.ts",
|
|
147
|
+
"ignore": ["src/graphql/", "src/theme/"]
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
| Field | Default | Description |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `provider` | `anthropic` | `anthropic` or `openai-compatible` |
|
|
154
|
+
| `model` | `claude-sonnet-4-6` | Model name |
|
|
155
|
+
| `apiKeyEnv` | `ANTHROPIC_API_KEY` | Env var holding your API key |
|
|
156
|
+
| `baseURL` | — | Required for `openai-compatible` provider |
|
|
157
|
+
| `testRunner` | auto-detect | `jest` \| `vitest` \| `pytest` \| `mocha` \| `go-test` |
|
|
158
|
+
| `coverageFormat` | `lcov` | `lcov` \| `json-summary` |
|
|
159
|
+
| `coverageDir` | `coverage` | Where your test runner writes coverage |
|
|
160
|
+
| `sourceDir` | `src` | Root directory of source files |
|
|
161
|
+
| `threshold` | `80` | Minimum line coverage % to pass |
|
|
162
|
+
| `maxIterations` | `3` | How many times to retry a failing generated test |
|
|
163
|
+
| `coverageTimeout` | `300` | Seconds before the test suite is killed (prevents hanging on open handles) |
|
|
164
|
+
| `mocksFile` | — | Path to shared mock file (see Enterprise Mocks below) |
|
|
165
|
+
| `setupFile` | — | Path to your test setup file — lacuna passes its contents to the AI so it knows which globals and matchers are already available |
|
|
166
|
+
| `ignore` | `[]` | Extra path substrings to exclude from gap detection (e.g. `"src/graphql/"`) |
|
|
167
|
+
| `maxTokens` | `16000` | Maximum output tokens per model call. Lower this for providers with strict limits (Groq free tier: ~8000, Ollama: depends on model). Raise it if large test files are being cut off mid-generation. |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Supported models
|
|
172
|
+
|
|
173
|
+
Lacuna works with any AI model — local or cloud.
|
|
174
|
+
|
|
175
|
+
| Preset | Model | API key env | Notes |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| Claude (default) | `claude-sonnet-4-6` | `ANTHROPIC_API_KEY` | Best for code |
|
|
178
|
+
| Claude Opus | `claude-opus-4-7` | `ANTHROPIC_API_KEY` | Most capable |
|
|
179
|
+
| DeepSeek | `deepseek-chat` | `DEEPSEEK_API_KEY` | Very cost-effective |
|
|
180
|
+
| DeepSeek R1 | `deepseek-reasoner` | `DEEPSEEK_API_KEY` | Reasoning model |
|
|
181
|
+
| GPT-4o | `gpt-4o` | `OPENAI_API_KEY` | |
|
|
182
|
+
| Groq | `llama-3.3-70b-versatile` | `GROQ_API_KEY` | Fast, free tier |
|
|
183
|
+
| Gemini 2.5 Pro | `gemini-2.5-pro` | `GEMINI_API_KEY` | Google's most capable |
|
|
184
|
+
| Gemini 2.5 Flash | `gemini-2.5-flash` | `GEMINI_API_KEY` | Fast & cheap |
|
|
185
|
+
| OpenRouter | any model | `OPENROUTER_API_KEY` | 100+ models, one key |
|
|
186
|
+
| Ollama | any local model | none | Fully local, free |
|
|
187
|
+
| LM Studio | any local model | none | Fully local, free |
|
|
188
|
+
| Custom | configurable | configurable | Any OpenAI-compatible API |
|
|
189
|
+
|
|
190
|
+
Switch models any time by re-running `lacuna init` or editing `.lacuna.json` directly.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Enterprise mocks
|
|
195
|
+
|
|
196
|
+
For large codebases, ad-hoc mocks in every test file create maintenance nightmares. Lacuna supports a **shared mock file** — a single source of truth for all mocks that every generated test imports from.
|
|
197
|
+
|
|
198
|
+
### Setup
|
|
199
|
+
|
|
200
|
+
1. Create `src/test/mocks.ts`:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { vi } from 'vitest'
|
|
204
|
+
|
|
205
|
+
// API clients
|
|
206
|
+
export const mockAxios = {
|
|
207
|
+
get: vi.fn(),
|
|
208
|
+
post: vi.fn(),
|
|
209
|
+
put: vi.fn(),
|
|
210
|
+
delete: vi.fn(),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Router
|
|
214
|
+
export const mockNavigate = vi.fn()
|
|
215
|
+
export const mockUseNavigate = () => mockNavigate
|
|
216
|
+
vi.mock('react-router-dom', () => ({
|
|
217
|
+
useNavigate: mockUseNavigate,
|
|
218
|
+
useParams: vi.fn(() => ({})),
|
|
219
|
+
}))
|
|
220
|
+
|
|
221
|
+
// Auth
|
|
222
|
+
export const mockUser = {
|
|
223
|
+
id: 'user-1',
|
|
224
|
+
email: 'test@example.com',
|
|
225
|
+
role: 'admin',
|
|
226
|
+
}
|
|
227
|
+
export const mockUseAuth = vi.fn(() => ({ user: mockUser, isLoading: false }))
|
|
228
|
+
|
|
229
|
+
// Reset all mocks between tests
|
|
230
|
+
beforeEach(() => {
|
|
231
|
+
vi.clearAllMocks()
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
2. Add `mocksFile` to `.lacuna.json`:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"mocksFile": "src/test/mocks.ts"
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
3. Run lacuna normally:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
lacuna generate
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Every generated test will import from `src/test/mocks.ts` instead of creating its own `vi.fn()` calls. If a test needs a mock that doesn't exist yet, Claude will add it to the mocks file and import it — keeping everything centralized.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## CI / GitHub Actions
|
|
254
|
+
|
|
255
|
+
Add lacuna to your PR workflow to automatically generate tests and block merges below threshold.
|
|
256
|
+
|
|
257
|
+
Create `.github/workflows/lacuna.yml`:
|
|
258
|
+
|
|
259
|
+
```yaml
|
|
260
|
+
name: lacuna coverage
|
|
261
|
+
|
|
262
|
+
on:
|
|
263
|
+
pull_request:
|
|
264
|
+
branches: [main]
|
|
265
|
+
|
|
266
|
+
jobs:
|
|
267
|
+
coverage:
|
|
268
|
+
runs-on: ubuntu-latest
|
|
269
|
+
permissions:
|
|
270
|
+
contents: write
|
|
271
|
+
pull-requests: write
|
|
272
|
+
|
|
273
|
+
steps:
|
|
274
|
+
- uses: actions/checkout@v4
|
|
275
|
+
with:
|
|
276
|
+
ref: ${{ github.head_ref }}
|
|
277
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
278
|
+
|
|
279
|
+
- uses: actions/setup-node@v4
|
|
280
|
+
with:
|
|
281
|
+
node-version: '20'
|
|
282
|
+
cache: npm
|
|
283
|
+
- run: npm ci
|
|
284
|
+
|
|
285
|
+
- name: Run lacuna
|
|
286
|
+
uses: lacuna-dev/lacuna@v1
|
|
287
|
+
with:
|
|
288
|
+
threshold: 80
|
|
289
|
+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
290
|
+
|
|
291
|
+
- name: Commit generated tests
|
|
292
|
+
run: |
|
|
293
|
+
git config user.name "lacuna[bot]"
|
|
294
|
+
git config user.email "lacuna[bot]@users.noreply.github.com"
|
|
295
|
+
git add -A
|
|
296
|
+
git diff --staged --quiet || git commit -m "chore: lacuna — add generated tests"
|
|
297
|
+
git push
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
On every PR lacuna will:
|
|
301
|
+
- Generate missing tests
|
|
302
|
+
- Post a coverage report as a PR comment (updated on each push, no spam)
|
|
303
|
+
- Block the merge if coverage stays below your threshold
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Output formats
|
|
308
|
+
|
|
309
|
+
All commands support `--format` and `--output`:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
# Terminal (default)
|
|
313
|
+
lacuna analyze
|
|
314
|
+
|
|
315
|
+
# JSON — for scripts and CI pipelines
|
|
316
|
+
lacuna analyze --format json
|
|
317
|
+
lacuna generate --format json --output lacuna-report.json
|
|
318
|
+
|
|
319
|
+
# Markdown — for PR comments and docs
|
|
320
|
+
lacuna analyze --format markdown
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Exit codes
|
|
324
|
+
|
|
325
|
+
| Code | Meaning |
|
|
326
|
+
|---|---|
|
|
327
|
+
| `0` | Pass — coverage meets threshold |
|
|
328
|
+
| `1` | Fail — coverage below threshold |
|
|
329
|
+
| `2` | Error — test runner failed or config issue |
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Contextual tips
|
|
334
|
+
|
|
335
|
+
While tests are generating, lacuna shows rotating tips in the terminal — hints about flags and config options you might not be using yet. Tips are context-aware: if you're already using a flag, its tip won't appear.
|
|
336
|
+
|
|
337
|
+
**Tips shown during `lacuna generate`:**
|
|
338
|
+
- Use `-w 4` (`--workers`) to process multiple files in parallel
|
|
339
|
+
- Use `-f src/utils/math.ts` (`--file`) to target a single file
|
|
340
|
+
- Use `--dry-run` to preview without writing files
|
|
341
|
+
- Use `-v` (`--verbose`) to watch a live code panel as the AI writes each test file
|
|
342
|
+
- Use `-m claude-opus-4-7` (`--model`) to switch to a more capable model
|
|
343
|
+
- Use `--fresh` to force a new coverage run instead of reusing a cached report
|
|
344
|
+
- Use `-t 90` (`--threshold`) to raise the coverage bar
|
|
345
|
+
- Use `--format json --output report.json` to export results
|
|
346
|
+
- Set `mocksFile` in `.lacuna.json` to share mocks across all generated tests
|
|
347
|
+
- Add paths to `ignore[]` in `.lacuna.json` to skip directories
|
|
348
|
+
- Run `lacuna fix` to repair failing tests
|
|
349
|
+
- Run `lacuna analyze` to inspect gaps without writing files
|
|
350
|
+
- Increase `coverageTimeout` in `.lacuna.json` if your suite is being killed
|
|
351
|
+
- Set `maxTokens` in `.lacuna.json` if tests are cut off mid-generation (lower for Groq/Ollama, raise for large files)
|
|
352
|
+
|
|
353
|
+
**Tips shown during `lacuna fix`** are the same, minus flags that `fix` doesn't support (`--threshold`, `--format`).
|
|
354
|
+
|
|
355
|
+
In parallel mode (`--workers`), tips rotate every ~5 seconds in the live worker display. In single-worker mode, a different tip appears before each file is processed.
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## What gets skipped
|
|
360
|
+
|
|
361
|
+
Lacuna automatically skips files that have no testable runtime logic — no point generating tests for them.
|
|
362
|
+
|
|
363
|
+
**Skipped by directory name** (anywhere in the path):
|
|
364
|
+
`types/`, `constants/`, `assets/`, `images/`, `icons/`, `fonts/`, `styles/`, `generated/`, `__generated__/`, `mocks/`, `fixtures/`, `migrations/`, `i18n/`, `locales/`, `translations/`
|
|
365
|
+
|
|
366
|
+
**Skipped by file name pattern:**
|
|
367
|
+
`*.d.ts`, `*.test.*`, `*.spec.*`, `*.stories.*`, `*.config.*`, `*.mock.*`, `*.types.ts`, `*.constants.ts`, `*.enum.*`, `index.*`
|
|
368
|
+
|
|
369
|
+
**Skipped by content:** Even if a file doesn't match the patterns above, lacuna reads it and skips it if it contains no functions, arrow functions, or classes — i.e. only type/interface/enum/constant exports.
|
|
370
|
+
|
|
371
|
+
**Add your own exclusions** via `.lacuna.json`:
|
|
372
|
+
|
|
373
|
+
```json
|
|
374
|
+
{
|
|
375
|
+
"ignore": ["src/graphql/", "src/theme/", "src/generated/"]
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`ignore` entries are matched as path substrings — any file whose path contains the string is excluded.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Test placement
|
|
384
|
+
|
|
385
|
+
Lacuna follows your project's existing conventions:
|
|
386
|
+
|
|
387
|
+
- If test files exist **next to source files** (co-located), new tests go there too
|
|
388
|
+
- Otherwise, tests go in `__tests__/` inside the same directory as the source file
|
|
389
|
+
- `__tests__/` is created automatically if it doesn't exist
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Project structure
|
|
394
|
+
|
|
395
|
+
```
|
|
396
|
+
lacuna/
|
|
397
|
+
├── src/
|
|
398
|
+
│ ├── commands/ # CLI commands (analyze, generate, fix, run, init)
|
|
399
|
+
│ ├── agent/ # AI agent loop
|
|
400
|
+
│ │ ├── loop.ts # main generate → run → retry loop
|
|
401
|
+
│ │ ├── fix-loop.ts # fix → run → retry loop for failing tests
|
|
402
|
+
│ │ ├── context.ts # builds context for the AI (source + tests + mocks + type definitions)
|
|
403
|
+
│ │ ├── generator.ts # calls the AI model, manages conversation history
|
|
404
|
+
│ │ └── prompts.ts # system prompt + user prompt templates
|
|
405
|
+
│ ├── lib/
|
|
406
|
+
│ │ ├── config.ts # cosmiconfig loader + zod schema
|
|
407
|
+
│ │ ├── detector.ts # auto-detects test runner and language
|
|
408
|
+
│ │ ├── runner.ts # spawns test commands, captures output
|
|
409
|
+
│ │ ├── reporter.ts # terminal / JSON / markdown reporters
|
|
410
|
+
│ │ ├── skeleton.ts # collapses already-covered function bodies to reduce prompt size
|
|
411
|
+
│ │ ├── extract-error.ts # strips passing-test noise from runner output before retry
|
|
412
|
+
│ │ ├── validate.ts # checks generated code has real test calls; detects regressions and broken imports in retry output
|
|
413
|
+
│ │ ├── streaming-viewer.ts # live bordered code panel for --verbose mode (typewriter effect)
|
|
414
|
+
│ │ ├── typecheck.ts # post-vitest tsc pass; retries if type errors found
|
|
415
|
+
│ │ ├── providers/ # AI provider abstraction
|
|
416
|
+
│ │ │ ├── anthropic.ts
|
|
417
|
+
│ │ │ ├── openai-compatible.ts
|
|
418
|
+
│ │ │ └── types.ts # ModelProvider interface + presets
|
|
419
|
+
│ │ └── coverage/
|
|
420
|
+
│ │ ├── lcov.ts # LCOV parser
|
|
421
|
+
│ │ ├── json.ts # JSON summary parser
|
|
422
|
+
│ │ ├── gaps.ts # gap extractor
|
|
423
|
+
│ │ └── types.ts # shared coverage types
|
|
424
|
+
│ └── ci/
|
|
425
|
+
│ ├── comment.ts # posts coverage report as GitHub PR comment
|
|
426
|
+
│ └── parse-outputs.ts # sets GitHub Actions step outputs
|
|
427
|
+
├── app/ # SaaS dashboard (Next.js + Postgres + Payaza)
|
|
428
|
+
├── action.yml # GitHub Action definition
|
|
429
|
+
└── .github/workflows/
|
|
430
|
+
└── example.yml # example CI workflow to copy into your repo
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Contributing
|
|
436
|
+
|
|
437
|
+
Issues and PRs welcome. The codebase is TypeScript throughout.
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
git clone https://github.com/lacuna-dev/lacuna
|
|
441
|
+
cd lacuna
|
|
442
|
+
npm install
|
|
443
|
+
npm run build
|
|
444
|
+
npm link # makes `lacuna` available globally from your local build
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## License
|
|
450
|
+
|
|
451
|
+
MIT
|
package/bin/run.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DetectedEnvironment } from '../lib/detector.js';
|
|
2
|
+
import type { LacunaConfig } from '../lib/config.js';
|
|
3
|
+
export interface FileContext {
|
|
4
|
+
sourceFile: string;
|
|
5
|
+
sourceCode: string;
|
|
6
|
+
existingTestFile: string | null;
|
|
7
|
+
existingTestCode: string | null;
|
|
8
|
+
suggestedTestFile: string;
|
|
9
|
+
sourceImportPath: string | null;
|
|
10
|
+
mocksCode: string | null;
|
|
11
|
+
mocksImportPath: string | null;
|
|
12
|
+
setupFileCode: string | null;
|
|
13
|
+
packageDeps: string | null;
|
|
14
|
+
tsconfigPaths: string | null;
|
|
15
|
+
typeDefinitions: string | null;
|
|
16
|
+
localImportPaths: string[] | null;
|
|
17
|
+
reactMajorVersion: number | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function computeRelativeImport(fromFile: string, toFile: string): string;
|
|
20
|
+
export declare function collectTypeDefinitions(sourceCode: string, absoluteSourcePath: string, cwd: string): Promise<string | null>;
|
|
21
|
+
export declare function collectLocalImportPaths(sourceCode: string, absoluteSourcePath: string, absoluteTestFilePath: string, cwd: string): Promise<string[] | null>;
|
|
22
|
+
export declare function detectReactMajorVersion(cwd: string): Promise<number | null>;
|
|
23
|
+
export declare function buildFixFileContext(absTestPath: string, cwd: string, config?: LacunaConfig): Promise<Pick<FileContext, 'mocksCode' | 'mocksImportPath' | 'setupFileCode' | 'packageDeps' | 'tsconfigPaths'>>;
|
|
24
|
+
export declare function buildFileContext(sourceFilePath: string, cwd: string, env: DetectedEnvironment, config?: LacunaConfig): Promise<FileContext>;
|
|
25
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/agent/context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAEpD,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,iBAAiB,EAAE,MAAM,CAAA;IACzB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,gBAAgB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IACjC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAGD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAI9E;AA8JD,wBAAsB,sBAAsB,CAC1C,UAAU,EAAE,MAAM,EAClB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6CxB;AAMD,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,EAClB,kBAAkB,EAAE,MAAM,EAC1B,oBAAoB,EAAE,MAAM,EAC5B,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,CAmB1B;AAGD,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAWjF;AAkFD,wBAAsB,mBAAmB,CACvC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,iBAAiB,GAAG,eAAe,GAAG,aAAa,GAAG,eAAe,CAAC,CAAC,CAwBjH;AAED,wBAAsB,gBAAgB,CACpC,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,mBAAmB,EACxB,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,WAAW,CAAC,CAuDtB"}
|