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:
parent
8b729d7fc4
commit
4b4848c541
11
TODO.md
11
TODO.md
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue