// Integration test against a disposable, locally running panel. No model calls. import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; const base = process.env.TEST_PANEL_URL || 'http://127.0.0.1:18088'; const testDirectory='integration-'+Date.now(); let cookie = '', csrf = '', instance; const cookies = new Map(); function keepCookies(response) { for (const value of response.headers.getSetCookie()) { const pair=value.split(';')[0]; const at=pair.indexOf('='); cookies.set(pair.slice(0,at),pair.slice(at+1)); } cookie=[...cookies].map(([key,value])=>key+'='+value).join('; '); } async function request(path, method = 'GET', body) { if(path.startsWith('/api/ws/') && instance) path += (path.includes('?')?'&':'?')+'instance='+encodeURIComponent(instance.id); const response = await fetch(base + path, {method,headers:{Cookie:cookie,Origin:base,'X-CSRF-Token':csrf,...(body instanceof FormData ? {} : {'Content-Type':'application/json'})},body:body===undefined?undefined:body instanceof FormData?body:JSON.stringify(body)}); keepCookies(response); const text = await response.text(); let data; try {data=JSON.parse(text)}catch{data=text} if(!response.ok)throw new Error(method+' '+path+': '+response.status+' '+String(typeof data==='object'?data.error:data).slice(0,300)); return {response,data}; } try { const login=await request('/api/login','POST',{username:process.env.TEST_USERNAME || 'testadmin',password:process.env.TEST_PASSWORD || 'Local-Test-Only-1842'}); cookie=login.response.headers.getSetCookie().map(v=>v.split(';')[0]).join('; ');csrf=login.data.csrf_token; assert.equal((await request('/api/me')).data.user.username,process.env.TEST_USERNAME || 'testadmin'); await request('/api/refresh','POST'); assert.equal((await request('/api/me')).data.user.username,process.env.TEST_USERNAME || 'testadmin'); let images=(await request('/api/images')).data; const build=(await request('/api/builds','POST',{version:process.env.TEST_FROM_VERSION || images[0]?.version || '0.2.0-rc.2'})).data; let buildSucceeded=false; for(let n=0;n<120;n++){const jobs=(await request('/api/builds')).data;const job=jobs.find(j=>j.id===build.id);if(job?.status==='failed')throw new Error('build/validation failed: '+job.error);if(job?.status==='success'){buildSucceeded=true;break;}await new Promise(r=>setTimeout(r,1000));} assert.ok(buildSucceeded,'submitted build/validation job did not complete before timeout'); images=(await request('/api/images')).data.filter(i=>i.status==='available');assert.ok(images.length,'validated image required'); const source=images.find(i=>!process.env.TEST_FROM_VERSION || i.version===process.env.TEST_FROM_VERSION); const target=images.find(i=>!process.env.TEST_TO_VERSION || i.version===process.env.TEST_TO_VERSION); assert.ok(source,'source image required');assert.ok(target,'target image required'); console.log('PASS local image reuse, persistent build task and runtime verification'); instance=(await request('/api/instances','POST',{name:'Integration check',image_id:source.image_id || source.id,version:source.version})).data; await request('/api/ws/mkdir','POST',{path:testDirectory}); const form=new FormData();form.set('path',testDirectory);form.set('file',new Blob(['hello workspace']), 'hello.txt');await request('/api/ws/upload','POST',form); assert.equal((await request('/api/ws/download?path='+testDirectory+'/hello.txt')).data,'hello workspace'); await request('/api/ws/rename','POST',{path:testDirectory+'/hello.txt',new_path:testDirectory+'/renamed.txt'}); const zip=await fetch(base+'/api/ws/download?path='+testDirectory+'&instance='+encodeURIComponent(instance.id),{headers:{Cookie:cookie}}); assert.equal(zip.status,200);assert.match(zip.headers.get('content-type'),/zip/); assert.equal(new Uint8Array(await zip.arrayBuffer()).slice(0,2).toString(),'80,75'); console.log('PASS login, workspace upload/download/rename'); let ready=false; for(let n=0;n<45;n++){try{const r=await request('/dsh/'+instance.id+'/');if(typeof r.data==='string'&&r.data.includes('setTimeout(r,1000));} assert.ok(ready,'authenticated DSH proxy never became ready'); console.log('PASS real Docker create/start and authenticated DSH proxy'); if(process.env.TEST_PLUGIN==='1'){ const seed = `const fs=require('fs');const p=process.env.DSH_HOME+'/profiles/web/package.json';const pkg=JSON.parse(fs.readFileSync(p,'utf8'));pkg.dependencies={...(pkg.dependencies||{}),'is-number':'7.0.0'};fs.writeFileSync(p,JSON.stringify(pkg));`; execFileSync('docker',['exec','dsh-inst-'+instance.id,'node','-e',seed]); console.log('Seeded exact plugin dependency fixture'); } const root='/api/instances/'+instance.id; await request(root+'/stop','POST'); const profile=(await request(root+'/profile')).data;assert.equal(typeof profile.content,'string'); await request(root+'/profile','PUT',{content:profile.content}); const backup=(await request(root+'/backups','POST',{password:process.env.TEST_PASSWORD || 'Local-Test-Only-1842'})).data; assert.ok(backup.id,'backup ID'); const archive=await fetch(base+root+'/backups?download='+backup.id,{headers:{Cookie:cookie}}); assert.equal(archive.status,200); const importForm=new FormData();importForm.set('file',new Blob([await archive.arrayBuffer()]),'roundtrip.dshbak');importForm.set('password',process.env.TEST_PASSWORD || 'Local-Test-Only-1842'); const imported=(await request(root+'/backups','POST',importForm)).data;assert.ok(imported.id); await request(root+'/restore','POST',{backup_id:backup.id,password:process.env.TEST_PASSWORD || 'Local-Test-Only-1842'}); console.log('PASS stopped profile save, encrypted backup and restore'); await request(root+'/start','POST');await request(root+'/stop','POST'); if(process.env.TEST_UPGRADE==='1'){instance=(await request(root+'/upgrade','POST',{image_id:target.image_id||target.id,password:process.env.TEST_PASSWORD || 'Local-Test-Only-1842'})).data;console.log('PASS image replacement upgrade '+source.version+' -> '+target.version);} if(process.env.TEST_PLUGIN==='1'){ execFileSync('docker',['exec',instance.container_id || 'dsh-inst-'+instance.id,'node','-e',`const fs=require('fs');const p=process.env.DSH_HOME+'/profiles/web/node_modules/is-number/package.json';if(JSON.parse(fs.readFileSync(p)).version!=='7.0.0')process.exit(1)`]); console.log('PASS manifest dependency rebuilt across restore and upgrade'); } if(process.env.TEST_OFFLINE_ROLLBACK==='1'){ const container=instance.container_id;assert.ok(container); execFileSync('docker',['exec',container,'node','-e',`const fs=require('fs');const p=process.env.DSH_HOME+'/profiles/web/package.json';const pkg=JSON.parse(fs.readFileSync(p));pkg.dependencies['offline-design-fixture']='http://127.0.0.1:9/unavailable.tgz';fs.writeFileSync(p,JSON.stringify(pkg));fs.writeFileSync(process.env.DSH_HOME+'/rollback-marker','preserve-me');`]); await request(root+'/stop','POST'); await assert.rejects(request(root+'/upgrade','POST',{image_id:target.image_id||target.id,password:process.env.TEST_PASSWORD}),/upgrade readiness failed/); const current=(await request('/api/instances')).data.find(i=>i.id===instance.id); assert.equal(current.container_id,container);assert.ok(['stopped','exited'].includes(current.status));assert.equal(current.version,target.version); const inspect=JSON.parse(execFileSync('docker',['inspect',container]));assert.equal(inspect[0].State.Running,false); console.log('PASS offline dependency failure rolls back identity and original stopped state'); } await request('/api/ws/delete?path='+testDirectory+'/renamed.txt','DELETE'); await request('/api/ws/delete?path='+testDirectory,'DELETE'); console.log('PASS lifecycle and workspace cleanup'); }finally{ if(instance){await request('/api/instances/'+instance.id,'DELETE');console.log('Removed disposable instance; retained test data per API policy.');} }