const fs = require('node:fs') const path = require('node:path') const assert = require('node:assert/strict') const { execFileSync } = require('node:child_process') const root = path.resolve(__dirname, '../..') const directory = path.join(root, 'acceptance') const tenant = 10059 const businessIDs = ['16c3e59c-4bb6-41cf-ae0a-6d0941b3d032', '20b9280e-ed73-4700-8fa5-944d6ae6dd5f', 'bb86b44a-576b-4bf8-a6bf-8f1baca3a845', 'badd084b-f229-4005-8fb3-196fbe139fe0', '5d28a7e5-e00e-44a7-a0e1-c72342cdcf68', '3ae9257d-928a-4886-8101-7cfd0ed8c27c'] const model = '404e005b-83ae-5e2e-a3b3-999f9d5cd47e' const origin = 'http://127.0.0.1:18080/api/v1' const statePath = path.join(directory, 'business-wiki-backfill.json') const literal = s => `'${String(s).replaceAll("'", "''")}'` const docker = (args, input) => execFileSync('docker', ['--context', 'colima-yawan', ...args], {input, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 8 * 1024 * 1024}) const sql = statement => docker(['exec', '-i', 'WeKnora-postgres', 'psql', '-U', 'postgres', '-d', 'yawan_native_acceptance_20260915', '-At', '-v', 'ON_ERROR_STOP=1'], statement).trim() let auth async function api(route, method = 'GET', body) { const response = await fetch(origin + route, {method, headers: {'Content-Type': 'application/json', ...(auth ? {Authorization: `Bearer ${auth.token}`, 'X-Tenant-ID': String(tenant)} : {})}, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(30000)}) const value = await response.json() assert.ok(response.ok && value.success !== false, `${method} ${route}: HTTP ${response.status}`) return value } function inventory() { return JSON.parse(sql(`SELECT coalesce(json_agg(row_to_json(t)),'[]'::json) FROM ( SELECT k.id,k.name,k.type,k.indexing_strategy,k.wiki_config,k.embedding_model_id, (SELECT count(*) FROM knowledges n WHERE n.knowledge_base_id=k.id AND n.deleted_at IS NULL) AS documents, (SELECT count(*) FROM chunks c WHERE c.knowledge_base_id=k.id AND c.deleted_at IS NULL) AS chunks, (SELECT count(*) FROM wiki_pages p WHERE p.knowledge_base_id=k.id AND p.deleted_at IS NULL) AS wiki_pages, (SELECT count(*) FROM task_pending_ops o WHERE o.scope_id=k.id AND o.task_type='wiki:ingest') AS pending_documents, (SELECT count(*) FROM task_pending_ops o WHERE o.scope_id=k.id AND o.task_type='wiki:finalize') AS pending_finalization, (SELECT count(*) FROM task_dead_letters d WHERE d.scope_id=k.id AND d.task_type='wiki:ingest') AS failed_documents FROM knowledge_bases k WHERE k.tenant_id=${tenant} AND k.id IN (${businessIDs.map(literal).join(',')}) AND k.deleted_at IS NULL AND k.is_temporary=false ORDER BY k.name ) t;`)) } async function main() { const credentials = JSON.parse(fs.readFileSync(path.join(directory, 'super-admin-login.json'), 'utf8')) auth = await api('/auth/login', 'POST', {username: credentials.username, password: credentials.password}) assert.equal(auth.user.is_system_admin, true) const before = inventory() assert.equal(before.length, businessIDs.length, 'All six business knowledge bases must exist') let state = fs.existsSync(statePath) ? JSON.parse(fs.readFileSync(statePath, 'utf8')) : {started_at: new Date().toISOString(), tenant_id: tenant, before} if (process.argv.includes('--apply')) { const modelRow = sql(`SELECT count(*) FROM models WHERE id=${literal(model)} AND tenant_id=${tenant} AND deleted_at IS NULL AND type='KnowledgeQA';`) assert.equal(modelRow, '1') fs.writeFileSync(path.join(directory, `business-wiki-config-before-${Date.now()}.json`), JSON.stringify(before, null, 2), {mode: 0o600, flag: 'wx'}) for (const row of before) { assert.equal(row.type, 'document', 'Only document knowledge bases are supported') assert.equal(row.indexing_strategy.vector_enabled, true, 'Existing vector index is required') assert.equal(row.indexing_strategy.keyword_enabled, true, 'Existing keyword index is required') const kb = (await api(`/knowledge-bases/${row.id}`)).data const updated = await api(`/knowledge-bases/${row.id}`, 'PUT', {name: kb.name, description: kb.description, config: { chunking_config: kb.chunking_config, image_processing_config: kb.image_processing_config, faq_config: kb.faq_config, auto_tag_config: kb.auto_tag_config, indexing_strategy: {...kb.indexing_strategy, wiki_enabled: true}, wiki_config: {...kb.wiki_config, synthesis_model_id: model, extraction_granularity: 'standard', max_pages_per_ingest: 0, ingest_batch_size: 1, ingest_map_parallel: 1, ingest_reduce_parallel: 1, ingest_max_inflight: 1}, }}) assert.equal(updated.data.indexing_strategy.wiki_enabled, true) assert.equal(updated.data.embedding_model_id, row.embedding_model_id) } const ids = before.map(k => literal(k.id)).join(',') // Backfill the Wiki queue without resetting document status or rebuilding RAG indexes. state.enqueued = Number(sql(`WITH added AS ( INSERT INTO task_pending_ops (tenant_id,task_type,scope,scope_id,op,dedup_key,payload,fail_count,enqueued_at) SELECT n.tenant_id,'wiki:ingest','knowledge_base',n.knowledge_base_id,'ingest',n.id, jsonb_build_object('op','ingest','knowledge_id',n.id,'language','zh-CN'),0,now() FROM knowledges n JOIN knowledge_bases k ON k.id=n.knowledge_base_id AND k.tenant_id=n.tenant_id WHERE n.tenant_id=${tenant} AND n.knowledge_base_id IN (${ids}) AND n.deleted_at IS NULL AND k.deleted_at IS NULL AND k.indexing_strategy->>'wiki_enabled'='true' AND n.parse_status='completed' AND n.pending_subtasks_count=0 AND EXISTS (SELECT 1 FROM chunks c WHERE c.knowledge_id=n.id AND c.deleted_at IS NULL) AND NOT EXISTS (SELECT 1 FROM task_pending_ops o WHERE o.tenant_id=n.tenant_id AND o.scope_id=n.knowledge_base_id AND o.task_type='wiki:ingest' AND o.dedup_key=n.id) AND NOT EXISTS (SELECT 1 FROM wiki_pages p WHERE p.knowledge_base_id=n.knowledge_base_id AND p.deleted_at IS NULL AND p.source_refs @> jsonb_build_array(n.id)) RETURNING id ) SELECT count(*) FROM added;`)) state.configured_at = new Date().toISOString() fs.writeFileSync(statePath, JSON.stringify(state, null, 2), {mode: 0o600}) const scopes = before.map(k => ({tenant_id: tenant, knowledge_base_id: k.id, language: 'zh-CN'})) docker(['cp', path.join(directory, 'wiki-backfill-trigger'), 'yawan-native-acceptance:/tmp/wiki-backfill-trigger']) state.trigger_result = JSON.parse(docker(['exec', '-i', 'yawan-native-acceptance', '/tmp/wiki-backfill-trigger'], JSON.stringify(scopes))) } state.checked_at = new Date().toISOString() state.current = inventory() for (const prior of state.before) { const current = state.current.find(k => k.id === prior.id) assert.equal(current.documents, prior.documents) assert.equal(current.chunks, prior.chunks) assert.equal(current.embedding_model_id, prior.embedding_model_id) } state.rag_preserved = true state.all_wiki_enabled = state.current.every(k => k.indexing_strategy.wiki_enabled && k.indexing_strategy.vector_enabled && k.indexing_strategy.keyword_enabled) if (process.argv.includes('--apply')) assert.equal(state.all_wiki_enabled, true) fs.writeFileSync(statePath, JSON.stringify(state, null, 2), {mode: 0o600}) console.log(JSON.stringify({checked_at: state.checked_at, all_wiki_enabled: state.all_wiki_enabled, rag_preserved: true, enqueued: state.enqueued, knowledge_bases: state.current.map(k => ({name: k.name,documents: k.documents,chunks: k.chunks,wiki_pages: k.wiki_pages,pending_documents:k.pending_documents,pending_finalization:k.pending_finalization,failed_documents:k.failed_documents}))})) } main().catch(error => {console.error(error.message); process.exitCode = 1})