接通审核中结果核对、受理记账与后续调度

This commit is contained in:
Longtail committed 2026-10-31 17:20:36 +08:00
1 parent d157c179c8
commit f0be4f5596
18 files changed
+510 -57

No files matched your search

+4
View File
@@ -11,3 +11,7 @@
- `artifacts/`:本轮页面截图,属于生成产物。
原始受理去重记录继续保存在 `../发布去重记录.json`,服务启动时自动导入;没有作为测试残留移走或删除。
- `results-browser.test.cjs`:0.2.0 结果适配回归,使用临时 Chrome 与模拟真实结构;阻断全部网络,覆盖真实 TID/PID、首帖作者范围、标题审核标志、图片占位及明确标题拒绝。需要 Playwright。
- 新增 Python 回归覆盖补核 HTTP 鉴权、历史受理不计当前批次、返回首页步骤、旧标识保留、事务回滚、停止后的被动记账与换号未改变 UID。
- 建议使用 Node.js 24 LTS;本机旧 Node 20 的测试报告解析中文名称异常,使用已安装的 Node 24 完成验证。
+2 -1
View File
@@ -49,6 +49,7 @@ const output=path.join(__dirname,'artifacts');fs.mkdirSync(output,{recursive:tru
const importerCore=require('../browser/importer.js');
await mock.addScriptTag({content:'(()=>{'+importerCore.attachmentId.toString()+';'+importerCore.verifyEditorImages.toString()+';Object.assign(window.DWW11Importer,{attachmentId,verifyEditorImages});})();'});
await mock.addScriptTag({path:path.join(ROOT,'browser/formatter.js')});
await mock.addScriptTag({path:path.join(ROOT,'browser/results.js')});
await mock.addScriptTag({path:path.join(ROOT,'browser/runner.js')});
await mock.waitForFunction(()=>window.job.state==='awaiting_user_review');
await mock.waitForTimeout(2400);
@@ -60,7 +61,7 @@ const output=path.join(__dirname,'artifacts');fs.mkdirSync(output,{recursive:tru
assert.equal(after,before,'repeated formatting should be idempotent');
await mock.locator('#postsubmit').click();
await mock.waitForFunction(()=>window.job.state==='user_submitting');
await mock.evaluate(()=>history.pushState({},'', '/thread/999.html'));
await mock.evaluate(()=>{history.pushState({},'', '/thread/999.html');const saved=JSON.parse(sessionStorage.getItem('auto-news-current'));saved.clickedAt-=60000;sessionStorage.setItem('auto-news-current',JSON.stringify(saved));});
await mock.waitForFunction(()=>window.job.state==='submit_unknown');
assert.equal(await mock.evaluate(()=>window.flow.running),false);
assert.equal(await mock.evaluate(()=>window.events.some(e=>e.route==='accept')),false,'unknown page must not count accepted');
+41
View File
@@ -0,0 +1,41 @@
'use strict';
// Separate temporary browser; all network requests blocked. No user profile or forum access.
const {test}=require('node:test');
const assert=require('node:assert/strict');
const {chromium}=require('playwright');
const path=require('node:path');
const gif='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
function html({moderation=true,author=true,duplicateTid=false,lazy=false}={}) {
return `<div class="mumucms_pt"><div class="z"><a href="https://www.dww11.com/forum.php?mod=forumdisplay&fid=90">版块</a></div></div>
<table><tr><td class="plc mumucms_ftit vwthd"><h1><span id="thread_subject">原创标题</span></h1>
<span class="xg1">${moderation?'(审核中)':''}<a href="https://www.dww11.com/forum.php?mod=viewthread&tid=236999">复制链接</a></span>
${duplicateTid?'<a href="https://www.dww11.com/forum.php?mod=viewthread&tid=999">冲突链接</a>':''}</td></tr></table>
<a id="mumucms_username" href="https://www.dww11.com/home.php?mod=space&uid=999">登录者</a>
<div id="post_238791"><table id="pid238791"><tr><td class="plc"><a id="postnum238791">楼主</a><div class="pti"><div class="authi">${author?'<a href="https://www.dww11.com/home.php?mod=space&uid=123">首帖作者</a>':''}</div></div>
<div><table><tr><td id="postmessage_238791">原创正文<img id="aimg_42" aid="42" src="${lazy?'https://www.dww11.com/static/image/common/none.gif':gif}" file="https://www.dww11.com/real.png"></td></tr></table></div></td></tr></table></div>
<div id="post_999"><div class="authi"><a href="https://www.dww11.com/home.php?mod=space&uid=456">回帖作者</a></div><div id="postmessage_999">回帖(审核中)</div></div>`;
}
test('结果页适配:真实主题、首帖范围、状态、图文与延迟加载',async()=>{
const browser=await chromium.launch({headless:true,executablePath:process.env.CHROME_PATH||'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'});
try {
const page=await browser.newPage();await page.route('**/*',r=>r.abort());
async function inspect(options={},snapshot={title:'原创标题',text:'原创正文',images:[{aid:'attachimg_42'}]}) {
await page.setContent(html(options));await page.addScriptTag({path:path.resolve(__dirname,'../browser/results.js')});
return page.evaluate(snapshot=>window.DWW11Results.inspect(document,'https://www.dww11.com/thread/23699911.html',snapshot),snapshot);
}
const accepted=await inspect();assert.equal(accepted.kind,'accepted');assert.equal(accepted.thread_id,'236999');assert.equal(accepted.first_post_pid,'238791');assert.equal(accepted.author_uid,'123');assert.equal(accepted.fid,90);assert.deepEqual(accepted.evidence.integrity_issues,[]);
assert.equal((await inspect({author:false})).kind,'incomplete','top login/replies cannot supply the author');
assert.equal((await inspect({moderation:false})).kind,'incomplete','absence of moderation is not public proof');
assert.equal((await inspect({duplicateTid:true})).kind,'incomplete');
const lazy=await inspect({lazy:true});assert.equal(lazy.evidence.images[0].loaded,false);
assert.ok(lazy.evidence.integrity_issues.length);
const changed=await inspect({}, {text:'被修改的正文',images:[{aid:'attachimg_99'}]});assert.equal(changed.evidence.integrity_issues.length,2);
await page.setContent('<div id="messagetext">新主题需要审核,您的帖子通过审核后才能显示<a href="https://www.dww11.com/thread/23699911.html">继续</a></div>');
await page.addScriptTag({path:path.resolve(__dirname,'../browser/results.js')});
const prompt=await page.evaluate(()=>window.DWW11Results.inspect(document,'https://www.dww11.com/forum.php?topicsubmit=yes'));
assert.equal(prompt.kind,'waiting');assert.ok(prompt.prompt.includes('需要审核'));
await page.setContent('<div id="messagetext">抱歉,您填写的标题超过 80 个字符限制</div>');
await page.addScriptTag({path:path.resolve(__dirname,'../browser/results.js')});
assert.equal((await page.evaluate(()=>window.DWW11Results.inspect(document,'https://www.dww11.com/forum.php'))).kind,'rejected');
}finally{await browser.close();}
});
+29 -2
View File
@@ -6,7 +6,7 @@ const path=require('node:path');
const vm=require('node:vm');
const flush=async()=>{for(let i=0;i<12;i++)await new Promise(resolve=>setImmediate(resolve));};
function fixture({holdImport=false,holdHeartbeat=false,rejectSkip=false}={}) {
function fixture({holdImport=false,holdHeartbeat=false,rejectSkip=false,result=null,stopOnAccept=false}={}) {
const handlers=new Map(),calls=[],navigations=[],storage=new Map(),timers=new Set();
const job={id:'first',state:'preparing',title:'标题',source:'来源',fid:125,account:{uid:'123',name:'账号'}};
const run={running:true,epoch:1,active:job.id,owner:'tab-a'};
@@ -38,6 +38,8 @@ function fixture({holdImport=false,holdHeartbeat=false,rejectSkip=false}={}) {
if(rejectSkip){reply(id,null,'状态已改变');return;}
job.state='skipped';run.active=null;run.epoch++;value={ok:true,fid:125,run};
}
if(route==='current')value={job,run};
if(route==='accept'){job.state=body.state;run.active=null;value={ok:true};if(stopOnAccept)run.running=false;}
if(route==='stop')run.running=false;
reply(id,value);
});
@@ -47,7 +49,7 @@ function fixture({holdImport=false,holdHeartbeat=false,rejectSkip=false}={}) {
location:{href:'https://www.dww11.com/forum.php?mod=post&action=newthread&fid=125',assign:url=>navigations.push(url)},
setInterval:fn=>{tick=fn;},setTimeout:(fn,ms)=>{const t=setTimeout(fn,ms);timers.add(t);return t;},clearTimeout:t=>{clearTimeout(t);timers.delete(t);}
};
context.window={DWW11Importer:{stagePackage:async()=>({}),importPrepared:async({signal})=>{
context.window={DWW11Results:result?{inspect:()=>result,clean:v=>String(v||'').trim()}:null,DWW11Importer:{stagePackage:async()=>({}),importPrepared:async({signal})=>{
if(holdImport)await new Promise((resolve,reject)=>{finishImport=()=>{rolledBack=true;reject(signal.reason);};signal.addEventListener('abort',()=>{}, {once:true});});
signal.throwIfAborted();subject.value='标题';return {attachmentOrder:[]};
},verifyEditorImages:async()=>{}},DWW11Formatter:{format(){}}};
@@ -104,3 +106,28 @@ test('跳过请求失败时暂停且保留当前稿件',async()=>{
assert.equal(f.storage.has('auto-news-current'),true);
}finally{f.close();}
});
const acceptedResult={kind:'accepted',thread_id:'236999',first_post_pid:'238791',author_uid:'123',fid:125,title:'标题',state:'accepted_pending_moderation',post_url:'https://www.dww11.com/thread/23699911.html',evidence:{thread_id:'236999',integrity_issues:[]}};
for(const stopOnAccept of [false,true])test(`受理后先持久记账;停止=${stopOnAccept} 时禁止回首页`,async()=>{
const f=fixture({result:acceptedResult,stopOnAccept});try{
await flush();f.doc.dispatchEvent(new f.Event('click',{isTrusted:true,target:{closest:()=>true}}));await flush();
await f.tick();await flush();
assert.equal(f.calls.filter(x=>x.route==='accept').length,1);
assert.equal(f.calls.find(x=>x.route==='accept').body.thread_id,'236999');
assert.deepEqual(f.navigations,stopOnAccept?[]:['https://www.dww11.com/']);
assert.equal(f.storage.has('auto-news-current'),false);
}finally{f.close();}
});
test('结果首帖作者不匹配时暂停且不记账',async()=>{
const f=fixture({result:{...acceptedResult,author_uid:'999'}});try{
await flush();f.doc.dispatchEvent(new f.Event('click',{isTrusted:true,target:{closest:()=>true}}));await flush();await f.tick();
assert.equal(f.calls.some(x=>x.route==='accept'),false);assert.equal(f.run.running,false);assert.deepEqual(f.navigations,[]);
}finally{f.close();}
});
test('明确标题拒绝只记录并暂停,不修稿或重试发表',async()=>{
const f=fixture({result:{kind:'rejected',error:'标题超过80字符'}});try{
await flush();f.doc.dispatchEvent(new f.Event('click',{isTrusted:true,target:{closest:()=>true}}));await flush();await f.tick();
assert.equal(f.job.state,'rejected');assert.equal(f.calls.some(x=>x.route==='accept'),false);assert.deepEqual(f.navigations,[]);
}finally{f.close();}
});
+19
View File
@@ -84,5 +84,24 @@ class ControllerTests(unittest.TestCase):
self.assertIsNone(store.run['active'])
def test_ui_historical_result_reconciliation(self):
store = self.controller.store
store.enqueue(ROOT / 'test/fixtures/article')
store.configure(dict(fid=125, minimum=1, maximum=1, interval=0, history_checked=True))
store.control('start')
job = store.next(dict(owner='tab-a', account=dict(uid='123', name='测试账号')))['job']
for state in ('importing','formatting','awaiting_user_review','user_submitting'):
store.event(dict(owner='tab-a',id=job['id'],state=state,epoch=store.run['epoch'],snapshot={'title':job['title']}))
store.control('stop')
data=dict(id=job['id'],article_key=job['article_key'],confirmed=True,thread_id='236999',first_post_pid='238791',
post_url='https://www.dww11.com/thread/23699911.html',title=job['title'],author_uid='123',fid=125,
state='accepted_pending_moderation',evidence={'thread_id':'236999'})
with self.assertRaises(HTTPError):
self.request('/api/reconcile',data,token=self.controller.bridge_token)
self.assertEqual(self.request('/api/reconcile',data,token=self.controller.ui_token)[0],200)
self.assertEqual(store.run['batch']['count'],0)
self.assertFalse(store.run['running'])
if __name__ == '__main__':
unittest.main()
+79 -2
View File
@@ -22,7 +22,7 @@ class QueueTests(unittest.TestCase):
self.md.write_text('# 测试新闻\n\n正文内容。\n\n来源:测试来源,如有侵权,请联系删除。\n')
self.store = Store(self.root / 'queue.sqlite3')
self.store.configure(dict(fid=125, minimum=1, maximum=1, interval=0, history_checked=True))
self.owner = dict(owner='tab-a', account=dict(uid='123', name='测试账号'), extension={'idle': True})
self.owner = dict(owner='tab-a', account=dict(uid='123', name='测试账号'), extension={'idle': True}, url='https://www.dww11.com/')
def tearDown(self):
self.tmp.cleanup()
@@ -42,7 +42,7 @@ class QueueTests(unittest.TestCase):
def accept(self, job):
return self.store.accept(dict(owner='tab-a', id=job['id'], post_url='https://www.dww11.com/thread/12345.html',
author_uid='123', fid=125, title=job['title'], state='accepted_pending_moderation', evidence={'test': 'fixture only'}))
thread_id='123', first_post_pid='456', author_uid='123', fid=125, title=job['title'], state='accepted_pending_moderation', evidence={'thread_id': '123', 'test': 'fixture only'}))
def test_stable_identity_and_original_preserved(self):
before = self.md.read_bytes()
@@ -201,6 +201,83 @@ class QueueTests(unittest.TestCase):
self.store.skip(dict(owner='tab-a', id=job['id'], epoch=self.store.run['epoch']))
self.assertEqual(self.store.run['active'], job['id'])
def test_real_tid_and_return_home_gate(self):
job = self.begin(); self.submit(job); self.accept(job)
saved = self.store.state()['jobs'][0]
self.assertEqual(saved['thread_id'], '123')
self.assertEqual(saved['first_post_pid'], '456')
self.assertEqual(self.store.next({**self.owner, 'url': saved['post_url']})['action'], 'home')
self.assertEqual(self.store.next(self.owner)['action'], 'idle')
def test_integrity_issue_records_acceptance_but_stops(self):
job = self.begin(); self.submit(job)
self.store.accept(dict(owner='tab-a', id=job['id'], post_url='https://www.dww11.com/thread/12311.html',
thread_id='123', author_uid='123', fid=125, title=job['title'], state='accepted_pending_moderation',
evidence={'thread_id':'123','integrity_issues':['正文图片位置改变']}))
self.assertFalse(self.store.run['running'])
self.assertEqual(self.store.run['batch']['count'], 1)
self.assertIn('已受理', self.store.enqueue(self.article))
def test_historical_reconciliation_does_not_count_or_resume(self):
job = self.begin(); self.submit(job); self.store.control('stop')
data=dict(id=job['id'], article_key=job['article_key'], confirmed=True,
post_url='https://www.dww11.com/thread/12311.html',thread_id='123',author_uid='123',fid=125,
title=job['title'],state='accepted_pending_moderation',evidence={'thread_id':'123'})
self.store.reconcile(data); self.store.reconcile(data)
self.assertFalse(self.store.run['running'])
self.assertEqual(self.store.run['batch']['count'], 0)
self.assertIn('已受理', self.store.enqueue(self.article))
def test_transaction_rolls_back_in_memory_run(self):
import sqlite3
self.begin()
before = json.loads(json.dumps(self.store.run))
with self.assertRaises(sqlite3.IntegrityError):
with self.store.db() as db:
self.store.run['batch']['count'] = 999
db.execute("INSERT INTO posts VALUES(NULL,NULL)")
self.assertEqual(self.store.run, before)
def test_accept_after_stop_only_records_no_resumption(self):
job = self.begin(); self.submit(job); self.store.control('stop'); self.accept(job)
self.assertFalse(self.store.run['running'])
self.assertEqual(self.store.run['batch']['count'], 1)
self.assertEqual(self.store.next(self.owner)['action'], 'idle')
def test_switch_same_uid_and_idle_stops_without_reclick(self):
import time
job = self.begin(); self.submit(job); self.accept(job)
other = self.root / 'other'; shutil.copytree(self.article, other)
(other / 'a.md').write_text(self.md.read_text().replace('正文内容', '其他内容'))
self.store.enqueue(other)
self.assertEqual(self.store.next(self.owner)['action'], 'switch')
self.store.switch_begin({'owner':'tab-a'})
self.store.run['switch']['clicked_at'] = time.time() - 11
self.assertEqual(self.store.next(self.owner)['action'], 'idle')
self.assertFalse(self.store.run['running'])
self.assertEqual(self.store.run['switch']['state'], 'clicked')
def test_rejected_submission_requires_manual_resolution(self):
job = self.begin(); self.submit(job)
self.transition(job, 'rejected', error='标题超限')
self.assertFalse(self.store.run['running'])
self.assertEqual(self.store.run['batch']['count'], 0)
self.store.resolve(dict(id=job['id'],confirmed_not_accepted=True,retry=True))
saved = self.store.state()['jobs'][0]
self.assertNotIn('submitted_at', saved)
self.assertEqual(saved['state'], 'queued')
def test_legacy_identity_is_preserved_without_guessing_tid(self):
job = self.begin(); self.submit(job); self.accept(job)
with self.store.db() as db:
saved = self.store._job(db,job['id']); saved.pop('thread_id')
self.store._save(db,saved)
restarted = Store(self.root/'queue.sqlite3')
saved = restarted.state()['jobs'][0]
self.assertEqual(saved['legacy_url_id'],'123')
self.assertTrue(saved['identity_needs_reconciliation'])
self.assertFalse(restarted.run['running'])
def test_bad_result_does_not_increment(self):
job = self.begin();self.submit(job)
with self.assertRaises(ValueError):
+10
View File
@@ -59,3 +59,13 @@
已保存详情页关键结构与审核状态截图;提示页文字和跳转链接已捕捉,跳转前的容器 HTML 未保存成功。文件名按访问地址命名,不能将 `23699911` 直接当作真实主题 ID。账号名称与 UID 仅保留在本地排除目录,正文档继续匿名化。
本次未验证公开后的访客可见性、失败/超限提示、结果自动识别、受理接口自动记账、重复计数保护或真实换号链路;未补写运行队列与去重库。已有现场资料不等于结果适配已经实现。详见[操作规范第 12 节](../发布流程操作记录与自动化规范.md#12-2026-10-31实际提交与审核中页面观察)。
## 2026-10-31:0.2.0 结果核对与接续
- Python:42/42 通过,使用缓存的 Python 3.12 和依赖,临时数据库与模拟响应。
- JavaScript、连接桥、执行器、模拟结果页:27/27 通过,Node.js 24.22.1;临时 Chrome 阻断全部网络。
- 完整浏览器回归:控制台配置/启停、桌面/手机布局、模拟导入/排版、人工点击记录、未知结果暂停和换号中终止通过。截图位于 `test/artifacts/`。
- 只读核对实际已受理帖子结构,确认作者、版块和图片占位规则;没有在线上传、发表、编辑或换号。
- 已先备份正式 SQLite,再根据已有提交快照与现场观察记录补核 TID 236999 / PID 238791。批次计数保持 0、流程保持暂停、重复入队检查返回“已受理,跳过”。原 JSON 去重记录同步保留文章哈希与主题信息,仍在 Git 排除范围。
- 尚未验收:公开成功页、真实标题超限提示、完整线上下一篇与换号、图片 HTTP 500 根因。离线通过不等于以上实机链路通过。