import assert from 'node:assert/strict'; import http from 'node:http'; import test from 'node:test'; import { SYNTHETIC_ROWS, compareCase, deriveCases, runCheck, emitNumberVectors, emitScreenVectors, equals, matchesSearch, screenFacets, screenFound, screenRows, } from './svod2_search_oracle.mjs'; const mk = (id, type, dir, org) => ({ data: { line_id: id, header: { smeta_type: type, smeta_direction: dir, org_name: org } } }); const rows2 = [mk(1, 'КВ', 'Развитие', 'А'), mk(2, 'АХР', 'Поддержка', 'Б')]; const rows3 = [mk(1, 'КВ', 'Р', 'Филиал Ёлки'), mk(2, 'КВ', 'Р', 'Филиал Ёлки'), mk(3, 'КВ', 'Р', null)]; test('перекрытое поле contract_summary не ищется', () => { const row = { data: { header: { name: 'x' }, contract_summary: { counterparty: 'ООО Ромашка' }, contract_detail: { counterparty: null } } }; assert.equal(matchesSearch(row, 'ромаш'), false); }); test('число ищется в записи JS', () => { const row = { data: { header: {}, contract_summary: {}, contract_detail: JSON.parse('{"ceiling_amount": 1234.50}') } }; assert.equal(matchesSearch(row, '1234.5'), true); assert.equal(matchesSearch(row, '1234.50'), false); }); test('screenRows: trim + lower, NBSP по краям', () => { const rows = [{ data: { line_id: 1, header: { name: 'Аренда помещения' } } }]; assert.deepEqual(screenRows(rows, { search: ' АРЕНДА ' }).map((r) => r.data.line_id), [1]); assert.deepEqual(screenRows(rows, { search: ' аренда ' }).map((r) => r.data.line_id), [1]); }); test('equals: регистр и пробелы по краям, null не проходит', () => { const r = (v) => ({ getValue: () => v }); assert.equal(equals(r('КВ'), 'smeta_type', 'кв '), true); assert.equal(equals(r(null), 'smeta_type', 'кв'), false); }); test('screenFacets: свой фильтр не применяется, чужие — да', () => { const f = screenFacets(rows2, { filters: { smeta_type: 'КВ' } }); assert.deepEqual(f.smeta_type, [['АХР', 1], ['КВ', 1]]); assert.deepEqual(f.smeta_direction, [['Развитие', 1]]); }); test('screenFacets: одинаковые названия — один вариант, null не выдаётся', () => { assert.deepEqual(screenFacets(rows3).org_name, [['Филиал Ёлки', 2]]); }); test('screenFound считает только поиск', () => { assert.deepEqual(screenFound(rows2, ''), { found: 2, total: 2 }); }); test('векторы чисел', () => { const v = Object.fromEntries(emitNumberVectors().map(({ input, js }) => [input, js])); assert.equal(v['1e21'], '1e+21'); assert.equal(v['-0'], '0'); assert.equal(v['1e-7'], '1e-7'); assert.equal(v['0.000001'], '0.000001'); assert.equal(v['100.00'], '100'); assert.equal(v['9007199254740993'], '9007199254740992'); }); test('векторы экрана: не меньше 12 случаев', () => { const s = emitScreenVectors(); assert.ok(s.cases.length >= 12); assert.ok(s.rows.length >= 10 && s.rows.length <= 15); }); test('deriveCases: не меньше 8 случаев и есть дробное число', () => { const cases = deriveCases(SYNTHETIC_ROWS); assert.ok(cases.length >= 8); for (const required of ['аренда', 'АРЕНДА', '1', ',']) { assert.ok(cases.some((c) => c.search === required && Object.keys(c.filters).length === 0), required); } assert.ok(cases.some((c) => /\d\.\d|\de-\d/.test(c.search))); }); const expectedFor = (rows, { search, filters }) => ({ line_ids: screenRows(rows, { search, filters }).map((r) => r.data.line_id), total: screenRows(rows, { search, filters }).length, found: screenFound(rows, search).found, facets: screenFacets(rows, { search, filters }), }); test('compareCase: совпадение даёт пустой массив, различия находятся', () => { const base = expectedFor(SYNTHETIC_ROWS, { search: 'ё', filters: {} }); assert.deepEqual(compareCase(base, structuredClone(base)), []); const swapped = structuredClone(base); swapped.line_ids.reverse(); assert.ok(compareCase(base, swapped).length > 0); const counted = structuredClone(base); counted.facets.smeta_type[0][1] += 1; assert.ok(compareCase(base, counted).length > 0); }); const startStub = async ({ corrupt = false } = {}) => { const server = http.createServer((req, res) => { const url = new URL(req.url, 'http://x'); const send = (body, status = 200) => { res.writeHead(status, { 'content-type': 'application/json' }); res.end(JSON.stringify(body)); }; if (url.pathname === '/api/v1/auth/login') return send({ access_token: 't' }); const m = url.pathname.match(/^\/api\/v1\/svod\/2\/(GO|RF)\/(\d+)(\/facets)?$/); if (!m) return send({}, 404); const q = url.searchParams; const opts = { search: q.get('search') ?? '', filters: Object.fromEntries(['smeta_type', 'smeta_direction', 'org_name'].filter((k) => q.get(k)).map((k) => [k, q.get(k)])), }; const hasNew = ['view', 'search', 'smeta_type', 'smeta_direction', 'org_name'].some((k) => q.has(k)); if (!hasNew) return send({ result: SYNTHETIC_ROWS.map((r) => ({ row_type: 'row', depth: 0, sort_order: null, data: r.data })) }); if (m[3]) { const f = screenFacets(SYNTHETIC_ROWS, opts); const out = { total: f.total, search_total: f.search_total }; for (const id of ['smeta_type', 'smeta_direction', 'org_name']) out[id] = f[id].map(([value, count]) => ({ value, count })); return send({ result: out }); } const all = screenRows(SYNTHETIC_ROWS, opts); const offset = Number(q.get('offset') ?? 0); const limit = Number(q.get('limit') ?? all.length); let page = all.slice(offset, offset + limit); if (corrupt && offset === 0) page = page.slice().reverse(); return send({ count: page.length, total: all.length, result: page.map((r) => ({ row_type: 'row', depth: 0, sort_order: null, data: r.data })) }); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); return { server, api: `http://127.0.0.1:${server.address().port}` }; }; test('runCheck на заглушке API: ok true', async () => { const { server, api } = await startStub(); try { const r = await runCheck({ api, username: 'u', password: 'p', year: 2026, pageSize: 3 }); assert.deepEqual(r.report, []); assert.equal(r.ok, true); } finally { server.close(); } }); test('runCheck: испорченная страница даёт ok false', async () => { const { server, api } = await startStub({ corrupt: true }); try { const r = await runCheck({ api, username: 'u', password: 'p', year: 2026, pageSize: 3 }); assert.equal(r.ok, false); assert.ok(r.report.length > 0); } finally { server.close(); } });