mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
When WFIGS was unreachable the proxy served its last snapshot, marked stale
(X-Data-Stale, body.stale), with no age limit, and the client source kept
only the rows. The layer then read healthy with a fresh update time and
get_fire_perimeters reported the perimeters as current; three days into an
outage it still said "1 mapped wildfire perimeter in the requested box."
The source now offers getSnapshotDetail ({rows, stale, fetchedAt}); the
layer reports stale with the snapshot's fetch time, the tool adds stale,
retrieved_at and "(data may be stale)", and the proxy serves a stale
snapshot for at most 24 hours, as the GNSS proxy caps its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
588 lines
19 KiB
JavaScript
588 lines
19 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createFirePerimetersLayer } from './index.js';
|
|
|
|
function harness(
|
|
source,
|
|
{
|
|
pick = () => null,
|
|
inciwebIndex = null,
|
|
cardHit = () => null,
|
|
publication = async () => ({
|
|
createdMs: 1757000000000,
|
|
changedMs: 1757900000000,
|
|
}),
|
|
} = {},
|
|
) {
|
|
const sources = [];
|
|
const overlay = { entries: new Map(), visible: null };
|
|
const clicks = { handler: null, destroyed: 0 };
|
|
const owners = new Map();
|
|
const opened = [];
|
|
const viewer = {
|
|
scene: { pick },
|
|
dataSources: {
|
|
add(value) {
|
|
sources.push(value);
|
|
},
|
|
remove(value) {
|
|
sources.splice(sources.indexOf(value), 1);
|
|
},
|
|
},
|
|
};
|
|
const layer = createFirePerimetersLayer({
|
|
source,
|
|
overlayHost: {
|
|
setEntries(sourceId, entries) {
|
|
overlay.entries.set(sourceId, entries);
|
|
},
|
|
setVisible(sourceId, visible) {
|
|
overlay.visible = visible;
|
|
},
|
|
clearSource(sourceId) {
|
|
overlay.entries.delete(sourceId);
|
|
},
|
|
hitTest: (x, y, options) => cardHit(x, y, options),
|
|
},
|
|
screenSpaceEventHandlerFactory: () => ({
|
|
setInputAction(callback) {
|
|
clicks.handler = callback;
|
|
},
|
|
destroy() {
|
|
clicks.destroyed += 1;
|
|
clicks.handler = null;
|
|
},
|
|
}),
|
|
picking: {
|
|
resolvePickId: (picked) => picked?.id ?? null,
|
|
isOwnedByOtherLayer: (layerId, pickedId) =>
|
|
String(pickedId).startsWith('other-layer:'),
|
|
registerPickOwner: (layerId, predicate) => owners.set(layerId, predicate),
|
|
unregisterPickOwner: (layerId) => owners.delete(layerId),
|
|
},
|
|
pointer: { isPointerFree: () => true },
|
|
inciwebSource: inciwebIndex
|
|
? { getIndex: async () => inciwebIndex }
|
|
: { getIndex: async () => [] },
|
|
inciwebPublications: { getPublication: publication },
|
|
openExternal: (url) => opened.push(url),
|
|
});
|
|
layer.init(viewer);
|
|
layer.enable(viewer);
|
|
return { layer, viewer, sources, overlay, clicks, owners, opened };
|
|
}
|
|
|
|
const ring = [
|
|
[-108.1, 35.2],
|
|
[-108.0, 35.2],
|
|
[-108.0, 35.3],
|
|
[-108.1, 35.2],
|
|
];
|
|
const row = {
|
|
stableId: '2026-NMGNF-000123',
|
|
name: 'Fixture Fire',
|
|
acres: 512.5,
|
|
containedPct: 40,
|
|
state: 'US-NM',
|
|
category: 'WF',
|
|
discoveredTime: 1757900000000,
|
|
updatedTime: 1757950000000,
|
|
cause: 'Natural',
|
|
behavior: 'Active',
|
|
personnel: 380,
|
|
county: 'Sandoval',
|
|
costToDate: 4200000,
|
|
complexity: 'Type 3 Incident',
|
|
complexName: 'ROWE CREEK COMPLEX',
|
|
polygons: [[ring]],
|
|
};
|
|
|
|
test('each perimeter polygon renders as one filled entity with its fire line', async () => {
|
|
const h = harness({
|
|
getSnapshot: async () => [
|
|
row,
|
|
{ ...row, stableId: 'other', polygons: [[ring], [ring]] },
|
|
],
|
|
});
|
|
assert.equal(await h.layer.update(h.viewer), true);
|
|
const entities = h.sources[0].entities.values;
|
|
assert.equal(entities.length, 3);
|
|
assert.equal(entities[0].id, 'fire-perimeter:2026-NMGNF-000123:0');
|
|
assert.ok(entities[0].polygon, 'perimeter fill missing');
|
|
assert.ok(entities[0].polyline, 'fire line missing');
|
|
assert.equal(entities[1].id, 'fire-perimeter:other:0');
|
|
assert.equal(entities[2].id, 'fire-perimeter:other:1');
|
|
assert.equal(h.layer.getStats().count, 2);
|
|
});
|
|
|
|
test('late refresh cannot publish after disable or destroy', async () => {
|
|
for (const action of ['disable', 'destroy']) {
|
|
let resolve, signal;
|
|
const h = harness({
|
|
getSnapshot(options) {
|
|
signal = options.signal;
|
|
return new Promise((done) => {
|
|
resolve = done;
|
|
});
|
|
},
|
|
});
|
|
const pending = h.layer.update(h.viewer);
|
|
h.layer[action](h.viewer);
|
|
assert.equal(signal.aborted, true);
|
|
if (action === 'disable') h.layer.enable(h.viewer);
|
|
resolve([row]);
|
|
assert.equal(await pending, false);
|
|
assert.equal(h.layer.getStats().count, 0);
|
|
h.layer.destroy(h.viewer);
|
|
}
|
|
});
|
|
|
|
test('clicking a perimeter publishes its incident card; empty space clears it', async () => {
|
|
let pickResult = null;
|
|
const h = harness(
|
|
{ getSnapshot: async () => [row] },
|
|
{
|
|
pick: () => pickResult,
|
|
},
|
|
);
|
|
await h.layer.update(h.viewer);
|
|
assert.equal(typeof h.clicks.handler, 'function', 'click handler missing');
|
|
// Deliberately NOT registered as a pick owner: sibling layers' click
|
|
// handlers yield to owned picks before their own card hit-tests, so
|
|
// claiming these large ground polygons would make FIRMS/vessel/CCTV
|
|
// overlay cards inert anywhere over a perimeter.
|
|
assert.equal(h.owners.size, 0);
|
|
|
|
pickResult = { id: 'fire-perimeter:2026-NMGNF-000123:0' };
|
|
h.clicks.handler({ position: { x: 10, y: 10 } });
|
|
const entries = h.overlay.entries.get('fire-perimeters');
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(entries[0].id, 'fire-perimeter-card:2026-NMGNF-000123');
|
|
assert.equal(entries[0].title, 'FIRE · Fixture Fire');
|
|
assert.ok(entries[0].position, 'card must carry a world anchor');
|
|
|
|
pickResult = { id: 'other-layer:aircraft-1' };
|
|
h.clicks.handler({ position: { x: 10, y: 10 } });
|
|
assert.equal(
|
|
h.overlay.entries.get('fire-perimeters')?.length,
|
|
1,
|
|
'sibling-layer picks must not clear the selection',
|
|
);
|
|
|
|
pickResult = null;
|
|
h.clicks.handler({ position: { x: 10, y: 10 } });
|
|
assert.equal(h.overlay.entries.get('fire-perimeters')?.length ?? 0, 0);
|
|
});
|
|
|
|
test('disable removes the click handler, pick ownership, and any card', async () => {
|
|
const h = harness(
|
|
{ getSnapshot: async () => [row] },
|
|
{ pick: () => ({ id: 'fire-perimeter:2026-NMGNF-000123:0' }) },
|
|
);
|
|
await h.layer.update(h.viewer);
|
|
h.clicks.handler({ position: { x: 10, y: 10 } });
|
|
h.layer.disable(h.viewer);
|
|
assert.equal(h.clicks.destroyed, 1);
|
|
assert.equal(h.owners.has('fire-perimeters'), false);
|
|
assert.equal(h.overlay.entries.has('fire-perimeters'), false);
|
|
});
|
|
|
|
test('a refresh that drops the selected incident also drops its card', async () => {
|
|
let rows = [row];
|
|
const h = harness(
|
|
{ getSnapshot: async () => rows },
|
|
{ pick: () => ({ id: 'fire-perimeter:2026-NMGNF-000123:0' }) },
|
|
);
|
|
await h.layer.update(h.viewer);
|
|
h.clicks.handler({ position: { x: 10, y: 10 } });
|
|
assert.equal(h.overlay.entries.get('fire-perimeters').length, 1);
|
|
rows = [{ ...row, stableId: 'different' }];
|
|
await h.layer.update(h.viewer);
|
|
assert.equal(h.overlay.entries.get('fire-perimeters')?.length ?? 0, 0);
|
|
});
|
|
|
|
test('a matched incident card carries the InciWeb line and click-through', async () => {
|
|
const inciwebIndex = [
|
|
{
|
|
incident_id: '329300',
|
|
tau: 'NMGNF Fixture Fire',
|
|
incident_title: 'Fixture Fire',
|
|
},
|
|
];
|
|
let cardHitResult = null;
|
|
const h = harness(
|
|
{ getSnapshot: async () => [{ ...row, name: 'Fixture' }] },
|
|
{
|
|
pick: () => null,
|
|
inciwebIndex,
|
|
cardHit: () => cardHitResult,
|
|
},
|
|
);
|
|
await h.layer.update(h.viewer);
|
|
// Select via a perimeter pick.
|
|
h.viewer.scene.pick = () => ({
|
|
id: 'fire-perimeter:2026-NMGNF-000123:0',
|
|
});
|
|
h.clicks.handler({ position: { x: 10, y: 10 } });
|
|
// The card publishes immediately; the link line lands once the
|
|
// publication's currency is validated.
|
|
assert.equal(h.overlay.entries.get('fire-perimeters').length, 1);
|
|
await new Promise((done) => setTimeout(done, 0));
|
|
const entries = h.overlay.entries.get('fire-perimeters');
|
|
assert.equal(entries[0].details.at(-1), 'InciWeb ↗ · click card to open');
|
|
|
|
// Clicking the card itself opens the InciWeb page.
|
|
cardHitResult = { entryId: entries[0].id };
|
|
h.viewer.scene.pick = () => null;
|
|
h.clicks.handler({ position: { x: 12, y: 12 } });
|
|
assert.deepEqual(h.opened, ['https://inciweb.wildfire.gov/node/329300']);
|
|
assert.equal(
|
|
h.overlay.entries.get('fire-perimeters').length,
|
|
1,
|
|
'opening the link must not clear the selection',
|
|
);
|
|
});
|
|
|
|
test('an unmatched incident renders no InciWeb line and card clicks stay inert', async () => {
|
|
let cardHitResult = null;
|
|
const h = harness(
|
|
{ getSnapshot: async () => [row] },
|
|
{
|
|
pick: () => ({ id: 'fire-perimeter:2026-NMGNF-000123:0' }),
|
|
inciwebIndex: [],
|
|
cardHit: () => cardHitResult,
|
|
},
|
|
);
|
|
await h.layer.update(h.viewer);
|
|
h.clicks.handler({ position: { x: 10, y: 10 } });
|
|
const entries = h.overlay.entries.get('fire-perimeters');
|
|
assert.equal(
|
|
entries[0].details.some((line) => line.includes('InciWeb')),
|
|
false,
|
|
);
|
|
cardHitResult = { entryId: entries[0].id };
|
|
h.viewer.scene.pick = () => null;
|
|
h.clicks.handler({ position: { x: 12, y: 12 } });
|
|
assert.deepEqual(h.opened, []);
|
|
assert.equal(h.overlay.entries.get('fire-perimeters').length, 1);
|
|
});
|
|
|
|
test('a complex member resolves to its complex page from the catalog', async () => {
|
|
const h = harness(
|
|
{ getSnapshot: async () => [row] },
|
|
{
|
|
pick: () => ({ id: 'fire-perimeter:2026-NMGNF-000123:0' }),
|
|
inciwebIndex: [
|
|
{
|
|
// Same state as the member fire: the state filter applies to
|
|
// complex fallbacks too.
|
|
incident_id: '328923',
|
|
tau: 'NMPRD Rowe Creek Complex',
|
|
incident_title: 'Rowe Creek Complex',
|
|
},
|
|
],
|
|
},
|
|
);
|
|
await h.layer.update(h.viewer);
|
|
h.clicks.handler({ position: { x: 10, y: 10 } });
|
|
await new Promise((done) => setTimeout(done, 0));
|
|
const entries = h.overlay.entries.get('fire-perimeters');
|
|
assert.equal(entries[0].details.at(-1), 'InciWeb ↗ · click card to open');
|
|
});
|
|
|
|
test('a stale or unverifiable publication yields no link', async () => {
|
|
const inciwebIndex = [
|
|
{
|
|
incident_id: '100',
|
|
tau: 'NMGNF Fixture Fire',
|
|
incident_title: 'Fixture Fire',
|
|
},
|
|
];
|
|
for (const publication of [
|
|
// Created two years before this fire's discovery: an archived page.
|
|
async () => ({
|
|
createdMs: 1757900000000 - 700 * 86400000,
|
|
changedMs: null,
|
|
}),
|
|
// Validation endpoint down: fail closed, no link.
|
|
async () => {
|
|
throw new Error('InciWeb HTTP 503');
|
|
},
|
|
]) {
|
|
const h = harness(
|
|
{ getSnapshot: async () => [row] },
|
|
{
|
|
pick: () => ({ id: 'fire-perimeter:2026-NMGNF-000123:0' }),
|
|
inciwebIndex,
|
|
publication,
|
|
},
|
|
);
|
|
await h.layer.update(h.viewer);
|
|
h.clicks.handler({ position: { x: 10, y: 10 } });
|
|
await new Promise((done) => setTimeout(done, 0));
|
|
const entries = h.overlay.entries.get('fire-perimeters');
|
|
assert.equal(entries.length, 1);
|
|
assert.equal(
|
|
entries[0].details.some((line) => line.includes('InciWeb')),
|
|
false,
|
|
);
|
|
assert.equal(entries[0].interactive, false);
|
|
h.layer.destroy(h.viewer);
|
|
}
|
|
});
|
|
|
|
test('a hanging InciWeb index fetch never blocks the perimeter refresh', async () => {
|
|
const h = harness({ getSnapshot: async () => [row] }, { inciwebIndex: null });
|
|
// Replace the index source with one that never resolves.
|
|
h.layer.destroy(h.viewer);
|
|
const hanging = harness({ getSnapshot: async () => [row] });
|
|
hanging.layer.destroy(hanging.viewer);
|
|
const sources = [];
|
|
const viewer = {
|
|
scene: { pick: () => null },
|
|
dataSources: {
|
|
add: (v) => sources.push(v),
|
|
remove: (v) => sources.splice(sources.indexOf(v), 1),
|
|
},
|
|
};
|
|
const layer = createFirePerimetersLayer({
|
|
source: { getSnapshot: async () => [row] },
|
|
overlayHost: {
|
|
setEntries() {},
|
|
setVisible() {},
|
|
clearSource() {},
|
|
hitTest: () => null,
|
|
},
|
|
screenSpaceEventHandlerFactory: () => ({
|
|
setInputAction() {},
|
|
destroy() {},
|
|
}),
|
|
picking: {
|
|
resolvePickId: () => null,
|
|
isOwnedByOtherLayer: () => false,
|
|
registerPickOwner() {},
|
|
unregisterPickOwner() {},
|
|
},
|
|
pointer: { isPointerFree: () => true },
|
|
inciwebSource: { getIndex: () => new Promise(() => {}) },
|
|
inciwebPublications: {
|
|
getPublication: async () => ({ createdMs: 1, changedMs: 1 }),
|
|
},
|
|
openExternal: () => {},
|
|
});
|
|
layer.init(viewer);
|
|
layer.enable(viewer);
|
|
assert.equal(await layer.update(viewer), true);
|
|
assert.equal(layer.getStats().count, 1);
|
|
layer.destroy(viewer);
|
|
});
|
|
|
|
test('an InciWeb outage never breaks the perimeter refresh', async () => {
|
|
const h = harness({ getSnapshot: async () => [row] });
|
|
const failing = createFirePerimetersLayer({
|
|
source: { getSnapshot: async () => [row] },
|
|
overlayHost: {
|
|
setEntries() {},
|
|
setVisible() {},
|
|
clearSource() {},
|
|
hitTest: () => null,
|
|
},
|
|
screenSpaceEventHandlerFactory: () => ({
|
|
setInputAction() {},
|
|
destroy() {},
|
|
}),
|
|
picking: {
|
|
resolvePickId: () => null,
|
|
isOwnedByOtherLayer: () => false,
|
|
registerPickOwner() {},
|
|
unregisterPickOwner() {},
|
|
},
|
|
pointer: { isPointerFree: () => true },
|
|
inciwebSource: {
|
|
getIndex: async () => {
|
|
throw new Error('InciWeb HTTP 503');
|
|
},
|
|
},
|
|
openExternal: () => {},
|
|
});
|
|
const viewer = {
|
|
scene: { pick: () => null },
|
|
dataSources: { add() {}, remove() {} },
|
|
};
|
|
failing.init(viewer);
|
|
failing.enable(viewer);
|
|
assert.equal(await failing.update(viewer), true);
|
|
assert.equal(failing.getStats().count, 1);
|
|
failing.destroy(viewer);
|
|
h.layer.destroy(h.viewer);
|
|
});
|
|
|
|
test('analyst records expose incident facts without geometry payloads', async () => {
|
|
const h = harness({ getSnapshot: async () => [row] });
|
|
await h.layer.update(h.viewer);
|
|
const records = h.layer.getAnalystRecords();
|
|
assert.equal(records.length, 1);
|
|
assert.equal(records[0].name, 'Fixture Fire');
|
|
assert.equal(records[0].acres, 512.5);
|
|
assert.equal(records[0].containedPct, 40);
|
|
assert.equal(records[0].state, 'US-NM');
|
|
assert.equal(records[0].cause, 'Natural');
|
|
assert.equal(records[0].behavior, 'Active');
|
|
assert.equal(records[0].personnel, 380);
|
|
assert.equal(records[0].county, 'Sandoval');
|
|
assert.equal(records[0].costToDate, 4200000);
|
|
assert.equal(records[0].complexity, 'Type 3 Incident');
|
|
// Spatial scoping in the analyst engine requires coordinates.
|
|
assert.ok(Math.abs(records[0].lat - 35.2333) < 0.01);
|
|
assert.ok(Math.abs(records[0].lon - -108.0333) < 0.01);
|
|
assert.equal('polygons' in records[0], false);
|
|
});
|
|
|
|
test('unchanged snapshots retain entity identity even if source ordering changes', async () => {
|
|
let rows = [row, { ...row, stableId: 'other' }];
|
|
const h = harness({ getSnapshot: async () => rows });
|
|
await h.layer.update();
|
|
const original = [...h.sources[0].entities.values];
|
|
let removals = 0;
|
|
const entities = h.sources[0].entities;
|
|
const removeAll = entities.removeAll.bind(entities);
|
|
entities.removeAll = () => {
|
|
removals++;
|
|
removeAll();
|
|
};
|
|
rows = [...rows].reverse();
|
|
await h.layer.update();
|
|
assert.equal(removals, 0);
|
|
assert.equal(entities.values[0], original[0]);
|
|
rows = rows.map((value) => ({
|
|
...value,
|
|
updatedTime: value.updatedTime + 1,
|
|
}));
|
|
await h.layer.update();
|
|
assert.equal(removals, 1);
|
|
assert.notEqual(entities.values[0], original[0]);
|
|
rows = rows.map((value) => ({ ...value, containedPct: 100 }));
|
|
await h.layer.update();
|
|
assert.equal(
|
|
removals,
|
|
2,
|
|
'incident facts can change without a new polygon timestamp',
|
|
);
|
|
assert.equal(h.layer.getRowControls().legend[3].count, 2);
|
|
rows = [];
|
|
await h.layer.update();
|
|
assert.equal(entities.values.length, 0);
|
|
h.layer.destroy();
|
|
});
|
|
|
|
test('containment legend uses the existing thresholds, colours and incident counts', async () => {
|
|
const values = [null, -1, 0, 0.5, 49.9, 50, 99.9, 100, 101];
|
|
const h = harness({
|
|
getSnapshot: async () =>
|
|
values.map((containedPct, index) => ({
|
|
...row,
|
|
stableId: String(index),
|
|
containedPct,
|
|
})),
|
|
});
|
|
await h.layer.update();
|
|
const controls = h.layer.getRowControls();
|
|
assert.deepEqual(controls.chips, []);
|
|
assert.deepEqual(
|
|
controls.legend.map(({ color, count }) => [color, count]),
|
|
[
|
|
['#ff3b30', 3],
|
|
['#ff7a00', 2],
|
|
['#ffb300', 2],
|
|
['#8bc34a', 2],
|
|
],
|
|
);
|
|
assert.ok(
|
|
controls.legend.every(
|
|
({ label }) => typeof label === 'string' && label.length,
|
|
),
|
|
);
|
|
assert.equal(
|
|
controls.legend[0].blurb,
|
|
'Colour shows reported containment. Perimeters are simplified to about 100 m.',
|
|
);
|
|
h.layer.destroy();
|
|
assert.deepEqual(
|
|
h.layer.getRowControls().legend.map(({ count }) => count),
|
|
[0, 0, 0, 0],
|
|
);
|
|
});
|
|
|
|
for (const action of ['disable', 'destroy', 'selection']) {
|
|
test(`incident-link verification aborts on ${action} and ignores late completion`, async () => {
|
|
let finish, signal;
|
|
let pick = { id: `fire-perimeter:${row.stableId}:0` };
|
|
const h = harness(
|
|
{
|
|
getSnapshot: async () => [
|
|
row,
|
|
{ ...row, stableId: 'other', name: 'Other Fire' },
|
|
],
|
|
},
|
|
{
|
|
pick: () => pick,
|
|
inciwebIndex: [
|
|
{ incident_id: '123', incident_title: row.name, tau: 'nm' },
|
|
],
|
|
publication: (_id, options) => {
|
|
signal = options.signal;
|
|
return new Promise((resolve) => {
|
|
finish = resolve;
|
|
});
|
|
},
|
|
},
|
|
);
|
|
await h.layer.update();
|
|
h.clicks.handler({ position: { x: 1, y: 1 } });
|
|
assert.equal(signal.aborted, false);
|
|
if (action === 'selection') {
|
|
pick = { id: 'fire-perimeter:other:0' };
|
|
h.clicks.handler({ position: { x: 1, y: 1 } });
|
|
} else h.layer[action]();
|
|
assert.equal(signal.aborted, true);
|
|
const entries = h.overlay.entries.get('fire-perimeters');
|
|
finish({ createdMs: row.discoveredTime, changedMs: row.updatedTime });
|
|
await new Promise(setImmediate);
|
|
assert.equal(h.overlay.entries.get('fire-perimeters'), entries);
|
|
if (action !== 'destroy') h.layer.destroy();
|
|
});
|
|
}
|
|
|
|
test('a stale snapshot reads stale, dated by its own fetch, and a fresh one clears it', async () => {
|
|
const fetchedAt = Date.now() - 3 * 60 * 60 * 1000;
|
|
let snapshot = { rows: [row], stale: true, fetchedAt };
|
|
const h = harness({
|
|
getSnapshot: async () => snapshot.rows,
|
|
getSnapshotDetail: async () => snapshot,
|
|
});
|
|
await h.layer.update(h.viewer);
|
|
assert.equal(h.layer.getStats().stale, true);
|
|
assert.equal(h.layer.getStats().lastUpdate, fetchedAt);
|
|
snapshot = { rows: [row], stale: false, fetchedAt: Date.now() };
|
|
await h.layer.update(h.viewer);
|
|
assert.equal(h.layer.getStats().stale, false);
|
|
assert.ok(h.layer.getStats().lastUpdate > fetchedAt);
|
|
});
|
|
|
|
test('a failed refresh after a stale snapshot reads as the failure, not stale', async () => {
|
|
let fail = false;
|
|
const h = harness({
|
|
getSnapshot: async () => [row],
|
|
getSnapshotDetail: async () => {
|
|
if (fail) throw new Error('WFIGS HTTP 502');
|
|
return { rows: [row], stale: true, fetchedAt: Date.now() - 60_000 };
|
|
},
|
|
});
|
|
await h.layer.update(h.viewer);
|
|
assert.equal(h.layer.getStats().stale, true);
|
|
fail = true;
|
|
await h.layer.update(h.viewer);
|
|
assert.equal(h.layer.getStats().stale, false);
|
|
assert.equal(h.layer.getStats().error, 'WFIGS HTTP 502');
|
|
});
|