const assert = require('node:assert/strict') const crypto = require('node:crypto') const fs = require('node:fs') const path = require('node:path') const { execFileSync } = require('node:child_process') const root = path.resolve(__dirname, '../..') const modelID = '404e005b-83ae-5e2e-a3b3-999f9d5cd47e' const receiptPath = path.join(root, 'acceptance/graph-model-thinking-fix-1789827335493.json') const docker = args => execFileSync('docker', ['--context', 'colima-yawan', ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 8 * 1024 * 1024, timeout: 30000 }) function query(sql) { return JSON.parse(execFileSync('docker', ['--context', 'colima-yawan', 'exec', '-i', 'WeKnora-postgres', 'psql', '-X', '-qAt', '-v', 'ON_ERROR_STOP=1', '-U', 'postgres', '-d', 'yawan_native_acceptance_20260915'], { input: `BEGIN READ ONLY;\n${sql}\nCOMMIT;`, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 15000 })) } const literal = value => "'" + JSON.stringify(value).replaceAll("'", "''") + "'::jsonb" function decodeCopyField(value) { if (value === '\\N') return null const escapes = { b: '\b', f: '\f', n: '\n', r: '\r', t: '\t', v: '\v', '\\': '\\' } return value.replace(/\\([bfnrtv\\])/g, (_, key) => escapes[key]) } function decrypt(value, key) { if (!value || !value.startsWith('enc:v1:')) return value || '' const data = Buffer.from(value.slice(7), 'base64url') const decipher = crypto.createDecipheriv('aes-256-gcm', key, data.subarray(0, 12)) decipher.setAuthTag(data.subarray(-16)) return Buffer.concat([decipher.update(data.subarray(12, -16)), decipher.final()]).toString('utf8') } let stage = 'read_backup' try { const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8')) const dump = docker(['run', '--rm', '--entrypoint', 'pg_restore', '-v', `${root}/releases/public-20260919-214700:/backup:ro`, 'paradedb/paradedb:v0.22.2-pg17', '--data-only', '--table=models', '--file=-', '/backup/yawan_native_acceptance_20260915.dump']) stage = 'parse_backup' const copy = /COPY public\.models \(([^)]+)\) FROM stdin;\n([\s\S]*?)\n\\\./.exec(dump) assert.ok(copy) const columns = copy[1].split(', ') const rows = copy[2].split('\n').map(line => line.split('\t').map(decodeCopyField)) const fields = rows.find(row => row[columns.indexOf('id')] === modelID) assert.equal(fields?.length, columns.length) const oldRow = Object.fromEntries(columns.map((name, index) => [name, fields[index]])) oldRow.parameters = JSON.parse(oldRow.parameters) stage = 'compare_database' const audit = query(`WITH previous AS (SELECT (jsonb_populate_record(null::models, ${literal(oldRow)})).*), current AS (SELECT * FROM models WHERE id='${modelID}' AND tenant_id=10059) SELECT json_build_object( 'backup_fingerprint',encode(sha256(convert_to((to_jsonb(p) - 'updated_at' - 'parameters' || jsonb_build_object('parameters',p.parameters #- '{extra_config,thinking_control}'))::text,'UTF8')),'hex'), 'metadata_preserved',(to_jsonb(p)-'parameters'-'updated_at')=(to_jsonb(c)-'parameters'-'updated_at'), 'parameters_preserved', (p.parameters-'api_key'-'app_secret' #- '{extra_config,thinking_control}')= (c.parameters-'api_key'-'app_secret' #- '{extra_config,thinking_control}'), 'parameters',c.parameters) FROM previous p CROSS JOIN current c;`) stage = 'compare_snapshot' assert.equal(audit.backup_fingerprint, receipt.before.unchanged_fields_sha256) stage = 'compare_metadata' assert.equal(audit.metadata_preserved, true) assert.equal(audit.parameters_preserved, true) assert.equal(oldRow.parameters.extra_config.thinking_control, 'none') assert.equal(audit.parameters.extra_config.thinking_control, 'thinking_type') stage = 'compare_credentials' const environment = JSON.parse(docker(['inspect', 'yawan-native-acceptance', '--format', '{{json .Config.Env}}'])) const aes = Buffer.from(environment.find(value => value.startsWith('SYSTEM_AES_KEY=')).slice('SYSTEM_AES_KEY='.length)) assert.equal(aes.length, 32) for (const name of ['api_key', 'app_secret']) { assert.ok(decrypt(oldRow.parameters[name], aes) === decrypt(audit.parameters[name], aes)) } const verification = { checked_at: new Date().toISOString(), passed: true, model_calls: 0, production_write: false, backup_matches_immediate_before_snapshot: true, unrelated_fields_preserved: true, credential_plaintexts_preserved: true, ciphertext_changed_due_to_aes_gcm_nonce: oldRow.parameters.api_key !== audit.parameters.api_key, effective_change: { thinking_control: { before: 'none', after: 'thinking_type' } } } stage = 'write_report' fs.writeFileSync(path.join(root, 'acceptance/graph-model-thinking-fix-verification-20260919.json'), JSON.stringify(verification, null, 2) + '\n', { flag: 'wx', mode: 0o600 }) console.log(JSON.stringify(verification)) } catch { console.error(`Read-only model configuration comparison failed at ${stage}; no secrets or raw database output are displayed.`) process.exitCode = 1 }