Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
212 lines
6.4 KiB
JavaScript
212 lines
6.4 KiB
JavaScript
// ADR-0012 §Phase 5 (M3 backend) — integration tests for
|
|
// GET /api/v3/widgets/:widgetId/tickets/:ticketId/resolve
|
|
//
|
|
// Uses supertest against a mini express app that mounts the controller's
|
|
// `buildResolveRouter` factory with mocked DB / widget loaders. No real DB.
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import express from 'express';
|
|
import request from 'supertest';
|
|
import { buildResolveRouter } from '../resolveTicketRefController.js';
|
|
|
|
const FIXED_NOW = () => new Date('2026-04-25T12:00:00.000Z');
|
|
|
|
function buildApp({ widgets = {}, ticketRows = {}, columns = {} } = {}) {
|
|
const app = express();
|
|
|
|
// mock dbGet — called with first row in `SELECT ... FROM table_rows`.
|
|
const dbGetMock = async (_sql, params) => {
|
|
const [rowId, tableId] = params;
|
|
const key = `${tableId}:${rowId}`;
|
|
return ticketRows[key] ?? null;
|
|
};
|
|
|
|
// mock dbAll — called with column lookup `SELECT id, column_name FROM
|
|
// table_columns WHERE table_id = ?`. Returns the column array configured
|
|
// via `columns[tableId]`.
|
|
const dbAllMock = async (_sql, params) => {
|
|
const [tableId] = params;
|
|
return columns[tableId] ?? [];
|
|
};
|
|
|
|
const loadWidget = async (id) => widgets[id] ?? null;
|
|
|
|
app.use(buildResolveRouter({
|
|
dbGet: dbGetMock,
|
|
dbAll: dbAllMock,
|
|
loadWidget,
|
|
now: FIXED_NOW,
|
|
}));
|
|
|
|
return app;
|
|
}
|
|
|
|
describe('GET /widgets/:widgetId/tickets/:ticketId/resolve', () => {
|
|
it('happy path — returns ticket + snapshot', async () => {
|
|
const widgets = {
|
|
218: {
|
|
id: 218,
|
|
preset_name: 'documents',
|
|
config: { tickets_table_id: 1708 },
|
|
},
|
|
};
|
|
const ticketRows = {
|
|
'1708:131090': {
|
|
id: 131090,
|
|
base_id: 'b-131090',
|
|
table_id: 1708,
|
|
// jsonb data uses column-id keys; we'll map to names via dbAllMock
|
|
data: { 'col-what': 'Hello atom', 'col-state': 24276, 'col-assigned': '16' },
|
|
created_at: '2026-04-20T00:00:00.000Z',
|
|
updated_at: '2026-04-25T09:00:00.000Z',
|
|
},
|
|
};
|
|
const columns = {
|
|
1708: [
|
|
{ id: 'col-what', column_name: 'what' },
|
|
{ id: 'col-state', column_name: 'state' },
|
|
{ id: 'col-assigned', column_name: 'assigned_to' },
|
|
],
|
|
};
|
|
const app = buildApp({ widgets, ticketRows, columns });
|
|
|
|
const res = await request(app)
|
|
.get('/widgets/218/tickets/131090/resolve')
|
|
.expect(200);
|
|
|
|
expect(res.body.success).toBe(true);
|
|
expect(res.body.data.ticket.id).toBe(131090);
|
|
expect(res.body.data.ticket.data.what).toBe('Hello atom');
|
|
expect(res.body.data.ticket.data.state).toBe(24276);
|
|
expect(res.body.data.ticket.data.assigned_to).toBe('16');
|
|
|
|
expect(res.body.data.snapshot).toEqual({
|
|
title: 'Hello atom',
|
|
status: 24276,
|
|
assigned_to: '16',
|
|
updated_at: '2026-04-25T09:00:00.000Z',
|
|
snapshotted_at: '2026-04-25T12:00:00.000Z',
|
|
});
|
|
});
|
|
|
|
it('happy path with column data already named (no map)', async () => {
|
|
const widgets = {
|
|
218: {
|
|
id: 218,
|
|
preset_name: 'tickets_list',
|
|
config: { tickets_table_id: 1708 },
|
|
},
|
|
};
|
|
const ticketRows = {
|
|
'1708:42': {
|
|
id: 42,
|
|
base_id: 'b-42',
|
|
table_id: 1708,
|
|
data: { what: 'Direct names', state: 24275 },
|
|
created_at: '2026-04-25T08:00:00.000Z',
|
|
updated_at: '2026-04-25T08:00:00.000Z',
|
|
},
|
|
};
|
|
const app = buildApp({ widgets, ticketRows, columns: { 1708: [] } });
|
|
|
|
const res = await request(app)
|
|
.get('/widgets/218/tickets/42/resolve')
|
|
.expect(200);
|
|
|
|
expect(res.body.data.ticket.data.what).toBe('Direct names');
|
|
expect(res.body.data.snapshot.title).toBe('Direct names');
|
|
expect(res.body.data.snapshot.status).toBe(24275);
|
|
});
|
|
|
|
it('non-tickets-linked widget → 400 WIDGET_NOT_TICKETS_LINKED', async () => {
|
|
const widgets = {
|
|
500: {
|
|
id: 500,
|
|
preset_name: 'kanban', // not a tickets-linked preset
|
|
config: { board_id: 1 },
|
|
},
|
|
};
|
|
const app = buildApp({ widgets });
|
|
|
|
const res = await request(app)
|
|
.get('/widgets/500/tickets/1/resolve')
|
|
.expect(400);
|
|
|
|
expect(res.body.error).toBe('WIDGET_NOT_TICKETS_LINKED');
|
|
expect(res.body.code).toBe('WIDGET_NOT_TICKETS_LINKED');
|
|
});
|
|
|
|
it('widget not found → 404', async () => {
|
|
const app = buildApp({ widgets: {} });
|
|
|
|
const res = await request(app)
|
|
.get('/widgets/9999/tickets/1/resolve')
|
|
.expect(404);
|
|
|
|
// Uses default response.notFound shape: { success:false, error:{code,message} }
|
|
expect(res.body.success).toBe(false);
|
|
expect(res.body.error.code).toBe('NOT_FOUND');
|
|
expect(res.body.error.message).toMatch(/Widget/);
|
|
});
|
|
|
|
it('ticket not found in resolved tickets table → 404', async () => {
|
|
const widgets = {
|
|
218: {
|
|
id: 218,
|
|
preset_name: 'documents',
|
|
config: { tickets_table_id: 1708 },
|
|
},
|
|
};
|
|
const app = buildApp({ widgets, ticketRows: {} });
|
|
|
|
const res = await request(app)
|
|
.get('/widgets/218/tickets/77777/resolve')
|
|
.expect(404);
|
|
|
|
expect(res.body.success).toBe(false);
|
|
expect(res.body.error.code).toBe('NOT_FOUND');
|
|
expect(res.body.error.message).toMatch(/Ticket/);
|
|
});
|
|
|
|
it('non-numeric widgetId → 400', async () => {
|
|
const app = buildApp({});
|
|
const res = await request(app)
|
|
.get('/widgets/abc/tickets/1/resolve')
|
|
.expect(400);
|
|
expect(res.body.success).toBe(false);
|
|
});
|
|
|
|
it('non-numeric ticketId → 400', async () => {
|
|
const app = buildApp({});
|
|
const res = await request(app)
|
|
.get('/widgets/1/tickets/abc/resolve')
|
|
.expect(400);
|
|
expect(res.body.success).toBe(false);
|
|
});
|
|
|
|
it('documents widget without explicit tickets_table_id falls back to 1708', async () => {
|
|
const widgets = {
|
|
400: {
|
|
id: 400,
|
|
preset_name: 'documents',
|
|
config: { registry_table_id: 9999 }, // legacy doc widget, no tickets binding
|
|
},
|
|
};
|
|
const ticketRows = {
|
|
'1708:131090': {
|
|
id: 131090,
|
|
base_id: 'b',
|
|
table_id: 1708,
|
|
data: { what: 'Default-table fallback', state: 24275 },
|
|
created_at: '2026-04-25T08:00:00.000Z',
|
|
updated_at: '2026-04-25T08:00:00.000Z',
|
|
},
|
|
};
|
|
const app = buildApp({ widgets, ticketRows, columns: { 1708: [] } });
|
|
|
|
const res = await request(app)
|
|
.get('/widgets/400/tickets/131090/resolve')
|
|
.expect(200);
|
|
expect(res.body.data.snapshot.title).toBe('Default-table fallback');
|
|
});
|
|
});
|