mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-07 14:21:06 -10:00
QAGUI #2 (source): /api/native says checking until Control has answered; a failed start-time read is retried in 3 s, not held 45 s; page shows Checking, not Connect engine
This commit is contained in:
1 parent
0fa4288d30
commit
3c4afa8d78
4 files changed
+97
-4
No files matched your search
@@ -40,6 +40,8 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
const settingsPath=join(directory,'native-host.json');
|
||||
let preferences=readJson(settingsPath,{repo:defaultRepo});
|
||||
let child=null,seq=0,buffer='',pending=new Map(),definitions=[],refreshing=null,lastRead=0,connecting=null;
|
||||
/* QAGUI 0213: until Control has answered once, the state says it is still being asked (`checking`), not that nothing is installed. A failed read during Control's start is read again after a few seconds instead of standing for 45 s; after READ_TRIES failures the answer is final and the error shows. */
|
||||
let everRead=false,failedReads=0;const READ_TRIES=4,RETRY_MS=3000;
|
||||
let snapshot={available:false,repo:preferences.repo,summary:'Connect a local Strata installation.',catalog:null,installed:null,hardware:null,error:null};
|
||||
const instances=new Map(),processes=new Map(),jobs=new Map(),logs=[];
|
||||
const savedJobs=readJson(join(directory,'native-jobs.json'),[]);/* An app update ends the app on purpose: the installer replaces it and starts it again. The job that asked for it was saved as running together with the app version it started from (appUpdateFrom); a start that finds a different version knows the update finished, instead of calling it an interrupted operation. */
|
||||
@@ -73,7 +75,7 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
/* Does the engine in this release serve DFlash2? Control knows (strata_control/drafting.py); asked once, false until it answers. The Models screen offers the choice only when true. */
|
||||
let dflash2Served=null,dflash2Support=null,dflash2Selq4=false,dflash2Split=false;
|
||||
function probeDrafting(){if(!packaged||dflash2Served!==null||typeof control.draftingPlan!=='function')return;dflash2Served=false;control.draftingPlan({mode:'auto'}).then(p=>{dflash2Served=p.serves===true;dflash2Support=p.serves===true&&p.support&&p.support_line?{level:p.support,line:p.support_line}:null;dflash2Selq4=p.serves===true&&p.selq4===true;dflash2Split=p.split===true;emit();}).catch(()=>{});}
|
||||
function state(){const folder=preferences.downloadDirectory??(modelsDirectory?modelsDirectory():snapshot.installed?.data_folder),space=folder?diskSpace(folder):null;return {...snapshot,downloadDirectory:folder,downloadFreeBytes:space?.free??null,downloadTotalBytes:space?.total??null,fields:nativeFields(),instances:[...instances.values()].map(({key,configPath,fingerprint,mcpServers,...i})=>structuredClone(i)),defaultId,stoppedForUpdate:stoppedForUpdate?{updated:stoppedForUpdate.updated,at:stoppedForUpdate.at,models:stoppedForUpdate.instances.map(i=>({id:i.id,label:i.label}))}:null,jobs:[...jobs.values()].slice(-12).map(({process,abort,engineAbort,rawResult,...j})=>j),capabilities:{responses:sourceText().includes('/v1/responses'),updates:!!snapshot.available,rollback:previousEngine(),componentUpdates:packaged&&typeof control.update==='function',calibration:packaged?false:snapshot.hardware?.gpus?.some(g=>/nvidia/i.test(g.name??''))??false,dflash2:dflash2Served===true,dflash2Support,dflash2Selq4,dflash2Split}};}
|
||||
function state(){const checking=!everRead&&failedReads<READ_TRIES;const folder=preferences.downloadDirectory??(modelsDirectory?modelsDirectory():snapshot.installed?.data_folder),space=folder?diskSpace(folder):null;return {...snapshot,checking,downloadDirectory:folder,downloadFreeBytes:space?.free??null,downloadTotalBytes:space?.total??null,fields:nativeFields(),instances:[...instances.values()].map(({key,configPath,fingerprint,mcpServers,...i})=>structuredClone(i)),defaultId,stoppedForUpdate:stoppedForUpdate?{updated:stoppedForUpdate.updated,at:stoppedForUpdate.at,models:stoppedForUpdate.instances.map(i=>({id:i.id,label:i.label}))}:null,jobs:[...jobs.values()].slice(-12).map(({process,abort,engineAbort,rawResult,...j})=>j),capabilities:{responses:sourceText().includes('/v1/responses'),updates:!!snapshot.available,rollback:previousEngine(),componentUpdates:packaged&&typeof control.update==='function',calibration:packaged?false:snapshot.hardware?.gpus?.some(g=>/nvidia/i.test(g.name??''))??false,dflash2:dflash2Served===true,dflash2Support,dflash2Selq4,dflash2Split}};}
|
||||
let sourceCache={path:'',time:0,text:''};
|
||||
function sourceText(){try{const path=join(packaged?control.control.dir:preferences.repo,'serve/server.py'),time=statSync(path).mtimeMs;if(sourceCache.path===path&&sourceCache.time===time)return sourceCache.text;sourceCache={path,time,text:readFileSync(path,'utf8')};return sourceCache.text;}catch{return '';}}
|
||||
function nativeFields(){const source=sourceText();return NATIVE_FIELDS.map(field=>({...field,supported:APP_OWNED_FIELDS.includes(field.id)||field.id.startsWith('vision_')||field.id.startsWith('mcp_')||field.id==='vram_reserve_mib'||source.includes(field.id)}));}
|
||||
@@ -150,7 +152,7 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
is refreshed in the background. Only the first read, and a forced one after an operation, waits. */
|
||||
if(refreshing)return !force&&snapshot.available?state():force?refreshing.then(()=>refresh(true)):refreshing; /* a forced read that meets one already running (started before an engine update finished) waits for it and reads again, so it never settles on the old answer */if(!force&&Date.now()-lastRead<45000)return state();
|
||||
probeDrafting();
|
||||
refreshing=(async()=>{try{const [catalog,status]=packaged?await (async()=>{const s=await tool('strata_status',{hardware:true});const c=await tool('strata_models').catch(error=>({families:[],contexts:[],recommendation:{family:null,model:null},context_note:'',error:error.message}));return [c,s];})():await Promise.all([tool('strata_models'),tool('strata_status',{hardware:true})]);let visionReader=false;if(packaged){try{const e=await control.engine();const root=e?.engine?.root??(e?.engine?.exe?dirname(e.engine.exe):'');visionReader=!!root&&['strata-vision','strata-vision.exe'].some(n=>existsSync(join(root,n)));}catch{visionReader=snapshot.visionReader===true;}}const markImages=c=>markPictures(c,visionReader);snapshot={available:true,repo:packaged?status.installDir:repo(),version:packaged?(control.version??null):await execute('git',['describe','--tags','--always'],repo()).then(s=>s.trim()).catch(()=>null),summary:status.summary,catalog:markEngine(packaged?markImages(catalog):catalog,status.engine),engineInfo:{version:status.engine?.version??null,source:status.engine?.source??null},visionReader:packaged?visionReader:true,installed:installedInfo(status.installed),hardware:status.hardware,error:null,install:status.install,external:status.running??null,...(packaged?{packaged:true,installDir:status.installDir,engine:status.engine,installerError:status.installerError}:{})};lastRead=Date.now();if(status.install?.running&&![...jobs.values()].some(j=>j.state==='running'))begin('install',j=>watchInstall(j,status.install));}catch(error){snapshot={...snapshot,available:false,error:error.message};lastRead=Date.now();}emit();return state();})().finally(()=>{refreshing=null;});const run=refreshing;if(!force&&snapshot.available){run.catch(()=>{});return state();}return run;
|
||||
refreshing=(async()=>{try{const [catalog,status]=packaged?await (async()=>{const s=await tool('strata_status',{hardware:true});const c=await tool('strata_models').catch(error=>({families:[],contexts:[],recommendation:{family:null,model:null},context_note:'',error:error.message}));return [c,s];})():await Promise.all([tool('strata_models'),tool('strata_status',{hardware:true})]);let visionReader=false;if(packaged){try{const e=await control.engine();const root=e?.engine?.root??(e?.engine?.exe?dirname(e.engine.exe):'');visionReader=!!root&&['strata-vision','strata-vision.exe'].some(n=>existsSync(join(root,n)));}catch{visionReader=snapshot.visionReader===true;}}const markImages=c=>markPictures(c,visionReader);snapshot={available:true,repo:packaged?status.installDir:repo(),version:packaged?(control.version??null):await execute('git',['describe','--tags','--always'],repo()).then(s=>s.trim()).catch(()=>null),summary:status.summary,catalog:markEngine(packaged?markImages(catalog):catalog,status.engine),engineInfo:{version:status.engine?.version??null,source:status.engine?.source??null},visionReader:packaged?visionReader:true,installed:installedInfo(status.installed),hardware:status.hardware,error:null,install:status.install,external:status.running??null,...(packaged?{packaged:true,installDir:status.installDir,engine:status.engine,installerError:status.installerError}:{})};lastRead=Date.now();everRead=true;failedReads=0;if(status.install?.running&&![...jobs.values()].some(j=>j.state==='running'))begin('install',j=>watchInstall(j,status.install));}catch(error){failedReads++;snapshot={...snapshot,available:false,error:error.message};lastRead=!everRead&&failedReads<READ_TRIES?Date.now()-45000+RETRY_MS:Date.now();}emit();return state();})().finally(()=>{refreshing=null;});const run=refreshing;if(!force&&snapshot.available){run.catch(()=>{});return state();}return run;
|
||||
}
|
||||
function configure({repository}){if(packaged){if(instances.size||[...jobs.values()].some(j=>j.state==='running'))throw new Error('Unload models and finish operations before changing the installation.');const next=realpathSync(String(repository));if(!statSync(next).isDirectory())throw new Error('Choose the folder that holds your Strata model configurations.');preferences={...preferences,installDir:next};packagedInstall=null;disconnect();writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});lastRead=0;return refresh(true);}if(instances.size||[...jobs.values()].some(j=>j.state==='running'))throw new Error('Unload models and finish operations before changing the installation.');const next=realpathSync(String(repository));const old=preferences;preferences={...preferences,repo:next};try{repo();}catch(e){preferences=old;throw e;}disconnect();writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});lastRead=0;return refresh(true);}
|
||||
function setDownloadDirectory(input,{insideJob=false}={}){if(!insideJob&&[...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish the current operation before changing the folder.');if(typeof input!=='string'||!input.trim()||/[\r\n\0]/.test(input))throw new Error('Choose a model folder.');let path;try{path=realpathSync(input.trim());}catch{throw new Error('That folder does not exist. Check the path, or that the drive is connected.');}if(!statSync(path).isDirectory())throw new Error('Choose a folder, not a file.');/* A whole drive (D:\) is not filled with models: D:\Strata is made and used. */if(isDriveRoot(path)){const own=dataFolderFromPick(path).path;makeFolder(own);path=realpathSync(own);}try{accessSync(path,constants.R_OK|constants.W_OK);}catch{throw new Error('Strata cannot write in that folder. Choose a folder you can save files in.');}preferences={...preferences,downloadDirectory:path};/* the same choice for the installer and the Terminal: the Control settings file */if(packaged)rememberDataFolder(controlSettingsFile(),path,Date.now()/1000,true);writeFileSync(settingsPath,JSON.stringify(preferences,null,2),{mode:0o600});shareDataFolder();emit();return state();}
|
||||
|
||||
@@ -246,7 +246,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
<div className="native-model-layout native-model-layout--fit">
|
||||
<div className="stack">
|
||||
{tr(native?.instances.length && expert ? <section className="st-card native-running"><h2 className="h4">{tr(native.instances.length === 1 ? 'Loaded model' : 'Loaded models')}</h2>{tr(native.instances.map(i => <div key={i.id}><span><strong>{tr(i.label)}</strong><small>{tr(i.gpus.length ? i.gpus.map((g, n) => `GPU ${g + 1}${i.gpus.length > 1 && n === i.gpus.length - 1 ? ' (Main)' : ''}`).join(' + ') : 'Automatic GPU')} · {tr(i.status === 'ready' ? i.phase : i.status)}{tr(native.defaultId === i.id ? ' · Chat default' : '')}</small>{tr(i.error ? <small className="inline-error">{tr(plainFailureText(i.error).message)}</small> : null)}</span>{tr(expert && multiple && native.defaultId !== i.id ? <Btn size="sm" variant="ghost" onClick={() => void loadedAction(`instances/${i.id}/default`)}>{tr("Use in chat")}</Btn> : null)}<Btn size="sm" disabled={pending || !!active} onClick={() => void loadedAction(`instances/${i.id}/unload`)}>{tr("Unload")}</Btn></div>))}{tr(loadedError ? <ErrorNotice message={loadedError} next={tr("Stop or answer the chat that is still using this model, then try again.")}/> : null)}</section> : null)}
|
||||
<section className="st-card native-choice">{tr(selectedFamily && (!selectedSize || selectedSize.needsNewerEngine) ? <><h2 className="h3">{tr("This model needs a newer engine.")}</h2><p className="small muted">{tr("Update your engine or import a local GGUF for this model.")}</p><Btn onClick={() => { setSettingsSection('Engine'); go('settings'); }}>{tr("Update engine")}</Btn><Btn variant="ghost" onClick={onImport ?? onOtherModels}>{tr("Use a local GGUF\u2026")}</Btn></> : null)}{tr(!selectedFamily ? <><h2 className="h3">{tr("Set up Strata")}</h2><p className="small muted">{tr("Connect your local installation to choose an optimized model.")}</p><Btn variant="primary" onClick={onConnectEngine ?? (() => { setSettingsSection('Engine'); go('settings'); })}>{tr("Connect engine")}</Btn>{tr(native?.error ? <p className="small inline-error">{tr(native.error)}</p> : null)}<Btn variant="ghost" onClick={onOtherModels}>{tr("Other local models")}</Btn></> : null)}
|
||||
<section className="st-card native-choice">{tr(selectedFamily && (!selectedSize || selectedSize.needsNewerEngine) ? <><h2 className="h3">{tr("This model needs a newer engine.")}</h2><p className="small muted">{tr("Update your engine or import a local GGUF for this model.")}</p><Btn onClick={() => { setSettingsSection('Engine'); go('settings'); }}>{tr("Update engine")}</Btn><Btn variant="ghost" onClick={onImport ?? onOtherModels}>{tr("Use a local GGUF\u2026")}</Btn></> : null)}{tr(!selectedFamily && !listPending ? <><h2 className="h3">{tr("Set up Strata")}</h2><p className="small muted">{tr("Connect your local installation to choose an optimized model.")}</p><Btn variant="primary" onClick={onConnectEngine ?? (() => { setSettingsSection('Engine'); go('settings'); })}>{tr("Connect engine")}</Btn>{tr(native?.error ? <p className="small inline-error">{tr(native.error)}</p> : null)}<Btn variant="ghost" onClick={onOtherModels}>{tr("Other local models")}</Btn></> : null)}
|
||||
{tr(selectedSize ? <>
|
||||
<div className="native-choice__head"><span className="native-choice__label">{tr("Version")}</span><span className="native-choice__tools"><NativePresets values={values} onApply={v => { const f = families.find(f => f.family === v.family || f.sizes.some(s => s.sourceFamily === v.family)); if (!f)
|
||||
return; presetPending.current = true; setFamily(f.family); setSize(f.sizes.find(s => s.model === v.model && (s.sourceFamily === v.family || f.family === v.family))?.id ?? ''); setContext(v.context ?? 32768); setVision(v.vision ?? 'no'); setKv(v.kv ?? 'int8'); setLowRam(v.low_ram ?? 'auto'); }}/><Btn icon="models" size="sm" variant="ghost" onClick={() => setManage(true)}>{tr("Manage models")}</Btn></span></div>
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface NativeField {id:string;label:string;group:string;/** Not shown
|
||||
export interface NativeJob {id:string;kind:string;guided?:boolean;model?:string;family?:string;state:'running'|'done'|'error'|'cancelled';startedAt:string;summary:string;error:string|null;details?:string;result:Record<string,any>|null;progress?:Record<string,any>|null;/** An engine update beside a loaded model: which step it is on and the models it concerns (the app words these itself). */step?:'getting-ready'|'stopping'|'stopped';models?:string[]}
|
||||
export interface NativeInstance {id:string;apiModelId:string;model:string;label:string;aliases?:string[];vision?:boolean;resourceProfile?:'work'|'performance'|null;context?:number;port:number;gpus:number[];status:'loading'|'ready'|'unloading'|'error';phase:string;loadedAt:string;error:string|null;/** One short line when the model was loaded without picture reading although it was on (no internet for the picture files, no room on the disk). */picturesNote?:string;metrics:Record<string,any>|null}
|
||||
export interface NativeState {
|
||||
/** The host has not had an answer from Control yet (it is still starting): not "nothing installed". */
|
||||
checking?: boolean;
|
||||
/** The picture reader (strata-vision) is installed with the engine, so models marked for pictures can read them here. */
|
||||
/** Strata Control's view of the engine for this computer (the app): usable false = none found or recognised; `notice` says why in plain words. */
|
||||
engine?:{usable:boolean;notice?:string|null;wrongMake?:boolean;version?:string|null;/** What the last update check found newer (kept on this PC, read without the network): the Qwen engine, the DeepSeek engine, the app. */updateAvailable?:{engine?:string|null;dsv41?:string|null;app?:string|null}|null};
|
||||
@@ -66,7 +68,7 @@ export function useNative(){
|
||||
}
|
||||
export const nativeState=()=>state;
|
||||
/** QAGUI 0213: the engine answered but Control's list of installed models has not arrived yet (the first seconds after a start): a downloaded model must not be offered as a download. */
|
||||
export const installedListPending=(value:Pick<NativeState,'available'|'installed'|'error'>|null|undefined):boolean=>!!value&&value.available===true&&!value.installed&&!value.error;
|
||||
export const installedListPending=(value:Pick<NativeState,'available'|'installed'|'error'|'checking'>|null|undefined):boolean=>!!value&&(value.checking===true||value.available===true&&!value.installed&&!value.error);
|
||||
/** Connection-list projection only; memory reservations are not inferred. */
|
||||
export function nativeRouting(native:NativeState|null,hardware:Hardware|null):RuntimeModels|null{
|
||||
if(!native?.instances.length)return null;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* QAGUI 0213 #2 (second part): while Strata Control starts slowly, /api/native must say "not known yet" (checking), not "engine not
|
||||
* connected, nothing installed", and a read that failed during the start is read again within seconds, not after 45 s.
|
||||
*/
|
||||
import './helpers/isolate-profile.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { build } from 'esbuild';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createNativeControl } from '../server/native-control.mjs';
|
||||
|
||||
const here = p => fileURLToPath(new URL(p, import.meta.url));
|
||||
const result = await build({ entryPoints: [here('../src/lib/native.ts')], bundle: true, format: 'esm', platform: 'node', write: false, logLevel: 'silent', alias: { react: here('./helpers/react-stub.mjs') }, loader: { '.json': 'json' } });
|
||||
const { installedListPending } = await import(`data:text/javascript;base64,${Buffer.from(result.outputFiles[0].text).toString('base64')}`);
|
||||
|
||||
function slowControl({ failFirst }) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'strata-chk-'));
|
||||
const install = join(root, 'install'), data = join(root, 'data');
|
||||
mkdirSync(install, { recursive: true }); mkdirSync(data, { recursive: true });
|
||||
const configPath = join(install, 'strata-iq3_s.json');
|
||||
writeFileSync(configPath, JSON.stringify({ exe: 'component:strata-engine', args: [], model_name: 'm', port: 8080 }));
|
||||
let asked = 0;
|
||||
const control = {
|
||||
control: { dir: join(root, 'control'), python: join(root, 'control', 'python', 'python') }, version: '0.2.10',
|
||||
async installedModels() {
|
||||
asked++;
|
||||
if (asked <= failFirst) throw new Error('Strata Control is still starting');
|
||||
return { install_dir: install, models: [{ id: 'iq3_s', name: 'm', source_config: configPath, context_tokens: 8192, backend: 'cuda', component_status: 'present', missing: [], issue_codes: [] }] };
|
||||
},
|
||||
async engine() { return { usable: true, backend: 'cuda', components_dir: join(root, 'c'), engine: { version: '0.1.56', platform: 'linux-x86_64', backend: 'cuda', exe: join(root, 'e', 'strata'), root: join(root, 'e') } }; },
|
||||
};
|
||||
const catalog = { families: [{ family: 'qwen', title: 'Q', about: 'x', images: false, experimental: false, sizes: [{ id: 'iq3_s', model: 'IQ3_S', download_gb: 1, ram_needed_gb: 1, about: 'x', on_this_pc: 'fits', installed: true }] }], contexts: [8192], recommendation: { family: 'qwen', model: 'IQ3_S' }, context_note: '' };
|
||||
const spawnProcess = () => {
|
||||
const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.exitCode = null;
|
||||
child.kill = () => { child.exitCode = 0; return true; };
|
||||
child.stdin = { write(line) {
|
||||
const m = JSON.parse(line); if (!m.id) return;
|
||||
const result = m.method === 'initialize' ? { protocolVersion: '2024-11-05' }
|
||||
: m.method === 'tools/list' ? { tools: ['strata_models', 'strata_status'].map(name => ({ name })) }
|
||||
: m.params.name === 'strata_models' ? { structuredContent: catalog } : { structuredContent: { hardware: { ram_gb: 16, gpus: [] }, install: null, running: [], summary: 'x' } };
|
||||
queueMicrotask(() => child.stdout.emit('data', Buffer.from(JSON.stringify({ jsonrpc: '2.0', id: m.id, result }) + '\n')));
|
||||
} };
|
||||
return child;
|
||||
};
|
||||
const native = createNativeControl({ control, directory: data, spawnProcess, fetcher: async () => Response.json({ status: 'ok' }), requests: () => [], modelsDirectory: () => join(root, 'models'), hardwareProbe: () => [] });
|
||||
return { native, asked: () => asked, cleanup: () => rmSync(root, { recursive: true, force: true }) };
|
||||
}
|
||||
|
||||
test('a read that fails while Control starts is "checking", not "nothing installed", and the page waits on it', async () => {
|
||||
const f = slowControl({ failFirst: 1 });
|
||||
try {
|
||||
const first = await f.native.refresh(false);
|
||||
assert.equal(first.available, false);
|
||||
assert.equal(first.checking, true, 'not known yet');
|
||||
assert.equal(installedListPending(first), true, 'the page shows Checking, no Connect engine, no Download & load');
|
||||
const again = await f.native.refresh(true); // the next poll reads Control again (retry is soon, not after 45 s)
|
||||
assert.equal(again.available, true);
|
||||
assert.equal(again.checking, false);
|
||||
assert.equal(installedListPending(again), false);
|
||||
assert.equal(again.installed.models[0].ready, true, 'the downloaded model is shown as ready');
|
||||
} finally { f.cleanup(); }
|
||||
});
|
||||
|
||||
test('before the first answer the state is checking; a plain poll after a failed start is not held for 45 s', async () => {
|
||||
const f = slowControl({ failFirst: 1 });
|
||||
try {
|
||||
assert.equal(f.native.state().checking, true);
|
||||
await f.native.refresh(false);
|
||||
await new Promise(r => setTimeout(r, 3100));
|
||||
const next = await f.native.refresh(false); // an ordinary (not forced) poll, a few seconds later
|
||||
assert.equal(next.available, true);
|
||||
assert.equal(next.checking, false);
|
||||
} finally { f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a Control that keeps failing stops being "checking" so the real error shows', async () => {
|
||||
const f = slowControl({ failFirst: 99 });
|
||||
try {
|
||||
let s;
|
||||
for (let i = 0; i < 4; i++) s = await f.native.refresh(true);
|
||||
assert.equal(s.checking, false);
|
||||
assert.match(s.error, /still starting/);
|
||||
assert.equal(installedListPending(s), false);
|
||||
} finally { f.cleanup(); }
|
||||
});
|
||||
Reference in new issue
Block a user