pinokiod 8.2.28 → 8.2.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kernel/anywhere/index.js +53 -27
- package/package.json +1 -1
- package/test/anywhere.test.js +184 -1
package/kernel/anywhere/index.js
CHANGED
|
@@ -46,6 +46,7 @@ class Anywhere {
|
|
|
46
46
|
this.starting = null
|
|
47
47
|
this.lastStatus = null
|
|
48
48
|
this.lastStatusAt = 0
|
|
49
|
+
this.statusQuery = null
|
|
49
50
|
this.lastServe = undefined // last serve config we pushed (undefined = unknown)
|
|
50
51
|
this.lastServeError = null
|
|
51
52
|
this.portMap = null // app key -> stable remote port (ports.json)
|
|
@@ -53,6 +54,8 @@ class Anywhere {
|
|
|
53
54
|
this.feature = null // cached query-feature result
|
|
54
55
|
this.featureAt = 0
|
|
55
56
|
this.error = null
|
|
57
|
+
this.statusError = null
|
|
58
|
+
this.featureError = null
|
|
56
59
|
this.login = null
|
|
57
60
|
this.stopping = null
|
|
58
61
|
}
|
|
@@ -216,6 +219,9 @@ class Anywhere {
|
|
|
216
219
|
}
|
|
217
220
|
if (this.stopping) return false
|
|
218
221
|
this.error = null
|
|
222
|
+
this.statusError = null
|
|
223
|
+
this.featureError = null
|
|
224
|
+
this.statusQuery = null
|
|
219
225
|
if (this.api) this.api.cancel()
|
|
220
226
|
if (this.syncing) await this.syncing.catch(() => {})
|
|
221
227
|
const forceLogin = !!(this.login && this.login.error)
|
|
@@ -282,7 +288,7 @@ class Anywhere {
|
|
|
282
288
|
this.lastServeError = null
|
|
283
289
|
this.lastStatusAt = 0
|
|
284
290
|
const status = await this.refreshStatus(true)
|
|
285
|
-
if (!status) throw new Error(this.error)
|
|
291
|
+
if (!status) throw new Error(this.error || this.statusError || 'Could not read Tailscale status.')
|
|
286
292
|
if (status.BackendState !== 'Running') this.requestLogin(forceLogin)
|
|
287
293
|
return true
|
|
288
294
|
}
|
|
@@ -345,6 +351,7 @@ class Anywhere {
|
|
|
345
351
|
return inspection ? this.ownedPid(inspection, execFileSync(inspection.file, inspection.args, inspection.options)) : null
|
|
346
352
|
}
|
|
347
353
|
async _stop() {
|
|
354
|
+
this.statusQuery = null
|
|
348
355
|
if (this.api) this.api.cancel()
|
|
349
356
|
if (this.starting) await this.starting.catch(() => {})
|
|
350
357
|
if (this.syncing) await this.syncing.catch(() => {})
|
|
@@ -359,6 +366,8 @@ class Anywhere {
|
|
|
359
366
|
await sleep(100)
|
|
360
367
|
}
|
|
361
368
|
this.error = null
|
|
369
|
+
this.statusError = null
|
|
370
|
+
this.featureError = null
|
|
362
371
|
this.lastServeError = null
|
|
363
372
|
this.child = null
|
|
364
373
|
this.login = null
|
|
@@ -414,10 +423,13 @@ class Anywhere {
|
|
|
414
423
|
}
|
|
415
424
|
async logout() {
|
|
416
425
|
if (!(await this.assertOwnership()) || this.stopping) return false
|
|
426
|
+
this.statusQuery = null
|
|
417
427
|
if (this.api) this.api.cancel()
|
|
418
428
|
this.login = null
|
|
419
429
|
await this.client().logout()
|
|
420
430
|
this.error = null
|
|
431
|
+
this.statusError = null
|
|
432
|
+
this.featureError = null
|
|
421
433
|
this.lastStatus = null
|
|
422
434
|
this.lastStatusAt = 0
|
|
423
435
|
this.lastServe = undefined
|
|
@@ -429,24 +441,35 @@ class Anywhere {
|
|
|
429
441
|
|
|
430
442
|
// ---------- status ----------
|
|
431
443
|
async refreshStatus(force) {
|
|
444
|
+
if (!force && this.statusQuery && this.statusQuery.api === this.api) {
|
|
445
|
+
return this.statusQuery.promise
|
|
446
|
+
}
|
|
432
447
|
const now = Date.now()
|
|
433
448
|
if (!force && this.lastStatus && now - this.lastStatusAt < STATUS_CACHE_MS) {
|
|
434
449
|
return this.lastStatus
|
|
435
450
|
}
|
|
436
451
|
const api = this.client()
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
452
|
+
const query = { api, promise: null }
|
|
453
|
+
this.statusQuery = query
|
|
454
|
+
query.promise = (async () => {
|
|
455
|
+
try {
|
|
456
|
+
const status = await api.status()
|
|
457
|
+
if (this.api !== api || this.statusQuery !== query) return null
|
|
458
|
+
this.lastStatus = status
|
|
459
|
+
this.statusError = null
|
|
460
|
+
this.lastStatusAt = Date.now()
|
|
461
|
+
return status
|
|
462
|
+
} catch (e) {
|
|
463
|
+
if (this.api !== api || this.statusQuery !== query) return null
|
|
464
|
+
if (!e.cancelled) this.statusError = e.message
|
|
465
|
+
this.lastStatus = null
|
|
466
|
+
this.lastStatusAt = Date.now()
|
|
467
|
+
return null
|
|
468
|
+
} finally {
|
|
469
|
+
if (this.statusQuery === query) this.statusQuery = null
|
|
470
|
+
}
|
|
471
|
+
})()
|
|
472
|
+
return query.promise
|
|
450
473
|
}
|
|
451
474
|
dnsNameFrom(status) {
|
|
452
475
|
if (!status || !status.Self || !status.Self.DNSName) return null
|
|
@@ -457,6 +480,7 @@ class Anywhere {
|
|
|
457
480
|
if (!status || status.BackendState !== 'Running') return null
|
|
458
481
|
if (Array.isArray(status.CertDomains) && status.CertDomains.length > 0) {
|
|
459
482
|
this.feature = { Complete: true }
|
|
483
|
+
this.featureError = null
|
|
460
484
|
return this.feature
|
|
461
485
|
}
|
|
462
486
|
if (this.feature && Date.now() - this.featureAt < FEATURE_QUERY_CACHE_MS) {
|
|
@@ -470,9 +494,10 @@ class Anywhere {
|
|
|
470
494
|
})
|
|
471
495
|
}
|
|
472
496
|
this.feature = await this.featureQuery
|
|
497
|
+
this.featureError = null
|
|
473
498
|
this.featureAt = Date.now()
|
|
474
499
|
} catch (e) {
|
|
475
|
-
if (!e.cancelled) this.
|
|
500
|
+
if (!e.cancelled) this.featureError = e.message
|
|
476
501
|
this.feature = null
|
|
477
502
|
this.featureAt = 0
|
|
478
503
|
} finally {
|
|
@@ -600,9 +625,10 @@ class Anywhere {
|
|
|
600
625
|
if (!Array.isArray(menu) || !this.isNodeHost(host)) return menu
|
|
601
626
|
// Confirm new or changed routes on first render; refresh checks unchanged ones.
|
|
602
627
|
const apps = this.appTargets()
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
628
|
+
const targetsChanged = apps.length !== this.targets.length ||
|
|
629
|
+
apps.some((app, i) => app.key !== this.targets[i].key || app.local !== this.targets[i].local || app.origin !== this.targets[i].origin)
|
|
630
|
+
if (this.syncing || this.lastServe === undefined || this.lastServeError || targetsChanged) {
|
|
631
|
+
await this.sync({ recheck: targetsChanged })
|
|
606
632
|
}
|
|
607
633
|
const rewrite = (items) => {
|
|
608
634
|
for (const item of items) {
|
|
@@ -623,10 +649,10 @@ class Anywhere {
|
|
|
623
649
|
}
|
|
624
650
|
// Regenerates the Serve config from the apps' published URLs. Called from
|
|
625
651
|
// kernel.refresh() (same trigger as router.update()) and after start.
|
|
626
|
-
async sync() {
|
|
627
|
-
//
|
|
652
|
+
async sync({ recheck = true } = {}) {
|
|
653
|
+
// Reads join the pending work; only changed targets request another pass.
|
|
628
654
|
if (this.syncing) {
|
|
629
|
-
this.syncAgain = true
|
|
655
|
+
if (recheck) this.syncAgain = true
|
|
630
656
|
return this.syncing
|
|
631
657
|
}
|
|
632
658
|
this.syncing = (async () => {
|
|
@@ -707,8 +733,8 @@ class Anywhere {
|
|
|
707
733
|
if (this.stopping || !this.config.enabled) return inactive()
|
|
708
734
|
if (!status) {
|
|
709
735
|
return Object.assign(base, {
|
|
710
|
-
phase: this.error ? 'error' : 'starting',
|
|
711
|
-
error: this.error,
|
|
736
|
+
phase: this.error || this.statusError ? 'error' : 'starting',
|
|
737
|
+
error: this.error || this.statusError,
|
|
712
738
|
running: false,
|
|
713
739
|
})
|
|
714
740
|
}
|
|
@@ -761,7 +787,7 @@ class Anywhere {
|
|
|
761
787
|
if (this.stopping || !this.config.enabled) return inactive()
|
|
762
788
|
if (!feature) {
|
|
763
789
|
out.phase = 'error'
|
|
764
|
-
out.error = this.error || 'Could not query HTTPS approval.'
|
|
790
|
+
out.error = this.error || this.featureError || 'Could not query HTTPS approval.'
|
|
765
791
|
return out
|
|
766
792
|
}
|
|
767
793
|
if (!feature.Complete) {
|
|
@@ -772,10 +798,10 @@ class Anywhere {
|
|
|
772
798
|
}
|
|
773
799
|
return out
|
|
774
800
|
}
|
|
775
|
-
await this.sync()
|
|
801
|
+
await this.sync({ recheck: false })
|
|
776
802
|
if (this.stopping || !this.config.enabled) return inactive()
|
|
777
|
-
out.phase = this.error || this.lastServeError || this.lastServe === undefined || !this.lastStatus || this.lastStatus.BackendState !== 'Running' ? 'error' : 'ready'
|
|
778
|
-
if (out.phase === 'error') out.error = this.error || this.lastServeError || 'Routes are not confirmed'
|
|
803
|
+
out.phase = this.error || this.statusError || this.featureError || this.lastServeError || this.lastServe === undefined || !this.lastStatus || this.lastStatus.BackendState !== 'Running' ? 'error' : 'ready'
|
|
804
|
+
if (out.phase === 'error') out.error = this.error || this.statusError || this.featureError || this.lastServeError || 'Routes are not confirmed'
|
|
779
805
|
out.serveError = this.lastServeError
|
|
780
806
|
out.serve = describeServeConfig(this.lastServe, dnsName, Object.fromEntries(this.targets.map(target => [target.listen, target.name])))
|
|
781
807
|
return out
|
package/package.json
CHANGED
package/test/anywhere.test.js
CHANGED
|
@@ -505,7 +505,6 @@ test('a failed HTTPS query surfaces an error instead of looping in starting', as
|
|
|
505
505
|
assert.equal(snap.phase, 'error')
|
|
506
506
|
assert.match(snap.error, /503 no netmap/)
|
|
507
507
|
assert.equal(snap.approval, null)
|
|
508
|
-
anywhere.error = null
|
|
509
508
|
feature = () => ({ Complete: false, Text: 'approve', URL: 'https://login.tailscale.com/f/serve?node=1' })
|
|
510
509
|
anywhere.lastStatusAt = 0
|
|
511
510
|
snap = await anywhere.snapshot()
|
|
@@ -1404,3 +1403,187 @@ test('cancelling a pending HTTPS approval during Stop keeps the result Off', asy
|
|
|
1404
1403
|
assert.equal((await anywhere.snapshot()).approval.url, 'https://login.tailscale.com/f/fresh')
|
|
1405
1404
|
} finally { fs.rmSync(tmp, { recursive: true, force: true }) }
|
|
1406
1405
|
})
|
|
1406
|
+
|
|
1407
|
+
test('a transient status failure recovers on the next snapshot without restarting Anywhere', async t => {
|
|
1408
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-status-recovery-'))
|
|
1409
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }))
|
|
1410
|
+
const anywhere = new Anywhere(fakeKernel(tmp))
|
|
1411
|
+
await anywhere.saveConfig({ enabled: true })
|
|
1412
|
+
anywhere.readPid = () => 123
|
|
1413
|
+
let failed = true, statusReads = 0
|
|
1414
|
+
const routes = buildServeConfig({ dnsName: 'pinokio.tail1.ts.net', serverPort: 42000 })
|
|
1415
|
+
anywhere.api = {
|
|
1416
|
+
status: async () => {
|
|
1417
|
+
statusReads++
|
|
1418
|
+
if (failed) throw new Error('temporary CLI timeout')
|
|
1419
|
+
return runningStatus()
|
|
1420
|
+
},
|
|
1421
|
+
serveConfig: async () => routes,
|
|
1422
|
+
setServeConfig: async () => assert.fail('confirmed routes do not need rewriting'),
|
|
1423
|
+
up: async () => assert.fail('recovery must not reauthenticate'),
|
|
1424
|
+
}
|
|
1425
|
+
const failure = await anywhere.snapshot()
|
|
1426
|
+
assert.equal(failure.phase, 'error')
|
|
1427
|
+
assert.equal(failure.error, 'temporary CLI timeout')
|
|
1428
|
+
failed = false
|
|
1429
|
+
const recovered = await anywhere.snapshot()
|
|
1430
|
+
assert.equal(recovered.phase, 'ready')
|
|
1431
|
+
assert.equal(recovered.error, null)
|
|
1432
|
+
assert.equal(anywhere.statusError, null)
|
|
1433
|
+
assert.equal(statusReads, 2)
|
|
1434
|
+
})
|
|
1435
|
+
|
|
1436
|
+
test('successful status reads never clear a lifecycle failure', async () => {
|
|
1437
|
+
const anywhere = new Anywhere(fakeKernel(os.tmpdir()))
|
|
1438
|
+
anywhere.config = { enabled: true }
|
|
1439
|
+
anywhere.error = 'could not stop the owned daemon'
|
|
1440
|
+
anywhere.api = { status: async () => runningStatus() }
|
|
1441
|
+
await anywhere.refreshStatus(true)
|
|
1442
|
+
const snapshot = await anywhere.snapshot()
|
|
1443
|
+
assert.equal(snapshot.phase, 'error')
|
|
1444
|
+
assert.equal(snapshot.error, 'could not stop the owned daemon')
|
|
1445
|
+
})
|
|
1446
|
+
|
|
1447
|
+
test('repeated status snapshots join Serve sync without extending its passes', async t => {
|
|
1448
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-status-sync-'))
|
|
1449
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }))
|
|
1450
|
+
const anywhere = new Anywhere(fakeKernel(tmp))
|
|
1451
|
+
await anywhere.saveConfig({ enabled: true })
|
|
1452
|
+
anywhere.readPid = () => 123
|
|
1453
|
+
anywhere.api = { status: async () => runningStatus() }
|
|
1454
|
+
anywhere.lastServe = buildServeConfig({ dnsName: 'pinokio.tail1.ts.net', serverPort: 42000 })
|
|
1455
|
+
let release, passes = 0
|
|
1456
|
+
anywhere.pushServeConfig = async () => {
|
|
1457
|
+
passes++
|
|
1458
|
+
await new Promise(resolve => { release = resolve })
|
|
1459
|
+
return false
|
|
1460
|
+
}
|
|
1461
|
+
const work = anywhere.sync()
|
|
1462
|
+
// Actual target changes must still get their follow-up pass.
|
|
1463
|
+
const changed = anywhere.sync()
|
|
1464
|
+
const first = Array.from({ length: 6 }, () => anywhere.snapshot())
|
|
1465
|
+
await new Promise(setImmediate)
|
|
1466
|
+
release()
|
|
1467
|
+
await new Promise(setImmediate)
|
|
1468
|
+
assert.equal(passes, 2)
|
|
1469
|
+
const later = Array.from({ length: 6 }, () => anywhere.snapshot())
|
|
1470
|
+
await new Promise(setImmediate)
|
|
1471
|
+
assert.equal(anywhere.syncAgain, false, 'polling does not queue a third pass')
|
|
1472
|
+
release()
|
|
1473
|
+
await Promise.all([work, changed])
|
|
1474
|
+
for (const snapshot of await Promise.all([...first, ...later])) assert.equal(snapshot.phase, 'ready')
|
|
1475
|
+
assert.equal(passes, 2)
|
|
1476
|
+
assert.equal(anywhere.syncing, null)
|
|
1477
|
+
})
|
|
1478
|
+
|
|
1479
|
+
test('unchanged menu reads join a slow Serve sync without requesting another pass', async () => {
|
|
1480
|
+
const anywhere = new Anywhere(fakeKernel(os.tmpdir()))
|
|
1481
|
+
anywhere.config = { enabled: true }
|
|
1482
|
+
anywhere.lastStatus = runningStatus()
|
|
1483
|
+
anywhere.lastServe = buildServeConfig({ dnsName: 'pinokio.tail1.ts.net', serverPort: 42000 })
|
|
1484
|
+
let release, passes = 0
|
|
1485
|
+
anywhere.pushServeConfig = async () => {
|
|
1486
|
+
passes++
|
|
1487
|
+
await new Promise(resolve => { release = resolve })
|
|
1488
|
+
return false
|
|
1489
|
+
}
|
|
1490
|
+
const sync = anywhere.sync()
|
|
1491
|
+
const menus = Array.from({ length: 10 }, () => anywhere.rewriteMenu('pinokio.tail1.ts.net', []))
|
|
1492
|
+
assert.equal(anywhere.syncAgain, false)
|
|
1493
|
+
release()
|
|
1494
|
+
await Promise.all([sync, ...menus])
|
|
1495
|
+
assert.equal(passes, 1)
|
|
1496
|
+
})
|
|
1497
|
+
|
|
1498
|
+
test('startup preserves the cause of a failed initial status read and an explicit retry can recover', async t => {
|
|
1499
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-start-status-'))
|
|
1500
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }))
|
|
1501
|
+
const anywhere = new Anywhere(fakeKernel(tmp))
|
|
1502
|
+
anywhere.config = { enabled: true }
|
|
1503
|
+
anywhere.readPid = () => 123
|
|
1504
|
+
let failed = true
|
|
1505
|
+
anywhere.api = {
|
|
1506
|
+
cancel() {}, ping: async () => true, setPrefs: async () => {},
|
|
1507
|
+
status: async () => {
|
|
1508
|
+
if (failed) throw new Error('initial status unavailable')
|
|
1509
|
+
return runningStatus()
|
|
1510
|
+
},
|
|
1511
|
+
}
|
|
1512
|
+
await assert.rejects(anywhere.start(), /initial status unavailable/)
|
|
1513
|
+
assert.equal((await anywhere.snapshot()).error, 'initial status unavailable')
|
|
1514
|
+
failed = false
|
|
1515
|
+
assert.equal(await anywhere.start(), true)
|
|
1516
|
+
assert.equal(anywhere.error, null)
|
|
1517
|
+
assert.equal(anywhere.statusError, null)
|
|
1518
|
+
})
|
|
1519
|
+
|
|
1520
|
+
test('concurrent routine status reads share one CLI query and release it after success or failure', async () => {
|
|
1521
|
+
const anywhere = new Anywhere(fakeKernel(os.tmpdir()))
|
|
1522
|
+
let reads = 0, resolve, reject
|
|
1523
|
+
anywhere.api = { status() {
|
|
1524
|
+
reads++
|
|
1525
|
+
return new Promise((ok, fail) => { resolve = ok; reject = fail })
|
|
1526
|
+
} }
|
|
1527
|
+
for (const failure of [false, true]) {
|
|
1528
|
+
anywhere.lastStatusAt = 0
|
|
1529
|
+
const pending = Array.from({ length: 8 }, () => anywhere.refreshStatus(false))
|
|
1530
|
+
assert.equal(reads, failure ? 2 : 1, 'concurrent readers share one CLI process')
|
|
1531
|
+
if (failure) reject(new Error('temporary read failure'))
|
|
1532
|
+
else resolve(runningStatus())
|
|
1533
|
+
const results = await Promise.all(pending)
|
|
1534
|
+
assert.ok(results.every(result => failure ? result === null : result.BackendState === 'Running'))
|
|
1535
|
+
assert.equal(anywhere.statusQuery, null)
|
|
1536
|
+
}
|
|
1537
|
+
const recovered = anywhere.refreshStatus(false)
|
|
1538
|
+
resolve(runningStatus())
|
|
1539
|
+
assert.equal((await recovered).BackendState, 'Running')
|
|
1540
|
+
assert.equal(anywhere.statusError, null)
|
|
1541
|
+
})
|
|
1542
|
+
|
|
1543
|
+
test('a forced status read supersedes older success and failure without letting their finalizers remove its query', async () => {
|
|
1544
|
+
for (const oldFailure of [false, true]) {
|
|
1545
|
+
const anywhere = new Anywhere(fakeKernel(os.tmpdir()))
|
|
1546
|
+
const reads = []
|
|
1547
|
+
anywhere.api = { status() {
|
|
1548
|
+
return new Promise((resolve, reject) => reads.push({ resolve, reject }))
|
|
1549
|
+
} }
|
|
1550
|
+
const older = anywhere.refreshStatus(false)
|
|
1551
|
+
const newer = anywhere.refreshStatus(true)
|
|
1552
|
+
assert.equal(reads.length, 2, 'a forced read bypasses the pending routine query')
|
|
1553
|
+
if (oldFailure) reads[0].reject(new Error('older CLI timeout'))
|
|
1554
|
+
else reads[0].resolve({ BackendState: 'Stopped' })
|
|
1555
|
+
assert.equal(await older, null)
|
|
1556
|
+
const joining = anywhere.refreshStatus(false)
|
|
1557
|
+
assert.equal(reads.length, 2, 'an old finalizer cannot drop the replacement query')
|
|
1558
|
+
reads[1].resolve(runningStatus())
|
|
1559
|
+
await Promise.all([newer, joining])
|
|
1560
|
+
assert.equal(anywhere.lastStatus.BackendState, 'Running')
|
|
1561
|
+
assert.equal(anywhere.statusError, null)
|
|
1562
|
+
assert.equal(anywhere.statusQuery, null)
|
|
1563
|
+
// Also check the reversed completion order: old errors must not erase recovery.
|
|
1564
|
+
const late = anywhere.refreshStatus(true)
|
|
1565
|
+
const latest = anywhere.refreshStatus(true)
|
|
1566
|
+
reads[3].resolve(runningStatus())
|
|
1567
|
+
await latest
|
|
1568
|
+
if (oldFailure) reads[2].reject(new Error('late CLI timeout'))
|
|
1569
|
+
else reads[2].resolve({ BackendState: 'Stopped' })
|
|
1570
|
+
await late
|
|
1571
|
+
assert.equal(anywhere.lastStatus.BackendState, 'Running')
|
|
1572
|
+
assert.equal(anywhere.statusError, null)
|
|
1573
|
+
}
|
|
1574
|
+
})
|
|
1575
|
+
|
|
1576
|
+
test('a replaced CLI client has its own pending status query and ignores the former client', async () => {
|
|
1577
|
+
const anywhere = new Anywhere(fakeKernel(os.tmpdir()))
|
|
1578
|
+
let oldReply, newReply
|
|
1579
|
+
anywhere.api = { status: () => new Promise(resolve => { oldReply = resolve }) }
|
|
1580
|
+
const old = anywhere.refreshStatus(false)
|
|
1581
|
+
anywhere.api = { status: () => new Promise(resolve => { newReply = resolve }) }
|
|
1582
|
+
const fresh = anywhere.refreshStatus(false)
|
|
1583
|
+
newReply(runningStatus())
|
|
1584
|
+
await fresh
|
|
1585
|
+
oldReply({ BackendState: 'Stopped' })
|
|
1586
|
+
assert.equal(await old, null)
|
|
1587
|
+
assert.equal(anywhere.lastStatus.BackendState, 'Running')
|
|
1588
|
+
assert.equal(anywhere.statusQuery, null)
|
|
1589
|
+
})
|