'use strict'; const assert=require('assert/strict'); const vm=require('vm'); const {read,functionSource,buildMeta}=require('./helpers/app-source'); const SharedContracts=require('../shared-contracts'); const ServerContracts=require('../server'); const packageVersion=JSON.parse(read('package.json')).version; assert.equal(buildMeta.PACKAGE_VERSION,packageVersion); assert.deepEqual( SharedContracts.normalizeSpecialMechanics(['internalGate','warp','crossing','lock','warp','invalid']), ['crossing','internalGate','lock','warp'] ); assert.deepEqual( ServerContracts.sanitizeWorldGlobal({specialMechanicsSeen:['internalGate','warp','invalid']},{global:{}}).specialMechanicsSeen, ['internalGate','warp'] ); const catalog=JSON.parse(read('store-catalog.json')); const catalogMap=new Map(catalog.map(item=>[item.id,item])); const generatedExpected=`'use strict';\n// Generated from store-catalog.json by scripts/generate-store-catalog.js. Do not edit.\nglobalThis.BendStoreCatalog=Object.freeze(${JSON.stringify(catalog)}.map(item=>Object.freeze(item)));\n`; assert.equal(read('store-catalog.generated.js'),generatedExpected,'Browser store catalog is stale; run the catalog generator'); const generatedContext={};vm.createContext(generatedContext);vm.runInContext(generatedExpected,generatedContext); assert.deepEqual(JSON.parse(JSON.stringify(generatedContext.BendStoreCatalog)),catalog); const fixtures=[ {purchaseId:'purchase:1',boardId:'B0',itemId:'score-lens',buyer:' Alice Example ',boughtAt:100,paidCost:200000}, {purchaseId:'purchase:1',boardId:'B1',itemId:'score-lens',buyer:'duplicate-id',boughtAt:101,paidCost:200000}, {purchaseId:'purchase:2',boardId:'B0',itemId:'score-lens',buyer:'duplicate-item',boughtAt:102,paidCost:200000}, {purchaseId:'purchase:3',boardId:'B01',itemId:'score-lens',buyer:'bad-board',boughtAt:103,paidCost:200000}, {purchaseId:'purchase:4',boardId:'B2',itemId:'cursor-face-1f600',buyer:'123456789012345678901234567890',boughtAt:104,paidCost:Number.MAX_SAFE_INTEGER}, {purchaseId:'bad purchase',boardId:'B3',itemId:'score-lens',buyer:'bad-id',boughtAt:105,paidCost:200000} ]; const clientContext={ SharedContracts, storeItem:id=>catalogMap.get(String(id||''))||null, MAX_SCORE:Number.MAX_SAFE_INTEGER, DEFAULT_PLAYER_NAME:'旅人' }; vm.createContext(clientContext); vm.runInContext(`${functionSource('normalizePlayerPurchases')}\nthis.normalizePlayerPurchases=normalizePlayerPurchases;`,clientContext); const client=JSON.parse(JSON.stringify(clientContext.normalizePlayerPurchases(fixtures))); const server=ServerContracts.normalizePlayerPurchases(fixtures); assert.deepEqual(client,server,'Client and server purchase normalization diverged'); assert.equal(client.length,2); assert.equal(client[0].buyer,'Alice Example'); assert.equal(client[1].buyer.length,24); assert.equal(client[1].paidCost,Number.MAX_SAFE_INTEGER); const runtimeContext={globalThis:null};runtimeContext.globalThis=runtimeContext;vm.createContext(runtimeContext);vm.runInContext(read('runtime-config.js'),runtimeContext); assert.equal(runtimeContext.BendRuntimeConfig.cloudApi,true,'LinkField no longer supports a local-only runtime'); assert.equal(runtimeContext.BendRuntimeConfig.singleSharedWorld,true,'Runtime must require the single shared world'); const hostedRuntimeContext={globalThis:null,URL,location:{protocol:'https:',href:'https://host.example/~333/link-field/'},document:{currentScript:{src:'https://host.example/~333/link-field/runtime-config.js'}}};hostedRuntimeContext.globalThis=hostedRuntimeContext;vm.createContext(hostedRuntimeContext);vm.runInContext(read('runtime-config.js'),hostedRuntimeContext); assert.equal(hostedRuntimeContext.BendRuntimeConfig.cloudApi,true,'HTTP hosting must enable the shared-world API'); assert.equal(hostedRuntimeContext.BendRuntimeConfig.appBaseUrl,'https://host.example/~333/link-field/','Hosted runtime did not preserve the application mount path'); assert.equal(hostedRuntimeContext.BendRuntimeConfig.apiBridgeUrl,'https://host.example/~333/link-field/api-bridge.php','Hosted runtime did not configure the PHP API bridge'); assert.equal(hostedRuntimeContext.BendRuntimeConfig.realtimeTransport,'http-poll','Static hosting must use HTTP realtime polling'); const endpointContext={URL,cloudApiBaseUrl:'https://host.example/~333/link-field/api/'};vm.createContext(endpointContext);vm.runInContext(`${functionSource('cloudEndpointUrl')} this.cloudEndpointUrl=cloudEndpointUrl;`,endpointContext); assert.equal(endpointContext.cloudEndpointUrl('/api/cloud/status'),'https://host.example/~333/link-field/api/cloud/status','Cloud API URL lost the mounted application path'); const bridgeEndpointContext={URL,cloudApiBaseUrl:'https://host.example/~333/link-field/api-bridge.php',cloudApiBridgeUrl:'https://host.example/~333/link-field/api-bridge.php'};vm.createContext(bridgeEndpointContext);vm.runInContext(`${functionSource('cloudEndpointUrl')}\nthis.cloudEndpointUrl=cloudEndpointUrl;`,bridgeEndpointContext); assert.equal(bridgeEndpointContext.cloudEndpointUrl('/api/cloud/pull?since=7'),'https://host.example/~333/link-field/api-bridge.php?path=%2Fapi%2Fcloud%2Fpull&since=7','PHP bridge URL did not preserve the API path and query'); console.log('Canonical build, store, mechanics, runtime mode, and purchase contracts passed');