Skip to content

feat: final test coverage gaps for #54#77

Merged
dean0x merged 3 commits intomainfrom
feat/54-final-coverage
Mar 9, 2026
Merged

feat: final test coverage gaps for #54#77
dean0x merged 3 commits intomainfrom
feat/54-final-coverage

Conversation

@dean0x
Copy link
Owner

@dean0x dean0x commented Mar 9, 2026

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 rejection
  • output-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 tolerance
  • process-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 safety
  • git-state.test.ts (5 tests): Branch/SHA capture, non-git-repo → ok(null), empty repo handling, dirty file parsing from porcelain output, status failure resilience

Updates test:repositories and test:services scripts in package.json.

Coverage summary for #54

Tier Files Status
Tier 1 task-manager, event-driven-worker-pool, worktree-manager (removed) ✅ Complete
Tier 2 autoscaling-manager, resource-monitor, recovery-manager, container ✅ Complete
Tier 3 validation, output-repository, process-connector, git-state ✅ This PR
Skip bootstrap.ts (DI wiring, integration-tested), process-spawner-adapter.ts (temporary shim), CLI commands (thin wrappers) N/A

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 — clean
  • npx biome check src/ tests/ — no errors

…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
@greptile-apps
Copy link

greptile-apps bot commented Mar 9, 2026

Confidence Score: 5/5

  • Safe to merge — the only source change is a well-scoped, regression-tested bug fix; all other changes are new test files.
  • The production code change is a single-line bug fix with a dedicated regression test. All new test files are well-structured, match established patterns in the repo, and include proper setup/teardown. The three style-level comments are non-blocking. No logic or syntax issues were found.
  • No files require special attention.

Important Files Changed

Filename Overview
src/utils/git-state.ts Bug fix: removes .trim() before .split('\n') and replaces it with a .filter((line) => line.length > 0) guard, correctly preserving leading-space status prefixes (e.g., M src/foo.ts) that git status --porcelain emits for modified-but-unstaged files. The fix is minimal and correct.
tests/unit/implementations/output-repository.test.ts New test file covering SQLite persistence, file-backed fallback for large output, append, delete, and ENOENT tolerance. Uses a real in-memory DB with proper FK setup and afterEach cleanup of the output/ directory. Minor gap: the "delete file-backed output" test doesn't assert the physical file was removed from disk.
tests/unit/services/process-connector.test.ts New test file covering stdout/stderr wiring, exit-code preservation for 0 via nullish coalescing, error-to-stderr capture, double-exit guard, and null stream safety. Minor: the getOutput mock return value uses an unbranded 'test' string where TaskId is expected.
tests/unit/utils/git-state.test.ts New test file covering branch/SHA capture, non-git-repo ok(null), empty repo, dirty file porcelain parsing (including the leading-space edge case from the accompanying bug fix), and status failure resilience. The mockExecFileSequence helper has no bounds guard, which could produce cryptic errors if a test triggers more execFile calls than expected responses.
tests/unit/utils/validation.test.ts New test file with solid coverage of path traversal prevention (symlink and ../ vectors), buffer size and timeout boundary validation, and NaN rejection. Uses a real temp directory with proper beforeEach/afterEach cleanup including macOS symlink resolution via realpathSync.
package.json Adds process-connector.test.ts to test:services, adds output-repository.test.ts to test:repositories, and — importantly — adds both checkpoint-repository.test.ts and output-repository.test.ts to the --exclude list in test:implementations, eliminating the double-run issues flagged in previous review threads.

Sequence Diagram

sequenceDiagram
    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)
Loading

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
@dean0x dean0x merged commit 3d40172 into main Mar 9, 2026
2 checks passed
@dean0x dean0x deleted the feat/54-final-coverage branch March 9, 2026 15:42
Comment on lines +48 to +52

it('should capture stdout data', () => {
const capture = createMockOutputCapture();
const logger = createTestLogger();
const connector = new ProcessConnector(capture, logger);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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.

Comment on lines +123 to +140

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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment on lines +26 to +38
// 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;
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: Add tests for critical untested source files

1 participant