Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: CI

on:
push:
branches:
- main
- master
pull_request:

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Unit tests
run: npm test

- name: Coverage summary
run: npm run test:coverage -- --top=15
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ concierge/
└── docs/
├── ARCHITECTURE.md # System architecture
├── FEATURES.md # Feature inventory
├── testing/ # Testing and coverage docs
│ └── COVERAGE_BASELINE.md
└── REFERENCE.md # Developer quick reference
```

Expand All @@ -187,13 +189,16 @@ concierge/
```bash
npm test # Run unit tests
npm run lint # Run ESLint
npm run test:coverage # Run tests with coverage + summary
npm run test:coverage -- --line-min=45 # Optional coverage threshold gate
```

## Documentation

- [Architecture Guide](docs/ARCHITECTURE.md) - Detailed system design, data flow, APIs, and component breakdown
- [Feature Catalog](docs/FEATURES.md) - Canonical list of user-facing capabilities
- [Developer Reference](docs/REFERENCE.md) - Quick-reference for file map, data models, functions, and common patterns
- [Coverage Baseline](docs/testing/COVERAGE_BASELINE.md) - Current coverage snapshot and milestone targets

## License

Expand Down
9 changes: 9 additions & 0 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Developer Quick Reference

## Testing Commands

```bash
npm test
npm run lint
npm run test:coverage
npm run test:coverage -- --line-min=45 --branch-min=65 --func-min=50
```

## File Structure

### Backend
Expand Down
57 changes: 57 additions & 0 deletions docs/testing/COVERAGE_BASELINE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Coverage Baseline

Last measured with:

```bash
npm run test:coverage
```

## Baseline Snapshot

- Date: 2026-02-28
- Line coverage: `41.79%`
- Branch coverage: `69.55%`
- Function coverage: `51.35%`

## Risk-First Milestones

### Milestone 1: Routes + Providers

Primary targets:

- `lib/routes/conversations.js`
- `lib/routes/files.js`
- `lib/routes/git.js`
- `lib/routes/preview.js`
- `lib/routes/workflow.js`
- `lib/providers/claude.js`
- `lib/providers/codex.js`
- `lib/providers/ollama.js`

Expected outcome:

- Substantially improved backend regression detection on API and provider lifecycle paths.

### Milestone 2: Frontend Core Unit Coverage

Primary targets:

- `public/js/conversations.js`
- `public/js/ui.js`
- `public/js/render.js`
- `public/js/websocket.js`
- `public/js/app.js` (targeted initialization paths)

Expected outcome:

- Core chat/file-panel UI behavior covered by deterministic Node-based unit tests.

## Coverage Commands

```bash
npm run test:coverage
npm run test:coverage -- --top=20
npm run test:coverage -- --line-min=45 --branch-min=65 --func-min=50
```

`test:coverage` exits non-zero if tests fail. Threshold flags are optional and can be used for CI gating.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"scripts": {
"start": "node server.js",
"test": "node --test 'test/*.test.js'",
"test:coverage": "node scripts/coverage-summary.js",
"test:coverage:raw": "node --test --experimental-test-coverage 'test/*.test.js'",
"lint": "eslint ."
},
"devDependencies": {
Expand Down
141 changes: 141 additions & 0 deletions scripts/coverage-summary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

function parseArgs(argv) {
const options = {
lineMin: null,
branchMin: null,
funcMin: null,
top: 12,
verbose: false,
};

for (const arg of argv) {
if (arg.startsWith('--line-min=')) {
options.lineMin = Number(arg.slice('--line-min='.length));
} else if (arg.startsWith('--branch-min=')) {
options.branchMin = Number(arg.slice('--branch-min='.length));
} else if (arg.startsWith('--func-min=')) {
options.funcMin = Number(arg.slice('--func-min='.length));
} else if (arg.startsWith('--top=')) {
const value = Number(arg.slice('--top='.length));
if (Number.isFinite(value) && value > 0) {
options.top = Math.floor(value);
}
} else if (arg === '--verbose') {
options.verbose = true;
}
}

return options;
}

function getTestFiles() {
const testDir = path.join(process.cwd(), 'test');
const files = fs.readdirSync(testDir)
.filter((name) => name.endsWith('.test.js'))
.sort()
.map((name) => path.join('test', name));
return files;
}

function parseCoverageRows(reportText) {
const rows = [];
const lines = String(reportText || '').split('\n');
const rowPattern = /^ℹ\s+(.+?)\s+\|\s+([0-9.]+)\s+\|\s+([0-9.]+)\s+\|\s+([0-9.]+)/;

for (const raw of lines) {
const match = raw.match(rowPattern);
if (!match) continue;
const name = match[1].trim();
const linePct = Number(match[2]);
const branchPct = Number(match[3]);
const funcPct = Number(match[4]);
rows.push({ name, linePct, branchPct, funcPct });
}

const overall = rows.find((row) => row.name === 'all files') || null;
const fileRows = rows.filter((row) => row.name.includes('.js'));
return { overall, fileRows };
}

function formatPct(value) {
if (!Number.isFinite(value)) return 'n/a';
return `${value.toFixed(2)}%`;
}

function checkThreshold(label, actual, min) {
if (!Number.isFinite(min)) return { ok: true };
if (Number.isFinite(actual) && actual >= min) return { ok: true };
return {
ok: false,
message: `[COVERAGE] ${label} ${formatPct(actual)} is below required ${formatPct(min)}`,
};
}

function main() {
const options = parseArgs(process.argv.slice(2));
const testFiles = getTestFiles();
if (testFiles.length === 0) {
console.error('[COVERAGE] No test files found under test/*.test.js');
process.exit(1);
}

const args = ['--test', '--experimental-test-coverage', ...testFiles];
const result = spawnSync(process.execPath, args, {
cwd: process.cwd(),
encoding: 'utf8',
env: process.env,
});

if (options.verbose || result.status !== 0) {
process.stdout.write(result.stdout || '');
process.stderr.write(result.stderr || '');
}

const reportText = `${result.stdout || ''}\n${result.stderr || ''}`;
const { overall, fileRows } = parseCoverageRows(reportText);

if (!overall) {
const code = Number.isInteger(result.status) ? result.status : 1;
process.exit(code);
}

const sortedByLine = [...fileRows]
.sort((a, b) => a.linePct - b.linePct)
.slice(0, options.top);

console.log('\n[COVERAGE] Summary');
console.log(`- Line: ${formatPct(overall.linePct)}`);
console.log(`- Branch: ${formatPct(overall.branchPct)}`);
console.log(`- Functions: ${formatPct(overall.funcPct)}`);

if (sortedByLine.length > 0) {
console.log(`[COVERAGE] Lowest ${sortedByLine.length} files by line coverage`);
for (const row of sortedByLine) {
console.log(`- ${row.name}: ${formatPct(row.linePct)} (branch ${formatPct(row.branchPct)}, funcs ${formatPct(row.funcPct)})`);
}
}

let exitCode = Number.isInteger(result.status) ? result.status : 1;
if (exitCode === 0) {
const checks = [
checkThreshold('line coverage', overall.linePct, options.lineMin),
checkThreshold('branch coverage', overall.branchPct, options.branchMin),
checkThreshold('function coverage', overall.funcPct, options.funcMin),
];
const failed = checks.filter((item) => !item.ok);
if (failed.length > 0) {
for (const item of failed) {
console.error(item.message);
}
exitCode = 1;
}
}

process.exit(exitCode);
}

main();
Loading
Loading