Files
xinmi/scripts/verify_current_generation.cjs
T
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

270 lines
19 KiB
JavaScript

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 directory = path.resolve(__dirname, '../../acceptance')
const reportPath = path.join(directory, 'current-generation-20260921.json')
const base = 'http://127.0.0.1:18080/api/v1'
const tenant = 10059
const jobLimitMS = 6 * 60 * 1000
const personaFields = 'persona_name identity tone pace vocab taboo opening_habit emotion_style interaction expertise_border sample_excerpt writing_style narrative_style thinking_style audience_profile hook_patterns pain_point_hooks itch_point_hooks sales_logic cta_transition expression_prompt expression_rules'.split(' ')
const sha256 = value => crypto.createHash('sha256').update(value).digest('hex')
const literal = value => "'" + String(value).replaceAll("'", "''") + "'"
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
function materials(start = 1, count = 99) {
return Array.from({ length: count }, (_, index) => {
const number = start + index
return { name: `style-${String(number).padStart(3, '0')}.txt`, text: `Writing sample ${number}: I explain purchase decision ${number} with a calm question, a concrete example, and a short recap.` }
})
}
function sourceSummary(sources) {
return sources.map(source => ({ name: source.name, sha256: sha256(source.text.trim()), runes: [...source.text.trim()].length }))
.sort((a, b) => a.name.localeCompare(b.name))
}
function versionWithSources(versions, number, expected) {
const version = versions.find(value => value.number === number)
assert.ok(version, 'The generated version exists')
assert.deepEqual(sourceSummary(version.sources || []), sourceSummary(expected), 'Every submitted source is preserved without truncation')
assert.ok(personaFields.every(field => typeof version.persona?.[field] === 'string'), 'All persona dimensions are saved')
return version
}
function cleanupBlocker(fixture, fixtures) {
if (!fixture.archived) return 'archive_missing'
if (fixture.uncertain) return 'request_outcome_uncertain'
if (fixture.jobs.some(job => !['succeeded', 'completed', 'failed', 'cancelled'].includes(job.status))) return 'job_not_terminal'
if (fixtures.some(other => other.depends_on.includes(fixture.id) && !other.deleted)) return 'dependent_fixture_retained'
return ''
}
async function main() {
if (process.env.RUN_CURRENT_GENERATION !== '1') {
console.log('Prepared only. RUN_CURRENT_GENERATION=1 is required for production fixture creation and model calls.')
return
}
const prefix = `current-generation-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`
const archivePath = path.join(directory, `${prefix}-private.json`)
const report = { started_at: new Date().toISOString(), prefix, checks: [], actions: [], fixtures: [], cleanup: [], archive_path: archivePath, passed: false }
const archive = { prefix, snapshots: [], submitted_materials: [], errors: [], usage: [] }
const fixtures = report.fixtures
let token, stage = 'preflight'
fs.mkdirSync(directory, { recursive: true })
// An existing report prevents accidental replays of paid production calls.
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), { flag: 'wx', mode: 0o600 })
const persist = () => fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), { mode: 0o600 })
const saveArchive = () => fs.writeFileSync(archivePath, JSON.stringify(archive, null, 2), { mode: 0o600 })
const check = (name, passed) => {
report.checks.push({ name, passed: !!passed })
persist()
assert.ok(passed, name)
}
const sql = statement => {
try {
return execFileSync('docker', ['--context', 'colima-yawan', 'exec', 'WeKnora-postgres', 'psql', '-U', 'postgres', '-d', 'yawan_native_acceptance_20260915', '-v', 'ON_ERROR_STOP=1', '-Atc', statement], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15000, maxBuffer: 16 * 1024 * 1024 }).trim()
} catch {
throw new Error('Read-only evidence query failed')
}
}
async function api(route, method = 'GET', body, expected = 200, timeout = 15000) {
const multipart = body instanceof FormData
let response
try {
response = await fetch(base + route, { method, headers: { ...(multipart ? {} : { 'Content-Type': 'application/json' }), ...(token ? { Authorization: `Bearer ${token}`, 'X-Tenant-ID': String(tenant) } : {}) }, body: body === undefined ? undefined : multipart ? body : JSON.stringify(body), signal: AbortSignal.timeout(timeout) })
} catch {
throw new Error(`${method} ${route}: request failed or timed out; no automatic retry`)
}
const text = await response.text()
let result
try { result = JSON.parse(text) } catch { result = null }
if (response.status !== expected || result === null) {
// Raw provider errors can contain private inputs. Keep them in the private archive only.
archive.errors.push({ stage, method, route, status: response.status, body: result || text })
throw new Error(`${method} ${route}: HTTP ${response.status}`)
}
return result
}
function register(kind, value, dependsOn = []) {
const fixture = { id: value.id, kind, name: value.name, depends_on: dependsOn, jobs: [], archived: false, deleted: false, uncertain: false }
fixtures.push(fixture)
archive.snapshots.push({ kind, id: value.id, at: new Date().toISOString(), data: value })
persist()
return fixture
}
async function create(kind, route, body, dependsOn = []) {
stage = `create_${kind}`
report.pending_mutation = { kind, name: body.name, route }
persist()
const value = (await api(route, 'POST', body, 201, 30000)).data
const fixture = register(kind, value, dependsOn)
delete report.pending_mutation
persist()
return { value, fixture }
}
async function poll(fixture, initial, route, success) {
const started = Date.now()
const action = { fixture_id: fixture.id, job_id: initial.id, action: stage, status: initial.status || 'queued', elapsed_ms: 0 }
fixture.jobs.push(action)
report.actions.push(action)
persist()
let lastProgress = ''
while (Date.now() - started < jobLimitMS) {
const remaining = jobLimitMS - (Date.now() - started)
const current = (await api(route, 'GET', undefined, 200, Math.min(15000, remaining))).data
action.status = current.status
action.elapsed_ms = Date.now() - started
action.progress = current.progress
archive.snapshots.push({ kind: 'job', id: initial.id, at: new Date().toISOString(), data: current })
const progress = `${current.status}:${current.stage}:${current.progress}`
if (lastProgress !== progress) {
console.log(JSON.stringify({ action: action.action, job_id: initial.id, status: current.status, stage: current.stage, progress: current.progress, elapsed_seconds: Math.round(action.elapsed_ms / 1000) }))
lastProgress = progress
}
persist()
if (current.status === success) return current
if (['failed', 'cancelled'].includes(current.status)) throw new Error(`${stage}: job ${current.status}; inspect private archive`)
await sleep(Math.min(1500, Math.max(0, jobLimitMS - (Date.now() - started))))
}
throw new Error(`${stage}: six-minute verification deadline; running fixtures retained`)
}
async function submit(fixture, route, body, jobRoute, success) {
fixture.uncertain = true
persist()
const job = (await api(route, 'POST', body, 202, 30000)).data
fixture.uncertain = false
return poll(fixture, job, jobRoute(job), success)
}
function humanVersions(id) {
return JSON.parse(sql(`SELECT coalesce(json_agg(json_build_object('id',id,'number',number,'status',status,'persona',persona,'sources',sources) ORDER BY number), '[]'::json) FROM digital_human_versions WHERE tenant_id=${tenant} AND digital_human_id=${literal(id)}`))
}
async function distill(fixture, human, sources, modelID) {
assert.ok(sources.length <= 99)
assert.ok(sources.reduce((total, source) => total + [...source.text].length, 0) < 12000, 'One complete material batch per distillation')
archive.submitted_materials.push({ human_id: human.id, sources })
saveArchive()
const form = new FormData()
form.append('model_id', modelID)
form.append('revision', String(human.revision))
form.append('advice', 'Analyze only the supplied neutral consumer-education writing samples. Keep all required fields concise, cite original evidence, and preserve previously supported traits when adding the new sample. State insufficient evidence explicitly. Do not invent biography or prices.')
for (const source of sources) form.append('files', new Blob([source.text], { type: 'text/plain' }), source.name)
const job = await submit(fixture, `/digital-humans/${human.id}/distill`, form, value => `/digital-humans/${human.id}/jobs/${value.id}`, 'completed')
const batches = JSON.parse(sql(`SELECT json_build_object('completed_batches',checkpoint->>'completed_batches','total_batches',checkpoint->>'total_batches') FROM digital_human_jobs WHERE tenant_id=${tenant} AND digital_human_id=${literal(human.id)} AND id=${literal(job.id)}`))
check(`${stage}_one_full_batch`, Number(batches.completed_batches) === 1 && Number(batches.total_batches) === 1)
return (await api(`/digital-humans/${human.id}`)).data
}
try {
const initialMaterials = materials()
check('99_unique_files_in_single_batch', initialMaterials.length === 99 && new Set(initialMaterials.map(source => sha256(source.text))).size === 99 && initialMaterials.reduce((sum, source) => sum + [...source.text].length, 0) < 12000)
check('read_only_database_access', sql('SELECT 1') === '1')
stage = 'login'
const credentials = JSON.parse(fs.readFileSync(path.join(directory, 'super-admin-login.json'), 'utf8'))
const auth = await api('/auth/login', 'POST', { email: credentials.email, password: credentials.password })
token = auth.token
check('authenticated', typeof token === 'string' && token.length > 0)
const models = (await api('/models')).data
const eligible = models.filter(model => model.type === 'KnowledgeQA' && model.status === 'active')
const model = process.env.CURRENT_GENERATION_MODEL_ID ? eligible.find(value => value.id === process.env.CURRENT_GENERATION_MODEL_ID) : eligible.find(value => value.name === 'deepseek-flash') || eligible[0]
check('active_chat_model_available', !!model)
report.model = { id: model.id, name: model.name }
const kb = (await api('/knowledge-bases')).data.find(value => value.name === '销售逻辑')
check('readable_sales_logic_kb_available', !!kb)
report.knowledge_base_id = kb.id
const agent = await create('agent', '/agents', { name: `${prefix}-writer`, config: { role: 'production', model_id: model.id, agent_mode: 'quick-answer', kb_selection_mode: 'selected', knowledge_bases: [kb.id], temperature: 0.2, max_completion_tokens: 1024, system_prompt: 'You teach ordinary consumer communication: clarify needs, set a budget, and make rational purchase decisions. Follow the current requested JSON schema exactly. Use three distinct consecutive short outline segments. Do not output a manuscript or adult sexual material.' } })
const flow = await create('flow', '/workbench/resources/flows', { name: `${prefix}-flow`, data: { nodes: [{ id: 'writer', label: '课程作者', agent_id: agent.value.id }], edges: [] } }, [agent.value.id])
const task = await create('task', '/workbench/resources/content-tasks', { name: `${prefix}-task`, data: { flow_id: flow.value.id, source_text: '理性消费沟通:第一部分,澄清购物需求与具体使用场景;第二部分,承接需求确定预算与比较条件;第三部分,用前两部分的条件完成购买决策并复述理由。', reading_mode: 'quick', duration_minutes: 5 } }, [flow.value.id])
stage = 'decompose'
const taskJob = await submit(task.fixture, `/workbench/resources/content-tasks/${task.value.id}/actions/decompose`, { version: task.value.version }, value => `/workbench/jobs/${value.id}`, 'succeeded')
const decomposed = (await api(`/workbench/resources/content-tasks/${task.value.id}`)).data
archive.snapshots.push({ kind: 'task', id: decomposed.id, data: decomposed })
const segments = decomposed.data.segments || []
check('decomposition_saved_segments', segments.length > 0)
check('all_segments_use_selected_agent_and_node', segments.every(segment => segment.agent_id === agent.value.id && segment.node_id === 'writer'))
check('quick_retrieval_returned_evidence', decomposed.data.reading?.mode === 'quick' && decomposed.data.reading?.evidence_chunks > 0)
const categories = new Set((decomposed.data.agent_category_options?.[agent.value.id] || []).map(category => category.id))
check('category_recommendations_within_selected_agent', segments.every(segment => !segment.recommended_category_id || categories.has(segment.recommended_category_id)))
archive.usage = JSON.parse(sql(`SELECT coalesce(json_agg(json_build_object('id',id,'job_id',job_id,'status',status,'tokens',input_tokens+output_tokens,'points',points,'metadata',metadata)), '[]'::json) FROM workbench_usage WHERE tenant_id=${tenant} AND job_id=${literal(taskJob.id)}`))
check('one_metered_decomposition_call', archive.usage.length === 1 && archive.usage[0].status === 'succeeded')
report.decomposition = { segments: segments.length, job_id: taskJob.id, reading: decomposed.data.reading, metered_calls: archive.usage.length }
const created = await create('human', '/digital-humans', { name: `${prefix}-human`, description: 'Disposable verification of 99 complete sources and one incremental source.', model_id: model.id })
const primaryAgentID = created.value.primary_agent_id
check('human_primary_agent_created', !!primaryAgentID)
created.fixture.primary_agent_id = primaryAgentID
stage = 'distill_99_files'
let human = await distill(created.fixture, created.value, initialMaterials, model.id)
const firstVersions = humanVersions(human.id)
const first = versionWithSources(firstVersions, human.latest_version, initialMaterials)
const firstHash = sha256(JSON.stringify(first))
check('first_distillation_99_sources_saved', first.sources.length === 99)
check('first_version_remains_unpublished', human.active_version === 0 && first.status === 'draft')
archive.snapshots.push({ kind: 'human_after_99', id: human.id, data: human, versions: firstVersions })
saveArchive()
stage = 'distill_one_additional_file'
const added = materials(100, 1)
human = await distill(created.fixture, human, added, model.id)
const nextVersions = humanVersions(human.id)
const latest = versionWithSources(nextVersions, human.latest_version, [...initialMaterials, ...added])
check('continuous_distillation_preserves_previous_version', sha256(JSON.stringify(nextVersions.find(value => value.number === first.number))) === firstHash)
check('continuous_distillation_saves_100_sources', latest.number === first.number + 1 && latest.sources.length === 100)
check('new_version_remains_unpublished', human.active_version === 0 && latest.status === 'draft')
archive.snapshots.push({ kind: 'human_after_100', id: human.id, data: human, versions: nextVersions })
report.distillation = { human_id: human.id, versions: nextVersions.map(version => ({ number: version.number, source_count: (version.sources || []).length, status: version.status })), source_hashes: sourceSummary(latest.sources), successful_jobs: created.fixture.jobs.length }
saveArchive()
report.passed = true
} catch (error) {
report.failure = { stage, name: error.name, message: error.message }
archive.errors.push({ stage, name: error.name, message: error.message })
process.exitCode = 1
} finally {
stage = 'archive_and_cleanup'
// Archive full fixture state before any delete. A failed archive preserves that fixture.
for (const fixture of fixtures) {
const route = fixture.kind === 'agent' ? `/agents/${fixture.id}` : fixture.kind === 'human' ? `/digital-humans/${fixture.id}` : `/workbench/resources/${fixture.kind === 'task' ? 'content-tasks' : 'flows'}/${fixture.id}`
fixture.route = route
try {
const detail = (await api(route)).data
const versions = fixture.kind === 'human' ? humanVersions(fixture.id) : undefined
if (fixture.kind === 'human' && detail.processing_job_id) fixture.uncertain = true
archive.snapshots.push({ kind: fixture.kind, id: fixture.id, at: new Date().toISOString(), data: detail, versions })
saveArchive()
fixture.archived = true
} catch {
report.cleanup.push({ id: fixture.id, status: 'retained', reason: 'archive_failed' })
}
persist()
}
try { saveArchive() } catch { report.archive_failed = true }
for (const fixture of [...fixtures].reverse()) {
const blocked = report.archive_failed ? 'archive_failed' : cleanupBlocker(fixture, fixtures)
if (blocked) {
report.cleanup.push({ id: fixture.id, status: 'retained', reason: blocked })
continue
}
try {
await api(fixture.route, 'DELETE')
fixture.deleted = true
report.cleanup.push({ id: fixture.id, status: 'deleted' })
if (fixture.kind === 'human' && fixture.primary_agent_id) {
const remaining = Number(sql(`SELECT count(*) FROM custom_agents WHERE tenant_id=${tenant} AND id=${literal(fixture.primary_agent_id)} AND deleted_at IS NULL`))
check('human_primary_agent_removed_with_human', remaining === 0)
}
} catch {
report.cleanup.push({ id: fixture.id, status: 'retained_or_unverified', reason: 'delete_request_failed' })
}
persist()
}
report.remaining = fixtures.filter(fixture => !fixture.deleted).map(fixture => ({ id: fixture.id, kind: fixture.kind, jobs: fixture.jobs }))
report.finished_at = new Date().toISOString()
report.passed = report.passed && !report.archive_failed && !report.pending_mutation && report.remaining.length === 0 && report.checks.every(value => value.passed) && !report.cleanup.some(value => value.status === 'retained_or_unverified')
if (!report.passed) process.exitCode = 1
persist()
console.log(JSON.stringify({ passed: report.passed, checks_passed: report.checks.filter(value => value.passed).length, checks_total: report.checks.length, remaining_fixture_count: report.remaining.length, report_path: reportPath }))
}
}
module.exports = { materials, sourceSummary, versionWithSources, cleanupBlocker }
if (require.main === module) main().catch(error => { console.error(JSON.stringify({ passed: false, error: error.code === 'EEXIST' ? 'Existing verification report prevents replay' : 'Verification setup or finalization failed; inspect private evidence' })); process.exitCode = 1 })