/** * Dummy backend for the visual testing rig. * * Serves the frozen fixture snapshot (test/fixtures/snapshot.json) on every * GET endpoint the frontend reads during screenshot runs. Always-authed stub: * every request is treated as the fixture user, so headless --screenshot runs * need no cookie juggling. (There is no logged-out/login screenshot.) * * Mutating methods (POST/PATCH/PUT/DELETE) return the stored fixture payload * (or a minimal echo) so UI actions don't crash mid-run — persistence is * in-memory only and resets on restart. * * Usage: node test/dummyBackend.mjs [--port 4218] */ import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FIXTURE_DIR = path.join(__dirname, 'fixtures'); const IMAGE_DIR = path.join(FIXTURE_DIR, 'images'); const portArg = process.argv.find((a) => a.startsWith('--port='))?.split('=')[1]; const PORT = Number(portArg ?? process.env.RIG_BACKEND_PORT ?? 4208); function loadSnapshot() { const file = path.join(FIXTURE_DIR, 'snapshot.json'); if (!fs.existsSync(file)) { console.error( `[dummy-backend] missing ${file}. Run: npm run rig:collect (live backend at :4208 required)`, ); process.exit(1); } return JSON.parse(fs.readFileSync(file, 'utf8')); } const snap = loadSnapshot(); const { auth, company, catalog, parties, documents, misc } = snap; const googleClientId = snap.meta?.googleClientId ?? null; const user = auth.me.user; function json(res, status, body) { const text = JSON.stringify(body); res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(text), 'Cache-Control': 'no-store', }); res.end(text); } function readBody(req) { return new Promise((resolve) => { const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => { const raw = Buffer.concat(chunks).toString('utf8'); if (!raw) return resolve(null); try { resolve(JSON.parse(raw)); } catch { resolve(raw); } }); }); } const server = http.createServer(async (req, res) => { const url = new URL(req.url, 'http://localhost'); const p = url.pathname; const method = req.method; // Public / auth bootstrap. The dummy is always-authed: every request is // treated as the fixture user, so headless --screenshot runs need no // cookie juggling. (There is no logged-out/login screenshot in the rig.) if (p === '/api/health') return json(res, 200, { ok: true }); if (p === '/api/auth/me' && method === 'GET') { return json(res, 200, { user, googleClientId }); } if (p === '/api/auth/login' && method === 'POST') { await readBody(req); res.setHeader('Set-Cookie', 'token=dummy-rig-token; Path=/; HttpOnly'); return json(res, 200, { message: 'Login successful', user }); } if (p === '/api/auth/logout' && method === 'POST') { res.setHeader('Set-Cookie', 'token=; Path=/; HttpOnly; Max-Age=0'); return json(res, 200, { ok: true }); } if (p === '/api/auth/selected-company' && method === 'PUT') { return json(res, 200, { user }); } // Everything else is served as the fixture user (always-authed stub). // SSE: hold the stream open with heartbeats, never emit (frozen fixtures). if (p === '/api/events' && method === 'GET') { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); res.write(': connected\n\n'); const beat = setInterval(() => { try { res.write(': heartbeat\n\n'); } catch { clearInterval(beat); } }, 20000); req.on('close', () => clearInterval(beat)); return; } // Binary images (byte-identical to the live backend at collect time). // Backend fullHash is an 8-char alphanumeric string (see dashback pic.js). const thumbMatch = p.match(/^\/api\/thumb\/([A-Za-z0-9]{8})$/); if (thumbMatch && method === 'GET') { const file = path.join(IMAGE_DIR, `${thumbMatch[1]}.avif`); if (!fs.existsSync(file)) return json(res, 404, { error: 'Image not found' }); const buf = fs.readFileSync(file); res.writeHead(200, { 'Content-Type': 'image/avif', 'Content-Length': buf.length, 'Cache-Control': 'private, max-age=31536000, immutable', ETag: `"${thumbMatch[1]}"`, }); return res.end(buf); } const imageMatch = p.match(/^\/api\/image\/([A-Za-z0-9]{8})$/); if (imageMatch && method === 'GET') { const file = path.join(IMAGE_DIR, `${imageMatch[1]}.avif`); if (!fs.existsSync(file)) return json(res, 404, { error: 'Image not found' }); const buf = fs.readFileSync(file); res.writeHead(200, { 'Content-Type': 'image/avif', 'Content-Length': buf.length, 'Cache-Control': 'private, max-age=31536000, immutable', ETag: `"${imageMatch[1]}"`, }); return res.end(buf); } // ── Reads ────────────────────────────────────────────── if (method === 'GET') { if (p === '/api/company') return json(res, 200, { companies: auth.companies }); if (p === '/api/category/tree') return json(res, 200, catalog.tree); const catArticles = p.match(/^\/api\/category\/(\d+)\/articles$/); if (catArticles) { return json(res, 200, { articles: catalog.articlesByCategory[catArticles[1]] ?? [], }); } const articleMatch = p.match(/^\/api\/article\/([^/]+)$/); if (articleMatch && !['search', 'history', 'export.zip'].includes(articleMatch[1])) { const sku = decodeURIComponent(articleMatch[1]); if (sku === catalog.articleDetail?.sku) return json(res, 200, { article: catalog.articleDetail }); // Any other SKU: clone the known detail so deep links still render. if (catalog.articleDetail) { return json(res, 200, { article: { ...catalog.articleDetail, sku, name: catalog.articleDetail.name }, }); } const catArticle = Object.values(catalog.articlesByCategory ?? {}).flat().find((a) => a.sku === sku); if (catArticle) { return json(res, 200, { article: { id: catArticle.id, sku: catArticle.sku, name: catArticle.name, unitamount: 1, unit: { si: 'stk', name: 'Stück' }, prices: [{ minmultiplier: 1, net: 10, taxRate: 19, customergroupId: 1 }], categoryIds: [], photos: catArticle.fullHash ? [{ fullHash: catArticle.fullHash, thumbHash: catArticle.thumbHash }] : [], children: [], }, }); } return json(res, 404, { error: 'Article not found' }); } if (p === '/api/article/history') return json(res, 200, { history: misc.articleHistoryGrid ?? [] }); const skuHistory = p.match(/^\/api\/article\/([^/]+)\/history$/); if (skuHistory) return json(res, 200, { history: catalog.articleHistory ?? [] }); const skuSuppliers = p.match(/^\/api\/article\/([^/]+)\/supplier$/); if (skuSuppliers) return json(res, 200, { suppliers: catalog.articleSuppliers ?? [] }); const skuChildren = p.match(/^\/api\/article\/([^/]+)\/children$/); if (skuChildren) return json(res, 200, { children: [] }); const skuAttr = p.match(/^\/api\/article\/([^/]+)\/attributevalue$/); if (skuAttr) return json(res, 200, { attributevalues: [] }); const skuProp = p.match(/^\/api\/article\/([^/]+)\/property$/); if (skuProp) return json(res, 200, { properties: [] }); const skuArticlelist = p.match(/^\/api\/article\/([^/]+)\/articlelist$/); if (skuArticlelist) return json(res, 200, { items: [] }); const skuOptions = p.match(/^\/api\/article\/([^/]+)\/optionslist$/); if (skuOptions) return json(res, 200, { options: [] }); if (p === '/api/brand') return json(res, 200, { brands: catalog.brands ?? [] }); if (p === '/api/unit') return json(res, 200, { units: catalog.units ?? [] }); if (p === '/api/warehouse') return json(res, 200, { warehouses: catalog.warehouses ?? [] }); const warehouseById = p.match(/^\/api\/warehouse\/(\d+)$/); if (warehouseById) { const found = (catalog.warehouses ?? []).find((w) => String(w.id) === warehouseById[1]); if (!found) return json(res, 404, { error: 'Warehouse not found' }); return json(res, 200, { warehouse: found }); } if (p === '/api/attributename') return json(res, 200, { attributenames: catalog.attributes ?? [] }); const attrValues = p.match(/^\/api\/attributename\/(\d+)\/value$/); if (attrValues) { const attr = (catalog.attributes ?? []).find((a) => String(a.id) === attrValues[1]); return json(res, 200, { values: attr?.values ?? [] }); } if (p === '/api/propertyname') return json(res, 200, { propertynames: catalog.properties ?? [] }); if (p === '/api/supplier') return json(res, 200, { suppliers: parties.suppliers ?? [] }); const supplierArticles = p.match(/^\/api\/supplier\/(\d+)\/articles$/); if (supplierArticles) return json(res, 200, { articles: [] }); if (p === '/api/entity') return json(res, 200, { entities: parties.entities ?? [] }); if (p === '/api/entity/next-number') return json(res, 200, { number: 'KD-001001' }); const entityMatch = p.match(/^\/api\/entity\/(\d+)$/); if (entityMatch) { const found = (parties.entities ?? []).find((e) => String(e.id) === entityMatch[1]); if (!found && parties.entityDetail && String(parties.entityDetail.id) === entityMatch[1]) { return json(res, 200, { entity: parties.entityDetail }); } if (!found) return json(res, 404, { error: 'Entity not found' }); return json(res, 200, { entity: found }); } const entityAddr = p.match(/^\/api\/entity\/(\d+)\/address$/); if (entityAddr) return json(res, 200, { addresses: parties.entityAddresses ?? [] }); const entityContact = p.match(/^\/api\/entity\/(\d+)\/contact$/); if (entityContact) return json(res, 200, { contacts: parties.entityContacts ?? [] }); if (p === '/api/customergroup') return json(res, 200, { customergroups: parties.customergroups ?? [] }); if (p === '/api/delivery') return json(res, 200, { deliveries: documents.deliveries ?? [] }); const deliveryMatch = p.match(/^\/api\/delivery\/(\d+)$/); if (deliveryMatch) { const found = (documents.deliveries ?? []).find((d) => String(d.id) === deliveryMatch[1]); const detail = documents.deliveryDetail && String(documents.deliveryDetail.id) === deliveryMatch[1] ? documents.deliveryDetail : null; if (detail) return json(res, 200, { delivery: detail }); if (found && !detail) { return json(res, 200, { delivery: { ...found, lines: [], files: found.files ?? [] }, }); } return json(res, 404, { error: 'Delivery not found' }); } if (p === '/api/quote') return json(res, 200, { quotes: documents.quotes ?? [] }); const quoteMatch = p.match(/^\/api\/quote\/(\d+)$/); if (quoteMatch) { const found = (documents.quotes ?? []).find((q) => String(q.id) === quoteMatch[1]); if (!found) return json(res, 404, { error: 'Quote not found' }); return json(res, 200, { quote: { ...found, lines: found.lines ?? [] } }); } if (p === '/api/order') return json(res, 200, { orders: documents.orders ?? [] }); const orderMatch = p.match(/^\/api\/order\/(\d+)$/); if (orderMatch) { const found = (documents.orders ?? []).find((o) => String(o.id) === orderMatch[1]); if (!found) return json(res, 404, { error: 'Order not found' }); return json(res, 200, { order: { ...found, lines: found.lines ?? [] } }); } if (p === '/api/number') return json(res, 200, { numbers: documents.numbers ?? [] }); if (p === '/api/posterminal') return json(res, 200, { posterminals: misc.posterminals ?? [] }); if (p === '/api/printer') return json(res, 200, { printers: misc.printers ?? [] }); const printerById = p.match(/^\/api\/printer\/(\d+)$/); if (printerById) { const found = (misc.printers ?? []).find((x) => String(x.id) === printerById[1]); if (!found) return json(res, 404, { error: 'Printer not found' }); return json(res, 200, { printer: found }); } if (p === '/api/labeltemplate') return json(res, 200, { labeltemplates: misc.labeltemplates ?? [] }); const labelById = p.match(/^\/api\/labeltemplate\/(\d+)$/); if (labelById) { const found = (misc.labeltemplates ?? []).find((x) => String(x.id) === labelById[1]); if (!found) return json(res, 404, { error: 'Label template not found' }); return json(res, 200, { labeltemplate: found }); } if (p === '/api/company/setting') return json(res, 200, { settings: company.settings ?? [] }); const settingByKey = p.match(/^\/api\/company\/setting\/(.+)$/); if (settingByKey) { const found = (company.settings ?? []).find((s) => s.key === decodeURIComponent(settingByKey[1])); if (!found) return json(res, 404, { error: 'Setting not found' }); return json(res, 200, { setting: found }); } if (p === '/api/vfs') return json(res, 200, { files: misc.vfs ?? [] }); if (p === '/api/history') return json(res, 200, { history: misc.globalHistory ?? [], rows: misc.globalHistory ?? [] }); if (p === '/api/modelconversation') return json(res, 200, { conversations: [] }); const conversationMsgs = p.match(/^\/api\/modelconversation\/(\d+)\/message$/); if (conversationMsgs) return json(res, 200, { messages: [] }); const ledgerSku = p.match(/^\/api\/ledger\/sku\/(.+)$/); if (ledgerSku) return json(res, 200, { ledger: [] }); const ledgerMa = p.match(/^\/api\/ledger\/manifestarticle\/(\d+)$/); if (ledgerMa) return json(res, 200, { ledger: [] }); if (p === '/api/ledger') return json(res, 200, { ledger: [] }); if (p === '/api/article/search') { const q = (url.searchParams.get('target') ?? '').toLowerCase(); const all = Object.values(catalog.articlesByCategory ?? {}).flat(); const seen = new Map(); for (const a of all) seen.set(String(a.sku), a); const rows = [...seen.values()].filter( (a) => !q || String(a.sku).toLowerCase().includes(q) || String(a.name ?? '').toLowerCase().includes(q), ); return json(res, 200, { articles: rows.slice(0, 50), total: rows.length }); } return json(res, 404, { error: 'Not found' }); } // ── Mutations: echo fixtures so the UI never crashes mid-run ── const body = await readBody(req); if (method === 'POST' && p === '/api/article') { return json(res, 201, { article: { ...catalog.articleDetail, ...(body ?? {}) } }); } const skuPatch = p.match(/^\/api\/article\/([^/]+)$/); if (skuPatch && (method === 'PATCH' || method === 'PUT')) { return json(res, 200, { article: { ...catalog.articleDetail, ...(body ?? {}) } }); } if (method === 'DELETE' && p.match(/^\/api\/article\/[^/]+(\/image\/.+)?$/)) { res.writeHead(204); return res.end(); } if (p.match(/^\/api\/article\/[^/]+\/(prices|weight|brand|eans|categories|images\/order|supplier\/order|attributevalue|property|articlelist|optionslist|parent|children|print)$/) && ['PUT', 'PATCH', 'POST'].includes(method)) { if (p.endsWith('/supplier/order')) return json(res, 200, { ids: body?.ids ?? [] }); if (p.endsWith('/images/order')) return json(res, 200, { fullHashes: body?.fullHashes ?? [] }); if (p.endsWith('/articlelist')) return json(res, 200, { items: body?.items ?? [] }); if (p.endsWith('/optionslist')) return json(res, 200, { options: body?.options ?? [] }); if (p.endsWith('/attributevalue')) return json(res, 200, { attributevalues: [] }); if (p.endsWith('/property')) return json(res, 200, { properties: [] }); return json(res, 200, { article: { ...catalog.articleDetail, ...(body ?? {}) } }); } if (p.match(/^\/api\/article\/[^/]+\/supplier(\/\d+)?$/) && ['POST', 'PATCH'].includes(method)) { return json(res, method === 'POST' ? 201 : 200, { supplier: { id: 1, entityId: 1, ...(body ?? {}) }, }); } if (method === 'DELETE' && p.match(/^\/api\/article\/[^/]+\/(supplier|property|children)\/.+$/)) { return json(res, 200, { ok: true }); } if (p === '/api/category' && method === 'POST') { return json(res, 201, { category: { id: 999001, parentId: body?.parentId ?? 1, sort: 0, ...(body ?? {}) } }); } if (p.match(/^\/api\/category\/\d+$/) && ['PATCH', 'PUT'].includes(method)) { const id = Number(p.split('/').pop()); return json(res, 200, { category: { id, ...(body ?? {}) } }); } if (p === '/api/category/order' && method === 'PUT') { return json(res, 200, { categoryIds: body?.categoryIds ?? [] }); } if (method === 'DELETE' && p.match(/^\/api\/category\/\d+(\/image)?$/)) { return json(res, 200, { category: { id: Number(p.split('/')[3]) } }); } if (p === '/api/brand' && method === 'PUT') { return json(res, 200, { brand: { id: 999001, ...(body ?? {}) } }); } if (method === 'DELETE' && p.match(/^\/api\/brand\/[^/]+(\/image)?$/)) { return json(res, 200, { brand: { id: 999001 } }); } if (p === '/api/unit' && method === 'PUT') { return json(res, 200, { unit: { ...(body ?? {}) } }); } if (method === 'DELETE' && p.startsWith('/api/unit')) { res.writeHead(204); return res.end(); } if (p === '/api/supplier' && method === 'POST') { return json(res, 201, { supplier: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/supplier\/\d+$/) && method === 'PATCH') { return json(res, 200, { supplier: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (method === 'DELETE' && p.match(/^\/api\/(supplier|warehouse|printer|labeltemplate|customergroup|entity|delivery|quote|order|posterminal)\/[^/]+(\/(address|contact|category|all))?$/)) { if (p.includes('/posterminal/') && (p.endsWith('/all') || p.includes('/category/'))) { return json(res, 200, { id: Number(p.split('/')[3]), categoryIds: [] }); } res.writeHead(204); return res.end(); } if (p === '/api/warehouse' && method === 'POST') { return json(res, 201, { warehouse: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/warehouse\/\d+$/) && method === 'PATCH') { return json(res, 200, { warehouse: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (p === '/api/warehouse/order' && method === 'PUT') { return json(res, 200, { warehouseIds: body?.warehouseIds ?? [] }); } if (p === '/api/stock/adjust' && method === 'POST') { return json(res, 200, { ok: true }); } if (p === '/api/delivery' && method === 'POST') { return json(res, 201, { delivery: { ...(documents.deliveryDetail ?? {}), ...(body ?? {}) } }); } if (p.match(/^\/api\/delivery\/\d+$/) && method === 'PATCH') { return json(res, 200, { delivery: { ...(documents.deliveryDetail ?? {}), ...(body ?? {}) } }); } if (p === '/api/quote' && method === 'POST') { return json(res, 201, { quote: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/quote\/\d+$/) && method === 'PATCH') { return json(res, 200, { quote: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (p === '/api/order' && method === 'POST') { return json(res, 201, { order: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/order\/\d+$/) && method === 'PATCH') { return json(res, 200, { order: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (p === '/api/entity' && method === 'POST') { return json(res, 201, { entity: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/entity\/\d+$/) && method === 'PATCH') { return json(res, 200, { entity: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (p.match(/^\/api\/entity\/\d+\/(address|contact)$/) && method === 'POST') { const kind = p.endsWith('address') ? 'address' : 'contact'; return json(res, 201, { [kind]: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/entity\/\d+\/(address|contact)\/\d+$/) && method === 'PATCH') { const kind = p.includes('/address/') ? 'address' : 'contact'; return json(res, 200, { [kind]: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (p === '/api/customergroup' && method === 'POST') { return json(res, 201, { customergroup: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/customergroup\/\d+$/) && method === 'PATCH') { return json(res, 200, { customergroup: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (p === '/api/customergroup/order' && method === 'PUT') { return json(res, 200, { customergroupIds: body?.customergroupIds ?? [] }); } if (p === '/api/company' && method === 'POST') { return json(res, 201, { company: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/company\/\d+$/) && method === 'PUT') { return json(res, 200, { company: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (p.match(/^\/api\/company\/setting\/.+$/) && method === 'PUT') { return json(res, 200, { setting: { key: decodeURIComponent(p.split('/').pop()), ...(body ?? {}) } }); } if (p === '/api/company/import' && method === 'POST') { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); res.write('event: status\ndata: {"message": "Import gestartet. Daten werden verarbeitet..."}\n\n'); res.write('event: progress\ndata: {"phase": "categories", "current": 1, "total": 1, "item": "Katalog"}\n\n'); res.write('event: done\ndata: {"ok": true, "message": "Import erfolgreich abgeschlossen.", "stats": {"categories": 1, "categoriesCreated": 1, "categoryPictures": 0, "articlesTotal": 1, "articlesImported": 1, "articlesUpdated": 0, "variantsLinked": 0, "articlePictures": 1}}\n\n'); res.end(); return; } if (p.match(/^\/api\/number\/\d+$/) && method === 'PATCH') { return json(res, 200, { number: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (p === '/api/posterminal' && method === 'PUT') { return json(res, 200, { posterminal: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/posterminal\/\d+$/) && method === 'PATCH') { return json(res, 200, { posterminal: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (p.match(/^\/api\/posterminal\/\d+\/(all|category\/\d+)$/) && ['PUT', 'DELETE'].includes(method)) { return json(res, 200, { id: Number(p.split('/')[3]), categoryIds: [] }); } if (p === '/api/printer' && method === 'POST') { return json(res, 201, { printer: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/printer\/\d+$/) && method === 'PATCH') { return json(res, 200, { printer: { id: Number(p.split('/').pop()), ...(body ?? {}) } }); } if (p === '/api/printer/order' && method === 'PUT') { return json(res, 200, { printerIds: body?.printerIds ?? [] }); } if (p === '/api/labeltemplate' && method === 'POST') { return json(res, 201, { labeltemplate: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/labeltemplate\/.+$/) && method === 'PATCH') { return json(res, 200, { labeltemplate: { id: p.split('/').pop(), ...(body ?? {}) } }); } if (p === '/api/ledger' && method === 'POST') { return json(res, 201, { ledger: { id: 999001, ...(body ?? {}) } }); } if (p === '/api/attributename' && method === 'POST') { return json(res, 201, { attributename: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/attributename\/.+$/) && ['PATCH', 'PUT'].includes(method)) { return json(res, 200, { attributename: { id: p.split('/').pop(), ...(body ?? {}) } }); } if (method === 'DELETE' && p.match(/^\/api\/(attributename|attributevalue|propertyname)\/.+$/)) { return json(res, 200, { ok: true }); } if (p === '/api/propertyname' && method === 'POST') { return json(res, 201, { propertyname: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/propertyname\/.+$/) && ['PATCH', 'PUT'].includes(method)) { return json(res, 200, { propertyname: { id: p.split('/').pop(), ...(body ?? {}) } }); } if (p === '/api/modelconversation' && method === 'POST') { return json(res, 201, { conversation: { id: 999001, ...(body ?? {}) } }); } if (p.match(/^\/api\/modelconversation\/\d+\/message$/) && method === 'POST') { return json(res, 201, { message: { id: 999001, ...(body ?? {}) } }); } if (method === 'DELETE' && p.match(/^\/api\/modelconversation\/\d+$/)) { res.writeHead(204); return res.end(); } if (p === '/api/vfs/upload' && method === 'POST') { return json(res, 200, { files: [] }); } if (p === '/api/vfs/skills/restore' && method === 'POST') { return json(res, 200, { success: true, restored: ['system.SKILL'], count: 1 }); } if (p === '/api/vfs/applets/restore' && method === 'POST') { return json(res, 200, { success: true, restored: ['example.applet.js'], count: 1 }); } if (p.startsWith('/api/vfs/') && method === 'PUT') { return json(res, 200, { ok: true }); } if (method === 'DELETE' && p.startsWith('/api/vfs/')) { res.writeHead(204); return res.end(); } return json(res, 404, { error: 'Not found' }); }); server.listen(PORT, () => { console.log(`[dummy-backend] fixtures on http://localhost:${PORT} (${Object.keys(catalog.articlesByCategory ?? {}).length} categories)`); });