mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
186 lines
7.8 KiB
JavaScript
186 lines
7.8 KiB
JavaScript
// workflow-view.test.js — task #138 batch 3 §1.6.
|
|
// Verifies:
|
|
// 1. classifyWorkflowKind normalises 'fanout' → 'fan-out' and rejects unknown kinds
|
|
// 2. summariseWorkflowProgress counts done/running/pending/failed across seq/iter shapes
|
|
// 3. buildWorkflowCard for seq shows the step list with statuses in order
|
|
// 4. buildWorkflowCard for dag stacks nodes by BFS level (in → out adjacency)
|
|
// 5. buildWorkflowCard for iter renders the predicate + iteration list
|
|
// 6. isMock adds the '未上 wire' chip; absent isMock leaves it off
|
|
// 7. showReplayBar mounts prev/next with correct enable/disable at ends
|
|
// 8. onStepClick fires with the step id
|
|
|
|
'use strict'
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert')
|
|
const path = require('node:path')
|
|
const fs = require('node:fs')
|
|
|
|
const view = require('../src/renderer/workflow-view.js')
|
|
|
|
function makeDoc() {
|
|
const store = new Map()
|
|
const doc = {
|
|
createElement(tag) {
|
|
const el = {
|
|
tagName: tag.toUpperCase(),
|
|
className: '',
|
|
_children: [],
|
|
_listeners: {},
|
|
dataset: {},
|
|
style: {},
|
|
textContent: '',
|
|
appendChild(c) { this._children.push(c); return c },
|
|
append(...cs) { for (const c of cs) this._children.push(c) },
|
|
addEventListener(name, fn) { (this._listeners[name] = this._listeners[name] || []).push(fn) },
|
|
setAttribute() {},
|
|
removeAttribute() {},
|
|
classList: {
|
|
_s: new Set(),
|
|
add(c) { this._s.add(c) },
|
|
remove(c) { this._s.delete(c) },
|
|
contains(c) { return this._s.has(c) },
|
|
},
|
|
}
|
|
Object.defineProperty(el, 'children', { get() { return this._children } })
|
|
Object.defineProperty(el, 'firstChild', { get() { return this._children[0] || null } })
|
|
Object.defineProperty(el, 'lastChild', { get() { return this._children[this._children.length - 1] || null } })
|
|
Object.defineProperty(el, 'disabled', {
|
|
get() { return this._disabled === true },
|
|
set(v) { this._disabled = v },
|
|
})
|
|
return el
|
|
},
|
|
}
|
|
return doc
|
|
}
|
|
|
|
function collectByClass(root, className, out = []) {
|
|
if (!root) return out
|
|
if (root.className && root.className.split(' ').includes(className)) out.push(root)
|
|
for (const c of (root._children || [])) collectByClass(c, className, out)
|
|
return out
|
|
}
|
|
|
|
function loadFixture(name) {
|
|
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'fixtures', 'trace-samples', name), 'utf-8'))
|
|
}
|
|
|
|
test('classifyWorkflowKind normalises fanout to fan-out and drops garbage', () => {
|
|
assert.strictEqual(view.classifyWorkflowKind('seq'), 'seq')
|
|
assert.strictEqual(view.classifyWorkflowKind('fanout'), 'fan-out')
|
|
assert.strictEqual(view.classifyWorkflowKind('fan-out'), 'fan-out')
|
|
assert.strictEqual(view.classifyWorkflowKind('DAG'), 'dag')
|
|
assert.strictEqual(view.classifyWorkflowKind('unknown-kind'), 'unknown')
|
|
assert.strictEqual(view.classifyWorkflowKind(null), 'unknown')
|
|
})
|
|
|
|
test('summariseWorkflowProgress counts across seq/iter', () => {
|
|
const seq = loadFixture('1.6-workflow-seq.json')
|
|
const p = view.summariseWorkflowProgress(seq.workflow)
|
|
assert.strictEqual(p.total, 5)
|
|
assert.strictEqual(p.done, 2)
|
|
assert.strictEqual(p.running, 1)
|
|
assert.strictEqual(p.pending, 2)
|
|
assert.strictEqual(p.failed, 0)
|
|
const iter = loadFixture('1.6-workflow-iter.json')
|
|
const q = view.summariseWorkflowProgress(iter.workflow)
|
|
assert.strictEqual(q.total, 3)
|
|
assert.strictEqual(q.done, 2)
|
|
assert.strictEqual(q.running, 1)
|
|
})
|
|
|
|
test('buildWorkflowCard for seq mounts step rows in order', () => {
|
|
const doc = makeDoc()
|
|
const seq = loadFixture('1.6-workflow-seq.json')
|
|
const card = view.buildWorkflowCard(doc, seq.workflow, {})
|
|
const steps = collectByClass(card, 'workflow-step')
|
|
assert.strictEqual(steps.length, 5)
|
|
// First step is done (has glyph ✓), third step is running (⋯).
|
|
const glyphs = steps.map(s => collectByClass(s, 'workflow-step-glyph')[0].textContent)
|
|
assert.strictEqual(glyphs[0], '✓')
|
|
assert.strictEqual(glyphs[2], '⋯')
|
|
assert.strictEqual(glyphs[3], '·')
|
|
})
|
|
|
|
test('buildWorkflowCard for dag stacks nodes by BFS level', () => {
|
|
const doc = makeDoc()
|
|
const dag = loadFixture('1.6-workflow-dag.json')
|
|
const card = view.buildWorkflowCard(doc, dag.workflow, {})
|
|
const layers = collectByClass(card, 'workflow-dag-layer')
|
|
// The dag fixture has a root (checkout) with two out edges — two levels at
|
|
// least. We don't hard-code the exact count (fixture shape may evolve) but
|
|
// require ≥2 layers and that the total node count matches step count.
|
|
assert.ok(layers.length >= 2, `expected ≥2 layers, got ${layers.length}`)
|
|
const nodes = collectByClass(card, 'workflow-dag-node')
|
|
assert.strictEqual(nodes.length, dag.workflow.steps.length)
|
|
})
|
|
|
|
test('buildWorkflowCard for iter renders predicate + iterations', () => {
|
|
const doc = makeDoc()
|
|
const iter = loadFixture('1.6-workflow-iter.json')
|
|
const card = view.buildWorkflowCard(doc, iter.workflow, {})
|
|
const pred = collectByClass(card, 'workflow-iter-predicate')[0]
|
|
assert.ok(pred)
|
|
assert.match(pred.textContent, /while \(hasMoreFiles/)
|
|
const items = collectByClass(card, 'workflow-step')
|
|
assert.strictEqual(items.length, 3)
|
|
})
|
|
|
|
test('isMock flag adds the mock chip; without it, chip is absent', () => {
|
|
const doc = makeDoc()
|
|
const seq = loadFixture('1.6-workflow-seq.json')
|
|
const withMock = view.buildWorkflowCard(doc, seq.workflow, { isMock: true })
|
|
assert.strictEqual(collectByClass(withMock, 'workflow-card-chip--mock').length, 1)
|
|
const doc2 = makeDoc()
|
|
const withoutMock = view.buildWorkflowCard(doc2, seq.workflow, { isMock: false })
|
|
assert.strictEqual(collectByClass(withoutMock, 'workflow-card-chip--mock').length, 0)
|
|
})
|
|
|
|
test('isLive flag adds the live chip (not the mock chip); the two are exclusive', () => {
|
|
const seq = loadFixture('1.6-workflow-seq.json')
|
|
const live = view.buildWorkflowCard(makeDoc(), seq.workflow, { isLive: true })
|
|
assert.strictEqual(collectByClass(live, 'workflow-card-chip--live').length, 1)
|
|
assert.strictEqual(collectByClass(live, 'workflow-card-chip--mock').length, 0)
|
|
// isMock wins when both are (wrongly) set — a card is never both live+mock.
|
|
const both = view.buildWorkflowCard(makeDoc(), seq.workflow, { isMock: true, isLive: true })
|
|
assert.strictEqual(collectByClass(both, 'workflow-card-chip--mock').length, 1)
|
|
assert.strictEqual(collectByClass(both, 'workflow-card-chip--live').length, 0)
|
|
})
|
|
|
|
test('showReplayBar mounts prev/next and clamps at ends', () => {
|
|
const doc = makeDoc()
|
|
const seq = loadFixture('1.6-workflow-seq.json')
|
|
const moves = []
|
|
const card = view.buildWorkflowCard(doc, seq.workflow, {
|
|
showReplayBar: true,
|
|
onReplayMove: (id) => moves.push(id),
|
|
})
|
|
const bar = collectByClass(card, 'workflow-replay-bar')[0]
|
|
assert.ok(bar, 'expected a replay bar')
|
|
const buttons = collectByClass(bar, 'workflow-replay-btn')
|
|
assert.strictEqual(buttons.length, 2)
|
|
// Default idx = running step (s3, index 2). prev should be enabled, next enabled.
|
|
assert.strictEqual(buttons[0].disabled, false)
|
|
// Firing next twice takes us to s5 (idx 4), then a third click clamps.
|
|
const next = buttons[1]
|
|
const nextClick = (next._listeners.click || [])[0]
|
|
nextClick(); nextClick()
|
|
assert.deepStrictEqual(moves.slice(-2), ['s4', 's5'])
|
|
assert.strictEqual(next.disabled, true)
|
|
})
|
|
|
|
test('onStepClick fires with the step id when a seq step is clicked', () => {
|
|
const doc = makeDoc()
|
|
const seq = loadFixture('1.6-workflow-seq.json')
|
|
const clicked = []
|
|
const card = view.buildWorkflowCard(doc, seq.workflow, {
|
|
onStepClick: (id) => clicked.push(id),
|
|
})
|
|
const steps = collectByClass(card, 'workflow-step')
|
|
const clickFn = (steps[0]._listeners.click || [])[0]
|
|
assert.ok(clickFn, 'expected a click listener on the first step')
|
|
clickFn()
|
|
assert.deepStrictEqual(clicked, ['s1'])
|
|
})
|