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 })