bend_puzzle/test/v4794-background-deploy-smoke-test.js
33333-33333 9d70afb4cc
Some checks failed
BEND FIELD CI / release (push) Has been cancelled
BEND FIELD CI / production-bridge (push) Has been cancelled
t
2026-08-01 22:31:04 +09:00

81 lines
8.2 KiB
JavaScript

'use strict';
const assert=require('assert/strict');
const fs=require('fs');
const fsp=fs.promises;
const os=require('os');
const path=require('path');
const {spawn,spawnSync}=require('child_process');
const BuildMeta=require('../build-meta');
const service=require('../scripts/service-control');
(async()=>{
assert.equal(BuildMeta.APP_VERSION,'48.0');
const root=await fsp.mkdtemp(path.join(os.tmpdir(),'linkfield-v4794-'));
const publicDir=path.join(root,'public_html','link-field');
const dataDir=path.join(root,'data');
const logFile=path.join(root,'server.log');
const serviceDir=path.join(root,'service');
const env={...process.env,LINK_FIELD_PUBLIC_DIR:publicDir,LINK_FIELD_TEST_DATA_ROOT:path.join(root,'shared-root'),LINK_FIELD_APACHE_BRIDGE:'1',LINK_FIELD_SERVICE_DIR:serviceDir,LINK_FIELD_LOG_FILE:logFile,LINK_FIELD_LOG_MAX_BYTES:'4096',LINK_FIELD_LOG_ROTATE_INTERVAL_MS:'100',PORT:'0'};
let orphan=null;
try{
await service.deployPublicFiles(publicDir);
const deployedBootstrap=await fsp.readFile(path.join(publicDir,'.htaccess'),'utf8');assert.match(deployedBootstrap,/api-bridge\.php/);assert.match(deployedBootstrap,/RewriteRule \^ - \[F,L\]/);assert.match(deployedBootstrap,/server\\\.js\$/);assert.doesNotMatch(deployedBootstrap,/127\.0\.0\.1:\d+/);
for(const relative of ['index.html','app.js','runtime-config.js','api-bridge.php','assets','client']){
assert.equal(fs.existsSync(path.join(publicDir,relative)),true,`${relative} was not deployed`);
}
const manifest=JSON.parse(await fsp.readFile(path.join(publicDir,'.linkfield-deployment.json'),'utf8'));
assert.equal(manifest.version,'48.0');
await fsp.writeFile(path.join(publicDir,'retired-managed.js'),'obsolete');await fsp.writeFile(path.join(publicDir,'operator-note.txt'),'preserve');
await fsp.writeFile(path.join(publicDir,'.linkfield-deployment.json'),JSON.stringify({...manifest,entries:[...manifest.entries,'retired-managed.js']}));
await service.deployPublicFiles(publicDir);assert.equal(fs.existsSync(path.join(publicDir,'retired-managed.js')),false);assert.equal(await fsp.readFile(path.join(publicDir,'operator-note.txt'),'utf8'),'preserve');
const worldDir=path.join(env.LINK_FIELD_TEST_DATA_ROOT,'world'),worldLockFile=path.join(worldDir,'server.pid');
orphan=spawn(process.execPath,[path.join(service.ROOT,'server.js')],{cwd:service.ROOT,env,stdio:'ignore'});let orphanReady=false;for(let attempt=0;attempt<100;attempt++){await new Promise(resolve=>setTimeout(resolve,50));try{const orphanPort=Number((await fsp.readFile(path.join(publicDir,'.linkfield-port'),'utf8')).trim()),health=await fetch(`http://127.0.0.1:${orphanPort}/api/cloud/status`);if(health.ok){orphanReady=true;break}}catch(_){}}assert(orphanReady,'Legacy-lock recovery fixture did not start');await fsp.writeFile(worldLockFile,String(orphan.pid));const blockedStart=spawnSync(process.execPath,[path.join(service.ROOT,'scripts','service-control.js'),'start'],{cwd:service.ROOT,env,encoding:'utf8',timeout:5000});assert.notEqual(blockedStart.status,0);assert.match(blockedStart.stderr,/npm run recover/i);assert.equal(service.isProcessRunning(orphan.pid),true,'Legacy lock preflight stopped an unverified process');
const recovery=spawnSync(process.execPath,[path.join(service.ROOT,'scripts','service-control.js'),'recover'],{cwd:service.ROOT,env,encoding:'utf8',timeout:20_000});assert.equal(recovery.status,0,recovery.stderr||recovery.stdout);assert.match(recovery.stdout,/Recovered the LinkField world lock/i);assert.equal(service.isProcessRunning(orphan.pid),false,'Explicit recovery did not stop the legacy numeric lock holder');orphan=null;
orphan=spawn(process.execPath,[path.join(service.ROOT,'server.js')],{cwd:service.ROOT,env,stdio:'ignore'});orphanReady=false;for(let attempt=0;attempt<100;attempt++){await new Promise(resolve=>setTimeout(resolve,50));try{const orphanPort=Number((await fsp.readFile(path.join(publicDir,'.linkfield-port'),'utf8')).trim()),health=await fetch(`http://127.0.0.1:${orphanPort}/api/cloud/status`);if(health.ok){orphanReady=true;break}}catch(_){}}assert(orphanReady,'Orphan-server replacement fixture did not start');assert((await service.legacyLinkFieldPids({worldDir})).includes(orphan.pid),'World-lock discovery did not find the orphan LinkField server');
const start=spawnSync(process.execPath,[path.join(service.ROOT,'scripts','service-control.js'),'start'],{cwd:service.ROOT,env,encoding:'utf8',timeout:20_000});
assert.equal(start.status,0,start.stderr||start.stdout);
assert.equal(service.isProcessRunning(orphan.pid),false,'npm start did not stop the untracked LinkField server');orphan=null;
assert.match(start.stdout,/started in the background/i);
assert.match(start.stdout,/command prompt is available again/i);
const pidFile=path.join(serviceDir,'server.pid');
const pidRecord=JSON.parse(await fsp.readFile(pidFile,'utf8')),pid=pidRecord.pid;
assert.match(pidRecord.nonce,/^[a-f0-9]{32}$/);
assert.equal(service.isProcessRunning(pid),true);
const port=Number((await fsp.readFile(path.join(publicDir,'.linkfield-port'),'utf8')).trim());
assert.ok(Number.isSafeInteger(port)&&port>0);
const response=await fetch(`http://127.0.0.1:${port}/api/cloud/status`);
assert.equal(response.ok,true);
const status=await response.json();
assert.equal(status.sharedWorld,true);
assert.equal(status.appVersion,'48.0');
assert.equal(fs.existsSync(path.join(publicDir,'.htaccess')),true);
await fsp.appendFile(logFile,'x'.repeat(8192));let rotated=false;for(let attempt=0;attempt<50;attempt++){await new Promise(resolve=>setTimeout(resolve,100));if(fs.existsSync(`${logFile}.1`)){rotated=true;break}}assert(rotated,'Supervisor did not rotate a live oversized log');
await fsp.writeFile(pidFile,JSON.stringify({...pidRecord,nonce:'0'.repeat(32)}));const refusedStop=spawnSync(process.execPath,[path.join(service.ROOT,'scripts','service-control.js'),'stop'],{cwd:service.ROOT,env,encoding:'utf8',timeout:10_000});assert.notEqual(refusedStop.status,0);assert.match(refusedStop.stderr,/not a verified LinkField supervisor/i);assert.equal(service.isProcessRunning(pid),true);await fsp.writeFile(pidFile,JSON.stringify(pidRecord));
const identityFile=path.join(serviceDir,'service.identity.json'),firstIdentity=JSON.parse(await fsp.readFile(identityFile,'utf8')),firstChildPid=firstIdentity.childPid;
process.kill(firstChildPid,'SIGKILL');
let restartedChildPid=null;
for(let attempt=0;attempt<100;attempt++){await new Promise(resolve=>setTimeout(resolve,100));try{const identity=JSON.parse(await fsp.readFile(identityFile,'utf8')),activePort=Number((await fsp.readFile(path.join(publicDir,'.linkfield-port'),'utf8')).trim());if(identity.childPid&&identity.childPid!==firstChildPid){const health=await fetch(`http://127.0.0.1:${activePort}/api/cloud/status`).catch(()=>null);if(health?.ok){restartedChildPid=identity.childPid;break}}}catch(_){}}
assert(restartedChildPid,'Supervisor did not restart a crashed LinkField server');
await fsp.unlink(pidFile);
const secondStart=spawnSync(process.execPath,[path.join(service.ROOT,'scripts','service-control.js'),'start'],{cwd:service.ROOT,env,encoding:'utf8',timeout:20_000});
assert.equal(secondStart.status,0,secondStart.stderr||secondStart.stdout);
assert.match(secondStart.stdout,/Replacing the running LinkField server/i);
const replacementPid=JSON.parse(await fsp.readFile(pidFile,'utf8')).pid;
assert.notEqual(replacementPid,pid);
assert.equal(service.isProcessRunning(pid),false);
assert.equal(service.isProcessRunning(replacementPid),true);
const stop=spawnSync(process.execPath,[path.join(service.ROOT,'scripts','service-control.js'),'stop'],{cwd:service.ROOT,env,encoding:'utf8',timeout:10_000});
assert.equal(stop.status,0,stop.stderr||stop.stdout);
assert.equal(service.isProcessRunning(replacementPid),false);
}finally{
if(orphan&&service.isProcessRunning(orphan.pid))try{process.kill(orphan.pid,'SIGKILL')}catch(_){}
spawnSync(process.execPath,[path.join(service.ROOT,'scripts','service-control.js'),'stop'],{cwd:service.ROOT,env,encoding:'utf8',timeout:10_000});
await fsp.rm(root,{recursive:true,force:true});
}
console.log('LinkField v48.0 background deployment and service smoke test passed');
})().catch(error=>{console.error(error);process.exitCode=1});