import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { compileFunction } from 'node:vm'; import { createRequire } from 'node:module'; import { renderToStaticMarkup } from 'react-dom/server'; import ts from 'typescript'; const require = createRequire(import.meta.url); function load(file, mocks = {}) { const code = ts.transpileModule( fs.readFileSync(new URL(file, import.meta.url), 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX, }, }, ).outputText; const module = { exports: {} }; compileFunction(code, ['require', 'module', 'exports'])( (key) => mocks[key] ?? require(key), module, module.exports, ); return module.exports; } const lib = load('../../resources/js/lib/product-experiments.ts'); const usage = load('../../resources/js/lib/product-usage.ts'); const fields = [ 'title', 'state', 'change_kind', 'reference', 'hypothesis', 'indicator_id', 'indicator_title', 'indicator_definition', 'indicator_unit', 'period_from', 'period_to', 'segment', 'control', 'variant', 'assignment', 'criterion', 'transparency', 'consent', 'security', 'equality', 'preserves_requirements', 'observed_from', 'observed_to', 'control_result', 'variant_result', 'result', 'limitations', 'evidence', 'decision', 'decision_reason', 'report_service', ]; const values = { ...Object.fromEntries(fields.map((k) => [k, ''])), title: 'SYNTHETIC experiment', state: 'planned', change_kind: 'form', hypothesis: 'SYNTHETIC base hypothesis', segment: 'SYNTHETIC base segment', indicator_id: 'submission_rate', indicator_title: 'SYNTHETIC inactive custom title', indicator_definition: 'SYNTHETIC inactive definition', indicator_unit: '%', period_from: '2026-11-02', period_to: '2026-11-04', preserves_requirements: 'yes', }; const indicator = { id: 'submission_rate', title: 'Submission rate', unit: '%', source: 'SYNTHETIC frozen definition', limits: 'SYNTHETIC limits', definition_version: 'SYNTHETIC-v1', }; const document = { definition_version: 'SYNTHETIC registry', version: 1, at: '2026-11-03T12:00:00Z', actor: { id: 1, name: 'SYNTHETIC actor' }, reason: 'SYNTHETIC reason', values, indicator, plan_sha256: 'a'.repeat(64), observation_plan_sha256: 'a'.repeat(64), registration: { first_at: '2026-11-03T12:00:00Z', original_plan_sha256: 'a'.repeat(64), amendments: 0, amended_after_observation: false, }, operational_report: null, report_matches_observation: null, report_scope: 'operational_cohort_not_experiment_arm', }; const props = { installed: true, id: 'synthetic-id', currentVersion: 1, historical: false, document, values, states: { draft: 'Draft', planned: 'Plan registered', observing: 'Observing', reviewed: 'Decision recorded', cancelled: 'Cancelled', }, changeKinds: { form: 'Form' }, decisions: { inconclusive: 'Inconclusive' }, labels: Object.fromEntries(fields.map((k) => [k, 'Label ' + k])), catalogue: { definition_version: 'SYNTHETIC-v2', items: [{ ...indicator, source: 'SYNTHETIC current definition' }], services: [], }, history: [ { version: 1, at: document.at, actor: 'SYNTHETIC actor', state: 'planned', reason: 'SYNTHETIC reason', }, ], historyNext: null, }; class ValidationError extends Error { constructor(message, fields = {}) { super(message); this.fields = fields; } } function harness( initial = props, respond = async () => ({ id: 'synthetic-id' }), ) { const slots = []; let cursor = 0, key = 0; const calls = [], visits = []; const page = load( '../../resources/js/pages/management/experiments/edit.tsx', { react: { ...require('react'), useState: (value) => { const index = cursor++; if (!(index in slots)) slots[index] = typeof value === 'function' ? value() : value; return [ slots[index], (v) => { slots[index] = typeof v === 'function' ? v(slots[index]) : v; }, ]; }, }, '@inertiajs/react': { Link: 'a', router: { visit: (...args) => visits.push(args) }, }, '@/pages/investment/shared': { Frame: 'main', card: '', input: '', button: '', }, '@/lib/profile-json': { ProfileValidationError: ValidationError, profileJson: async (...args) => { calls.push(args); return respond(...args); }, }, '@/lib/operation-key': { createOperationKey: () => `SYNTHETIC-operation-${++key}`, }, '@/lib/product-experiments': lib, '@/lib/product-usage': usage, }, ); return { calls, visits, render() { cursor = 0; const outer = page.default(initial); return outer.type(outer.props); }, }; } function walk(node, predicate) { if (Array.isArray(node)) return node.flatMap((n) => walk(n, predicate)); if (!node || typeof node !== 'object' || !node.props) return []; return [ ...(predicate(node) ? [node] : []), ...walk(node.props.children, predicate), ]; } function content(node) { if (Array.isArray(node)) return node.map(content).join(''); if (node === null || node === undefined || typeof node === 'boolean') return ''; if (typeof node === 'object') return content(node.props?.children); return String(node); } function button(tree, text) { const node = walk( tree, (n) => n.type === 'button' && content(n).includes(text), )[0]; assert.ok(node, 'button ' + text); return node; } function inputValue(tree, value) { const node = walk( tree, (n) => ['input', 'textarea'].includes(n.type) && n.props.value === value, )[0]; assert.ok(node, 'input ' + value); return node; } function check(tree, text) { const label = walk( tree, (n) => n.type === 'label' && content(n).includes(text), )[0]; assert.ok(label, 'checkbox ' + text); return walk( label, (n) => n.type === 'input' && n.props.type === 'checkbox', )[0]; } test('three-way comparison reports actual conflicts and merges untouched fields without losing deliberate edits', () => { const base = { hypothesis: 'base', segment: 'old', title: 'same' }; const mine = { ...base, hypothesis: 'mine' }; const current = { ...base, hypothesis: 'theirs', segment: 'new' }; assert.deepEqual(lib.experimentDifferences(base, mine, current), [ 'hypothesis', ]); assert.deepEqual( lib.experimentDifferences(base, base, current, ['hypothesis']), ['hypothesis'], ); assert.equal( lib.mergeExperimentValues(base, base, current, ['hypothesis']) .hypothesis, 'base', ); assert.deepEqual(lib.mergeExperimentValues(base, mine, current), { hypothesis: 'mine', segment: 'new', title: 'same', }); assert.deepEqual(base, { hypothesis: 'base', segment: 'old', title: 'same', }); }); test('stored indicator edition remains visible, explicit refresh can be reversed, and inactive custom fields are hidden', () => { const h = harness(); let tree = h.render(), html = renderToStaticMarkup(tree); assert.match(html, /SYNTHETIC frozen definition/); assert.doesNotMatch( html, /SYNTHETIC current definition|SYNTHETIC inactive definition/, ); check(tree, 'Adoptar explicitamente').props.onChange({ target: { checked: true }, }); tree = h.render(); html = renderToStaticMarkup(tree); assert.match(html, /SYNTHETIC current definition/); check(tree, 'Adoptar explicitamente').props.onChange({ target: { checked: false }, }); assert.match( renderToStaticMarkup(h.render()), /SYNTHETIC frozen definition/, ); const manual = harness({ ...props, values: { ...values, indicator_id: 'manual' }, document: { ...document, indicator: { ...indicator, id: 'manual' } }, }); assert.doesNotMatch( renderToStaticMarkup(manual.render()), /Adoptar explicitamente/, ); }); test('conflict recovery preserves the draft, compares changed fields, and saves against the reviewed current version', async () => { const current = { ...props, currentVersion: 2, values: { ...values, hypothesis: 'SYNTHETIC their hypothesis', segment: 'SYNTHETIC their segment', }, document: { ...document, version: 2 }, }; let writes = 0; const h = harness(props, async (url, data, method) => { if (method === 'GET') return current; if (++writes === 1) throw new ValidationError('SYNTHETIC conflict'); return { id: 'synthetic-id', version: 3 }; }); inputValue(h.render(), values.hypothesis).props.onChange({ target: { value: 'SYNTHETIC my deliberate hypothesis' }, }); walk( h.render(), (n) => n.type === 'textarea' && n.props.maxLength === 3000, )[0].props.onChange({ target: { value: 'SYNTHETIC reviewed conflict' } }); walk(h.render(), (n) => n.type === 'form')[0].props.onSubmit({ preventDefault() {}, }); await new Promise(setImmediate); assert.match( renderToStaticMarkup(h.render()), /SYNTHETIC my deliberate hypothesis/, ); inputValue(h.render(), 'SYNTHETIC my deliberate hypothesis').props.onChange( { target: { value: values.hypothesis } }, ); await button(h.render(), 'Consultar versão actual').props.onClick(); const beforeReview = h.calls.length; walk(h.render(), (n) => n.type === 'form')[0].props.onSubmit({ preventDefault() {}, }); await new Promise(setImmediate); assert.equal( h.calls.length, beforeReview, 'Do not submit before the current version has been reviewed', ); assert.match( renderToStaticMarkup(h.render()), /SYNTHETIC their hypothesis/, ); button(h.render(), 'Manter a minha edição').props.onClick(); button(h.render(), 'Usar esta versão como base').props.onClick(); let tree = h.render(); assert.equal( inputValue(tree, values.hypothesis).props.value, values.hypothesis, ); assert.equal( inputValue(tree, 'SYNTHETIC their segment').props.value, 'SYNTHETIC their segment', ); walk(tree, (n) => n.type === 'form')[0].props.onSubmit({ preventDefault() {}, }); await new Promise(setImmediate); const requests = h.calls.filter((c) => c[2] !== 'GET'); assert.equal(requests[1][1].version, 2); assert.notEqual(requests[0][1].operation_key, requests[1][1].operation_key); assert.equal(requests[1][1].values.hypothesis, values.hypothesis); assert.equal(requests[1][1].values.segment, 'SYNTHETIC their segment'); assert.equal( requests[1][1].values.indicator_definition, 'SYNTHETIC inactive definition', ); assert.ok(h.calls.every((c) => !c[0].includes('hypothesis'))); assert.equal(h.visits.length, 1); }); test('historical versions are read only and an unavailable registry is distinct from an empty one', async () => { const h = harness({ ...props, historical: true, currentVersion: 3 }); const tree = h.render(), html = renderToStaticMarkup(tree); assert.match(html, /Consulta da versão 1/); assert.match(html, /SYNTHETIC frozen definition/); assert.doesNotMatch( html, /Guardar versão do registo|Consultar versão actual para comparar/, ); assert.equal( walk(tree, (n) => n.type === 'fieldset' && !n.props.disabled).length, 0, ); walk(tree, (n) => n.type === 'form')[0].props.onSubmit({ preventDefault() {}, }); await new Promise(setImmediate); assert.equal( h.calls.length, 0, 'Historical consultation cannot submit even if a submit event is dispatched', ); const index = load( '../../resources/js/pages/management/experiments/index.tsx', { '@inertiajs/react': { Link: 'a' }, '@/pages/investment/shared': { Frame: 'main', card: '', input: '', button: '', }, }, ).default; const missing = renderToStaticMarkup( index({ installed: false, states: {}, filters: { state: '' }, items: [], page: 1, pages: 1, total: 0, }), ); assert.match(missing, /ainda não está disponível/); assert.doesNotMatch( missing, /0 registos|Preparar experiência|Ainda não existem registos/, ); }); test('an attached operational report is labelled as context and retains unknown values without inventing arm results', () => { const report = { filters: { from: '2026-11-02', to: '2026-11-02', service_code: null }, generated_at: '2026-11-03T12:00:00Z', metrics: [ { id: 'rate', title: 'SYNTHETIC rate', value: null, unit: '%', state: 'unmeasured', }, ], }; const h = harness({ ...props, values: { ...values, state: 'reviewed', result: 'SYNTHETIC declared result', }, document: { ...document, operational_report: report, report_matches_observation: false, }, }); const html = renderToStaticMarkup(h.render()); assert.match( html, /Não separa grupos da experiência nem comprova causalidade/, ); assert.match(html, /filtros deste relatório diferem/); assert.match(html, /Sem medição/); assert.doesNotMatch(html, />0 %