feat: 知识治理平台(含已验证发布/回滚契约)

全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
This commit is contained in:
2026-09-13 12:02:58 +08:00
committed by 姚章浩
parent ed48c4ceda
commit dcdc8d1ddd
410 changed files with 56661 additions and 1 deletions
+141
View File
@@ -0,0 +1,141 @@
"""Live browser acceptance for document workbench V2.
Writes one PP category rule and one file override through the UI, verifies each
read-back, then clears both. It never uploads, retries, reviews or publishes.
"""
import json
from pathlib import Path
import time
import urllib.request
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.support.ui import WebDriverWait, Select
BASE='http://127.0.0.1:8088'
ROOT=Path(__file__).resolve().parents[1]
def request_json(path,method='GET',body=None):
data=None if body is None else json.dumps(body,ensure_ascii=False).encode()
request=urllib.request.Request(BASE+path,data=data,method=method,headers={'Accept':'application/json','Content-Type':'application/json'})
with urllib.request.urlopen(request,timeout=10) as response:return json.load(response)
def catalog():
items=[]
while True:
page=request_json(f'/api/v1/source-items?limit=100&offset={len(items)}')
items.extend(page['data'])
if len(items)>=page['summary']['total']:break
assert page['data']
assert len(items)==len({item['id'] for item in items})==page['summary']['total']
return items
def rules(connection):return request_json('/api/v1/scope-rules?connection_id='+connection)['data']
def eventually(check,seconds=25):
deadline=time.monotonic()+seconds
while time.monotonic()<deadline:
value=check()
if value:return value
time.sleep(.2)
raise AssertionError('condition did not become true')
before=catalog()
pp=[item for item in before if item['source_type']=='pp']
manual=[item for item in before if item['source_type']=='manual']
category=[item for item in pp if item['directory_path']=='产品介绍']
assert len(before)==618 and len(pp)==613 and len(manual)==5 and len(category)==5
connection=category[0]['connection_id']
assert not rules(connection),'acceptance requires no pre-existing PP scope rule'
selected_id=category[0]['id']
options=Options();options.add_argument('--headless');options.binary_location='/snap/firefox/current/usr/lib/firefox/firefox'
driver=webdriver.Firefox(options=options,service=Service('/snap/bin/geckodriver'))
driver.set_window_size(1440,1100)
wait=WebDriverWait(driver,30,poll_frequency=.1)
write_actions=0
def body():return driver.find_element(By.TAG_NAME,'body').text
def root_fits():assert driver.execute_script('return document.documentElement.scrollWidth <= window.innerWidth'),'root overflow'
def fill_editor(editor,operator,reason,visibility):
Select(editor.find_element(By.TAG_NAME,'select')).select_by_value(visibility)
operator_input=editor.find_element(By.CSS_SELECTOR,'input[placeholder="姓名或工号"]');operator_input.clear();operator_input.send_keys(operator)
reason_input=editor.find_element(By.CSS_SELECTOR,'input[placeholder*="官网"]');reason_input.clear();reason_input.send_keys(reason)
def api_category(expected,origin):
current=[item for item in catalog() if item['connection_id']==connection and item['directory_path']=='产品介绍']
return len(current)==5 and all(item['visibility']==expected and item['visibility_origin']==origin for item in current)
try:
driver.get(BASE)
wait.until(lambda d:d.find_element(By.TAG_NAME,'h1').text=='来源与目录')
wait.until(lambda d:d.find_element(By.CSS_SELECTOR,'.directory-panel .folder b').text=='613')
assert 'P3 持久队列' not in body()
assert '显示 5 份验收文档' in body()
assert driver.find_element(By.CSS_SELECTOR,'.document-counts span:first-child b').text=='613'
root_fits()
# Test data is deliberately opt-in and returns to hidden state.
toggle=driver.find_element(By.CSS_SELECTOR,'.test-document-toggle input');toggle.click()
wait.until(lambda d:d.find_element(By.CSS_SELECTOR,'.directory-panel .folder b').text=='618')
toggle.click();wait.until(lambda d:d.find_element(By.CSS_SELECTOR,'.directory-panel .folder b').text=='613')
# Select the real PP category.
driver.find_element(By.XPATH,"//aside//button[.//span[contains(.,'PP公开文档')]]").click()
wait.until(lambda d:d.find_elements(By.CSS_SELECTOR,'.scope-editor'))
driver.find_element(By.XPATH,"//aside//button[.//span[contains(.,'产品介绍')]]").click()
editor=wait.until(lambda d:d.find_element(By.CSS_SELECTOR,'.document-content .scope-editor'))
assert '此目录及子目录' in editor.text and '当前没有本级设置' in editor.text
fill_editor(editor,'browser-acceptance','验证目录继承,完成后恢复','internal')
editor.find_element(By.XPATH,".//button[.='保存范围']").click();write_actions+=1
eventually(lambda:api_category('internal','directory_rule'))
wait.until(lambda d:'对内' in d.find_element(By.CSS_SELECTOR,'.document-table tbody').text)
assert 'PP公开文档' in driver.find_element(By.CSS_SELECTOR,'.workspace-context-tabs').text
assert '产品介绍' in driver.find_element(By.CSS_SELECTOR,'.workspace-context-tabs').text
# Override one file, verify precedence, then restore inheritance.
first_link=driver.find_element(By.CSS_SELECTOR,'.file-link');file_title=first_link.text;first_link.click()
inspector=wait.until(lambda d:d.find_element(By.CSS_SELECTOR,'.document-inspector'))
for expected in ['取得文件','上传测试库','解析与索引','测试检索','原文与版本差异尚未接通']:
assert expected in inspector.text
assert file_title in driver.find_element(By.CSS_SELECTOR,'.workspace-context-tabs').text
file_editor=inspector.find_element(By.CSS_SELECTOR,'.scope-editor')
fill_editor(file_editor,'browser-acceptance','验证文件单独覆盖,完成后恢复','external')
file_editor.find_element(By.XPATH,".//button[.='保存范围']").click();write_actions+=1
eventually(lambda:next(item for item in catalog() if item['id']==selected_id)['visibility_origin']=='file_override')
wait.until(lambda d:'当前设置:对外' in d.find_element(By.CSS_SELECTOR,'.document-inspector').text)
file_editor=driver.find_element(By.CSS_SELECTOR,'.document-inspector .scope-editor')
fill_editor(file_editor,'browser-acceptance','恢复目录继承','external')
file_editor.find_element(By.XPATH,".//button[contains(.,'恢复继承')]").click();write_actions+=1
eventually(lambda:(lambda item:item['visibility']=='internal' and item['visibility_origin']=='directory_rule')(next(item for item in catalog() if item['id']==selected_id)))
# Clicking the source context closes directory/file context.
driver.find_element(By.XPATH,"//div[@aria-label='当前来源目录和文件']//button[.='PP公开文档']").click()
wait.until(lambda d:not d.find_elements(By.CSS_SELECTOR,'.document-inspector'))
assert '产品介绍' not in driver.find_element(By.CSS_SELECTOR,'.workspace-context-tabs').text
# Return to the category and clear its direct rule.
driver.find_element(By.XPATH,"//aside//button[.//span[contains(.,'产品介绍')]]").click()
editor=wait.until(lambda d:d.find_element(By.CSS_SELECTOR,'.document-content .scope-editor'))
fill_editor(editor,'browser-acceptance','恢复来源原始标记','internal')
editor.find_element(By.XPATH,".//button[contains(.,'恢复继承')]").click();write_actions+=1
eventually(lambda:not rules(connection))
eventually(lambda:api_category('external','pp_public_tag'))
assert write_actions==4
# Source-level truthful monitor reconciles every PP item.
driver.find_element(By.XPATH,"//nav//button[contains(.,'解析监控')]").click()
wait.until(lambda d:d.find_element(By.TAG_NAME,'h1').text=='解析监控')
card=wait.until(lambda d:d.find_element(By.CSS_SELECTOR,'.source-progress-card'))
for expected in ['PP公开文档','已就绪 559 / 613','独立解析 530','处理中 0','需处理 54','内容复用 29','等待 0']:
assert expected in card.text
assert '100%' not in body()
root_fits()
driver.get_full_page_screenshot_as_file(str(ROOT/'tmp/DOCUMENT_MONITOR_V2_FIREFOX.png'))
driver.find_element(By.XPATH,"//nav//button[contains(.,'来源与目录')]").click()
wait.until(lambda d:d.find_element(By.TAG_NAME,'h1').text=='来源与目录')
for width in (1440,1024,768):
driver.set_window_size(width,1000);root_fits()
driver.set_window_size(1440,1100)
driver.get_full_page_screenshot_as_file(str(ROOT/'tmp/DOCUMENT_WORKBENCH_V2_FIREFOX.png'))
capabilities=request_json('/api/v1/release-capabilities')['data']
assert capabilities['production_release_requests_enabled'] is False
result={'catalog':618,'default_visible':613,'test_documents_hidden':5,'category':'产品介绍','category_items':5,'directory_inheritance':'pass','file_override':'pass','clear_restores_source':'pass','scope_writes':4,'active_rules_after_test':0,'source_monitor':{'completed':530,'total':613,'active':0,'attention':54,'reused':29,'waiting':0},'context_tabs':'pass','pipeline_detail':'pass','root_overflow':False,'production_release_requests_enabled':False}
(ROOT/'tmp/document-workbench-v2-acceptance.json').write_text(json.dumps(result,ensure_ascii=False,indent=2))
print(json.dumps(result,ensure_ascii=False))
finally:
driver.quit()