@antprofuse/saddle-islands-tools 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/tools/cli/main.py +3 -3
- package/tools/cli/minifish.mjs +10 -4
- package/tools/docs/iteration.md +2 -0
- package/tools/docs/minifish-testing.md +8 -0
- package/tools/modules/minifish/checks/package.test.mjs +1 -1
- package/tools/modules/minifish/compiler/compile-check.mjs +65 -0
- package/tools/modules/minifish/harness/browser.mjs +2 -1
- package/tools/modules/minifish/harness/jsapi.mjs +31 -3
- package/tools/modules/workflow/compile_gate.py +51 -0
- package/tools/modules/workflow/iteration.py +4 -0
package/package.json
CHANGED
package/tools/cli/main.py
CHANGED
|
@@ -22,12 +22,12 @@ def main():
|
|
|
22
22
|
if len(sys.argv) > 1 and sys.argv[1] == 'minifish':
|
|
23
23
|
args = sys.argv[2:]
|
|
24
24
|
if not args or args == ['--help']:
|
|
25
|
-
print('minifish build|package|plugin|batch|run|report <explicit options>; see tools/docs/minifish-testing.md')
|
|
25
|
+
print('minifish compile-check|build|package|plugin|batch|run|report <explicit options>; see tools/docs/minifish-testing.md')
|
|
26
26
|
return 0
|
|
27
27
|
command = args[0]
|
|
28
|
-
if command not in ('build', 'package', 'plugin', 'batch', 'run', 'report'):
|
|
28
|
+
if command not in ('compile-check', 'build', 'package', 'plugin', 'batch', 'run', 'report'):
|
|
29
29
|
raise SystemExit('Unknown minifish command: ' + command)
|
|
30
|
-
entry = 'minifish.mjs' if command in ('build', 'package', 'plugin') else 'minifish.py'
|
|
30
|
+
entry = 'minifish.mjs' if command in ('compile-check', 'build', 'package', 'plugin') else 'minifish.py'
|
|
31
31
|
return subprocess.call(['node' if entry.endswith('.mjs') else sys.executable, str(Path(__file__).with_name(entry)), *args])
|
|
32
32
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
33
33
|
parser.add_argument('command', choices=['identity', 'backend', 'minifish', 'iteration'], help='semantic identities, backend tests or Minifish tests')
|
package/tools/cli/minifish.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {createRequire,registerHooks} from 'node:module';
|
|
|
8
8
|
import {pathToFileURL} from 'node:url';
|
|
9
9
|
const [command,...args]=process.argv.slice(2),flags={};
|
|
10
10
|
for(let i=0;i<args.length;i+=2){assert(args[i]?.startsWith('--')&&args[i+1],'Expected named option and value');assert(!Object.hasOwn(flags,args[i].slice(2)),'Duplicate option');flags[args[i].slice(2)]=args[i+1];}
|
|
11
|
-
assert(['build','package','plugin'].includes(command),'Usage: minifish.mjs build|package|plugin --project-root ROOT --config FILE --output DIR --definition CASE | --plan JSON [--tool-node-modules DIR]');
|
|
11
|
+
assert(['compile-check','build','package','plugin'].includes(command),'Usage: minifish.mjs build|package|plugin --project-root ROOT --config FILE --output DIR --definition CASE | --plan JSON [--tool-node-modules DIR]');
|
|
12
12
|
assert(flags['project-root']&&flags.config&&flags.output,'Explicit project root, config and output required');
|
|
13
13
|
if(!process.execArgv.includes('--experimental-vm-modules')){const child=spawnSync(process.execPath,['--experimental-vm-modules',process.argv[1],...process.argv.slice(2)],{stdio:'inherit',env:process.env});process.exit(child.status??1);}
|
|
14
14
|
const packageRoot=path.resolve(import.meta.dirname,'../..');
|
|
@@ -25,14 +25,20 @@ if(flags['tool-node-modules']){
|
|
|
25
25
|
registerHooks({resolve(specifier,context,next){if(['typescript','@playwright/test'].includes(specifier)){let file=require.resolve(specifier);if(specifier==='@playwright/test')file=path.join(path.dirname(file),'index.mjs');return {url:pathToFileURL(file).href,shortCircuit:true};}return next(specifier,context);}});
|
|
26
26
|
}
|
|
27
27
|
registerHooks({resolve(specifier,context,next){const prefix='@antprofuse/saddle-islands-tools/';if(specifier.startsWith(prefix)){const file=path.resolve(packageRoot,specifier.slice(prefix.length));assert(file.startsWith(packageRoot+path.sep),'Public package import escaped');return {url:pathToFileURL(file).href,shortCircuit:true};}return next(specifier,context);}});
|
|
28
|
-
const {buildCase,buildPlanPackage}=await import('../modules/minifish/compiler/build-case.mjs');
|
|
29
28
|
const options={projectRoot:path.resolve(flags['project-root']),config:path.resolve(flags.config)};
|
|
30
29
|
let result;
|
|
31
|
-
if(command==='
|
|
30
|
+
if(command==='compile-check'){
|
|
31
|
+
const {compileCheck}=await import('../modules/minifish/compiler/compile-check.mjs');
|
|
32
|
+
result=await compileCheck({...options,manifest:flags.manifest,output:flags.output});
|
|
33
|
+
if(result.failed)process.exitCode=3;
|
|
34
|
+
}else if(command==='plugin'){
|
|
32
35
|
const {loadProject}=await import('../modules/minifish/compiler/config.mjs');
|
|
33
36
|
const project=await loadProject(options.projectRoot,options.config);
|
|
34
37
|
const {validatePluginEvents}=await import(pathToFileURL(await project.resolveAdapter(project.config.pluginAdapter)).href);
|
|
35
38
|
const {buildPluginArtifact}=await import('../modules/minifish/harness/build-plugin-artifact.mjs');
|
|
36
39
|
result=await buildPluginArtifact({planFile:flags.plan,output:flags.output,modules:flags['sdk-node-modules'],source:flags.source,repositoryRoot:path.resolve(project.root,project.config.repositoryRoot),validatePluginEvents});
|
|
37
|
-
}else
|
|
40
|
+
}else{
|
|
41
|
+
const {buildCase,buildPlanPackage}=await import('../modules/minifish/compiler/build-case.mjs');
|
|
42
|
+
result=command==='build'?await buildCase(flags.definition,flags.output,options):await buildPlanPackage(JSON.parse(await readFile(flags.plan,'utf8')),flags.output,options);
|
|
43
|
+
}
|
|
38
44
|
console.log(JSON.stringify(result));
|
package/tools/docs/iteration.md
CHANGED
|
@@ -61,3 +61,5 @@ python3 tools/cli/main.py iteration archive \
|
|
|
61
61
|
`kind`为reports或records。目标在持久迭代目录下,以新内容版本命名,不覆盖原件。逐文件摘要校验复制一致,拒绝符号链接,检查静态HTML/CSS引用是否缺失或逃出归档目录;保存归档清单到records/archives。不会生成ZIP,不删除源目录。源需已停止写入;先后摘要不同则拒绝交付。
|
|
62
62
|
|
|
63
63
|
不执行或解析JavaScript生成的动态URL、不下载外部HTTP资源,也不证明图片能够解码或预览服务可达。最终检查点须实际打开用户预览链接、检查截图;动态资源在浏览器中核验。服务与持久文件分别管理。结束CoTask前归档并核验,不能结束后才尝试复制已回收文件。
|
|
64
|
+
|
|
65
|
+
MT(minifish-test)的stage-gate另需 `--compile-result`,指向当前候选通过公开 `minifish compile-check` 生成的全量编译收据。它与Git路径范围检查共同构成交付门禁,不启动产品;命令及清单格式见minifish-testing.md。
|
|
@@ -42,3 +42,11 @@ Legacy component-host integrations must supply an explicit `repositoryRoot` to `
|
|
|
42
42
|
可选 `carrier` 默认为 `semantic-root`。`source-content-group` 在语义root内对可见文字/图像取最低公共元素祖先;`owned-text-block` 在该组内要求唯一不属于其他语义目标的文字分支。选择与期待样式、class、原型HTML标签无关,候选缺失或歧义明确失败;不适用的拓扑不得借此猜测观察目标。`viewportInsets` 可声明 left/right/top/bottom 到业务frame视口的设计长度,返回采样记录包含原root盒及是否选中root。
|
|
43
43
|
|
|
44
44
|
按压快照目前只支持 `semantic-root`,其他carrier在按下前拒绝,不能释放后采样冒充按住状态。这是明确能力限制,不能默默降级。内容分组不覆盖背景装饰/无文字无图像部件或Shadow DOM;相应责任另用适用的公开观察合同。
|
|
45
|
+
|
|
46
|
+
### 全量定义编译门禁
|
|
47
|
+
|
|
48
|
+
`minifish compile-check --tool-source TRUSTED_MANIFEST --project-root PROJECT --config PROJECT/minifish.config.json --manifest PROJECT/catalog/case-map.json --output NEW_EXTERNAL_DIR --tool-node-modules COMPILER/node_modules` 使用项目已声明的编译适配器,逐项检查当前活动定义、来源责任和准备祖先,不启动产品或模拟器,不生成独立运行包。
|
|
49
|
+
|
|
50
|
+
项目配置的 `caseManifest` 指定唯一正式活动清单,命令的 `--manifest` 必须与其一致。活动清单包含 `cases`(definition、sourceCaseIds;定义路径为编译条目的唯一标识)及可选 `pending`(sourceCaseId、reason)。活动与非执行责任必须互斥、唯一且覆盖配置中的完整catalog。历史未激活定义不隐式加入;不可执行原因仍需Owner审核,不计编译通过或业务通过。输入须为干净候选提交内的文件。结果 `compile-result.json` 绑定候选提交、项目树、清单和目录摘要、已验证工具身份,并含逐case结果;有编译错误返回非零。
|
|
51
|
+
|
|
52
|
+
MT使用 `iteration stage-gate` 时必须提供 `--compile-result NEW_EXTERNAL_DIR/compile-result.json`,缺失、过期、漏项、身份不匹配或编译失败均拒绝交付;其他阶段不增加此检查。
|
|
@@ -17,7 +17,7 @@ async function fixture(){
|
|
|
17
17
|
await writeFile(path.join(project,'harness/regions/alternate.mjs'),"import {semanticLocator} from '@antprofuse/saddle-islands-tools/tools/modules/minifish/harness/semantic-anchor.mjs';export function locate(root,name){return semanticLocator(root,name)};export function assertTarget(){}");
|
|
18
18
|
return {root,project,configFile,config,loaded:await loadProject(project,configFile)};
|
|
19
19
|
}
|
|
20
|
-
test('alternate project static closure records tool/adapter separately and copies no unrelated region',async()=>{const f=await fixture();try{const output=path.join(f.root,'artifact'),records=await packageRunner(f.loaded,output,'regions/alternate.mjs');assert(records.some(r=>r.kind==='tool'&&r.path==='harness/semantic-anchor.mjs'));for(const name of ['visual-expectation.mjs','visual-carrier.mjs']){assert(records.some(r=>r.kind==='tool'&&r.path==='harness/'+name));assert((await readFile(path.join(output,'tools/harness',name),'utf8')).length>0);}assert(records.some(r=>r.kind==='adapter'&&r.path==='harness/regions/alternate.mjs'));assert(records.every(r=>/^[a-f0-9]{64}$/.test(r.sha256)));assert(!records.some(r=>r.path.includes('T1')));assert((await readFile(path.join(output,'adapters/harness/regions/alternate.mjs'),'utf8')).includes('../../../tools/harness/semantic-anchor.mjs'));}finally{await rm(f.root,{recursive:true,force:true});}});
|
|
20
|
+
test('alternate project static closure records tool/adapter separately and copies no unrelated region',async()=>{const f=await fixture();try{const output=path.join(f.root,'artifact'),records=await packageRunner(f.loaded,output,'regions/alternate.mjs');assert(records.some(r=>r.kind==='tool'&&r.path==='harness/semantic-anchor.mjs'));for(const name of ['visual-expectation.mjs','visual-carrier.mjs','jsapi.mjs','browser.mjs']){assert(records.some(r=>r.kind==='tool'&&r.path==='harness/'+name));assert((await readFile(path.join(output,'tools/harness',name),'utf8')).length>0);}assert(records.some(r=>r.kind==='adapter'&&r.path==='harness/regions/alternate.mjs'));assert(records.every(r=>/^[a-f0-9]{64}$/.test(r.sha256)));assert(!records.some(r=>r.path.includes('T1')));assert((await readFile(path.join(output,'adapters/harness/regions/alternate.mjs'),'utf8')).includes('../../../tools/harness/semantic-anchor.mjs'));}finally{await rm(f.root,{recursive:true,force:true});}});
|
|
21
21
|
for(const [name,code] of [['unknown package',"import 'fs';"],['dependency traversal',"import '../../outside.mjs';"],['direct DOM',"export const read=root=>root.evaluate(()=>1);"],['dynamic import',"export const read=()=>import('./other.mjs');"]])test(name+' is rejected from the closed observation dependency graph',async()=>{const f=await fixture();try{await writeFile(path.join(f.project,'harness/regions/alternate.mjs'),code);await writeFile(path.join(f.project,'outside.mjs'),'export const leaked=true;');await assert.rejects(packageRunner(f.loaded,path.join(f.root,'artifact'),'regions/alternate.mjs'));}finally{await rm(f.root,{recursive:true,force:true});}});
|
|
22
22
|
test('symlink aliases, undeclared module, missing config, and package output are rejected',async()=>{const f=await fixture();try{const module=path.join(f.project,'harness/regions/alternate.mjs');await rm(module);await writeFile(path.join(f.project,'harness/other.mjs'),'export const safe=true;');await symlink('../other.mjs',module);await assert.rejects(packageRunner(f.loaded,path.join(f.root,'alias'),'regions/alternate.mjs'),/Symlink/);await assert.rejects(packageRunner(f.loaded,path.join(f.root,'unknown'),'regions/unknown.mjs'),/declared module/);await assert.rejects(loadProject(f.project,path.join(f.project,'missing.json')));await assert.rejects(packageRunner(f.loaded,path.join(f.project,'artifact'),'regions/alternate.mjs'),/outside/);}finally{await rm(f.root,{recursive:true,force:true});}});
|
|
23
23
|
test('generic composition preserves exact preparation and original ID mappings without project names',()=>{const noop=()=>{},policy={validateSemanticTargets:noop,supplementalDefinition:()=>null,officialTitle:()=>false,validatePreparationOverrides:noop,validateFixtureTypes:noop,validateSpecialBoundaries:noop,validateIndependentFixture:noop,validatePacketPlan:noop,validatePluginEvents:noop,validatePluginAssertionIdentity:noop,validateObservationModuleName:noop};const catalog={cases:[{id:'alternate / page / value / content',title:'value'}]},definition={caseId:catalog.cases[0].id,ancestors:['open'],basis:[{path:'alternate/source.yaml'}],preparation:{},assertions:[{id:'value',kind:'text',target:'alternate / page / value',expected:'42',count:1}]},fragment={id:'open',inputs:{query:{key:'42'}},boundaries:[],actions:[{kind:'open-page'}]};const plan=compileCase(catalog,definition,[fragment],policy);assert.deepEqual(plan.inputs,fragment.inputs);assert.deepEqual(plan.assertions,definition.assertions);assert.equal(plan.case.id,definition.caseId);assert.throws(()=>compileCase(catalog,{...definition,ancestors:['unknown']},[fragment],policy),/missing preparation/);});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {readFile,realpath,mkdir,writeFile} from 'node:fs/promises';
|
|
4
|
+
import {execFileSync} from 'node:child_process';
|
|
5
|
+
import {createHash} from 'node:crypto';
|
|
6
|
+
import {pathToFileURL} from 'node:url';
|
|
7
|
+
import {performance} from 'node:perf_hooks';
|
|
8
|
+
import {loadProject,checkOutput} from './config.mjs';
|
|
9
|
+
import {toolIdentity} from './tool-identity.mjs';
|
|
10
|
+
const sha=x=>createHash('sha256').update(x).digest('hex');
|
|
11
|
+
export function activeCases(manifest,catalog){
|
|
12
|
+
assert(Array.isArray(manifest.cases)&&manifest.cases.length,'Nonempty active case manifest required');
|
|
13
|
+
const files=new Set(),covered=new Set();
|
|
14
|
+
const official=new Set(catalog.cases.map(c=>c.id));
|
|
15
|
+
assert.equal(official.size,catalog.cases.length,'Duplicate catalog responsibility');
|
|
16
|
+
for(const row of manifest.cases){
|
|
17
|
+
assert(typeof row.definition==='string'&&row.definition&&!files.has(row.definition),'Duplicate/missing definition');files.add(row.definition);
|
|
18
|
+
assert(Array.isArray(row.sourceCaseIds)&&row.sourceCaseIds.length,'Source responsibilities required');
|
|
19
|
+
for(const id of row.sourceCaseIds){assert(official.has(id)&&!covered.has(id),'Unknown/duplicate source responsibility');covered.add(id);}
|
|
20
|
+
}
|
|
21
|
+
for(const row of manifest.pending??[]){assert(typeof row.reason==='string'&&row.reason.trim(),'Nonexecutable responsibility requires reason');assert(official.has(row.sourceCaseId)&&!covered.has(row.sourceCaseId),'Unknown/duplicate pending responsibility');covered.add(row.sourceCaseId);}
|
|
22
|
+
assert.equal(covered.size,official.size,'Active manifest omits catalog responsibilities');
|
|
23
|
+
return manifest.cases;
|
|
24
|
+
}
|
|
25
|
+
export async function compileCheck(options){
|
|
26
|
+
const started=performance.now(),project=await loadProject(options.projectRoot,options.config),{root,config}=project;
|
|
27
|
+
const repo=await realpath(path.resolve(root,config.repositoryRoot));
|
|
28
|
+
const git=(...args)=>execFileSync('git',['-C',repo,...args],{encoding:'utf8'}).trim();
|
|
29
|
+
const head=git('rev-parse','HEAD');git('diff','--exit-code','HEAD','--');
|
|
30
|
+
const projectPath=path.relative(repo,root).split(path.sep).join('/');
|
|
31
|
+
assert(projectPath&&!projectPath.startsWith('../'),'Project must belong to Git repository');
|
|
32
|
+
const tracked=new Set(git('ls-files','-z','--',projectPath).split('\0'));
|
|
33
|
+
const readTracked=async file=>{const declared=path.resolve(file),full=await realpath(declared),relative=path.relative(repo,full).split(path.sep).join('/');assert.equal(full,declared,'Symlink input forbidden');assert(full.startsWith(root+path.sep),'Input escaped project');assert(tracked.has(relative),'Input must be committed: '+relative);return {relative,bytes:await readFile(full)};};
|
|
34
|
+
await readTracked(options.config);await readTracked(await project.resolveAdapter(config.compilerAdapter));
|
|
35
|
+
assert(typeof config.caseManifest==='string'&&config.caseManifest,'Project config caseManifest required');
|
|
36
|
+
assert.equal(path.resolve(options.manifest),path.resolve(root,config.caseManifest),'Use the configured complete case manifest');
|
|
37
|
+
const manifest=await readTracked(options.manifest),catalog=await readTracked(path.resolve(root,config.catalog));
|
|
38
|
+
const parsedManifest=JSON.parse(manifest.bytes),parsedCatalog=JSON.parse(catalog.bytes);
|
|
39
|
+
const rows=activeCases(parsedManifest,parsedCatalog);
|
|
40
|
+
const adapter=await import(pathToFileURL(await project.resolveAdapter(config.compilerAdapter)).href);
|
|
41
|
+
const definitionRoot=await realpath(path.resolve(root,config.definitions)),results=[];
|
|
42
|
+
for(const row of rows){
|
|
43
|
+
try{
|
|
44
|
+
const file=await realpath(path.resolve(root,row.definition));assert(file.startsWith(definitionRoot+path.sep),'Definition escaped definitions root');
|
|
45
|
+
const definition=JSON.parse((await readTracked(file)).bytes),fragments=[];
|
|
46
|
+
assert.deepEqual(definition.sourceCaseIds??[definition.caseId],row.sourceCaseIds,'Manifest/definition source IDs differ');
|
|
47
|
+
for(let dir=path.dirname(file);;dir=path.dirname(dir)){
|
|
48
|
+
try{fragments.unshift(JSON.parse((await readTracked(path.join(dir,'prepare.json'))).bytes));}catch(e){if(e.code!=='ENOENT')throw e;}
|
|
49
|
+
if(dir===definitionRoot)break;assert(dir.startsWith(definitionRoot+path.sep),'Preparation escaped root');
|
|
50
|
+
}
|
|
51
|
+
assert.deepEqual(definition.ancestors,fragments.map(f=>f.id),'Preparation ancestry mismatch');
|
|
52
|
+
await adapter.compile({projectRoot:root,config,catalog:parsedCatalog,definition,fragments});
|
|
53
|
+
results.push({id:row.definition,definition:row.definition,status:'passed'});
|
|
54
|
+
}catch(e){results.push({id:row.definition,definition:row.definition,status:'failed',error:e.message});}
|
|
55
|
+
}
|
|
56
|
+
git('diff','--exit-code','HEAD','--');assert.equal(git('rev-parse','HEAD'),head,'Candidate changed during compilation');
|
|
57
|
+
const result={schemaVersion:1,kind:'minifish-full-compile',head,projectPath,projectTree:git('rev-parse',head+':'+projectPath),
|
|
58
|
+
manifest:{path:manifest.relative,sha256:sha(manifest.bytes)},catalog:{path:catalog.relative,sha256:sha(catalog.bytes)},
|
|
59
|
+
toolIdentity:await toolIdentity(config.toolSource,path.resolve(root,config.toolSourceManifest)),
|
|
60
|
+
total:results.length,passed:results.filter(r=>r.status==='passed').length,failed:results.filter(r=>r.status==='failed').length,
|
|
61
|
+
nonExecutable:(parsedManifest.pending??[]).map(r=>({id:r.sourceCaseId,reason:r.reason})),
|
|
62
|
+
durationMs:performance.now()-started,productExecution:false,results};
|
|
63
|
+
const destination=await checkOutput(options.output,project);await mkdir(destination,{recursive:false});
|
|
64
|
+
await writeFile(path.join(destination,'compile-result.json'),JSON.stringify(result,null,2)+'\n');return result;
|
|
65
|
+
}
|
|
@@ -9,7 +9,7 @@ import {pluginPackagePlan} from './plugin-package.mjs';
|
|
|
9
9
|
import {installHostLanguage} from './host-language.mjs';
|
|
10
10
|
import {installResponseConsumption} from './response-consumption.mjs';
|
|
11
11
|
import { fulfillRpc } from './rpc-wire.mjs';
|
|
12
|
-
import { bridgePath, jsapiPath, bridgeScript, fulfillJsapi, businessJsapiOption } from './jsapi.mjs';
|
|
12
|
+
import { bridgePath, jsapiPath, jsapiDeliveryPath, acknowledgeJsapiDelivery, bridgeScript, fulfillJsapi, businessJsapiOption } from './jsapi.mjs';
|
|
13
13
|
|
|
14
14
|
// Official Lyra keeps earlier pages painted in the same simulator stack.
|
|
15
15
|
// The last non-exiting sibling is the active shell; route name alone is ambiguous.
|
|
@@ -54,6 +54,7 @@ export async function openMinifish({ page, plan, query, simulatorUrl, pageRoute,
|
|
|
54
54
|
const request = route.request(), target = new URL(request.url());
|
|
55
55
|
try {
|
|
56
56
|
if (target.origin === url.origin && target.pathname === bridgePath) return await route.fulfill({contentType:'application/javascript',body:bridgeScript(url.origin,nativeApis,Boolean(process.env.MINIFISH_LOADING_DIAGNOSTICS_DIR),officialUi,process.env.MINIFISH_CALLBACK_CORRELATION==='1')});
|
|
57
|
+
if(target.origin===url.origin&&target.pathname===jsapiDeliveryPath)return await acknowledgeJsapiDelivery(route,plan);
|
|
57
58
|
if (target.origin === url.origin && target.pathname === jsapiPath) {const wire=request.postDataJSON();callbackCorrelation?.transport(wire,request.frame());observeLocalNavigationCandidate(page,wire);if(officialUi.includes(wire.api)){plan.recordNativeUiRequest('jsapi:'+wire.api,businessJsapiOption(wire.option),wire);return await route.fulfill({status:204});}if(delegateLocalNavigation(page,wire))return await route.fulfill({status:204});return await fulfillJsapi(route, plan);}
|
|
58
59
|
if(target.origin===url.origin&&target.pathname===nativeUiResultPath){assert.equal(request.method(),'POST');const receipt=request.postDataJSON();plan.recordNativeUiResult('jsapi:'+receipt.api,businessJsapiOption(receipt.option),receipt);return await route.fulfill({status:204});}
|
|
59
60
|
if (target.origin === url.origin && target.pathname.startsWith('/lyraParams/')) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
1
2
|
import {hostDeviceFixture,configureHostDeviceMount,hostDeviceMountEvidence} from './host-device-fixture.mjs';
|
|
2
3
|
import assert from 'node:assert/strict';
|
|
3
4
|
import {nativeUiResultPath,nativeUiApis} from './native-ui-contract.mjs';
|
|
@@ -5,6 +6,9 @@ import {nativeUiResultPath,nativeUiApis} from './native-ui-contract.mjs';
|
|
|
5
6
|
// Captures native calls without reading or altering product business state.
|
|
6
7
|
export const bridgePath = '/__bill_tests__/bridge.js';
|
|
7
8
|
export const jsapiPath = '/__bill_tests__/jsapi';
|
|
9
|
+
export const jsapiDeliveryPath = '/__bill_tests__/jsapi-delivery';
|
|
10
|
+
const deliveryPlans = new WeakMap();
|
|
11
|
+
const deliveryHeader = 'x-minifish-response-id';
|
|
8
12
|
export function bridgeScript(origin, additionalApis = [], observeLoading = false, officialUi = [], observeCorrelation = false) {
|
|
9
13
|
assert(!additionalApis.includes('pageScrollTo'),'Official scroll cannot enter renderer Mock list');
|
|
10
14
|
assert(officialUi.every(api=>nativeUiApis.includes(api)),'Finite official UI delegation list');
|
|
@@ -50,7 +54,12 @@ export function bridgeScript(origin, additionalApis = [], observeLoading = false
|
|
|
50
54
|
return undefined;
|
|
51
55
|
}
|
|
52
56
|
if (!response.ok) return {error:99999,errorMessage:'test boundary rejected native call'};
|
|
53
|
-
|
|
57
|
+
const result = await response.json();
|
|
58
|
+
const token = response.headers.get(${JSON.stringify(deliveryHeader)});
|
|
59
|
+
if(!token)throw Error('Mock response missing delivery identity');
|
|
60
|
+
const receipt = await fetch(${JSON.stringify(origin+jsapiDeliveryPath)},{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({token})});
|
|
61
|
+
if(!receipt.ok)throw Error('Mock response delivery rejected');
|
|
62
|
+
return result;
|
|
54
63
|
}
|
|
55
64
|
});});`;
|
|
56
65
|
}
|
|
@@ -70,8 +79,27 @@ export async function fulfillJsapi(route, plan) {
|
|
|
70
79
|
const call=decodeNativeStorage(wire.api,businessJsapiOption(wire.option));
|
|
71
80
|
let result = await plan.receive(`jsapi:${call.api}`, call.params, wire);
|
|
72
81
|
if(wire.api==='getTinyLocalStorage'&&Object.hasOwn(result??{},'data'))result={...result,data:JSON.stringify({APDataStorage:result.data})};
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
let deliveries=deliveryPlans.get(plan);
|
|
83
|
+
if(!deliveries){deliveries=new Map();deliveryPlans.set(plan,deliveries);}
|
|
84
|
+
const token=randomUUID();
|
|
85
|
+
deliveries.set(token,{operation:`jsapi:${call.api}`,request:call.params,frame:request.frame()});
|
|
86
|
+
try{await route.fulfill({json:result,headers:{[deliveryHeader]:token}});}
|
|
87
|
+
catch(error){deliveries.delete(token);throw error;}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Browser acknowledges a fully parsed response before returning it to AppX.
|
|
91
|
+
// This records delivery, not product callback consumption; consumption stays independent.
|
|
92
|
+
export async function acknowledgeJsapiDelivery(route,plan){
|
|
93
|
+
const request=route.request();
|
|
94
|
+
assert.equal(request.method(),'POST','Delivery receipt must POST');
|
|
95
|
+
const receipt=request.postDataJSON();
|
|
96
|
+
assert.deepEqual(Object.keys(receipt).sort(),['token'],'Exact delivery receipt required');
|
|
97
|
+
const deliveries=deliveryPlans.get(plan),entry=deliveries?.get(receipt.token);
|
|
98
|
+
assert(entry,'Unknown, duplicate or failed response delivery identity');
|
|
99
|
+
assert.equal(request.frame(),entry.frame,'Response receipt must belong to original frame');
|
|
100
|
+
deliveries.delete(receipt.token);
|
|
101
|
+
plan.markDelivered(entry.operation,entry.request);
|
|
102
|
+
await route.fulfill({status:204});
|
|
75
103
|
}
|
|
76
104
|
|
|
77
105
|
// Official @ali/antcube-appx-bridge-my root/storage/boost/impl.js maps
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Bind a full MT compilation receipt to the candidate's committed active catalog."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .iteration import Invalid
|
|
6
|
+
from .diff_review import git
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def verify_compile(repo, head, bizdir, receipt_file):
|
|
10
|
+
if not receipt_file:
|
|
11
|
+
raise Invalid('MT requires --compile-result from minifish compile-check')
|
|
12
|
+
r = json.loads(Path(receipt_file).read_text())
|
|
13
|
+
project = bizdir + '/code/minifish-tests'
|
|
14
|
+
def check(ok, message):
|
|
15
|
+
if not ok:
|
|
16
|
+
raise Invalid(message)
|
|
17
|
+
check(r.get('schemaVersion') == 1 and r.get('kind') == 'minifish-full-compile', 'Invalid compile receipt')
|
|
18
|
+
check(r.get('head') == head and r.get('projectPath') == project, 'Stale or wrong-project compile receipt')
|
|
19
|
+
check(r.get('projectTree') == git(repo, 'rev-parse', head + ':' + project).decode().strip(), 'Compile tree mismatch')
|
|
20
|
+
def read_bound(key):
|
|
21
|
+
entry = r[key]
|
|
22
|
+
from pathlib import PurePosixPath
|
|
23
|
+
p = PurePosixPath(entry['path'])
|
|
24
|
+
check(not p.is_absolute() and '..' not in p.parts and entry['path'].startswith(project + '/'), 'Compile input outside project')
|
|
25
|
+
raw = git(repo, 'show', head + ':' + entry['path'])
|
|
26
|
+
check(hashlib.sha256(raw).hexdigest() == entry['sha256'], 'Compile input hash mismatch')
|
|
27
|
+
return json.loads(raw)
|
|
28
|
+
manifest, catalog = read_bound('manifest'), read_bound('catalog')
|
|
29
|
+
config = json.loads(git(repo, 'show', head + ':' + project + '/minifish.config.json'))
|
|
30
|
+
check(r['manifest']['path'] == project + '/' + config['caseManifest'], 'Unexpected active manifest')
|
|
31
|
+
check(r['catalog']['path'] == project + '/' + config['catalog'], 'Unexpected source catalog')
|
|
32
|
+
identity = r.get('toolIdentity', {})
|
|
33
|
+
check(identity.get('kind') == 'verified-content', 'Missing verified compiler identity')
|
|
34
|
+
for key, value in config['toolSource'].items():
|
|
35
|
+
check(identity.get(key) == value, 'Compiler identity mismatch: ' + key)
|
|
36
|
+
lock = git(repo, 'show', head + ':' + project + '/' + config['toolSourceManifest'])
|
|
37
|
+
check(identity.get('manifestSha256') == hashlib.sha256(lock).hexdigest(), 'Compiler manifest mismatch')
|
|
38
|
+
rows, results = manifest['cases'], r.get('results', [])
|
|
39
|
+
check(bool(rows) and len(rows) == len(results), 'Incomplete compile results')
|
|
40
|
+
check(len({x['definition'] for x in rows}) == len(rows), 'Duplicate active definition')
|
|
41
|
+
check([(x['definition'], x['definition']) for x in rows] == [(x.get('id'), x.get('definition')) for x in results], 'Compile case set mismatch')
|
|
42
|
+
official = [x['id'] for x in catalog['cases']]
|
|
43
|
+
covered = [i for row in rows for i in row['sourceCaseIds']]
|
|
44
|
+
pending = manifest.get('pending', [])
|
|
45
|
+
check(all(isinstance(x.get('reason'), str) and x['reason'].strip() for x in pending), 'Missing nonexecutable reason')
|
|
46
|
+
covered += [x['sourceCaseId'] for x in pending]
|
|
47
|
+
check(len(covered) == len(set(covered)) and len(official) == len(set(official)) and set(covered) == set(official), 'Incomplete catalog coverage')
|
|
48
|
+
check(r.get('nonExecutable') == [{'id': x['sourceCaseId'], 'reason': x['reason']} for x in pending], 'Nonexecutable decisions mismatch')
|
|
49
|
+
check(r.get('productExecution') is False, 'Not a static compilation receipt')
|
|
50
|
+
check(r.get('total') == len(rows) and r.get('passed') == len(rows) and r.get('failed') == 0 and all(x.get('status') == 'passed' for x in results), 'Full compilation did not pass')
|
|
51
|
+
return f"\n\nMT全量编译:{len(rows)}/{len(rows)} 通过;非执行责任 {len(pending)} 项保留独立处置。\n"
|
|
@@ -196,6 +196,7 @@ def main(argv=None):
|
|
|
196
196
|
parser.add_argument('--repository')
|
|
197
197
|
parser.add_argument('--base')
|
|
198
198
|
parser.add_argument('--head')
|
|
199
|
+
parser.add_argument('--compile-result', help='Full static MT compilation receipt')
|
|
199
200
|
parser.add_argument('--stage-id', action='append')
|
|
200
201
|
parser.add_argument('--registry', help='Initiator workspace persistent iterations directory')
|
|
201
202
|
parser.add_argument('--expected-sha256')
|
|
@@ -230,6 +231,9 @@ def main(argv=None):
|
|
|
230
231
|
require('repository','head')
|
|
231
232
|
from .diff_review import review
|
|
232
233
|
checked=review(args.repository,config['inputCommit'],args.head,scope_nodes(config,args.workflow))
|
|
234
|
+
if config['stage'] == 'minifish-test':
|
|
235
|
+
from .compile_gate import verify_compile
|
|
236
|
+
checked['markdown'] += verify_compile(args.repository,args.head,config['bizdir'],args.compile_result)
|
|
233
237
|
target.write_text(checked['markdown']); return 0 if checked['passed'] else 3
|
|
234
238
|
result=render(config,args.workflow)
|
|
235
239
|
if args.action == 'scope':
|