This commit is contained in:
33333-33333 2026-08-01 16:06:14 +09:00
commit 4c4e767ec6
73 changed files with 3502 additions and 741 deletions

View file

@ -8,9 +8,10 @@ const {root,starterPuzzle}=require('./helpers/app-source');
const {connectRealtime}=require('./helpers/realtime-client');
const port=19000+Math.floor(Math.random()*10000);
const dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'bend-field-shared-world-'));
const dataDir=fs.mkdtempSync(path.join(os.tmpdir(),'link-field-shared-world-'));
const worldDir=path.join(dataDir,'world');
const child=spawn(process.execPath,[path.join(root,'server.js')],{
env:{...process.env,PORT:String(port),HOST:'127.0.0.1',BEND_FIELD_DATA_DIR:dataDir},stdio:['ignore','pipe','pipe']
env:{...process.env,PORT:String(port),HOST:'127.0.0.1',LINK_FIELD_TEST_DATA_ROOT:dataDir},stdio:['ignore','pipe','pipe']
});
let stderr='';child.stderr.on('data',chunk=>stderr+=chunk);
const base=`http://127.0.0.1:${port}`;
@ -27,8 +28,11 @@ function solvedState(puzzle){return{paths:puzzle.solution.map(route=>({startGate
(async()=>{
for(let i=0;i<60;i++){try{const {response}=await request('/api/cloud/status');if(response.ok)break}catch(_){}await sleep(50)}
const status=await request('/api/cloud/status');assert.equal(status.response.status,200);assert.equal(status.body.sharedWorld,true);
const mountedStatus=await request('/~333/link-field/api/cloud/status');assert.equal(mountedStatus.response.status,200);assert.equal(mountedStatus.body.sharedWorld,true);
const page=await requestText('/');assert.equal(page.response.status,200);assert(page.body.indexOf('build-meta.js')<page.body.indexOf('puzzle-core.js')&&page.body.indexOf('app-logic.js')>page.body.indexOf('puzzle-core.js')&&page.body.indexOf('app-logic.js')<page.body.indexOf('app.js'));
const mountedPage=await requestText('/~333/link-field/');assert.equal(mountedPage.response.status,200);assert.match(mountedPage.body,/LinkField/);
const runtimeConfig=await requestText('/runtime-config.js');assert.equal(runtimeConfig.response.status,200);assert.match(runtimeConfig.body,/cloudApi:true/);
const mountedRuntimeConfig=await requestText('/~333/link-field/runtime-config.js');assert.equal(mountedRuntimeConfig.response.status,200);assert.match(mountedRuntimeConfig.body,/cloudApi:true/);
const logicAsset=await requestText('/app-logic.js');assert.equal(logicAsset.response.status,200);assert.match(logicAsset.body,/BendAppLogic/);
const alice=(await request('/api/cloud/session',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:'Alice'})})).body;
@ -37,12 +41,12 @@ function solvedState(puzzle){return{paths:puzzle.solution.map(route=>({startGate
assert.equal(alice.name,'Alice');assert.equal(bob.name,'Bob');
const starter=starterPuzzle(),b0=boardMeta('B0',0,123456,starter);
const bootstrap={baseRevision:0,global:{schema:31,appVersion:'47.70',generatorVersion:5,worldGeneration:'v47-field-reset-20260728-interaction-fix',nextId:1,score:999,cursorStyle:'do-not-share',cloudProfile:{token:'do-not-store'}},metas:[b0],states:[{id:'B0',value:{paths:[],solved:false,rev:1,revAuthor:'alice'}}],deleted:[]};
const bootstrap={baseRevision:0,global:{schema:31,appVersion:'47.70',generatorVersion:5,worldGeneration:'linkfield-single-world-20260801',nextId:1,score:999,cursorStyle:'do-not-share',cloudProfile:{token:'do-not-store'}},metas:[b0],states:[{id:'B0',value:{paths:[],solved:false,rev:1,revAuthor:'alice'}}],deleted:[]};
const pushed=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify(bootstrap)});assert.equal(pushed.response.status,200);assert.equal(pushed.body.revision,1);assert.deepEqual(pushed.body.clearEvents,[]);
const bobInitial=await request('/api/cloud/pull?since=0&eventsSince=0',{headers:auth(bob)});assert.equal(bobInitial.response.status,200);assert.equal(bobInitial.body.changed,true);assert.equal(bobInitial.body.fullSnapshot,true);assert.equal(bobInitial.body.page.metas.B0.seed,123456);assert.equal(bobInitial.body.page.states.B0.solved,false);assert(bobInitial.body.page.metas.B0.rev>1_000_000_000_000);assert(bobInitial.body.page.states.B0.rev>1_000_000_000_000);assert.equal(bobInitial.body.page.global.score,undefined);assert.equal(bobInitial.body.page.global.cursorStyle,undefined);assert.equal(bobInitial.body.page.global.cloudProfile,null);assert.equal(bobInitial.body.player.name,'Bob');
aliceRealtime=await connectRealtime(base,alice);aliceRealtime.send({type:'viewport',minX:-10,minY:-10,maxX:10,maxY:10});await aliceRealtime.waitFor('snapshot');aliceRealtime.send({type:'claim',requestId:'server-smoke-claim',boardId:'B0'});const claimResult=await aliceRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='server-smoke-claim');assert.equal(claimResult.ok,true);
aliceRealtime=await connectRealtime(base+'/~333/link-field',alice);aliceRealtime.send({type:'viewport',minX:-10,minY:-10,maxX:10,maxY:10});await aliceRealtime.waitFor('snapshot');aliceRealtime.send({type:'claim',requestId:'server-smoke-claim',boardId:'B0'});const claimResult=await aliceRealtime.waitFor(message=>message.type==='claim-result'&&message.requestId==='server-smoke-claim');assert.equal(claimResult.ok,true);
const clearPayload={baseRevision:1,global:{nextId:1,lastSolveAt:Date.now()},metas:[],states:[{id:'B0',value:solvedState(starter)}],deleted:[]};
const cleared=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify(clearPayload)});assert.equal(cleared.response.status,200);assert.equal(cleared.body.revision,2);assert.equal(cleared.body.clearEvents.length,1);assert.equal(cleared.body.clearEvents[0].playerName,'Alice');assert.equal(cleared.body.clearEvents[0].id,'B0');assert.equal(cleared.body.clearEvents[0].level,1);
@ -63,8 +67,8 @@ function solvedState(puzzle){return{paths:puzzle.solution.map(route=>({startGate
const overlap=await request('/api/cloud/push',{method:'POST',headers:auth(alice),body:JSON.stringify({baseRevision:4,global:{},metas:[boardMeta('B2',1,222,starter)],states:[],deleted:[]})});assert.equal(overlap.response.status,400);
const aliceRecord=JSON.parse(fs.readFileSync(path.join(dataDir,`${alice.playerId}.json`),'utf8'));assert.equal(aliceRecord.name,'Alice');assert.notEqual(aliceRecord.tokenHash,alice.token);assert.equal(aliceRecord.boardVersions,undefined);assert.equal(aliceRecord.global,undefined);
const world=JSON.parse(fs.readFileSync(path.join(dataDir,'shared-world.json'),'utf8'));assert.equal(world.revision,4);assert.equal(world.global.score,undefined);assert.equal(world.global.cursorStyle,undefined);assert.equal(world.boardVersions.B0,3);assert.equal(world.boardVersions.B1,4);assert.equal(world.clearEvents[0].playerName,'Alice');
const b0Shard=JSON.parse(fs.readFileSync(path.join(dataDir,'shared-world.boards','B0.3.json'),'utf8'));assert.equal(b0Shard.state.solvedBy,'Alice');assert.equal(b0Shard.state.paths.length,starter.solution.length);
const aliceRecord=JSON.parse(fs.readFileSync(path.join(worldDir,`${alice.playerId}.json`),'utf8'));assert.equal(aliceRecord.name,'Alice');assert.notEqual(aliceRecord.tokenHash,alice.token);assert.equal(aliceRecord.boardVersions,undefined);assert.equal(aliceRecord.global,undefined);
const world=JSON.parse(fs.readFileSync(path.join(worldDir,'shared-world.json'),'utf8'));assert.equal(world.revision,4);assert.equal(world.global.score,undefined);assert.equal(world.global.cursorStyle,undefined);assert.equal(world.boardVersions.B0,3);assert.equal(world.boardVersions.B1,4);assert.equal(world.clearEvents[0].playerName,'Alice');
const b0Shard=JSON.parse(fs.readFileSync(path.join(worldDir,'shared-world.boards','B0.3.json'),'utf8'));assert.equal(b0Shard.state.solvedBy,'Alice');assert.equal(b0Shard.state.paths.length,starter.solution.length);
console.log('BEND FIELD shared-world phase 2 server smoke test passed');
})().catch(error=>{console.error(error);if(stderr)console.error(stderr);process.exitCode=1}).finally(()=>{aliceRealtime?.close();child.kill('SIGTERM');fs.rmSync(dataDir,{recursive:true,force:true})});