Implement wiring hardening, runtime API config, smoke tests, and build scripts
This commit is contained in:
190
scripts/wiring-smoke-test.cjs
Normal file
190
scripts/wiring-smoke-test.cjs
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/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);
|
||||
});
|
||||
Reference in New Issue
Block a user