Conversation
…connector, git-state Add 37 tests across 4 previously untested source files: - validation.test.ts (13): path traversal prevention, symlink blocking, buffer size limits, timeout validation - output-repository.test.ts (8): SQLite persistence, file fallback for large output, append, delete with cleanup - process-connector.test.ts (7): stdout/stderr wiring, exit code 0 preservation, double-exit guard, missing streams - git-state.test.ts (5): branch/sha capture, non-git-repo handling, empty repo, dirty file parsing, status failure Update package.json test scripts to include new test files in test:repositories and test:services groups. Closes #54
Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant T as Test
participant PC as ProcessConnector
participant CP as ChildProcess (mock)
participant OC as OutputCapture (mock)
T->>PC: connect(proc, taskId, onExit)
PC->>CP: proc.stdout.on('data', ...)
PC->>CP: proc.stderr.on('data', ...)
PC->>CP: proc.on('exit', ...)
PC->>CP: proc.on('error', ...)
Note over CP,OC: stdout data flow
CP-->>PC: emit('data', buffer)
PC->>OC: capture(taskId, 'stdout', text)
Note over CP,OC: stderr data flow
CP-->>PC: emit('data', buffer)
PC->>OC: capture(taskId, 'stderr', text)
Note over CP,PC: exit with double-guard
CP-->>PC: emit('exit', code)
PC->>PC: exitHandled = true
PC-->>T: onExit(code ?? null)
CP-->>PC: emit('exit', code) [2nd — ignored]
PC->>PC: exitHandled already true — return
Note over CP,OC: error path
CP-->>PC: emit('error', err)
PC->>OC: capture(taskId, 'stderr', 'Process error: ...')
PC-->>T: onExit(1)
Last reviewed commit: d3118f6 |
Address Greptile review: output-repository.test.ts and checkpoint-repository.test.ts ran twice in test:all — once via test:repositories (explicit) and again via test:implementations (glob). Add both to the --exclude list.
- Fix git-state.ts: split before filter to preserve leading-space status prefixes in porcelain output (e.g., " M src/foo.ts") - Add regression test for single leading-space status line - Clean up ./output/ dir in output-repository tests afterEach
|
|
||
| it('should capture stdout data', () => { | ||
| const capture = createMockOutputCapture(); | ||
| const logger = createTestLogger(); | ||
| const connector = new ProcessConnector(capture, logger); |
There was a problem hiding this comment.
Unbranded string used for TaskId in mock return value
taskId: 'test' is a plain string, but TaskId is a branded type (string & { readonly __brand: 'TaskId' }). This is technically a type error in the mock's return value object.
While this doesn't cause a test failure (since getOutput is never actually invoked in any of these tests, and vi.fn() returns a loosely-typed mock where mockReturnValue doesn't enforce the generic bound), it is inconsistent with how TaskId is used elsewhere in the same file — e.g., const taskId = 'task-1' as TaskId. For consistency and correctness, use the TaskId() constructor:
| it('should capture stdout data', () => { | |
| const capture = createMockOutputCapture(); | |
| const logger = createTestLogger(); | |
| const connector = new ProcessConnector(capture, logger); | |
| getOutput: vi.fn().mockReturnValue(ok({ taskId: TaskId('test'), stdout: [], stderr: [], totalSize: 0 })), |
Note that TaskId would need to be imported at the top of the file alongside the other imports.
|
|
||
| describe('delete', () => { | ||
| it('should delete DB entry', async () => { | ||
| const output = { | ||
| taskId, | ||
| stdout: ['data'], | ||
| stderr: [], | ||
| totalSize: 4, | ||
| }; | ||
| await repo.save(taskId, output); | ||
|
|
||
| const deleteResult = await repo.delete(taskId); | ||
| expect(deleteResult.ok).toBe(true); | ||
|
|
||
| const getResult = await repo.get(taskId); | ||
| expect(getResult.ok).toBe(true); | ||
| if (!getResult.ok) return; | ||
| expect(getResult.value).toBeNull(); |
There was a problem hiding this comment.
File-backed delete test doesn't assert physical file removal
The "should delete file-backed output and clean up file" test verifies the DB entry is gone (via repo.get returning null), but the test name advertises "clean up file". The delete method has a specific code path to unlink the file from disk — that branch is untested here.
Consider asserting the file no longer exists on the filesystem after delete:
// After deleteResult assertion
const outputDir = path.join(process.cwd(), 'output');
const filePath = path.join(outputDir, `${taskId}.json`);
expect(fs.existsSync(filePath)).toBe(false);This would require importing path at the top of the file (it's already imported as part of the cleanup in afterEach).
| // promisify wraps execFile — the callback is the last argument | ||
| const cb = (callback ?? _opts) as ExecFileCallback; | ||
| const response = responses[callIndex++]; | ||
|
|
||
| if ('error' in response) { | ||
| cb(response.error, { stdout: '', stderr: '' }); | ||
| } else { | ||
| cb(null, { stdout: response.stdout, stderr: '' }); | ||
| } | ||
|
|
||
| return undefined as never; | ||
| }); | ||
| } |
There was a problem hiding this comment.
mockExecFileSequence has no bounds guard on responses array
If a test accidentally triggers more execFile calls than there are entries in responses, responses[callIndex++] returns undefined. The subsequent 'error' in response check will then throw TypeError: Cannot use 'in' operator to search for 'error' in undefined, producing a cryptic test failure that obscures the root cause.
Adding a guard would make debugging much easier:
mock.mockImplementation((_cmd: unknown, _args: unknown, _opts: unknown, callback?: unknown) => {
const cb = (callback ?? _opts) as ExecFileCallback;
const response = responses[callIndex++];
if (!response) {
throw new Error(`mockExecFileSequence: unexpected call #${callIndex} — no response configured`);
}
// ...rest of implementation
Summary
Closes #54 — adds tests for the 4 remaining non-trivial untested source files:
validation.test.ts(13 tests): Security-critical path traversal prevention (symlink blocking,../detection), buffer size limits (1KB–1GB), timeout validation (1s–24h), NaN rejectionoutput-repository.test.ts(8 tests): SQLite persistence with real DB, file fallback for large output (above threshold), append to existing/new, delete with file cleanup, ENOENT toleranceprocess-connector.test.ts(7 tests): stdout/stderr stream wiring, exit code 0 preservation (nullish coalescing), error capture with onExit(1), double-exit guard, missing streams safetygit-state.test.ts(5 tests): Branch/SHA capture, non-git-repo →ok(null), empty repo handling, dirty file parsing from porcelain output, status failure resilienceUpdates
test:repositoriesandtest:servicesscripts in package.json.Coverage summary for #54
worktree-manager(removed)Test plan
npm run test:repositories— 117 passed (includes new output-repository)npm run test:services— 141 passed (includes new process-connector)vitest run tests/unit/utils— 90 passed (includes new validation + git-state)npm run build— cleannpx biome check src/ tests/— no errors