bend_puzzle/test/shared-contracts-test.js

63 lines
5.2 KiB
JavaScript
Raw Normal View History

2026-07-31 12:54:46 +09:00
'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);
2026-08-01 16:06:14 +09:00
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');
2026-07-31 12:54:46 +09:00
console.log('Canonical build, store, mechanics, runtime mode, and purchase contracts passed');