#!/usr/bin/env node const { execSync } = require('child_process'); const args = new Set(process.argv.slice(2)); if (args.has('--help') || args.has('-h')) { console.log('Project Hub persistence self-test'); console.log(''); console.log('Usage:'); console.log(' node scripts/persistence-self-test.cjs [--restart-backend]'); console.log(''); console.log('Environment:'); console.log(' API_BASE default: http://localhost:4000/api'); console.log(' WRITE_API_KEY optional x-api-key for write endpoints'); process.exit(0); } const API_BASE = process.env.API_BASE || 'http://localhost:4000/api'; const HEALTH_URL = API_BASE.replace(/\/api\/?$/, '') + '/health'; const WRITE_API_KEY = process.env.WRITE_API_KEY || ''; const SHOULD_RESTART = args.has('--restart-backend'); const marker = `selftest-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const state = { memberId: null, projectId: null, taskId: null, }; function log(msg) { console.log(`[persistence-test] ${msg}`); } async function sleep(ms) { await new Promise(resolve => setTimeout(resolve, ms)); } async function request(path, options = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), options.timeoutMs || 10000); const headers = { ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...(WRITE_API_KEY ? { 'x-api-key': WRITE_API_KEY } : {}), ...(options.headers || {}), }; const response = await fetch(`${API_BASE}${path}`, { method: options.method || 'GET', headers, body: options.body ? JSON.stringify(options.body) : undefined, signal: controller.signal, }).finally(() => clearTimeout(timeout)); const text = await response.text(); let parsed = null; try { parsed = text ? JSON.parse(text) : null; } catch { parsed = null; } if (!response.ok) { const details = parsed ? JSON.stringify(parsed) : text; throw new Error(`${response.status} ${response.statusText}${details ? `: ${details}` : ''}`); } return parsed; } async function waitForHealth(maxSeconds = 30) { const start = Date.now(); while ((Date.now() - start) / 1000 < maxSeconds) { try { const response = await fetch(HEALTH_URL, { signal: AbortSignal.timeout(3000) }); if (response.ok) return; } catch { // Keep polling. } await sleep(1000); } throw new Error(`Health check failed after ${maxSeconds}s at ${HEALTH_URL}`); } function restartBackendContainer() { log('Restarting backend via docker compose...'); execSync('docker compose restart backend', { stdio: 'inherit' }); } async function cleanup() { if (state.projectId) { try { await request(`/projects/${state.projectId}`, { method: 'DELETE' }); log(`Cleanup: deleted project ${state.projectId}`); } catch (e) { log(`Cleanup warning: failed to delete project ${state.projectId}: ${e.message}`); } } if (state.memberId) { try { await request(`/members/${state.memberId}`, { method: 'DELETE' }); log(`Cleanup: deleted member ${state.memberId}`); } catch (e) { log(`Cleanup warning: failed to delete member ${state.memberId}: ${e.message}`); } } } async function run() { log(`Using API base ${API_BASE}`); await waitForHealth(); log('Health check passed.'); const member = await request('/members', { method: 'POST', body: { name: `Self Test ${marker}`, role: 'QA Bot', initials: 'ST', email: `${marker}@local.test`, }, }); state.memberId = member.id; log(`Created member ${state.memberId}`); const project = await request('/projects', { method: 'POST', body: { name: `Persistence ${marker}`, description: 'Automated persistence verification', status: 'active', color: '#10b981', startDate: new Date().toISOString().slice(0, 10), dueDate: new Date(Date.now() + 86400000).toISOString().slice(0, 10), members: [], tasks: [], milestones: [], }, }); state.projectId = project.id; log(`Created project ${state.projectId}`); await request(`/projects/${state.projectId}/members/${state.memberId}`, { method: 'POST' }); log(`Assigned member ${state.memberId} to project ${state.projectId}`); const task = await request(`/projects/${state.projectId}/tasks`, { method: 'POST', body: { title: `Task ${marker}`, description: 'Persistence check task', memberId: state.memberId, }, }); state.taskId = task.id; log(`Created task ${state.taskId}`); if (SHOULD_RESTART) { restartBackendContainer(); await waitForHealth(45); log('Backend is healthy after restart.'); } const projects = await request('/projects'); const persistedProject = projects.find(p => p.id === state.projectId); if (!persistedProject) { throw new Error(`Persisted project not found after verification step: ${state.projectId}`); } const tasks = await request(`/projects/${state.projectId}/tasks`); const persistedTask = tasks.find(t => t.id === state.taskId); if (!persistedTask) { throw new Error(`Persisted task not found after verification step: ${state.taskId}`); } log('Persistence verified: project and task were read back successfully.'); await cleanup(); log('PASS'); } run().catch(async err => { console.error(`[persistence-test] FAIL: ${err.message}`); await cleanup(); process.exit(1); });