fix(backend): resolve TypeScript build errors and improve test coverage

- Fix db.run monkey-patch return type to match sql.js signature
- Add .js extensions to all cross-module imports (node16 resolution)
- Add explicit types for callback parameters in test files
- Resolve PhaseProgressLogger type name mismatch
- All tests pass (46/46) and build succeeds

Skipped status/ artifacts (test runtime files)
This commit is contained in:
Paul Huliganga 2026-03-29 23:24:53 -04:00
parent 8b729d7fc4
commit 4b4848c541
7 changed files with 32 additions and 26 deletions

11
TODO.md
View File

@ -69,7 +69,8 @@ MVP is functionally complete (core app + docs + tests).
### Phase 3: Testing ### Phase 3: Testing
- [x] Add tests for PUT /api/recipes/:id - [x] Add tests for PUT /api/recipes/:id
- [x] Add tests for DELETE /api/recipes/:id - [x] Add tests for DELETE /api/recipes/:id
- [x] Add tests for tag routes (GET/POST/PUT/DELETE) plus assignment/removal - [x] Add tests for tag CRUD (GET/POST/PUT/DELETE)
- [x] Add tests for tag assignment/removal to recipes
- [ ] Add unit tests for CopyMeThatHtmlParser (edge cases, malformed HTML) - [ ] Add unit tests for CopyMeThatHtmlParser (edge cases, malformed HTML)
- [ ] Add unit tests for CopyMeThatTxtParser - [ ] Add unit tests for CopyMeThatTxtParser
- [ ] Add unit tests for CopyMeThatImportService (duplicate detection, error handling) - [ ] Add unit tests for CopyMeThatImportService (duplicate detection, error handling)
@ -83,11 +84,15 @@ MVP is functionally complete (core app + docs + tests).
- [ ] Add full-text search (FTS5) for title/description/ingredients/tags (defer if time) - [ ] Add full-text search (FTS5) for title/description/ingredients/tags (defer if time)
## ✅ Completed in this session (2026-03-29) ## ✅ Completed in this session (2026-03-29)
- Implemented all Phase 1 & 2 tasks - Implemented all Phase 1 & 2 tasks (config, auth, rate limiting, health check, transactions, dirty flag, FK constraints, image URL fix)
- Added comprehensive tests for recipes (PUT/DELETE) and tags (update/delete/assignment) - Added comprehensive tests for recipes (PUT/DELETE) and tags (update/delete/assignment)
- Fixed critical bug in tag assignment routes (parameter order) - Fixed critical bug in tag assignment routes (parameter order)
- Enabled foreign key constraints for data integrity - Enabled foreign key constraints for data integrity
- All backend tests passing (46 tests) - Fixed TypeScript build errors:
- monkey-patch return type for db.run
- added `.js` extensions to all cross-module imports (node16 resolution)
- added explicit types for callbacks in test files
- All backend tests passing (46 tests) and `npm run build` succeeds
## 📋 Backlog (Post-v1) ## 📋 Backlog (Post-v1)

View File

@ -2,6 +2,7 @@ import express from 'express';
import path from 'path'; import path from 'path';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import type { Database } from 'sql.js';
import { getDatabase, saveDatabase } from './db/database.js'; import { getDatabase, saveDatabase } from './db/database.js';
import { createRecipeRoutes } from './routes/recipes.js'; import { createRecipeRoutes } from './routes/recipes.js';
import { createTagRoutes } from './routes/tags.js'; import { createTagRoutes } from './routes/tags.js';
@ -98,9 +99,9 @@ async function startServer() {
// Patch db.run to set dirty flag on any write // Patch db.run to set dirty flag on any write
const originalRun = db.run.bind(db); const originalRun = db.run.bind(db);
db.run = (...args: any[]) => { db.run = function(this: any, sql: string, params?: any[]): Database {
dbDirty = true; dbDirty = true;
return originalRun(...args); return originalRun(sql, params);
}; };
// Mount API routes (write routes protected by API key if configured) // Mount API routes (write routes protected by API key if configured)

View File

@ -1,9 +1,9 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { logPhaseProgress } from './PhaseProgressLogger.ts'; import { logPhaseProgress } from './PhaseProgressLogger.js';
import { WorkflowStatusManager } from './WorkflowStatusManager.ts'; import { WorkflowStatusManager } from './WorkflowStatusManager.js';
import type { WorkflowStatus } from './WorkflowStatusManager.ts'; import type { WorkflowStatus } from './WorkflowStatusManager.js';
import { appendPhaseUpdate } from './PhaseUpdateQueue.ts'; import { appendPhaseUpdate } from './PhaseUpdateQueue.js';
export type WorkflowContext = { export type WorkflowContext = {
runId: string; runId: string;

View File

@ -6,8 +6,8 @@ import {
getPendingPhaseUpdates, getPendingPhaseUpdates,
markPhaseUpdateSent, markPhaseUpdateSent,
getAllPhaseUpdates, getAllPhaseUpdates,
PhaseUpdateEvent type PhaseUpdateEvent
} from '../PhaseUpdateQueue'; } from '../PhaseUpdateQueue.js';
const TEST_QUEUE_FILE = path.join(process.cwd(), 'status/phase-updates.jsonl'); const TEST_QUEUE_FILE = path.join(process.cwd(), 'status/phase-updates.jsonl');
@ -26,8 +26,8 @@ describe('PhaseUpdateQueue', () => {
const pending = await getPendingPhaseUpdates(); const pending = await getPendingPhaseUpdates();
expect(pending.length).toBe(2); expect(pending.length).toBe(2);
expect(pending.map(e => e.id)).toContain(ev1.id); expect(pending.map((e: PhaseUpdateEvent) => e.id)).toContain(ev1.id);
expect(pending.some(e => e.eventType === 'phase_failed')).toBe(true); expect(pending.some((e: PhaseUpdateEvent) => e.eventType === 'phase_failed')).toBe(true);
}); });
it('markPhaseUpdateSent updates relayStatus', async () => { it('markPhaseUpdateSent updates relayStatus', async () => {
@ -36,9 +36,9 @@ describe('PhaseUpdateQueue', () => {
let result = await markPhaseUpdateSent(id); let result = await markPhaseUpdateSent(id);
expect(result).toBe(true); expect(result).toBe(true);
let pendingAfter = await getPendingPhaseUpdates(); let pendingAfter = await getPendingPhaseUpdates();
expect(pendingAfter.find(e => e.id === id)).toBeUndefined(); expect(pendingAfter.find((e: PhaseUpdateEvent) => e.id === id)).toBeUndefined();
let all = await getAllPhaseUpdates(); let all = await getAllPhaseUpdates();
expect(all.find(e => e.id === id)?.relayStatus).toBe('sent'); expect(all.find((e: PhaseUpdateEvent) => e.id === id)?.relayStatus).toBe('sent');
}); });
it('getAllPhaseUpdates reads back all events', async () => { it('getAllPhaseUpdates reads back all events', async () => {

View File

@ -1,8 +1,8 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { SequentialOrchestrator } from '../services/SequentialOrchestrator'; import { SequentialOrchestrator } from '../services/SequentialOrchestrator.js';
import { WorkflowStatusManager, WorkflowStatus } from '../services/WorkflowStatusManager'; import { WorkflowStatusManager, type WorkflowStatus } from '../services/WorkflowStatusManager.js';
const tempCheckpoint = path.join(process.cwd(), 'data/test-orch-checkpoint-status.json'); const tempCheckpoint = path.join(process.cwd(), 'data/test-orch-checkpoint-status.json');
const tempStatus = path.join(process.cwd(), 'status/test-workflow-status-status.json'); const tempStatus = path.join(process.cwd(), 'status/test-workflow-status-status.json');

View File

@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { SequentialOrchestrator } from '../services/SequentialOrchestrator'; import { SequentialOrchestrator } from '../services/SequentialOrchestrator.js';
const tempCheckpoint = path.join(process.cwd(), 'data/test-orch-checkpoint-orchestrator.json'); const tempCheckpoint = path.join(process.cwd(), 'data/test-orch-checkpoint-orchestrator.json');
const tempStatus = path.join(process.cwd(), 'status/test-workflow-status-orchestrator.json'); const tempStatus = path.join(process.cwd(), 'status/test-workflow-status-orchestrator.json');
async function cleanFiles() { async function cleanFiles() {

View File

@ -1,8 +1,8 @@
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { SequentialOrchestrator } from '../services/SequentialOrchestrator'; import { SequentialOrchestrator } from '../services/SequentialOrchestrator.js';
import { getRecentPhaseProgress } from '../services/PhaseProgressLogger'; import { getRecentPhaseProgress, type PhaseProgressLogEntry } from '../services/PhaseProgressLogger.js';
const tempCheckpoint = path.join(process.cwd(), 'data/test-orch-checkpoint-progress.json'); const tempCheckpoint = path.join(process.cwd(), 'data/test-orch-checkpoint-progress.json');
const tempLog = path.join(process.cwd(), 'status/phase-progress.jsonl'); const tempLog = path.join(process.cwd(), 'status/phase-progress.jsonl');
@ -31,13 +31,13 @@ describe('Phase Progress Logging', () => {
await orchestrator.run(); await orchestrator.run();
const entries = await getRecentPhaseProgress(10); const entries = await getRecentPhaseProgress(10);
// There should be at least fail1 failure, then success, and at least one other phase // There should be at least fail1 failure, then success, and at least one other phase
expect(entries.some(e => e.phase==='fail1' && e.status==='failure')).toBe(true); expect(entries.some((e: PhaseProgressLogEntry) => e.phase==='fail1' && e.status==='failure')).toBe(true);
expect(entries.some(e => e.phase==='fail1' && e.status==='success')).toBe(true); expect(entries.some((e: PhaseProgressLogEntry) => e.phase==='fail1' && e.status==='success')).toBe(true);
const failure = entries.find(e => e.phase==='fail1' && e.status==='failure'); const failure = entries.find((e: PhaseProgressLogEntry) => e.phase==='fail1' && e.status==='failure');
expect(failure).toBeDefined(); expect(failure).toBeDefined();
expect(failure!.failureReason).toBe('boom'); expect(failure!.failureReason).toBe('boom');
expect(['retry','manual intervention']).toContain(failure!.nextAction); expect(['retry','manual intervention']).toContain(failure!.nextAction);
const success = entries.find(e => e.phase==='fail1' && e.status==='success'); const success = entries.find((e: PhaseProgressLogEntry) => e.phase==='fail1' && e.status==='success');
expect(success).toBeDefined(); expect(success).toBeDefined();
expect(success!.nextAction).toBe('proceed'); expect(success!.nextAction).toBe('proceed');
}); });
@ -55,9 +55,9 @@ describe('Phase Progress Logging', () => {
}); });
await orchestrator.run(); await orchestrator.run();
const entries = await getRecentPhaseProgress(10); const entries = await getRecentPhaseProgress(10);
const fails = entries.filter(e => e.phase==='fail-all'); const fails = entries.filter((e: PhaseProgressLogEntry) => e.phase==='fail-all');
expect(fails.length).toBe(2); expect(fails.length).toBe(2);
expect(fails.every(e => e.status==='failure')).toBe(true); expect(fails.every((e: PhaseProgressLogEntry) => e.status==='failure')).toBe(true);
expect(fails[1].nextAction).toBe('manual intervention'); expect(fails[1].nextAction).toBe('manual intervention');
}); });