2026-10-12 09:28:09 +08:00
// Run after HBuilderX MP-WEIXIN compilation; no network or real account access.
const assert = require ( 'node:assert/strict' );
const fs = require ( 'node:fs' );
const path = require ( 'node:path' );
const vm = require ( 'node:vm' );
const repo = path . resolve ( __dirname , '..' );
const built = path . resolve ( process . argv [ 2 ] || path . join ( repo , 'unpackage/dist/dev/mp-weixin' ));
const app = JSON . parse ( fs . readFileSync ( path . join ( built , 'app.json' ), 'utf8' ));
const roots = app . subPackages . map ( item => item . root . replace ( /\/$/ , '' ) + '/' );
const packageOf = file => roots . find ( root => file . startsWith ( root )) || '' ;
let checks = 0 ;
function test ( name , action ) { action (); checks ++ ; console . log ( 'PASS ' + name ); }
function jsFiles ( dir ) {
return fs . readdirSync ( dir , { withFileTypes : true }). flatMap ( item => {
const file = path . join ( dir , item . name );
return item . isDirectory () ? jsFiles ( file ) : item . name . endsWith ( '.js' ) ? [ file ] : [];
});
}
test ( 'all generated static requires stay within main package or their own subpackage' , () => {
const violations = [];
for ( const file of jsFiles ( built )) {
const relative = path . relative ( built , file ). split ( path . sep ). join ( '/' );
for ( const match of fs . readFileSync ( file , 'utf8' ). matchAll ( /\brequire\(["']([^"']+)["']\)/g )) {
if ( ! match [ 1 ]. startsWith ( '.' )) continue ;
const target = path . posix . normalize ( path . posix . join ( path . posix . dirname ( relative ), match [ 1 ]));
assert . ok ( fs . existsSync ( path . join ( built , target )), relative + ': missing module ' + target );
if ( packageOf ( target ) && packageOf ( target ) !== packageOf ( relative )) violations . push ( relative + ' -> ' + target );
}
}
assert . deepEqual ( violations , [], 'Unavailable subpackage dependencies: ' + violations . join ( ', ' ));
});
test ( 'route, selection and login output is freshly compiled' , () => {
for ( const file of [ 'services/match-route.uts' , 'packages/finance/services/compare.uts' , 'packages/account/login/login.uvue' ]) {
const output = path . join ( built , file . replace ( /\.(uts|uvue)$/ , '.js' ));
assert . ok ( fs . statSync ( output ). mtimeMs >= fs . statSync ( path . join ( repo , file )). mtimeMs , 'Recompile MP-WEIXIN: ' + file );
}
});
// Model a cold account-package load: finance/content modules are not registered.
const loaded = new Map ();
let page , onLoad , restoredPath ;
class JsonObject { constructor ( value ) { Object . assign ( this , value ); } }
class UtsType { static initProps ( options ) { return options ; } }
const vendor = {
UTSJSONObject : JsonObject , UTS : { UTSType : UtsType },
defineComponent : component => component ,
ref : value => ({ value }), watch : () => {}, onMounted : () => {}, onUnmounted : () => {},
2026-10-14 14:53:13 +08:00
onShow : () => {}, onHide : () => {}, onUnload : () => {},
2026-10-12 09:28:09 +08:00
onLoad : callback => { onLoad = callback ; },
index : {
getWindowInfo : () => ({ windowWidth : 390 , windowHeight : 844 , statusBarHeight : 24 }),
getMenuButtonBoundingClientRect : () => ({ width : 0 , height : 0 , top : 0 , left : 0 }),
onWindowResize : () => {}, offWindowResize : () => {}
}
};
function load ( relative ) {
assert . ok ( ! packageOf ( relative ) || packageOf ( relative ) === 'packages/account/' , 'module is not registered: ' + relative );
if ( relative === 'common/vendor.js' ) return vendor ;
if ( loaded . has ( relative )) return loaded . get ( relative );
const exports = {};
loaded . set ( relative , exports );
vm . runInNewContext ( fs . readFileSync ( path . join ( built , relative ), 'utf8' ), {
exports , setInterval , clearInterval ,
wx : { createPage : component => { page = component ; } },
require : module => load ( path . posix . normalize ( path . posix . join ( path . posix . dirname ( relative ), module )))
}, { filename : relative });
return exports ;
}
test ( 'login registers and initializes without loading the finance subpackage' , () => {
load ( 'packages/account/login/login.js' );
assert . equal ( typeof page . setup , 'function' );
page . setup ({});
assert . equal ( typeof onLoad , 'function' );
assert . ok ([... loaded . keys ()]. every ( file => ! file . startsWith ( 'packages/finance/' )));
});
const route = load ( 'services/match-route.js' );
const realReturnPath = route . matchReturnPath ;
route . matchReturnPath = (... args ) => { restoredPath = realReturnPath (... args ); return restoredPath ; };
test ( 'account login restores match context using the shared route module' , () => {
onLoad ({ resumeMatch : '1' , matchPage : 'product' , assessmentId : '42' , productIds : '3,7' , revisionId : '9' , productId : '7' , tab : 'details' , from : 'compare' });
assert . equal ( restoredPath , '/packages/finance/product/product?assessmentId=42&productIds=3,7&revisionId=9&productId=7&tab=details&from=compare' );
});
2026-10-19 16:11:59 +08:00
test ( 'account login restores the all-products preview group' , () => {
onLoad ({ resumeMatch : '1' , matchPage : 'matches' , assessmentId : '42' , productIds : '' , revisionId : '9' , group : 'all' });
assert . equal ( restoredPath , '/packages/finance/matches/matches?assessmentId=42&productIds=&revisionId=9&group=all' );
});
2026-10-12 09:28:09 +08:00
const cases = [
2026-10-19 16:11:59 +08:00
[ 'all matches preview with revision' , [ 'matches' , '42' , '' , '9' , '' , 'all' ], '/packages/finance/matches/matches?assessmentId=42&productIds=&revisionId=9&group=all' ],
2026-10-12 09:28:09 +08:00
[ 'matches with selected products and revision' , [ 'matches' , '42' , '3,7' , '9' ], '/packages/finance/matches/matches?assessmentId=42&productIds=3,7&revisionId=9&group=eligible' ],
[ 'matches without a selection' , [ 'matches' , '42' , '' , '' , '' , 'unknown' ], '/packages/finance/matches/matches?assessmentId=42&productIds=&group=unknown' ],
[ 'compare selection order' , [ 'compare' , '42' , '7,3,8' , '9' ], '/packages/finance/compare/compare?assessmentId=42&productIds=7,3,8&revisionId=9' ],
[ 'context product' , [ 'product' , '42' , '3' , '' , '3' ], '/packages/finance/product/product?assessmentId=42&productIds=3&productId=3&tab=explanation&from=' ],
[ 'public product details' , [ 'product' , '' , '' , '' , '3' , 'eligible' , 'details' ], '/packages/finance/product/product?productId=3&tab=details&from=' ],
[ 'public product conditions' , [ 'product' , '' , '' , '' , '3' , 'eligible' , 'conditions' , 'archive' ], '/packages/finance/product/product?productId=3&tab=conditions&from=archive' ],
[ 'external return URL' , [ 'https://example.invalid' , '42' , '' , '' ], '' ],
[ 'undeclared page' , [ 'login' , '42' , '' , '' ], '' ],
[ 'missing assessment for matches' , [ 'matches' , '' , '' , '' ], '' ],
[ 'missing assessment for compare' , [ 'compare' , '' , '3' , '' ], '' ],
[ 'invalid assessment' , [ 'compare' , '42&x=1' , '3' , '' ], '' ],
[ 'zero assessment' , [ 'matches' , '0' , '' , '' ], '' ],
[ 'invalid revision' , [ 'matches' , '42' , '' , '0' ], '' ],
[ 'duplicate products' , [ 'compare' , '42' , '3,3' , '' ], '' ],
[ 'too many products' , [ 'compare' , '42' , '1,2,3,4' , '' ], '' ],
[ 'invalid product list' , [ 'compare' , '42' , '3,x' , '' ], '' ],
[ 'empty list member' , [ 'compare' , '42' , '3,' , '' ], '' ],
[ 'invalid matches group' , [ 'matches' , '42' , '' , '' , '' , 'other' ], '' ],
[ 'missing product' , [ 'product' , '42' , '' , '' , '' ], '' ],
[ 'invalid product' , [ 'product' , '42' , '' , '' , '3&x=1' ], '' ],
[ 'invalid product tab' , [ 'product' , '42' , '' , '' , '3' , 'eligible' , 'other' ], '' ],
[ 'invalid product source' , [ 'product' , '42' , '' , '' , '3' , 'eligible' , 'details' , '//evil.invalid' ], '' ],
[ 'public product cannot restore an explanation' , [ 'product' , '' , '' , '' , '3' ], '' ],
[ 'public product cannot carry a selection' , [ 'product' , '' , '3' , '' , '3' , 'eligible' , 'details' ], '' ],
[ 'public product cannot carry a revision' , [ 'product' , '' , '' , '9' , '3' , 'eligible' , 'details' ], '' ]
];
for ( const [ name , args , expected ] of cases ) test ( name , () => assert . equal ( route . matchReturnPath (... args ), expected ));
test ( 'selection parser keeps the existing limits' , () => {
assert . deepEqual ( Array . from ( route . parseCompareIds ( '' )), []);
assert . deepEqual ( Array . from ( route . parseCompareIds ( '7,3,8' )), [ '7' , '3' , '8' ]);
for ( const raw of [ '1,2,3,4' , '3,3' , '0' , '03' , '-3' , '3,' , '3, 7' , '12345678901' , '3&tab=details' ]) assert . throws (() => route . parseCompareIds ( raw ));
});
test ( 'finance selection still uses shared validation and isolates session/assessment/revision' , () => {
let session = { userId : 'synthetic-user' , token : 'synthetic-token' };
const exports = {};
vm . runInNewContext ( fs . readFileSync ( path . join ( built , 'packages/finance/services/compare.js' ), 'utf8' ), {
exports ,
require ( module ) {
if ( module === '../../../services/match-route.js' ) return route ;
if ( module === './matches.js' ) return {};
if ( module === '../../../services/api.js' ) return { getSession : () => session && { getString : key => session [ key ] } };
throw Error ( 'Unexpected finance dependency: ' + module );
}
});
exports . saveCompareSelection ( '42' , [ '7' , '3' ], '9' );
assert . deepEqual ( Array . from ( exports . compareSelection ( '42' , '9' )), [ '7' , '3' ]);
assert . throws (() => exports . saveCompareSelection ( '42' , [ '7' , '7' ], '9' ));
assert . deepEqual ( Array . from ( exports . compareSelection ( '42' , '9' )), [ '7' , '3' ]);
for ( const change of [() => { session . token = 'another-token' ; }, () => { session . userId = 'another-user' ; }]) {
change (); assert . deepEqual ( Array . from ( exports . compareSelection ( '42' , '9' )), []);
exports . saveCompareSelection ( '42' , [ '3' ], '9' );
}
assert . deepEqual ( Array . from ( exports . compareSelection ( '43' , '9' )), []);
exports . saveCompareSelection ( '42' , [ '3' ], '9' );
assert . deepEqual ( Array . from ( exports . compareSelection ( '42' , '10' )), []);
exports . saveCompareSelection ( '42' , [ '3' ], '9' );
session = null ;
assert . deepEqual ( Array . from ( exports . compareSelection ( '42' , '9' )), []);
});
console . log ( checks + ' checks passed; no network calls.' );