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(`
本机等待样本