feat(desktop): rubric+growth+runtime three-page fusion
Architectural refactor unifying three previously-independent pages
around a single shared event-log model:
rubric-fusion-model.js — the shared model. One log of scored
events keyed by (rubric, runtime, timestamp), plus a small set
of derivations (recent-scores window, per-rubric aggregates,
per-runtime rubric grid) that all three views subscribe to.
rubric-fusion-seed.js — fixture seed used by tests and the
isolation-machine screenshots below.
The three page views:
Rubrics — now leads with a hint card (recent pass/fail streaks,
stalest rubric) and a stats strip (7-run rolling pass rate,
latest score, delta vs. previous). Rubric list is unchanged.
Growth — rewritten. The old compact-window event feed is gone;
the page is now a pure SVG time-series curve over the event
log (score-y × time-x), with per-rubric coloring and a
hover-time tooltip. -420 lines of the old feed → +630 of the
curve renderer.
Runtime — adds a "Rubric grid" tab next to the existing Status
tab. Grid is one row per rubric × one column per runtime;
cells are pass/fail/unrun, click-through to the raw event.
Wiring is careful about the five hot zones the parallel lanes are
each touching (nav / chat / context / trace / artifact) — Lane D
stays entirely inside rubric/growth/runtime and their shared model,
so this composes with the C/A lanes without stepping on their
finishTurnContainer or chat-triple regions. style.css conflict
against the C+A combined append was tail-append vs tail-append and
resolved by concatenation (brace balance verified).
rubric-fusion-model.js 419 +
rubric-fusion-seed.js 624 +
rubric-fusion-model.test.js 181 +
rubric-fusion-views.test.js 189 +
growth-v2.js +631/-420 (SVG time-series rewrite)
rubrics-page.js +89 -0 (hint + stats strip)
runtimes-page.js +198 -0 (rubric-grid tab)
index.html +7 (three mount points)
style.css +252 (new page sections)
rubric-fusion-fixture.json +94 (test/screenshot seed)
Test suite: 1704/1704 pass (+22 over Lane C+A baseline). Isolation-
machine fixture screenshots (rubrics/growth/runtimes,
docs/rubric-fusion-shots/0{1,2,3}-*.png) reproduce from the merged
HEAD.
This commit is contained in:
181
examples/desktop/test/rubric-fusion-model.test.js
Normal file
181
examples/desktop/test/rubric-fusion-model.test.js
Normal file
@@ -0,0 +1,181 @@
|
||||
// Rubric fusion model tests — pure derivations (recent scores, time series,
|
||||
// rollout grid, similar-session detection).
|
||||
|
||||
'use strict'
|
||||
|
||||
// Load rubrics-model first — fusion-model depends on it via global fallback.
|
||||
global.window = global.window || {}
|
||||
if (!global.window.__dshRubricsModel) {
|
||||
global.window.__dshRubricsModel = require('../src/renderer/rubrics-model.js')
|
||||
}
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert')
|
||||
|
||||
// require via a fresh module — the singleton in the fusion model would
|
||||
// otherwise carry state across tests.
|
||||
function freshStore() {
|
||||
delete require.cache[require.resolve('../src/renderer/rubric-fusion-model.js')]
|
||||
const api = require('../src/renderer/rubric-fusion-model.js')
|
||||
return api.create()
|
||||
}
|
||||
|
||||
const SAMPLE_RUBRIC = {
|
||||
id: 'svg-gen',
|
||||
name: 'SVG generation',
|
||||
dims: [
|
||||
{ id: 'shape', label: 'Shape', type: 'continuous', min: 0, max: 1 },
|
||||
{ id: 'pass', label: 'Pass', type: 'boolean' },
|
||||
{ id: 'quality', label: 'Quality', type: 'categorical', values: ['bad', 'ok', 'good'] },
|
||||
],
|
||||
}
|
||||
|
||||
test('registerRubric normalizes dims and returns a stable id', () => {
|
||||
const s = freshStore()
|
||||
const def = s.registerRubric(SAMPLE_RUBRIC)
|
||||
assert.equal(def.id, 'svg-gen')
|
||||
assert.equal(def.dims.length, 3)
|
||||
assert.equal(def.dims[0].type, 'continuous')
|
||||
assert.equal(def.dims[0].min, 0)
|
||||
assert.equal(def.dims[0].max, 1)
|
||||
})
|
||||
|
||||
test('addEvent derives passed from dim spec', () => {
|
||||
const s = freshStore()
|
||||
s.registerRubric(SAMPLE_RUBRIC)
|
||||
const hi = s.addEvent({ ts: 1000, rubricId: 'svg-gen', dimId: 'shape', sessionId: 's1', turnId: 't1', score: 0.9 })
|
||||
const lo = s.addEvent({ ts: 2000, rubricId: 'svg-gen', dimId: 'shape', sessionId: 's2', turnId: 't2', score: 0.2 })
|
||||
const boolT = s.addEvent({ ts: 3000, rubricId: 'svg-gen', dimId: 'pass', sessionId: 's3', turnId: 't3', score: true })
|
||||
const boolF = s.addEvent({ ts: 4000, rubricId: 'svg-gen', dimId: 'pass', sessionId: 's4', turnId: 't4', score: false })
|
||||
const catHi = s.addEvent({ ts: 5000, rubricId: 'svg-gen', dimId: 'quality', sessionId: 's5', turnId: 't5', score: 'good' })
|
||||
const catLo = s.addEvent({ ts: 6000, rubricId: 'svg-gen', dimId: 'quality', sessionId: 's6', turnId: 't6', score: 'bad' })
|
||||
assert.equal(hi.passed, true)
|
||||
assert.equal(lo.passed, false)
|
||||
assert.equal(boolT.passed, true)
|
||||
assert.equal(boolF.passed, false)
|
||||
assert.equal(catHi.passed, true)
|
||||
assert.equal(catLo.passed, false)
|
||||
})
|
||||
|
||||
test('addEvent rejects unknown rubric or dim', () => {
|
||||
const s = freshStore()
|
||||
s.registerRubric(SAMPLE_RUBRIC)
|
||||
assert.equal(s.addEvent({ rubricId: 'nope', dimId: 'shape', score: 0.5 }), null)
|
||||
assert.equal(s.addEvent({ rubricId: 'svg-gen', dimId: 'missing', score: 0.5 }), null)
|
||||
})
|
||||
|
||||
test('recentScoresFor computes pass rate + per-dim breakdown', () => {
|
||||
const s = freshStore()
|
||||
s.registerRubric(SAMPLE_RUBRIC)
|
||||
s.addEvent({ ts: 1, rubricId: 'svg-gen', dimId: 'shape', sessionId: 's', turnId: 't', score: 0.9 })
|
||||
s.addEvent({ ts: 2, rubricId: 'svg-gen', dimId: 'shape', sessionId: 's', turnId: 't', score: 0.9 })
|
||||
s.addEvent({ ts: 3, rubricId: 'svg-gen', dimId: 'shape', sessionId: 's', turnId: 't', score: 0.1 })
|
||||
s.addEvent({ ts: 4, rubricId: 'svg-gen', dimId: 'pass', sessionId: 's', turnId: 't', score: true })
|
||||
const r = s.recentScoresFor('svg-gen')
|
||||
assert.equal(r.total, 4)
|
||||
assert.equal(r.passRate, 0.75) // 3 of 4 passed
|
||||
assert.equal(r.byDim.shape.n, 3)
|
||||
assert.equal(r.byDim.shape.passRate, Math.round((2 / 3) * 1000) / 1000)
|
||||
assert.equal(r.byDim.pass.n, 1)
|
||||
assert.equal(r.byDim.pass.passRate, 1)
|
||||
assert.equal(r.latest[0].ts, 4)
|
||||
})
|
||||
|
||||
test('timeSeriesFor buckets by day and groups by dim', () => {
|
||||
const s = freshStore()
|
||||
s.registerRubric(SAMPLE_RUBRIC)
|
||||
const t1 = new Date('2026-07-15T12:00:00Z').getTime()
|
||||
const t2 = new Date('2026-07-16T12:00:00Z').getTime()
|
||||
s.addEvent({ ts: t1, rubricId: 'svg-gen', dimId: 'shape', sessionId: 'a', turnId: 't', score: 0.5 })
|
||||
s.addEvent({ ts: t2, rubricId: 'svg-gen', dimId: 'shape', sessionId: 'b', turnId: 't', score: 0.9 })
|
||||
s.addEvent({ ts: t2, rubricId: 'svg-gen', dimId: 'pass', sessionId: 'b', turnId: 't', score: true })
|
||||
const ts = s.timeSeriesFor({ by: 'day', groupBy: 'dim' })
|
||||
assert.deepEqual(ts.xAxis, ['2026-07-15', '2026-07-16'])
|
||||
assert.equal(ts.series.length, 2)
|
||||
const shapeSeries = ts.series.find(x => x.key.endsWith('::shape'))
|
||||
assert.ok(shapeSeries, 'shape series present')
|
||||
assert.equal(shapeSeries.points.length, 2)
|
||||
assert.equal(shapeSeries.points[0].mean01, 0.5)
|
||||
assert.equal(shapeSeries.points[1].mean01, 0.9)
|
||||
})
|
||||
|
||||
test('timeSeriesFor buckets by version', () => {
|
||||
const s = freshStore()
|
||||
s.registerRubric(SAMPLE_RUBRIC)
|
||||
s.addEvent({ ts: 1, rubricId: 'svg-gen', dimId: 'shape', sessionId: 'a', turnId: 't', score: 0.3, harnessVersion: 'v0.9' })
|
||||
s.addEvent({ ts: 2, rubricId: 'svg-gen', dimId: 'shape', sessionId: 'b', turnId: 't', score: 0.7, harnessVersion: 'v0.10' })
|
||||
s.addEvent({ ts: 3, rubricId: 'svg-gen', dimId: 'shape', sessionId: 'c', turnId: 't', score: 0.9, harnessVersion: 'v0.11' })
|
||||
const ts = s.timeSeriesFor({ by: 'version', groupBy: 'dim' })
|
||||
assert.deepEqual(ts.xAxis, ['v0.10', 'v0.11', 'v0.9']) // sorted
|
||||
})
|
||||
|
||||
test('rolloutGridFor produces one cell per (dim, rollout)', () => {
|
||||
const s = freshStore()
|
||||
s.registerRubric(SAMPLE_RUBRIC)
|
||||
s.addEvent({ ts: 1, rubricId: 'svg-gen', dimId: 'shape', sessionId: 's', turnId: 't', rolloutIdx: 1, score: 0.9 })
|
||||
s.addEvent({ ts: 2, rubricId: 'svg-gen', dimId: 'shape', sessionId: 's', turnId: 't', rolloutIdx: 2, score: 0.3 })
|
||||
s.addEvent({ ts: 3, rubricId: 'svg-gen', dimId: 'pass', sessionId: 's', turnId: 't', rolloutIdx: 1, score: true })
|
||||
s.addEvent({ ts: 4, rubricId: 'svg-gen', dimId: 'pass', sessionId: 's', turnId: 't', rolloutIdx: 2, score: false })
|
||||
const grid = s.rolloutGridFor('svg-gen', 's')
|
||||
assert.equal(grid.rubric.id, 'svg-gen')
|
||||
assert.deepEqual(grid.rollouts, [1, 2])
|
||||
assert.equal(grid.dims.length, 3)
|
||||
const cellR1Shape = grid.cells.find(c => c.dimId === 'shape' && c.rolloutIdx === 1)
|
||||
assert.equal(cellR1Shape.passed, true)
|
||||
const cellR2Shape = grid.cells.find(c => c.dimId === 'shape' && c.rolloutIdx === 2)
|
||||
assert.equal(cellR2Shape.passed, false)
|
||||
})
|
||||
|
||||
test('detectSimilarSessions filters by minCount', () => {
|
||||
const s = freshStore()
|
||||
s.loadFixture({
|
||||
rubrics: [SAMPLE_RUBRIC],
|
||||
events: [],
|
||||
similarClasses: [
|
||||
{ id: 'a', signature: 'sig-a', count: 5, sessionIds: [], promptSummary: 'a' },
|
||||
{ id: 'b', signature: 'sig-b', count: 2, sessionIds: [], promptSummary: 'b' },
|
||||
],
|
||||
})
|
||||
const withDefault = s.detectSimilarSessions()
|
||||
assert.equal(withDefault.length, 1)
|
||||
assert.equal(withDefault[0].id, 'a')
|
||||
const relaxed = s.detectSimilarSessions({ minCount: 1 })
|
||||
assert.equal(relaxed.length, 2)
|
||||
})
|
||||
|
||||
test('subscribe fires on every mutation', () => {
|
||||
const s = freshStore()
|
||||
let fires = 0
|
||||
const unsub = s.subscribe(() => { fires++ })
|
||||
s.registerRubric(SAMPLE_RUBRIC)
|
||||
s.addEvent({ ts: 1, rubricId: 'svg-gen', dimId: 'shape', sessionId: 's', turnId: 't', score: 0.5 })
|
||||
unsub()
|
||||
s.addEvent({ ts: 2, rubricId: 'svg-gen', dimId: 'shape', sessionId: 's', turnId: 't', score: 0.7 })
|
||||
assert.equal(fires, 2) // register + first add; second add is after unsub
|
||||
})
|
||||
|
||||
test('loadFixture returns counts', () => {
|
||||
const s = freshStore()
|
||||
const res = s.loadFixture({
|
||||
rubrics: [SAMPLE_RUBRIC],
|
||||
events: [
|
||||
{ rubricId: 'svg-gen', dimId: 'shape', sessionId: 's', turnId: 't', score: 0.5 },
|
||||
{ rubricId: 'nope', dimId: 'x', score: 0 },
|
||||
],
|
||||
similarClasses: [],
|
||||
})
|
||||
assert.equal(res.rubrics, 1)
|
||||
assert.equal(res.events, 1)
|
||||
})
|
||||
|
||||
test('timeSeriesFor filter chip: harnessVersion', () => {
|
||||
const s = freshStore()
|
||||
s.registerRubric(SAMPLE_RUBRIC)
|
||||
s.addEvent({ ts: 1, rubricId: 'svg-gen', dimId: 'shape', sessionId: 'a', turnId: 't', score: 0.3, harnessVersion: 'v0.9' })
|
||||
s.addEvent({ ts: 2, rubricId: 'svg-gen', dimId: 'shape', sessionId: 'b', turnId: 't', score: 0.9, harnessVersion: 'v0.11' })
|
||||
const ts = s.timeSeriesFor({ by: 'day', groupBy: 'dim', filter: { harnessVersion: 'v0.11' } })
|
||||
const shape = ts.series.find(x => x.key.endsWith('::shape'))
|
||||
assert.ok(shape)
|
||||
assert.equal(shape.points.length, 1)
|
||||
assert.equal(shape.points[0].mean01, 0.9)
|
||||
})
|
||||
189
examples/desktop/test/rubric-fusion-views.test.js
Normal file
189
examples/desktop/test/rubric-fusion-views.test.js
Normal file
@@ -0,0 +1,189 @@
|
||||
// Rubric fusion — cross-view smoke tests.
|
||||
//
|
||||
// We can't run the actual DOM controllers under `node --test` (no jsdom),
|
||||
// but we can:
|
||||
// 1. Verify the fusion seed JSON parses and drives all three view APIs.
|
||||
// 2. Verify each of the 3 view scripts loads cleanly with a minimal
|
||||
// document stub (catching syntax errors early).
|
||||
// 3. Verify the fusion-model events → view derivations pipeline
|
||||
// returns the expected shape for each view.
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const path = require('node:path')
|
||||
const fs = require('node:fs')
|
||||
|
||||
// Wire the rubrics-model global before anything requires fusion-model.
|
||||
global.window = global.window || {}
|
||||
global.window.__dshRubricsModel = require('../src/renderer/rubrics-model.js')
|
||||
|
||||
// Fresh singleton per test.
|
||||
function freshFusion() {
|
||||
delete require.cache[require.resolve('../src/renderer/rubric-fusion-model.js')]
|
||||
const api = require('../src/renderer/rubric-fusion-model.js')
|
||||
return api.create()
|
||||
}
|
||||
|
||||
const FIXTURE_PATH = path.join(__dirname, '..', 'docs', 'rubric-fusion-fixture.json')
|
||||
const FIXTURE = JSON.parse(fs.readFileSync(FIXTURE_PATH, 'utf8'))
|
||||
|
||||
test('fixture loads with 3 rubrics and > 30 events', () => {
|
||||
const s = freshFusion()
|
||||
const res = s.loadFixture(FIXTURE)
|
||||
assert.equal(res.rubrics, 3)
|
||||
assert.ok(res.events > 30, 'expected > 30 events, got ' + res.events)
|
||||
})
|
||||
|
||||
test('Rubrics view: recentScoresFor returns per-dim breakdown for each rubric', () => {
|
||||
const s = freshFusion()
|
||||
s.loadFixture(FIXTURE)
|
||||
for (const rubric of s.listRubrics()) {
|
||||
const stats = s.recentScoresFor(rubric.id)
|
||||
assert.ok(stats.total > 0, `${rubric.id}: expected events > 0`)
|
||||
assert.ok(stats.passRate >= 0 && stats.passRate <= 1, `${rubric.id}: passRate out of range`)
|
||||
for (const dim of rubric.dims) {
|
||||
const byDim = stats.byDim[dim.id]
|
||||
assert.ok(byDim, `${rubric.id}/${dim.id}: missing per-dim bucket`)
|
||||
assert.ok(byDim.n > 0, `${rubric.id}/${dim.id}: expected n > 0`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Growth view: timeSeriesFor by=day produces sorted xAxis and per-dim series', () => {
|
||||
const s = freshFusion()
|
||||
s.loadFixture(FIXTURE)
|
||||
const ts = s.timeSeriesFor({ by: 'day', groupBy: 'dim' })
|
||||
assert.ok(ts.xAxis.length >= 3, 'expected >= 3 days')
|
||||
// sorted asc
|
||||
const sorted = ts.xAxis.slice().sort()
|
||||
assert.deepEqual(ts.xAxis, sorted)
|
||||
assert.ok(ts.series.length >= 3, 'expected >= 3 dim series')
|
||||
for (const s2 of ts.series) {
|
||||
assert.ok(s2.points.length >= 1, 'each series has points: ' + s2.label)
|
||||
for (const p of s2.points) {
|
||||
assert.ok(p.mean01 >= 0 && p.mean01 <= 1, 'mean01 in range for ' + s2.label)
|
||||
assert.ok(p.passRate >= 0 && p.passRate <= 1, 'passRate in range for ' + s2.label)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Growth view: timeSeriesFor by=version buckets by harness version', () => {
|
||||
const s = freshFusion()
|
||||
s.loadFixture(FIXTURE)
|
||||
const ts = s.timeSeriesFor({ by: 'version', groupBy: 'dim' })
|
||||
const versions = new Set(ts.xAxis)
|
||||
assert.ok(versions.has('v0.9'), 'v0.9 present')
|
||||
assert.ok(versions.has('v0.11'), 'v0.11 present')
|
||||
})
|
||||
|
||||
test('Growth view: filter chip harnessVersion narrows the series', () => {
|
||||
const s = freshFusion()
|
||||
s.loadFixture(FIXTURE)
|
||||
const all = s.timeSeriesFor({ by: 'day', groupBy: 'dim' })
|
||||
const v11 = s.timeSeriesFor({ by: 'day', groupBy: 'dim', filter: { harnessVersion: 'v0.11' } })
|
||||
const totalAll = all.series.reduce((sum, sr) => sum + sr.points.reduce((a, p) => a + p.n, 0), 0)
|
||||
const totalV11 = v11.series.reduce((sum, sr) => sum + sr.points.reduce((a, p) => a + p.n, 0), 0)
|
||||
assert.ok(totalV11 < totalAll, 'filtered total should be strictly smaller')
|
||||
assert.ok(totalV11 > 0, 'v0.11 filtered total > 0')
|
||||
})
|
||||
|
||||
test('Runtime view: rolloutGridFor produces a matrix with cell pass/fail', () => {
|
||||
const s = freshFusion()
|
||||
s.loadFixture(FIXTURE)
|
||||
const grid = s.rolloutGridFor('svg-gen', 's-svg-live')
|
||||
assert.equal(grid.rubric.id, 'svg-gen')
|
||||
assert.ok(grid.rollouts.length >= 3, 'expected >= 3 rollouts for the live session')
|
||||
assert.ok(grid.dims.length === 3, 'svg-gen has 3 dims')
|
||||
// Assert we have at least one pass AND at least one fail — the fixture
|
||||
// deliberately spans both.
|
||||
const passed = grid.cells.filter(c => c.passed === true).length
|
||||
const failed = grid.cells.filter(c => c.passed === false).length
|
||||
assert.ok(passed > 0, 'at least one pass cell')
|
||||
assert.ok(failed > 0, 'at least one fail cell')
|
||||
})
|
||||
|
||||
test('Runtime view: rolloutGridFor sessionId=null aggregates all rollouts', () => {
|
||||
const s = freshFusion()
|
||||
s.loadFixture(FIXTURE)
|
||||
const grid = s.rolloutGridFor('svg-gen', null)
|
||||
// The fixture has rollouts 1..8 for svg-gen (5 seed sessions + 3 on
|
||||
// the live session).
|
||||
assert.deepEqual(grid.rollouts, [1, 2, 3, 4, 5, 6, 7, 8])
|
||||
})
|
||||
|
||||
test('Rubrics view: similar-sessions hint fires with count >= 3', () => {
|
||||
const s = freshFusion()
|
||||
s.loadFixture(FIXTURE)
|
||||
const classes = s.detectSimilarSessions()
|
||||
assert.equal(classes.length, 1)
|
||||
assert.equal(classes[0].id, 'similar-svg-gen')
|
||||
assert.ok(classes[0].count >= 3)
|
||||
})
|
||||
|
||||
// --- Script-load smoke tests: the 3 view scripts must load without
|
||||
// throwing under a minimal document stub. Catches syntax errors before
|
||||
// they hit the browser. ---
|
||||
|
||||
function stubDocument() {
|
||||
return {
|
||||
addEventListener() {},
|
||||
readyState: 'complete',
|
||||
querySelector() { return null },
|
||||
querySelectorAll() { return [] },
|
||||
createElement() {
|
||||
const node = {
|
||||
style: {},
|
||||
classList: { add() {}, remove() {}, toggle() {}, contains() { return false } },
|
||||
dataset: {},
|
||||
setAttribute() {},
|
||||
getAttribute() { return null },
|
||||
appendChild(c) { return c },
|
||||
replaceChildren() {},
|
||||
removeChild() {},
|
||||
addEventListener() {},
|
||||
set textContent(v) {},
|
||||
set innerHTML(v) {},
|
||||
set hidden(v) {},
|
||||
}
|
||||
return node
|
||||
},
|
||||
createElementNS() { return this.createElement() },
|
||||
getElementById() { return null },
|
||||
}
|
||||
}
|
||||
|
||||
test('rubrics-page.js loads under minimal document stub', () => {
|
||||
global.window = { __dshRubricsModel: require('../src/renderer/rubrics-model.js') }
|
||||
global.document = stubDocument()
|
||||
global.requestAnimationFrame = () => {}
|
||||
delete require.cache[require.resolve('../src/renderer/rubrics-page.js')]
|
||||
const page = require('../src/renderer/rubrics-page.js')
|
||||
assert.ok(page._internal, 'exposes _internal')
|
||||
})
|
||||
|
||||
test('growth-v2.js loads under minimal document stub', () => {
|
||||
global.window = {
|
||||
__dshRubricsModel: require('../src/renderer/rubrics-model.js'),
|
||||
__dshRubricFusion: require('../src/renderer/rubric-fusion-model.js'),
|
||||
}
|
||||
global.document = stubDocument()
|
||||
delete require.cache[require.resolve('../src/renderer/growth-v2.js')]
|
||||
require('../src/renderer/growth-v2.js')
|
||||
assert.ok(global.window.__dshGrowthV2, 'exposes __dshGrowthV2')
|
||||
assert.equal(typeof global.window.__dshGrowthV2.show, 'function')
|
||||
assert.equal(typeof global.window.__dshGrowthV2.render, 'function')
|
||||
})
|
||||
|
||||
test('runtimes-page.js loads under minimal document stub', () => {
|
||||
global.window = {
|
||||
__dshRubricsModel: require('../src/renderer/rubrics-model.js'),
|
||||
__dshRubricFusion: require('../src/renderer/rubric-fusion-model.js'),
|
||||
}
|
||||
global.document = stubDocument()
|
||||
delete require.cache[require.resolve('../src/renderer/runtimes-page.js')]
|
||||
require('../src/renderer/runtimes-page.js')
|
||||
assert.ok(global.window.__dshRuntimes, 'exposes __dshRuntimes')
|
||||
assert.equal(typeof global.window.__dshRuntimes.show, 'function')
|
||||
})
|
||||
Reference in New Issue
Block a user