import assert from 'node:assert/strict'; import fs from 'node:fs/promises'; import path from 'node:path'; import { createServer } from 'node:http'; import { fileURLToPath } from 'node:url'; import { setTimeout as delay } from 'node:timers/promises'; import { READONLY_POLICY_VERSION, endPlatformRequestPolicy, matchPlatformRequest } from './platform-policy.mjs'; // Official-origin cases are pure helper calls with synthetic metadata. The only // browser entry below is a new isolated profile on our own loopback HTTP page. const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); await fs.mkdir(path.join(root,'.tmp'),{recursive:true}); const output = await fs.mkdtemp(path.join(root, '.tmp', 'wait-preflight-')); process.env.ASSISTANT_LEARNING_HOME = path.join(output, 'learning'); process.env.ASSISTANT_BROWSER_HEADLESS = '1'; process.env.ASSISTANT_BROWSER_CHANNEL = process.platform === 'win32' ? 'msedge' : 'chromium'; const { preflightReadonlyWait, dispatch } = await import('./browser-worker.mjs'); const official = 'https://www.xiaohongshu.com'; const pageUrl = official + '/search_result_ai?keyword=fixture'; const ordinary = { tag:'a', readonly_root:true, sensitive:false, forbidden:false, visible:true, in_viewport:true }; const base = { sessionOrigin:official, pageUrl, kind:'wait', selector:'#global a.title', selectorObserved:true, observed:{kind:'click',tag:'a',unique:false,sensitive:false}, actualRoots:[{...ordinary},{...ordinary}], now:10000 }; const request = { automating:true, sessionOrigin:official, pageUrl, activeMainFrame:true, method:'POST', resourceType:'xhr', url:'https://t2.xiaohongshu.com/api/v2/collect', now:10001 }; let assertions = 0; let officialCases = 0; const equal = (actual, expected, message) => { assert.deepEqual(actual, expected, message); assertions += 1; }; const allowed = (policy, rule) => ({policy_version:READONLY_POLICY_VERSION,policy_rule:rule}); function rejected(patch, code = 'READONLY_WAIT_NOT_READY') { let returned; const context = {...base,...patch}; const before = JSON.stringify(context); assert.throws(() => { returned = preflightReadonlyWait(context); }, error => error.code === code); assertions += 1; equal(returned, undefined, 'preflight failure never returns an active capability'); equal(JSON.stringify(context), before, 'preflight does not mutate caller state or start an action'); officialCases += 1; } // Empty current collection and a visible but unobserved collection are two // distinct missing preconditions. Neither may borrow the previous search scope. for (const patch of [ {actualRoots:[]}, {actualRoots:[],selectorObserved:false}, {selectorObserved:false}, {selectorObserved:undefined}, {selectorObserved:null}, {actualRoots:null}, {actualRoots:undefined}, {actualRoots:[null]}, {actualRoots:[{...ordinary,readonly_root:false}]}, {actualRoots:[{...ordinary,readonly_root:undefined}]}, {selector:''}, {selector:'body',actualRoots:[ordinary]}, {pageUrl:'https://example.invalid/search_result'}, {actualRoots:[{...ordinary,tag:'input'}]}, ]) rejected(patch); for (const patch of [ {actualRoots:[{...ordinary,sensitive:true}]}, {actualRoots:[{...ordinary,forbidden:true}]}, {actualRoots:[ordinary,{...ordinary,sensitive:true}]}, {actualRoots:[{...ordinary,sensitive:true}],selectorObserved:false}, ]) rejected(patch,'SENSITIVE_OUTPUT'); const unchanged = JSON.stringify(base); const policy = preflightReadonlyWait(base); officialCases += 1; equal(policy?.scope,'readonly_ui'); equal(policy?.version,READONLY_POLICY_VERSION); equal(JSON.stringify(base),unchanged); equal(matchPlatformRequest({...request,policy}),allowed(policy,'collect')); equal(matchPlatformRequest({...request,policy,url:'https://apm-fe.xiaohongshu.com/api/data',resourceType:'fetch'}),allowed(policy,'apm_data')); for (const patch of [ {url:'https://apm-fe.xiaohongshu.com/api/data',resourceType:'xhr'}, {url:'https://as.xiaohongshu.com/api/sec/v1/shield/webprofile'}, {url:'https://so.xiaohongshu.com/api/sns/web/v2/search/notes'}, {url:'https://example.invalid/unknown-write'}, {url:'https://edith.xiaohongshu.com/api/sns/web/v2/comment/post'}, {activeMainFrame:false}, {method:'PUT'}, {now:25001}, ]) equal(matchPlatformRequest({...request,policy,...patch}),null); endPlatformRequestPolicy(policy); equal(matchPlatformRequest({...request,policy}),null,'returned wait policy is revoked with the existing end path'); equal(matchPlatformRequest({...request,policy:{...policy}}),null,'a copied policy is never an issued capability'); const bodyPolicy = preflightReadonlyWait({...base,selector:'body',selectorObserved:false,actualRoots:[{...ordinary,tag:'body'}]}); officialCases += 1; equal(bodyPolicy?.scope,'readonly_ui','only the existing wait body exception is retained'); equal(matchPlatformRequest({...request,policy:bodyPolicy}),allowed(bodyPolicy,'collect')); endPlatformRequestPolicy(bodyPolicy); equal(matchPlatformRequest({...request,policy:bodyPolicy}),null); for (const kind of ['fill','press','click','read','extract','scroll']) equal(preflightReadonlyWait({...base,kind}),null); for (const sessionOrigin of ['http://127.0.0.1:8765','https://example.invalid','https://www.xiaohongshu.com.evil.invalid']) { equal(preflightReadonlyWait({...base,sessionOrigin,actualRoots:[],selectorObserved:false}),null,'non-official behavior is unchanged'); } let pendingRelease; let fixtureRequests = 0; let fixtureWrites = 0; const server = createServer((req,res) => { fixtureRequests += 1; if (req.method !== 'GET') { fixtureWrites += 1; res.writeHead(405).end(); return; } const url = new URL(req.url,'http://127.0.0.1'); if (url.pathname === '/release') { pendingRelease = res; return; } if (url.pathname !== '/') { res.writeHead(404).end(); return; } res.writeHead(200,{'Content-Type':'text/html; charset=utf-8'}); res.end(`隔离等待前置回归

本机等待样本

结果稍后出现
`); }); let opened = false; let listening = false; try { await new Promise((resolve,reject) => { server.once('error',reject); server.listen(0,'127.0.0.1',resolve); }); listening = true; const entry = `http://127.0.0.1:${server.address().port}/`; const taskId = 'wait-preflight-demo'; await dispatch('open',{task_id:taskId,account_key:'isolated-wait-preflight',entry_url:entry}); opened = true; await dispatch('ready',{task_id:taskId,identity_label:'隔离等待测试'}); const before = await dispatch('capture',{task_id:taskId}); equal(before.elements.some(element=>element.selector==='#global a.title'),false,'latest capture does not invent a zero-item collection'); assert.ok(before.elements.some(element=>element.selector==='#global')); assertions += 1; for (let index=0;!pendingRelease && index<100;index+=1) await delay(10); assert.ok(pendingRelease,'fixture owns the pending loopback GET before testing the wait'); assertions += 1; let settled = false; const waiting = dispatch('action',{task_id:taskId,kind:'wait',selector:'#global a.title',state:'visible',min_count:2,timeout_ms:4000}); void waiting.then(()=>{settled=true;},()=>{settled=true;}); await delay(300); equal(settled,false,'unchanged demo wait does not fail on an initially missing collection'); pendingRelease.writeHead(200,{'Content-Type':'text/plain'}).end('release'); pendingRelease = null; const result = await waiting; equal(result.result.satisfied,true); equal(result.result.min_count,2); assert.ok(result.after.elements.some(element=>element.selector==='#global a.title' && element.count===2)); assertions += 1; for (const evidence of [result.before,result.after]) { const png = await fs.readFile(evidence.screenshot_path); equal(png.subarray(1,4).toString(),'PNG'); } equal(result.after.request_observations.filter(item=>item.blocked).length,0); equal(fixtureWrites,0); const summary = {result:'passed',assertions,official_preflight_cases:officialCases,official_case_environment:'pure helper only', official_browser_navigation:false,live_platform_requests:0,application_api_calls:0,model_calls:0, empty_or_unobserved_precondition_rejected:true,sensitive_precondition_distinct:true,old_readonly_rules_only:true, copied_or_ended_policy_rejected:true,caller_state_unchanged:true, loopback_edge_demo_wait_passed:true,initially_unobserved_collection:true,actual_pngs:true, fixture_requests:fixtureRequests,fixture_writes:fixtureWrites,production_official_wiring:'reviewed separately; not official browser end-to-end'}; await fs.writeFile(path.join(output,'summary.json'),JSON.stringify(summary,null,2)); console.log(JSON.stringify({...summary,output})); } finally { if (opened) await dispatch('close',{task_id:'wait-preflight-demo'}).catch(()=>{}); if (pendingRelease) pendingRelease.end('closed'); if (listening) await new Promise(resolve=>server.close(resolve)); }