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
- [x] Add tests for PUT /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 CopyMeThatTxtParser
- [ ] 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)
## ✅ 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)
- Fixed critical bug in tag assignment routes (parameter order)
- 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)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { SequentialOrchestrator } from '../services/SequentialOrchestrator';
import { getRecentPhaseProgress } from '../services/PhaseProgressLogger';
import { SequentialOrchestrator } from '../services/SequentialOrchestrator.js';
import { getRecentPhaseProgress, type PhaseProgressLogEntry } from '../services/PhaseProgressLogger.js';
const tempCheckpoint = path.join(process.cwd(), 'data/test-orch-checkpoint-progress.json');
const tempLog = path.join(process.cwd(), 'status/phase-progress.jsonl');
@ -31,13 +31,13 @@ describe('Phase Progress Logging', () => {
await orchestrator.run();
const entries = await getRecentPhaseProgress(10);
// 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 => e.phase==='fail1' && e.status==='success')).toBe(true);
const failure = entries.find(e => e.phase==='fail1' && e.status==='failure');
expect(entries.some((e: PhaseProgressLogEntry) => e.phase==='fail1' && e.status==='failure')).toBe(true);
expect(entries.some((e: PhaseProgressLogEntry) => e.phase==='fail1' && e.status==='success')).toBe(true);
const failure = entries.find((e: PhaseProgressLogEntry) => e.phase==='fail1' && e.status==='failure');
expect(failure).toBeDefined();
expect(failure!.failureReason).toBe('boom');
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!.nextAction).toBe('proceed');
});
@ -55,9 +55,9 @@ describe('Phase Progress Logging', () => {
});
await orchestrator.run();
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.every(e => e.status==='failure')).toBe(true);
expect(fails.every((e: PhaseProgressLogEntry) => e.status==='failure')).toBe(true);
expect(fails[1].nextAction).toBe('manual intervention');
});