Files
xinmi/scripts/verify_graph_model_thinking_fix.cjs
T

82 lines
4.9 KiB
JavaScript
Raw Normal View History

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
}