Submit joins the admission queue first and sends the PAT only once the reservation is held. Queued state shows the shaping orb, ordinal position with estimate, and an exit button whose label changes with the wait; running switches the orb to solving. Rejoin once on a lost reservation or ticket, best-effort leave on pagehide. Mock server gains the queue endpoints and five scenarios; contract, unit and e2e tests cover them.
509 lines
20 KiB
JavaScript
509 lines
20 KiB
JavaScript
/**
|
|
* Contract and Mock Server tests.
|
|
* Validates wire formats, security headers, session cookies, and scenarios against CONTRACTS.md.
|
|
*/
|
|
|
|
import { test, describe, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import http from 'node:http';
|
|
import { createMockServer, SCENARIOS } from '../dev/mock-server.js';
|
|
|
|
const TEST_PORT = 5199;
|
|
const BASE_URL = `http://127.0.0.1:${TEST_PORT}`;
|
|
|
|
describe('Mock Server and Wire Contract Tests', () => {
|
|
let server;
|
|
|
|
before(async () => {
|
|
server = createMockServer();
|
|
await new Promise((resolve) => {
|
|
server.listen(TEST_PORT, '127.0.0.1', resolve);
|
|
});
|
|
});
|
|
|
|
after(async () => {
|
|
await new Promise((resolve) => {
|
|
server.close(resolve);
|
|
});
|
|
});
|
|
|
|
test('GET / sets cw_session cookie and serves security headers', async () => {
|
|
const res = await fetch(`${BASE_URL}/`);
|
|
assert.equal(res.status, 200);
|
|
|
|
// Security headers
|
|
assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
|
|
assert.equal(res.headers.get('referrer-policy'), 'no-referrer');
|
|
const csp = res.headers.get('content-security-policy');
|
|
assert.ok(csp.includes("default-src 'none'"));
|
|
assert.ok(csp.includes("script-src 'self'"));
|
|
assert.ok(csp.includes("style-src 'self'"));
|
|
assert.ok(csp.includes("connect-src 'self'"));
|
|
assert.ok(csp.includes("img-src 'self'"));
|
|
|
|
// Session cookie
|
|
const cookie = res.headers.get('set-cookie');
|
|
assert.ok(cookie, 'Set-Cookie header must be present');
|
|
assert.ok(cookie.includes('cw_session='), 'Cookie name must be cw_session');
|
|
assert.ok(cookie.includes('HttpOnly'), 'Cookie must be HttpOnly');
|
|
assert.ok(cookie.includes('SameSite=Strict'), 'Cookie must be SameSite=Strict');
|
|
});
|
|
|
|
test('POST /api/v1/auth/verify succeeds with dummy credentials', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/auth/verify`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': BASE_URL
|
|
},
|
|
body: JSON.stringify({
|
|
url: 'https://confluence.example.com',
|
|
pat: 'dummy-pat-token-123'
|
|
})
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.headers.get('cache-control'), 'no-store');
|
|
const body = await res.json();
|
|
assert.deepEqual(body, { valid: true });
|
|
});
|
|
|
|
test('POST /api/v1/auth/verify fails with 403 in 403_verify scenario', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/auth/verify?scenario=403_verify`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': BASE_URL
|
|
},
|
|
body: JSON.stringify({
|
|
url: 'https://confluence.example.com',
|
|
pat: 'dummy-pat-token-123'
|
|
})
|
|
});
|
|
|
|
assert.equal(res.status, 403);
|
|
const body = await res.json();
|
|
assert.ok(body.error);
|
|
assert.equal(body.error.code, 'confluence_auth_failed');
|
|
});
|
|
|
|
test('POST /api/v1/query handles normal scenario matching section 7 shared contract', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=normal`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': BASE_URL
|
|
},
|
|
body: JSON.stringify({
|
|
prompt: 'deploy service X',
|
|
credentials: {
|
|
url: 'https://approved.example.com',
|
|
pat: 'dummy-pat-123'
|
|
}
|
|
})
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.headers.get('cache-control'), 'no-store');
|
|
const data = await res.json();
|
|
|
|
assert.ok(data.session_id);
|
|
assert.ok(data.markdown.includes('Deployment Guide'));
|
|
assert.equal(data.pages_accessed.length, 1);
|
|
assert.equal(data.pages_accessed[0].page_id, '847291');
|
|
assert.equal(data.pages_accessed[0].space, 'OPS');
|
|
assert.ok(data.pages_accessed[0].accessed_at);
|
|
|
|
assert.equal(data.tool_history.length, 2);
|
|
assert.equal(data.tool_history[0].tool, 'confluence_search');
|
|
assert.equal(data.tool_history[1].tool, 'confluence_view');
|
|
assert.equal(data.tool_history[1].cache_hit, false);
|
|
|
|
assert.equal(data.artifacts.length, 1);
|
|
assert.equal(data.artifacts[0].name, 'checklist.md');
|
|
assert.equal(data.artifacts[0].size_bytes, 32);
|
|
assert.ok(data.artifacts[0].expires_at);
|
|
});
|
|
|
|
test('GET /api/v1/artifacts/:id downloads exact 32 bytes for checklist.md', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/artifacts/art-checklist-01`);
|
|
assert.equal(res.status, 200);
|
|
assert.equal(res.headers.get('content-type'), 'application/octet-stream');
|
|
assert.equal(res.headers.get('x-content-type-options'), 'nosniff');
|
|
assert.ok(res.headers.get('content-disposition').includes('attachment; filename="checklist.md"'));
|
|
|
|
const text = await res.text();
|
|
assert.equal(text, '# Checklist\n\n- Deploy service X\n');
|
|
assert.equal(Buffer.byteLength(text, 'utf-8'), 32);
|
|
});
|
|
|
|
test('GET /api/v1/artifacts/:id returns 404 in unknown_expired_download scenario', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/artifacts/art-checklist-01?scenario=unknown_expired_download`);
|
|
assert.equal(res.status, 404);
|
|
const body = await res.json();
|
|
assert.equal(body.error.code, 'artifact_not_found');
|
|
});
|
|
|
|
test('POST /api/v1/query handles 409 busy scenario', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=409_busy`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': BASE_URL
|
|
},
|
|
body: JSON.stringify({
|
|
prompt: 'test prompt',
|
|
credentials: { url: 'https://example.com', pat: 'dummy' }
|
|
})
|
|
});
|
|
|
|
assert.equal(res.status, 409);
|
|
const body = await res.json();
|
|
assert.equal(body.error.code, 'busy');
|
|
});
|
|
|
|
test('POST /api/v1/query handles 504 timeout scenario', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=504_timeout`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': BASE_URL
|
|
},
|
|
body: JSON.stringify({
|
|
prompt: 'test prompt',
|
|
credentials: { url: 'https://example.com', pat: 'dummy' }
|
|
})
|
|
});
|
|
|
|
assert.equal(res.status, 504);
|
|
const body = await res.json();
|
|
assert.equal(body.error.code, 'query_timeout');
|
|
});
|
|
|
|
test('POST /api/v1/query handles empty_search scenario', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=empty_search`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': BASE_URL
|
|
},
|
|
body: JSON.stringify({
|
|
prompt: 'non-existent query',
|
|
credentials: { url: 'https://example.com', pat: 'dummy' }
|
|
})
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
const body = await res.json();
|
|
assert.equal(body.pages_accessed.length, 0);
|
|
assert.equal(body.artifacts.length, 0);
|
|
assert.equal(body.tool_history.length, 1);
|
|
assert.equal(body.tool_history[0].result.pages.length, 0);
|
|
});
|
|
|
|
test('POST /api/v1/query handles repeated_cached_view scenario', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=repeated_cached_view`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': BASE_URL
|
|
},
|
|
body: JSON.stringify({
|
|
prompt: 'cache test',
|
|
credentials: { url: 'https://example.com', pat: 'dummy' }
|
|
})
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
const body = await res.json();
|
|
assert.equal(body.pages_accessed.length, 1);
|
|
assert.equal(body.tool_history.length, 2);
|
|
assert.equal(body.tool_history[0].cache_hit, false);
|
|
assert.equal(body.tool_history[1].cache_hit, true);
|
|
});
|
|
|
|
test('POST /api/v1/query handles failed_tool scenario with status="error"', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=failed_tool`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': BASE_URL
|
|
},
|
|
body: JSON.stringify({
|
|
prompt: 'failed tool test',
|
|
credentials: { url: 'https://example.com', pat: 'dummy' }
|
|
})
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
const body = await res.json();
|
|
const errorTool = body.tool_history.find((t) => t.status === 'error');
|
|
assert.ok(errorTool);
|
|
assert.equal(errorTool.result, null);
|
|
assert.equal(errorTool.error.code, 'page_not_found');
|
|
});
|
|
|
|
test('POST /api/v1/query handles warning_truncated_history scenario', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/query?scenario=warning_truncated_history`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': BASE_URL
|
|
},
|
|
body: JSON.stringify({
|
|
prompt: 'truncation test',
|
|
credentials: { url: 'https://example.com', pat: 'dummy' }
|
|
})
|
|
});
|
|
|
|
assert.equal(res.status, 200);
|
|
const body = await res.json();
|
|
assert.ok(body.warnings.length > 0);
|
|
assert.equal(body.tool_history[0].parameters_truncated, true);
|
|
assert.equal(body.tool_history[0].result_truncated, true);
|
|
});
|
|
|
|
test('Origin check rejects untrusted external origins', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/query`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Origin': 'https://evil-attacker.example.com'
|
|
},
|
|
body: JSON.stringify({
|
|
prompt: 'attack',
|
|
credentials: { url: 'https://example.com', pat: 'dummy' }
|
|
})
|
|
});
|
|
|
|
assert.equal(res.status, 403);
|
|
const body = await res.json();
|
|
assert.equal(body.error.code, 'origin_denied');
|
|
});
|
|
|
|
/**
|
|
* Extracts `cw_session=...` from a Set-Cookie header so subsequent requests in the same
|
|
* test can carry the session forward (raw fetch has no cookie jar).
|
|
*/
|
|
function extractSessionCookie(res, previous) {
|
|
const setCookie = res.headers.get('set-cookie');
|
|
if (!setCookie) return previous;
|
|
const match = setCookie.match(/cw_session=[^;]+/);
|
|
return match ? match[0] : previous;
|
|
}
|
|
|
|
describe('Admission queue scenarios (docs/QUEUE_SPECIFICATION.md §10)', () => {
|
|
test('queue_full: join answers 503 queue_full', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=queue_full`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
|
|
body: '{}'
|
|
});
|
|
assert.equal(res.status, 503);
|
|
assert.equal(res.headers.get('cache-control'), 'no-store');
|
|
const body = await res.json();
|
|
assert.equal(body.error.code, 'queue_full');
|
|
});
|
|
|
|
test('queued: position 3 with eta, decrements on each poll, ready on the third, query succeeds', async () => {
|
|
let cookie;
|
|
const joinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=queued`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
|
|
body: '{}'
|
|
});
|
|
cookie = extractSessionCookie(joinRes, cookie);
|
|
assert.equal(joinRes.status, 200);
|
|
assert.equal(joinRes.headers.get('cache-control'), 'no-store');
|
|
const joinBody = await joinRes.json();
|
|
assert.equal(joinBody.status, 'queued');
|
|
assert.equal(joinBody.position, 3);
|
|
assert.equal(joinBody.ahead, 2);
|
|
assert.equal(joinBody.eta_seconds, 150);
|
|
assert.ok(joinBody.ticket_id);
|
|
|
|
const poll1 = await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued`, {
|
|
headers: { Cookie: cookie }
|
|
});
|
|
const poll1Body = await poll1.json();
|
|
assert.equal(poll1Body.status, 'queued');
|
|
assert.equal(poll1Body.position, 2);
|
|
assert.equal(poll1Body.ticket_id, joinBody.ticket_id);
|
|
|
|
const poll2 = await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued`, {
|
|
headers: { Cookie: cookie }
|
|
});
|
|
const poll2Body = await poll2.json();
|
|
assert.equal(poll2Body.status, 'queued');
|
|
assert.equal(poll2Body.position, 1);
|
|
|
|
const poll3 = await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued`, {
|
|
headers: { Cookie: cookie }
|
|
});
|
|
const poll3Body = await poll3.json();
|
|
assert.equal(poll3Body.status, 'ready');
|
|
assert.ok(poll3Body.reservation_expires_in_seconds > 0);
|
|
|
|
const queryRes = await fetch(`${BASE_URL}/api/v1/query?scenario=queued`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL, Cookie: cookie },
|
|
body: JSON.stringify({ prompt: 'deploy service X', credentials: { url: 'https://example.com', pat: 'dummy' } })
|
|
});
|
|
assert.equal(queryRes.status, 200);
|
|
const queryBody = await queryRes.json();
|
|
assert.ok(queryBody.markdown.includes('Deployment Guide'));
|
|
});
|
|
|
|
test('queued_no_estimate: eta_seconds is null at every step', async () => {
|
|
let cookie;
|
|
const joinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=queued_no_estimate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
|
|
body: '{}'
|
|
});
|
|
cookie = extractSessionCookie(joinRes, cookie);
|
|
const joinBody = await joinRes.json();
|
|
assert.equal(joinBody.status, 'queued');
|
|
assert.equal(joinBody.eta_seconds, null);
|
|
|
|
const poll1Body = await (await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued_no_estimate`, { headers: { Cookie: cookie } })).json();
|
|
assert.equal(poll1Body.eta_seconds, null);
|
|
|
|
const poll2Body = await (await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued_no_estimate`, { headers: { Cookie: cookie } })).json();
|
|
assert.equal(poll2Body.eta_seconds, null);
|
|
|
|
const poll3Body = await (await fetch(`${BASE_URL}/api/v1/queue/status?scenario=queued_no_estimate`, { headers: { Cookie: cookie } })).json();
|
|
assert.equal(poll3Body.status, 'ready');
|
|
});
|
|
|
|
test('reservation_lost: query 409s once after ready, rejoin succeeds, second query succeeds', async () => {
|
|
let cookie;
|
|
const joinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=reservation_lost`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
|
|
body: '{}'
|
|
});
|
|
cookie = extractSessionCookie(joinRes, cookie);
|
|
const joinBody = await joinRes.json();
|
|
assert.equal(joinBody.status, 'ready');
|
|
|
|
const firstQuery = await fetch(`${BASE_URL}/api/v1/query?scenario=reservation_lost`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL, Cookie: cookie },
|
|
body: JSON.stringify({ prompt: 'p', credentials: { url: 'https://example.com', pat: 'dummy' } })
|
|
});
|
|
assert.equal(firstQuery.status, 409);
|
|
const firstQueryBody = await firstQuery.json();
|
|
assert.equal(firstQueryBody.error.code, 'busy');
|
|
|
|
const rejoinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=reservation_lost`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL, Cookie: cookie },
|
|
body: '{}'
|
|
});
|
|
const rejoinBody = await rejoinRes.json();
|
|
assert.equal(rejoinBody.status, 'ready');
|
|
|
|
const secondQuery = await fetch(`${BASE_URL}/api/v1/query?scenario=reservation_lost`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL, Cookie: cookie },
|
|
body: JSON.stringify({ prompt: 'p', credentials: { url: 'https://example.com', pat: 'dummy' } })
|
|
});
|
|
assert.equal(secondQuery.status, 200);
|
|
});
|
|
|
|
test('ticket_lost: second status poll answers 404, rejoin answers ready', async () => {
|
|
let cookie;
|
|
const joinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=ticket_lost`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL },
|
|
body: '{}'
|
|
});
|
|
cookie = extractSessionCookie(joinRes, cookie);
|
|
const joinBody = await joinRes.json();
|
|
assert.equal(joinBody.status, 'queued');
|
|
|
|
const poll1Body = await (await fetch(`${BASE_URL}/api/v1/queue/status?scenario=ticket_lost`, { headers: { Cookie: cookie } })).json();
|
|
assert.equal(poll1Body.status, 'queued');
|
|
|
|
const poll2 = await fetch(`${BASE_URL}/api/v1/queue/status?scenario=ticket_lost`, { headers: { Cookie: cookie } });
|
|
assert.equal(poll2.status, 404);
|
|
const poll2Body = await poll2.json();
|
|
assert.equal(poll2Body.error.code, 'ticket_not_found');
|
|
|
|
const rejoinRes = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=ticket_lost`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'Origin': BASE_URL, Cookie: cookie },
|
|
body: '{}'
|
|
});
|
|
const rejoinBody = await rejoinRes.json();
|
|
assert.equal(rejoinBody.status, 'ready');
|
|
});
|
|
|
|
test('GET /api/v1/queue/status with no ticket answers 404 ticket_not_found', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/queue/status?scenario=normal`);
|
|
assert.equal(res.status, 404);
|
|
const body = await res.json();
|
|
assert.equal(body.error.code, 'ticket_not_found');
|
|
});
|
|
|
|
test('DELETE /api/v1/queue/ticket always answers 204', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/queue/ticket`, {
|
|
method: 'DELETE',
|
|
headers: { Origin: BASE_URL }
|
|
});
|
|
assert.equal(res.status, 204);
|
|
assert.equal(res.headers.get('cache-control'), 'no-store');
|
|
});
|
|
|
|
test('POST /api/v1/queue/join rejects a body containing credentials or pat', async () => {
|
|
const withCredentials = await fetch(`${BASE_URL}/api/v1/queue/join`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Origin: BASE_URL },
|
|
body: JSON.stringify({ credentials: { url: 'https://x', pat: 'y' } })
|
|
});
|
|
assert.equal(withCredentials.status, 400);
|
|
assert.equal((await withCredentials.json()).error.code, 'invalid_input');
|
|
|
|
const withPat = await fetch(`${BASE_URL}/api/v1/queue/join`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Origin: BASE_URL },
|
|
body: JSON.stringify({ pat: 'y' })
|
|
});
|
|
assert.equal(withPat.status, 400);
|
|
assert.equal((await withPat.json()).error.code, 'invalid_input');
|
|
});
|
|
|
|
test('non-queue scenarios answer join with ready immediately (unchanged flows)', async () => {
|
|
const res = await fetch(`${BASE_URL}/api/v1/queue/join?scenario=normal`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Origin: BASE_URL },
|
|
body: '{}'
|
|
});
|
|
assert.equal(res.status, 200);
|
|
const body = await res.json();
|
|
assert.equal(body.status, 'ready');
|
|
});
|
|
});
|
|
|
|
test('Mock dev toolbar external assets served with correct headers and zero inline script/style', async () => {
|
|
// CSS asset
|
|
const cssRes = await fetch(`${BASE_URL}/dev/scenario-toolbar.css`);
|
|
assert.equal(cssRes.status, 200);
|
|
assert.ok(cssRes.headers.get('content-type').includes('text/css'));
|
|
assert.equal(cssRes.headers.get('x-content-type-options'), 'nosniff');
|
|
|
|
// JS asset
|
|
const jsRes = await fetch(`${BASE_URL}/dev/scenario-toolbar.js`);
|
|
assert.equal(jsRes.status, 200);
|
|
assert.ok(jsRes.headers.get('content-type').includes('javascript'));
|
|
assert.equal(jsRes.headers.get('x-content-type-options'), 'nosniff');
|
|
|
|
// Root HTML page must not contain inline scripts or inline style attributes
|
|
const htmlRes = await fetch(`${BASE_URL}/`);
|
|
const html = await htmlRes.text();
|
|
assert.ok(!/<script(?![^>]*src=)[^>]*>/i.test(html), 'Root HTML in dev mode must not contain inline <script>');
|
|
assert.ok(!/style\s*=\s*["'][^"']*["']/i.test(html), 'Root HTML in dev mode must not contain inline style attributes');
|
|
});
|
|
});
|