fix(desktop): usability wiring — rubric fixture ids + cell-jump listener + artifact seed + empty panel
+ docs/integration-final-check.md — 5-item integration gate PASS
This commit is contained in:
100
examples/desktop/test/usability-p0-batch.test.js
Normal file
100
examples/desktop/test/usability-p0-batch.test.js
Normal file
@@ -0,0 +1,100 @@
|
||||
// Test locks for the fix/usability-p0-batch cycle. Each locks one of the
|
||||
// P0 findings from review-usability: fixture id/catalog match, fusion
|
||||
// loadFixture idempotency, rubric-cell-jump listener presence, artifact
|
||||
// blob banner on missing-blob chains, artifact seed API surface.
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const RENDERER = path.resolve(__dirname, '..', 'src', 'renderer')
|
||||
|
||||
// -- P0-3 fusion loadFixture is idempotent ---------------------------------
|
||||
test('rubric-fusion loadFixture is idempotent by fixture identity', () => {
|
||||
const fusion = require(path.join(RENDERER, 'rubric-fusion-model.js'))
|
||||
const store = fusion.create()
|
||||
const fixture = {
|
||||
rubrics: [{ id: 'r1', name: 'R1', dims: [{ id: 'd1', label: 'D1', type: 'continuous', min: 0, max: 1 }] }],
|
||||
events: [{ ts: 1, rubricId: 'r1', dimId: 'd1', sessionId: 's', turnId: 't', score: 0.5 }],
|
||||
}
|
||||
const first = store.loadFixture(fixture)
|
||||
const second = store.loadFixture(fixture)
|
||||
const third = store.loadFixture(fixture)
|
||||
assert.strictEqual(first.events, 1, 'first load registers events')
|
||||
assert.strictEqual(second.events, 0, 'second load is a no-op')
|
||||
assert.ok(second.deduped, 'second load reports deduped flag')
|
||||
assert.strictEqual(third.events, 0, 'third load is a no-op')
|
||||
assert.strictEqual(store.listEvents({}).length, 1, 'event count stays at 1 after 3 loads')
|
||||
})
|
||||
|
||||
// -- P0-1 fixture rubric ids match catalog ids -----------------------------
|
||||
test('rubric-fusion fixture covers every catalog rubric id', () => {
|
||||
const seedSrc = fs.readFileSync(path.join(RENDERER, 'rubrics-seed.js'), 'utf8')
|
||||
// Parse rubric names out of the inlined SKILL.md blobs.
|
||||
const catalogIds = []
|
||||
for (const m of seedSrc.matchAll(/---\\nname: ([\w-]+)/g)) catalogIds.push(m[1])
|
||||
assert.ok(catalogIds.length >= 4, 'catalog seed has at least 4 rubrics')
|
||||
const fixtureJson = JSON.parse(fs.readFileSync(
|
||||
path.join(__dirname, '..', 'docs', 'rubric-fusion-fixture.json'), 'utf8'))
|
||||
const fixtureIds = new Set((fixtureJson.rubrics || []).map(r => r.id))
|
||||
const missing = catalogIds.filter(id => !fixtureIds.has(id))
|
||||
assert.deepStrictEqual(missing, [],
|
||||
'every catalog rubric id has a matching fixture rubric def: missing=' + missing.join(','))
|
||||
})
|
||||
|
||||
// -- P0-2 tracing-page registers rubric-cell-jump listener -----------------
|
||||
test('tracing-page wires dsh:rubric-cell-jump → openDrill', () => {
|
||||
const src = fs.readFileSync(path.join(RENDERER, 'tracing-page.js'), 'utf8')
|
||||
assert.match(src, /addEventListener\(['"]dsh:rubric-cell-jump['"]/,
|
||||
'tracing-page must register dsh:rubric-cell-jump listener')
|
||||
assert.match(src, /openDrill\(detail\.sessionId\)/,
|
||||
'listener must call openDrill(detail.sessionId) so the jump lands')
|
||||
})
|
||||
|
||||
// -- P0-5 artifact-evolution shows blob-missing banner ---------------------
|
||||
// Static source-string lock (matches artifact-evolution-board.test.js
|
||||
// style for the artifacts.js IIFE — the module lives inside a closure
|
||||
// and we already prove the runtime path there. Here we just prove the
|
||||
// blob-missing banner path exists and gates on the right predicate).
|
||||
test('renderEvolution emits blob-missing banner when any hop lacks blob', () => {
|
||||
const src = fs.readFileSync(path.join(RENDERER, 'artifacts-board.js'), 'utf8')
|
||||
assert.match(src, /artifact-evolution-banner/, 'source must reference the banner class')
|
||||
assert.match(src, /missingBlobs\s*=\s*chain\.some/,
|
||||
'banner must gate on chain.some(missing-blob)')
|
||||
const css = fs.readFileSync(path.join(RENDERER, 'style.css'), 'utf8')
|
||||
assert.match(css, /\.artifact-evolution-banner\s*\{/,
|
||||
'style.css must style the new .artifact-evolution-banner selector')
|
||||
})
|
||||
|
||||
// -- P0-4 artifact seed fixture is inline-reachable ------------------------
|
||||
test('artifact-board-seed.js inlines fixture on window.__dshArtifactBoardSeed', () => {
|
||||
const src = fs.readFileSync(path.join(RENDERER, 'artifact-board-seed.js'), 'utf8')
|
||||
assert.match(src, /window\.__dshArtifactBoardSeed\s*=/, 'seed must attach to window')
|
||||
// Sanity — has multiple kinds and multiple versions.
|
||||
assert.match(src, /"kind":\s*"svg"/, 'seed must include an svg artifact')
|
||||
assert.match(src, /"kind":\s*"html"/, 'seed must include an html artifact')
|
||||
assert.match(src, /"version":\s*3/, 'seed must include multi-version chain (v3+)')
|
||||
// Wired into index.html so the browser actually loads it.
|
||||
const html = fs.readFileSync(path.join(RENDERER, 'index.html'), 'utf8')
|
||||
assert.match(html, /artifact-board-seed\.js/, 'index.html must include artifact-board-seed.js')
|
||||
})
|
||||
|
||||
// -- P1 Lane A: graph binds click on fork nodes + user nodes ---------------
|
||||
test('chat-session-graph binds click on actionable non-turn nodes', () => {
|
||||
const src = fs.readFileSync(path.join(RENDERER, 'chat-session-graph.js'), 'utf8')
|
||||
assert.match(src, /kind === ['"]fork['"] && node\.childSessionId/,
|
||||
'graph must bind click on fork nodes when childSessionId is present')
|
||||
assert.match(src, /kind === ['"]user['"] && node\.seq/,
|
||||
'graph must bind click on user nodes when seq is present')
|
||||
})
|
||||
|
||||
// -- P1 Lane A: renderer stores chatSelectedTurnId + wires user rows -------
|
||||
test('renderer propagates chatSelectedTurnId and wires user rows', () => {
|
||||
const src = fs.readFileSync(path.join(RENDERER, 'renderer.js'), 'utf8')
|
||||
assert.match(src, /state\.chatSelectedTurnId/, 'renderer must store chatSelectedTurnId in state')
|
||||
assert.match(src, /row\.kind === ['"]user['"] && row\.seq/,
|
||||
'renderer drawer onSelect must handle user rows (kind==="user" && seq)')
|
||||
})
|
||||
Reference in New Issue
Block a user