516 lines
34 KiB
JavaScript
516 lines
34 KiB
JavaScript
// Synthetic contract tests only. This test must never navigate to an official
|
|
// platform or expose an origin override in the production execution profile.
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import {fileURLToPath} from 'node:url';
|
|
import {createServer} from 'node:http';
|
|
import {ActionInputError} from './action-contract.mjs';
|
|
|
|
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','native-ui-profile-'));
|
|
process.env.ASSISTANT_LEARNING_HOME=path.join(output,'worker-learning');
|
|
process.env.ASSISTANT_BROWSER_HEADLESS='1';
|
|
process.env.ASSISTANT_BROWSER_CHANNEL=process.platform==='win32' ? 'msedge' : 'chromium';
|
|
const results=[];
|
|
let assertions=0;
|
|
let browserFixture=null;
|
|
const eq=(actual,expected,message)=>{assert.deepEqual(actual,expected,message);assertions+=1;};
|
|
const ok=(value,message)=>{assert.ok(value,message);assertions+=1;};
|
|
const throws=(fn,predicate,message)=>{assert.throws(fn,predicate,message);assertions+=1;};
|
|
const report=async(extra={})=>fs.writeFile(path.join(output,'report.json'),JSON.stringify({
|
|
result:'passed',passed:results.length,assertions,results,
|
|
environment:'synthetic execution-profile, action, and private request-capability helpers',
|
|
official_browser_navigation:false,live_platform_requests:0,application_api_calls:0,model_calls:0,
|
|
browser_fixture:browserFixture || 'not executed; no production origin or network bypass is introduced',...extra,
|
|
},null,2));
|
|
const group=async(name,run)=>{
|
|
const start=assertions;
|
|
try{const details=await run();results.push({name,assertions:assertions-start,...details});await report();}
|
|
catch(error){await report({result:'failed',failed_group:name,error:{name:error.name,message:error.message,code:error.code}});throw error;}
|
|
};
|
|
|
|
const official='https://www.xiaohongshu.com';
|
|
const noteId='a'.repeat(24),otherNoteId='b'.repeat(24);
|
|
const noteUrl=`${official}/explore/${noteId}?xsec_token=LOCAL_NATIVE_FIXTURE_TOKEN`;
|
|
const profile=await import('./execution-profile.mjs');
|
|
const {NATIVE_UI_PROFILE,NATIVE_UI_CONTRACT,resolveExecutionProfile,validNativePage,nativeNoteId,
|
|
nativeSearchKeyword,nativeReadRootsValid,assertNativeUiAction,assertNativeUiResultLocation,
|
|
executionContractMatches,executionMetadata}=profile;
|
|
|
|
await group('exact versioned profile and immutable execution contract',()=>{
|
|
eq(NATIVE_UI_PROFILE,'playwright-xhs-native-ui-v1');
|
|
eq(NATIVE_UI_CONTRACT,{engine:'playwright',action_policy:'xhs-bounded-readonly-v1',network_policy:'xhs-page-native-v1'});
|
|
eq(Object.isFrozen(NATIVE_UI_CONTRACT),true);
|
|
eq(resolveExecutionProfile(undefined),null,'only an omitted profile selects legacy');
|
|
eq(resolveExecutionProfile(NATIVE_UI_PROFILE),NATIVE_UI_CONTRACT);
|
|
eq(executionMetadata(undefined),{},'legacy omits the new contract fields');
|
|
eq(executionContractMatches({...NATIVE_UI_CONTRACT}),true);
|
|
for(const value of [null,undefined,[],{},NATIVE_UI_PROFILE,
|
|
{...NATIVE_UI_CONTRACT,engine:'browser-use'},
|
|
{...NATIVE_UI_CONTRACT,action_policy:'legacy'},
|
|
{...NATIVE_UI_CONTRACT,network_policy:'unknown'},
|
|
{...NATIVE_UI_CONTRACT,extra:'fixture'},
|
|
{engine:'playwright',action_policy:'xhs-bounded-readonly-v1'},
|
|
Object.create(NATIVE_UI_CONTRACT)]) eq(executionContractMatches(value),false);
|
|
const metadata=executionMetadata(NATIVE_UI_PROFILE);
|
|
eq(metadata,{execution_profile:NATIVE_UI_PROFILE,execution_contract:NATIVE_UI_CONTRACT});
|
|
metadata.execution_contract.engine='LOCAL_FIXTURE_MUTATION';
|
|
eq(executionMetadata(NATIVE_UI_PROFILE).execution_contract.engine,'playwright','returned DTOs cannot mutate the shared descriptor');
|
|
for(const value of [null,false,true,0,1,'','legacy','playwright-xhs-native-ui-v2',NATIVE_UI_PROFILE+' ',
|
|
' '+NATIVE_UI_PROFILE,NATIVE_UI_PROFILE.toUpperCase(),[],{},NATIVE_UI_CONTRACT]){
|
|
throws(()=>resolveExecutionProfile(value),error=>error instanceof ActionInputError,'unknown or object-valued profiles never silently downgrade');
|
|
}
|
|
});
|
|
|
|
await group('official page shape uses HTTPS exact host and declared paths',()=>{
|
|
for(const url of [official+'/explore',official+'/search_result',official+'/search_result_ai',
|
|
official+'/search_result?keyword=%E8%A3%85%E4%BF%AE',official+'/search_result_ai?keyword=fixture',
|
|
noteUrl,`${official}/search_result/${otherNoteId}`]) eq(validNativePage(url),true,url);
|
|
for(const url of [null,undefined,{},'', 'not a url','http://www.xiaohongshu.com/explore',
|
|
'https://xiaohongshu.com/explore','https://www.xiaohongshu.com.evil.invalid/explore',
|
|
'https://evilxiaohongshu.com/explore','https://www.xiaohongshu.com:8443/explore',
|
|
'https://user:pass@www.xiaohongshu.com/explore','https://www.xiaohongshu.com@evil.invalid/explore',
|
|
'https://so.xiaohongshu.com/explore','http://127.0.0.1/explore',official+'/',official+'/explore/',
|
|
official+'/message',official+'/notification',official+'/user/profile/'+noteId,
|
|
official+'/api/sns/web/v2/comment/post',official+'/search_result_ai/'+noteId,
|
|
official+'/explore/'+noteId+'/',official+'/explore/'+noteId.slice(1),
|
|
official+'/explore/'+'g'.repeat(24),official+'/explore/%2f'+noteId]) eq(validNativePage(url),false,String(url));
|
|
});
|
|
|
|
await group('public note identity and bounded decoding preserve the selected search',()=>{
|
|
eq(nativeNoteId(noteUrl),noteId);
|
|
eq(nativeNoteId(`${official}/search_result/${otherNoteId.toUpperCase()}?from=fixture`),otherNoteId);
|
|
for(const value of [official+'/search_result_ai/'+noteId,official+'/explore/'+noteId+'/',
|
|
'https://evil.invalid/explore/'+noteId,'https://u:p@www.xiaohongshu.com/explore/'+noteId,
|
|
official+'/user/profile/'+noteId,null,undefined]) eq(nativeNoteId(value),null);
|
|
const keyword='装修 案例';
|
|
eq(nativeSearchKeyword(official+'/search_result?keyword='+encodeURIComponent(keyword)),keyword);
|
|
eq(nativeSearchKeyword(official+'/search_result_ai?keyword='+encodeURIComponent(encodeURIComponent(keyword))),keyword);
|
|
eq(nativeSearchKeyword(official+'/search_result?keyword=fixture%20value'),'fixture value');
|
|
for(const value of [official+'/explore?keyword=fixture',official+'/search_result',
|
|
official+'/search_result?keyword=fixture&keyword=other',
|
|
official+'/search_result?keyword=%25E0%25A4%25A','https://evil.invalid/search_result?keyword=fixture']) eq(nativeSearchKeyword(value),null);
|
|
const threeTimes=encodeURIComponent(encodeURIComponent(encodeURIComponent(keyword)));
|
|
ok(nativeSearchKeyword(official+'/search_result?keyword='+threeTimes)!==keyword,'the helper never repeatedly decodes until a desired keyword happens to match');
|
|
});
|
|
|
|
const searchObserved={kind:'fill',tag:'input',unique:true,search_input:true,sensitive:false,stable_label:'Search'};
|
|
const searchActual={tag:'input',search_input:true,sensitive:false,visible:true,in_viewport:true,unobscured:true,
|
|
native_context:{search_input:true,note_link:false,read_root:false}};
|
|
const searchAction={kind:'fill',pageUrl:official+'/explore',selector:'#search-input-in-feeds',
|
|
observed:searchObserved,actualRoots:[searchActual],matchCount:1,keyword:'装修案例'};
|
|
const rejectsAction=(context,code='WRITE_ACTION_BLOCKED')=>{
|
|
const snapshot=JSON.stringify(context);
|
|
throws(()=>assertNativeUiAction(context),error=>error instanceof ActionInputError && error.code===code);
|
|
eq(JSON.stringify(context),snapshot,'rejected action metadata stays unchanged');
|
|
};
|
|
|
|
await group('only observed usable exact search controls accept fill and Enter',()=>{
|
|
const snapshot=JSON.stringify(searchAction);
|
|
for(const selector of ['#search-input-in-feeds','#search-input-ai']){
|
|
eq(assertNativeUiAction({...searchAction,selector}),true);
|
|
eq(assertNativeUiAction({...searchAction,selector,kind:'press',key:'Enter'}),true);
|
|
}
|
|
eq(JSON.stringify(searchAction),snapshot);
|
|
for(const patch of [
|
|
{selector:'#generic-search'},{selector:'#comment-input'},{selector:'input[type=search]'},
|
|
{matchCount:0},{matchCount:2},{actualRoots:[]},{actualRoots:[searchActual,searchActual]},
|
|
{observed:null},{observed:{...searchObserved,kind:'read'}},{observed:{...searchObserved,unique:false}},
|
|
{observed:{...searchObserved,search_input:false}},{observed:{...searchObserved,sensitive:true}},
|
|
{actualRoots:[{...searchActual,tag:'button'}]},{actualRoots:[{...searchActual,search_input:false}]},
|
|
{actualRoots:[{...searchActual,native_context:{...searchActual.native_context,search_input:false}}]},
|
|
{actualRoots:[{...searchActual,native_context:undefined}]},{actualRoots:[{...searchActual,sensitive:true}]},
|
|
{actualRoots:[{...searchActual,visible:false}]},{actualRoots:[{...searchActual,in_viewport:false}]},
|
|
{actualRoots:[{...searchActual,unobscured:false}]},{kind:'press',key:'Tab'},
|
|
{kind:'press',key:'Control+Enter'},{kind:'press'},
|
|
]) rejectsAction({...searchAction,...patch});
|
|
for(const keyword of [undefined,null,'',' ','x'.repeat(2001),'手机号 13800138000','验证码:123456','password:fixture']){
|
|
rejectsAction({...searchAction,keyword},'SENSITIVE_INPUT');
|
|
}
|
|
rejectsAction({...searchAction,pageUrl:official+'/message'},'SCOPE_BLOCKED');
|
|
});
|
|
|
|
const noteObserved={kind:'click',tag:'a',unique:true,sensitive:false};
|
|
const noteActual={tag:'a',href:noteUrl,sensitive:false,visible:true,in_viewport:true,unobscured:true,
|
|
native_context:{search_input:false,note_link:true,read_root:false}};
|
|
const noteAction={kind:'click',pageUrl:official+'/search_result?keyword=fixture',selector:'#global a.title',
|
|
observed:noteObserved,actualRoots:[noteActual],matchCount:1,href:noteUrl,noteId};
|
|
await group('clicks require a fixed actual standard note link and selected identity',()=>{
|
|
const snapshot=JSON.stringify(noteAction);
|
|
eq(assertNativeUiAction(noteAction),true);
|
|
eq(assertNativeUiAction({...noteAction,observed:{...noteObserved,unique:false}}),true,'the worker may supply a uniquely resolved collection member');
|
|
eq(JSON.stringify(noteAction),snapshot);
|
|
for(const patch of [
|
|
{kind:'click',observed:null},{matchCount:0},{matchCount:2},{actualRoots:[]},
|
|
{observed:{...noteObserved,kind:'read'}},{observed:{...noteObserved,tag:'button'}},
|
|
{observed:{...noteObserved,sensitive:true}},
|
|
{actualRoots:[{...noteActual,tag:'button',href:''}]},
|
|
{actualRoots:[{...noteActual,native_context:{...noteActual.native_context,note_link:false}}]},
|
|
{actualRoots:[{...noteActual,native_context:undefined}]},
|
|
{actualRoots:[{...noteActual,sensitive:true}]},{actualRoots:[{...noteActual,visible:false}]},
|
|
{actualRoots:[{...noteActual,in_viewport:false}]},{actualRoots:[{...noteActual,unobscured:false}]},
|
|
{actualRoots:[{...noteActual,href:`${official}/explore/${otherNoteId}`}]},
|
|
{noteId:otherNoteId},
|
|
]) rejectsAction({...noteAction,...patch});
|
|
for(const href of [official+'/message',official+'/explore',official+'/user/profile/'+noteId,
|
|
official+'/explore/not-a-note',official+'/explore/'+noteId+'/',
|
|
official+'/api/sns/web/v2/comment/post','https://evil.invalid/explore/'+noteId,
|
|
'javascript:void(0)','data:text/html,fixture','']){
|
|
rejectsAction({...noteAction,href,actualRoots:[{...noteActual,href}]});
|
|
}
|
|
});
|
|
|
|
const readRoot={tag:'section',readonly_root:true,sensitive:false,forbidden:false,
|
|
native_context:{read_root:true,note_link:false,search_input:false}};
|
|
await group('read wait extract and bounded scroll require fixed ordinary roots',()=>{
|
|
for(const kind of ['read','wait','extract','scroll']) eq(assertNativeUiAction({kind,pageUrl:noteUrl,actualRoots:[readRoot]}),true);
|
|
eq(nativeReadRootsValid([readRoot]),true);
|
|
eq(nativeReadRootsValid(Array.from({length:500},()=>readRoot)),true);
|
|
for(const actualRoots of [null,undefined,[],[null],[readRoot,null],Array.from({length:501},()=>readRoot),
|
|
[{...readRoot,readonly_root:false}],[{...readRoot,sensitive:true}],[{...readRoot,forbidden:true}],
|
|
[{...readRoot,native_context:undefined}],[{...readRoot,native_context:{...readRoot.native_context,read_root:false}}]]){
|
|
eq(nativeReadRootsValid(actualRoots),false);
|
|
rejectsAction({kind:'read',pageUrl:noteUrl,actualRoots},'SENSITIVE_OUTPUT');
|
|
}
|
|
for(const kind of ['evaluate','fetch','request','goto','send_message','follow','comment','arbitrary_js']){
|
|
rejectsAction({kind,pageUrl:noteUrl,actualRoots:[readRoot]},'SENSITIVE_OUTPUT');
|
|
}
|
|
});
|
|
|
|
await group('completed actions require the actual expected final source rather than only permitted in-flight traffic',()=>{
|
|
const failLocation=(context,pageUrl,code)=>throws(()=>assertNativeUiResultLocation(context,pageUrl),
|
|
error=>error instanceof ActionInputError && error.code===code);
|
|
const press={...searchAction,kind:'press',key:'Enter'};
|
|
for(const path of ['/search_result','/search_result_ai']){
|
|
eq(assertNativeUiResultLocation(press,official+path+'?keyword='+encodeURIComponent(press.keyword)),true);
|
|
}
|
|
failLocation(press,press.pageUrl,'SEARCH_TRANSITION_NOT_READY');
|
|
failLocation(press,official+'/search_result?keyword=other','SEARCH_TRANSITION_NOT_READY');
|
|
failLocation(press,official+'/search_result?keyword='+encodeURIComponent(press.keyword)+'&keyword=other','SEARCH_TRANSITION_NOT_READY');
|
|
failLocation(press,official+'/message','SOURCE_CHANGED');
|
|
eq(assertNativeUiResultLocation(noteAction,noteUrl),true);
|
|
eq(assertNativeUiResultLocation(noteAction,official+'/search_result/'+noteId),true);
|
|
failLocation(noteAction,noteAction.pageUrl,'NOTE_BINDING_MISMATCH');
|
|
failLocation(noteAction,official+'/explore/'+otherNoteId,'NOTE_BINDING_MISMATCH');
|
|
failLocation(noteAction,'https://evil.invalid/explore/'+noteId,'SOURCE_CHANGED');
|
|
for(const kind of ['fill','read','extract','wait','scroll']){
|
|
const context={kind,pageUrl:searchAction.pageUrl};
|
|
eq(assertNativeUiResultLocation(context,context.pageUrl),true);
|
|
failLocation(context,context.pageUrl+'?fixture=changed','SOURCE_CHANGED');
|
|
failLocation(context,context.pageUrl+'#changed','SOURCE_CHANGED');
|
|
failLocation(context,noteUrl,'SOURCE_CHANGED');
|
|
}
|
|
});
|
|
|
|
const {beginNativeUiRequestPolicy,evaluatePlatformRequest,matchPlatformRequest,endPlatformRequestPolicy,
|
|
NATIVE_UI_POLICY_VERSION}=await import('./platform-policy.mjs');
|
|
const policyContext={...searchAction,sessionOrigin:official,now:10000};
|
|
const nativeRequest={automating:true,sessionOrigin:official,pageUrl:searchAction.pageUrl,activeMainFrame:true,
|
|
method:'POST',resourceType:'xhr',url:'https://as.xiaohongshu.com/api/sec/v1/fixture',now:10001};
|
|
const allowed={allowed:true,policy_version:'xhs-page-native-v1',policy_rule:'native_page_traffic'};
|
|
const nativePolicies=new Set();
|
|
const mint=context=>{
|
|
const snapshot=JSON.stringify(context),policy=beginNativeUiRequestPolicy(context);
|
|
eq(JSON.stringify(context),snapshot,'minting never changes action evidence');
|
|
ok(policy,'a permitted finite UI action can mint its private request policy');
|
|
nativePolicies.add(policy);return policy;
|
|
};
|
|
const denied=(request,reason)=>{
|
|
const actual=evaluatePlatformRequest(request);
|
|
eq(actual.allowed,false);
|
|
if(reason!==undefined) eq(actual.reason,reason);
|
|
eq(matchPlatformRequest(request),null,'the public matcher uses the same rejection');
|
|
};
|
|
|
|
await group('only valid exact native actions mint private fifteen-second capabilities',()=>{
|
|
eq(NATIVE_UI_POLICY_VERSION,'xhs-page-native-v1');
|
|
const policy=mint(policyContext);
|
|
eq(policy,{version:NATIVE_UI_POLICY_VERSION,scope:'native_ui',expiresAt:25000});
|
|
eq(Object.isFrozen(policy),true);
|
|
for(const patch of [
|
|
{sessionOrigin:'https://xiaohongshu.com'},{sessionOrigin:'http://127.0.0.1'},
|
|
{pageUrl:'https://evil.invalid/explore'},{pageUrl:official+'/message'},
|
|
{now:NaN},{now:Infinity},{now:'10000'},{kind:'evaluate'},
|
|
{selector:'#comment-input'},{actualRoots:[]},{observed:null},
|
|
{actualRoots:[{...searchActual,native_context:{search_input:false}}]},
|
|
]) eq(beginNativeUiRequestPolicy({...policyContext,...patch}),null);
|
|
eq(evaluatePlatformRequest({...nativeRequest,policy}),allowed);
|
|
denied({...nativeRequest,policy:{...policy}},'inactive_policy');
|
|
denied({...nativeRequest,policy:null},'missing_policy');
|
|
endPlatformRequestPolicy(policy);
|
|
denied({...nativeRequest,policy},'inactive_policy');
|
|
});
|
|
|
|
await group('native page POST permits only exact first-party HTTPS main-frame xhr or fetch',()=>{
|
|
const policy=mint(policyContext);
|
|
const start=assertions;
|
|
for(const host of ['www.xiaohongshu.com','xiaohongshu.com','as.xiaohongshu.com',
|
|
'apm-fe.xiaohongshu.com','so.xiaohongshu.com','nested.fixture.xiaohongshu.com']){
|
|
for(const resourceType of ['xhr','fetch']){
|
|
const request={...nativeRequest,policy,resourceType,url:`https://${host}/unknown-native-page-traffic?fixture=1`};
|
|
eq(evaluatePlatformRequest(request),allowed);
|
|
eq(matchPlatformRequest(request),{policy_version:NATIVE_UI_POLICY_VERSION,policy_rule:'native_page_traffic'});
|
|
}
|
|
}
|
|
for(const url of ['http://as.xiaohongshu.com/native','https://u:p@as.xiaohongshu.com/native',
|
|
'https://as.xiaohongshu.com:8443/native','https://as.xiaohongshu.com.evil.invalid/native',
|
|
'https://evilxiaohongshu.com/native','https://evil.invalid/xiaohongshu.com/native',
|
|
'https://xiaohongshu.com@evil.invalid/native','https://xiaohongshu.com./native',
|
|
'http://127.0.0.1/native','https://127.0.0.1/native','file:///fixture','data:text/plain,fixture','invalid']){
|
|
denied({...nativeRequest,policy,url});
|
|
}
|
|
for(const resourceType of ['document','script','stylesheet','image','font','media','websocket','eventsource','other','',null]){
|
|
const request={...nativeRequest,policy,resourceType};
|
|
denied(request,'resource_type_not_allowed');
|
|
eq(evaluatePlatformRequest(request).expected_resource_types,['xhr','fetch']);
|
|
}
|
|
for(const method of ['GET','HEAD','OPTIONS','PUT','PATCH','DELETE','CONNECT','TRACE','post','',null]){
|
|
denied({...nativeRequest,policy,method},'method_not_allowed');
|
|
}
|
|
denied({...nativeRequest,policy,activeMainFrame:false},'not_main_frame');
|
|
denied({...nativeRequest,policy,automating:false},'automation_inactive');
|
|
denied({...nativeRequest,policy,sessionOrigin:'https://evil.invalid'},'session_out_of_scope');
|
|
denied({...nativeRequest,policy,pageUrl:'https://evil.invalid/search_result'},'page_out_of_scope');
|
|
return {request_boundary_assertions:assertions-start,
|
|
scope:'POST policy only; ordinary GET/HEAD/OPTIONS routing is not represented as an issued native permission',
|
|
http_side_effect_free_claim:false};
|
|
});
|
|
|
|
await group('native capabilities reject early exact-expiry late invalid and revoked use',()=>{
|
|
const policy=mint(policyContext);
|
|
eq(evaluatePlatformRequest({...nativeRequest,policy,now:10000}),allowed);
|
|
eq(evaluatePlatformRequest({...nativeRequest,policy,now:24999}),allowed);
|
|
denied({...nativeRequest,policy,now:9999},'policy_not_started');
|
|
denied({...nativeRequest,policy,now:25000},'policy_expired');
|
|
denied({...nativeRequest,policy,now:25001},'policy_expired');
|
|
for(const now of [NaN,Infinity,-Infinity,'10001',null]) denied({...nativeRequest,policy,now},'invalid_time');
|
|
endPlatformRequestPolicy(policy);
|
|
denied({...nativeRequest,policy,now:10001},'inactive_policy');
|
|
});
|
|
|
|
await group('fill and ordinary reads cannot borrow page-transition authority',()=>{
|
|
const fillPolicy=mint(policyContext);
|
|
for(const pageUrl of [official+'/search_result?keyword='+encodeURIComponent(searchAction.keyword),
|
|
official+'/explore?other=1',official+'/explore#changed',noteUrl]){
|
|
denied({...nativeRequest,policy:fillPolicy,pageUrl},'page_changed');
|
|
}
|
|
for(const kind of ['read','wait','extract','scroll']){
|
|
const pageUrl=official+'/search_result?keyword=fixture';
|
|
const policy=mint({kind,pageUrl,actualRoots:[readRoot],sessionOrigin:official,now:10000});
|
|
eq(evaluatePlatformRequest({...nativeRequest,policy,pageUrl}),allowed);
|
|
for(const changed of [pageUrl+'&page=2',pageUrl+'#changed',official+'/search_result_ai?keyword=fixture',noteUrl]){
|
|
denied({...nativeRequest,policy,pageUrl:changed},'page_changed');
|
|
}
|
|
}
|
|
});
|
|
|
|
await group('Enter allows only the selected keyword search-result transition',()=>{
|
|
const keyword='装修 案例';
|
|
const policy=mint({...policyContext,kind:'press',key:'Enter',keyword});
|
|
eq(evaluatePlatformRequest({...nativeRequest,policy}),allowed);
|
|
for(const pathname of ['/search_result','/search_result_ai']){
|
|
for(const encoded of [encodeURIComponent(keyword),encodeURIComponent(encodeURIComponent(keyword))]){
|
|
eq(evaluatePlatformRequest({...nativeRequest,policy,pageUrl:official+pathname+'?keyword='+encoded}),allowed);
|
|
}
|
|
}
|
|
for(const pageUrl of [official+'/search_result?keyword=other',official+'/search_result',
|
|
official+'/search_result?keyword='+encodeURIComponent(keyword)+'&keyword=other',
|
|
official+'/explore?keyword='+encodeURIComponent(keyword),noteUrl,official+'/message',
|
|
'https://www.xiaohongshu.com.evil.invalid/search_result?keyword='+encodeURIComponent(keyword)]){
|
|
denied({...nativeRequest,policy,pageUrl});
|
|
}
|
|
denied({...nativeRequest,policy,pageUrl:official+'/search_result?keyword='+encodeURIComponent(keyword),now:25000},'policy_expired');
|
|
});
|
|
|
|
await group('note click permits only its bound public note identity transition',()=>{
|
|
const policy=mint({...noteAction,sessionOrigin:official,now:10000});
|
|
eq(evaluatePlatformRequest({...nativeRequest,policy,pageUrl:noteAction.pageUrl}),allowed);
|
|
for(const pageUrl of [noteUrl,official+'/search_result/'+noteId,official+'/explore/'+noteId+'?from=fixture']){
|
|
eq(evaluatePlatformRequest({...nativeRequest,policy,pageUrl}),allowed);
|
|
}
|
|
for(const pageUrl of [official+'/explore/'+otherNoteId,official+'/search_result/'+otherNoteId,
|
|
noteAction.pageUrl+'&changed=1',official+'/search_result?keyword=other',official+'/explore',
|
|
official+'/user/profile/'+noteId,official+'/search_result_ai/'+noteId]){
|
|
denied({...nativeRequest,policy,pageUrl},'page_changed');
|
|
}
|
|
});
|
|
|
|
await group('issued policy stores immutable source facts and never accepts fabricated contracts',()=>{
|
|
const context=structuredClone(policyContext),policy=mint(context);
|
|
context.pageUrl=official+'/search_result?keyword=other';
|
|
context.keyword='other';context.actualRoots[0].native_context.search_input=false;
|
|
eq(evaluatePlatformRequest({...nativeRequest,policy}),allowed);
|
|
denied({...nativeRequest,policy,pageUrl:context.pageUrl},'page_changed');
|
|
for(const fake of [{...policy},{version:NATIVE_UI_POLICY_VERSION,scope:'native_ui',expiresAt:25000},
|
|
{version:NATIVE_UI_POLICY_VERSION,scope:'search',expiresAt:25000},
|
|
{execution_profile:NATIVE_UI_PROFILE,execution_contract:NATIVE_UI_CONTRACT}]){
|
|
denied({...nativeRequest,policy:fake},'inactive_policy');
|
|
}
|
|
});
|
|
|
|
await group('real loopback Edge DOM produces restricted search link and ordinary-root facts',async()=>{
|
|
const {inspectElements}=await import('./browser-worker.mjs');
|
|
const {chromium}=await import('playwright');
|
|
let browser,fixtureRequests=0,unexpectedNetwork=0;
|
|
const observations=[];
|
|
const server=createServer((request,response)=>{
|
|
fixtureRequests+=1;
|
|
if(request.method!=='GET' || request.url!=='/') {response.writeHead(404).end();return;}
|
|
response.writeHead(200,{'content-type':'text/html; charset=utf-8'}).end('<!doctype html><title>Local native UI DOM fixture</title><p>Loopback only</p>');
|
|
});
|
|
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
|
|
const loopback=`http://127.0.0.1:${server.address().port}`;
|
|
try{
|
|
browser=await chromium.launch({channel:process.platform==='win32' ? 'msedge' : 'chromium',headless:true});
|
|
const context=await browser.newContext({viewport:{width:1280,height:840},serviceWorkers:'block'});
|
|
await context.route('**/*',async route=>{
|
|
const url=new URL(route.request().url());
|
|
if(url.origin!==loopback){unexpectedNetwork+=1;await route.abort();return;}
|
|
await route.continue();
|
|
});
|
|
const page=await context.newPage();
|
|
await page.goto(loopback+'/',{waitUntil:'domcontentloaded'});
|
|
const render=body=>page.setContent(`<!doctype html><html><head><title>Local native UI contract fixture</title><style>body{font:18px sans-serif;margin:24px}input,a,button,section,textarea{margin:8px;padding:8px}section{border:1px solid #888;min-height:60px}a{display:inline-block}</style></head><body><h1>Local native UI DOM fixture</h1><p>Only metadata is inspected; no search or link is activated.</p>${body}</body></html>`);
|
|
const inspect=async(selector,nativeProfile=true)=>{
|
|
const facts=await page.locator(selector).evaluate(inspectElements,{origin:official,nativeProfile});
|
|
eq(facts.length,1);return facts[0];
|
|
};
|
|
const remember=(name,state,accepted)=>observations.push({name,accepted,
|
|
tag:state.tag,sensitive:state.sensitive,search_input:state.search_input,
|
|
readonly_root:state.readonly_root,native_context:state.native_context});
|
|
const searchFixtures=[
|
|
['known-feed-search','<input id="search-input-in-feeds" type="search" aria-label="Search notes">','#search-input-in-feeds',true],
|
|
['known-ai-search','<input id="search-input-ai" type="search" aria-label="Search notes">','#search-input-ai',true],
|
|
['generic-search-name','<input id="generic-search" type="search" aria-label="Search notes">','#generic-search',false],
|
|
['comment-input','<textarea id="comment-input" aria-label="Comment"></textarea>','#comment-input',false],
|
|
['post-form-search','<form method="post"><input id="search-input-in-feeds" type="search" aria-label="Search notes"></form>','#search-input-in-feeds',false],
|
|
['editable-search','<div contenteditable="true"><input id="search-input-in-feeds" type="search" aria-label="Search notes"></div>','#search-input-in-feeds',false],
|
|
['button-role-search','<input id="search-input-in-feeds" type="search" role="button" aria-label="Search notes">','#search-input-in-feeds',false],
|
|
['checkbox-role-search','<input id="search-input-in-feeds" type="search" role="checkbox" aria-label="Search notes">','#search-input-in-feeds',false],
|
|
['button-ancestor-search','<div role="button"><input id="search-input-in-feeds" type="search" aria-label="Search notes"></div>','#search-input-in-feeds',false],
|
|
['searchbox-role-search','<input id="search-input-in-feeds" type="search" role="searchbox" aria-label="Search notes">','#search-input-in-feeds',true],
|
|
];
|
|
for(const [name,html,selector,accepted] of searchFixtures){
|
|
await render(html);const state=await inspect(selector);
|
|
eq(state.native_context.search_input,accepted,name);
|
|
const action={...searchAction,selector,actualRoots:[state]};
|
|
if(accepted){eq(assertNativeUiAction(action),true);eq(assertNativeUiAction({...action,kind:'press',key:'Enter'}),true);}
|
|
else rejectsAction(action);
|
|
remember(name,state,accepted);
|
|
}
|
|
const linkFixtures=[
|
|
['standard-note',`<a id="fixture" href="${noteUrl}">Synthetic note</a>`,true],
|
|
['self-target-note',`<a id="fixture" target="_self" href="${noteUrl}">Synthetic note</a>`,true],
|
|
['download-note',`<a id="fixture" download href="${noteUrl}">Synthetic note</a>`,false],
|
|
['blank-target-note',`<a id="fixture" target="_blank" href="${noteUrl}">Synthetic note</a>`,false],
|
|
['named-target-note',`<a id="fixture" target="fixture-window" href="${noteUrl}">Synthetic note</a>`,false],
|
|
['editable-note',`<a id="fixture" contenteditable="true" href="${noteUrl}">Synthetic note</a>`,false],
|
|
['plaintext-editable-note',`<div contenteditable="plaintext-only"><a id="fixture" href="${noteUrl}">Synthetic note</a></div>`,false],
|
|
['form-note',`<form><a id="fixture" href="${noteUrl}">Synthetic note</a></form>`,false],
|
|
['button-role-note',`<a id="fixture" role="button" href="${noteUrl}">Synthetic note</a>`,false],
|
|
['checkbox-role-note',`<a id="fixture" role="checkbox" href="${noteUrl}">Synthetic note</a>`,false],
|
|
['switch-role-note',`<a id="fixture" role="switch" href="${noteUrl}">Synthetic note</a>`,false],
|
|
['menuitem-role-note',`<a id="fixture" role="menuitem" href="${noteUrl}">Synthetic note</a>`,false],
|
|
['button-ancestor-note',`<div role="button"><a id="fixture" href="${noteUrl}">Synthetic note</a></div>`,false],
|
|
['checkbox-ancestor-note',`<div role="checkbox"><a id="fixture" href="${noteUrl}">Synthetic note</a></div>`,false],
|
|
['link-role-note',`<a id="fixture" role="link" href="${noteUrl}">Synthetic note</a>`,true],
|
|
];
|
|
for(const [name,html,accepted] of linkFixtures){
|
|
await render(html);const state=await inspect('#fixture');
|
|
eq(state.native_context.note_link,accepted,name);
|
|
const action={...noteAction,selector:'#fixture',actualRoots:[state]};
|
|
if(accepted) eq(assertNativeUiAction(action),true);else rejectsAction(action);
|
|
remember(name,state,accepted);
|
|
}
|
|
const rootFixtures=[
|
|
['ordinary-section','<section id="fixture">Plain result text</section>',true],
|
|
['editable-section','<section id="fixture" contenteditable="true">Plain result text</section>',false],
|
|
['plaintext-editable-section','<section id="fixture" contenteditable="plaintext-only">Plain result text</section>',false],
|
|
['inherited-editable-section','<div contenteditable="true"><section id="fixture">Plain result text</section></div>',false],
|
|
['button-root','<button id="fixture">Load data</button>',false],
|
|
['button-role-root','<section id="fixture" role="button">Plain result text</section>',false],
|
|
['checkbox-role-root','<section id="fixture" role="checkbox">Plain result text</section>',false],
|
|
['button-ancestor-root','<div role="button"><section id="fixture">Plain result text</section></div>',false],
|
|
['checkbox-ancestor-root','<div role="checkbox"><section id="fixture">Plain result text</section></div>',false],
|
|
['form-root','<form><section id="fixture">Plain result text</section></form>',false],
|
|
['anchor-root',`<a id="fixture" href="${noteUrl}">Synthetic note</a>`,false],
|
|
];
|
|
for(const [name,html,accepted] of rootFixtures){
|
|
await render(html);const state=await inspect('#fixture');
|
|
eq(state.native_context.read_root,accepted,name);
|
|
const action={kind:'read',pageUrl:noteUrl,selector:'#fixture',actualRoots:[state]};
|
|
if(accepted) eq(assertNativeUiAction(action),true);else rejectsAction(action,'SENSITIVE_OUTPUT');
|
|
remember(name,state,accepted);
|
|
}
|
|
await render('<input id="search-input-in-feeds" type="search" aria-label="Search notes"><section id="fixture">Plain result text</section>');
|
|
eq(Object.hasOwn(await inspect('#fixture',false),'native_context'),false,'legacy inspection omits the new metadata');
|
|
const screenshot=path.join(output,'loopback-native-dom.png');
|
|
await page.screenshot({path:screenshot,fullPage:true});
|
|
const png=await fs.readFile(screenshot);eq(png.subarray(1,4).toString(),'PNG');
|
|
eq(unexpectedNetwork,0,'synthetic links are never navigated or fetched');
|
|
browserFixture={browser:process.platform==='win32' ? 'Edge' : 'Chromium',origin:'loopback only',
|
|
inspected_cases:observations.length,fixture_requests:fixtureRequests,unexpected_network_attempts:unexpectedNetwork,
|
|
actual_png:true,screenshot,worker_native_profile_end_to_end:false};
|
|
await fs.writeFile(path.join(output,'loopback-dom-observations.json'),JSON.stringify(observations,null,2));
|
|
return browserFixture;
|
|
}finally{
|
|
if(browser) await browser.close();
|
|
await new Promise(resolve=>server.close(resolve));
|
|
}
|
|
});
|
|
|
|
await group('real legacy worker DTOs omit native fields and reject profile overrides before navigation',async()=>{
|
|
const {dispatch}=await import('./browser-worker.mjs');
|
|
const task='native-ui-profile-legacy-shape';
|
|
let requests=0,opened=false;
|
|
const server=createServer((request,response)=>{
|
|
requests+=1;
|
|
response.writeHead(200,{'content-type':'text/html; charset=utf-8'}).end('<!doctype html><title>Local legacy shape fixture</title><section id="local-read">Loopback only</section>');
|
|
});
|
|
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
|
|
const entry=`http://127.0.0.1:${server.address().port}/`;
|
|
const omitted=value=>{
|
|
eq(Object.hasOwn(value,'execution_profile'),false);
|
|
eq(Object.hasOwn(value,'execution_contract'),false);
|
|
};
|
|
const rejectedAsync=async(run,code)=>{
|
|
await assert.rejects(run,error=>error.code===code);assertions+=1;
|
|
};
|
|
try{
|
|
await rejectedAsync(()=>dispatch('open',{task_id:task,account_key:'isolated-native-profile-shape',entry_url:entry,
|
|
execution_profile:NATIVE_UI_PROFILE}),'EXECUTION_PROFILE_INVALID');
|
|
eq(requests,0,'native mode never introduces a loopback origin override');
|
|
const initial=await dispatch('open',{task_id:task,account_key:'isolated-native-profile-shape',entry_url:entry});opened=true;
|
|
omitted(initial);
|
|
omitted(await dispatch('ready',{task_id:task,identity_label:'Local legacy shape'}));
|
|
omitted(await dispatch('status',{task_id:task}));
|
|
const capture=await dispatch('capture',{task_id:task});omitted(capture);
|
|
eq(capture.elements.some(element=>element.selector==='#local-read'),true);
|
|
const action=await dispatch('action',{task_id:task,kind:'read',selector:'#local-read'});
|
|
omitted(action);omitted(action.before);omitted(action.after);
|
|
const verifyParams={task_id:task,schema_version:2,entry_url:entry,
|
|
steps:[{kind:'read',selector:'#local-read',output_name:'note'}],parameters:{},expected_outputs:{note:'Loopback only'}};
|
|
const previous=requests;
|
|
await rejectedAsync(()=>dispatch('verify',{...verifyParams,execution_profile:NATIVE_UI_PROFILE,
|
|
execution_contract:{...NATIVE_UI_CONTRACT}}),'EXECUTION_PROFILE_MISMATCH');
|
|
await rejectedAsync(()=>dispatch('verify',{...verifyParams,execution_contract:{...NATIVE_UI_CONTRACT}}),'EXECUTION_PROFILE_MISMATCH');
|
|
await rejectedAsync(()=>dispatch('verify',{...verifyParams,execution_profile:'unknown-mode'}),'EXECUTION_PROFILE_INVALID');
|
|
eq(requests,previous,'profile failures cannot navigate or replay the legacy session');
|
|
const verified=await dispatch('verify',verifyParams);
|
|
eq(verified.passed,true);omitted(verified);
|
|
ok(verified.evidence.length>0);for(const evidence of verified.evidence) omitted(evidence);
|
|
return {fixture_requests:requests,legacy_worker_native_fields_absent:true,
|
|
mismatched_or_unknown_profile_before_navigation:true,native_loopback_override:false};
|
|
}finally{
|
|
if(opened) await dispatch('close',{task_id:task}).catch(()=>{});
|
|
await new Promise(resolve=>server.close(resolve));
|
|
}
|
|
});
|
|
|
|
for(const policy of nativePolicies) endPlatformRequestPolicy(policy);
|
|
await report({status:'pure profile/action/request contract checks complete; worker wiring and platform execution are separate'});
|
|
console.log(JSON.stringify({passed:results.length,assertions,output,
|
|
official_browser_navigation:false,live_platform_requests:0,application_api_calls:0,model_calls:0}));
|