#!/usr/bin/env node 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 marker = `wiring-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; const state = { memberId: null, projectId: null, taskId: null, inviteToken: null, }; function log(message) { console.log(`[wiring-test] ${message}`); } async function sleep(ms) { await new Promise(resolve => setTimeout(resolve, ms)); } async function waitForHealth(maxSeconds = 30) { const started = Date.now(); while ((Date.now() - started) / 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(`Backend not healthy at ${HEALTH_URL} after ${maxSeconds}s`); } async function request(path, options = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), options.timeoutMs || 10000); const headers = { ...(options.body || options.formData ? {} : {}), ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...(options.write && 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.formData || (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 cleanup() { if (state.projectId) { try { await request(`/projects/${state.projectId}`, { method: 'DELETE', write: true }); log(`Cleanup: deleted project ${state.projectId}`); } catch (err) { log(`Cleanup warning: project delete failed: ${err.message}`); } } if (state.memberId) { try { await request(`/members/${state.memberId}`, { method: 'DELETE', write: true }); log(`Cleanup: deleted member ${state.memberId}`); } catch (err) { log(`Cleanup warning: member delete failed: ${err.message}`); } } } async function run() { log(`Using API base ${API_BASE}`); await waitForHealth(); log('Health check passed'); const member = await request('/members', { method: 'POST', write: true, body: { name: `Wiring Member ${marker}`, role: 'QA Bot', email: `${marker}@local.test`, }, }); state.memberId = member.id; log(`Created member ${state.memberId}`); const project = await request('/projects', { method: 'POST', write: true, body: { name: `Wiring Project ${marker}`, description: 'Wiring smoke test', status: 'active', color: '#6366f1', members: [], tasks: [], milestones: [], }, }); state.projectId = project.id; log(`Created project ${state.projectId}`); const assigned = await request(`/projects/${state.projectId}/members/${state.memberId}`, { method: 'POST', write: true, }); if (!Array.isArray(assigned.members) || !assigned.members.some(m => m.id === state.memberId)) { throw new Error('Assigned member missing from project.members'); } log('Assigned member to project'); const task = await request(`/projects/${state.projectId}/tasks`, { method: 'POST', write: true, body: { title: `Wiring Task ${marker}`, description: 'Task route check', assignedTo: state.memberId, status: 'todo', priority: 'medium', }, }); state.taskId = task.id; log(`Created task ${state.taskId}`); const updatedTask = await request(`/projects/${state.projectId}/tasks/${state.taskId}`, { method: 'PUT', write: true, body: { status: 'done' }, }); if (updatedTask.status !== 'done') { throw new Error('Task update did not persist expected status'); } log('Updated task status to done'); const invite = await request(`/projects/${state.projectId}/invites`, { method: 'POST', write: true, body: { email: `${marker}.invite@local.test` }, }); state.inviteToken = invite.token; log('Created invite token'); await request(`/invites/${state.inviteToken}`); await request(`/invites/${state.inviteToken}/accept`, { method: 'POST', body: { name: `Invitee ${marker}`, role: 'Collaborator', email: `${marker}.invite@local.test` }, }); log('Invite get/accept flow passed'); await request(`/projects/${state.projectId}/tasks/${state.taskId}`, { method: 'DELETE', write: true, }); log('Deleted task'); const tasksAfterDelete = await request(`/projects/${state.projectId}/tasks`); if (tasksAfterDelete.some(t => t.id === state.taskId)) { throw new Error('Task still present after delete'); } log('PASS'); await cleanup(); } run().catch(async err => { console.error(`[wiring-test] FAIL: ${err.message}`); await cleanup(); process.exit(1); });