186 lines
5.3 KiB
JavaScript
186 lines
5.3 KiB
JavaScript
#!/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);
|
|
});
|