Files
deepseek-harness/examples/desktop/test/trace-signal-overlay.test.js
ZiyaZhang 47949244c8 feat(desktop): trace signal detection + tri-view badges
Adds a small signal-detection layer over trace payloads and surfaces
three kinds of semantic signals inline in the trace tri-view (Timeline
/ Graph / turn footer chips) so a researcher can spot repetitive
tool-call loops, redundant identical calls, and plan-completion beats
without hand-scanning the raw log:

- loop-detected  — three or more repeats of the same tool.method
                   signature inside a window, badge in Timeline gutter
                   + colored ring on the Graph node
- redundant-call — identical (tool, arg-hash) call twice without any
                   state change in between, badge + subdued styling
                   so it stays advisory not alarmist
- plan-* chips   — plan/subplan step-complete beats surfaced above the
                   assistant body in the turn footer, wired through
                   the finishTurnContainer tail

Pure rendering; no wire-format changes needed on the runtime side —
signal detection is fixture-driven off the same payload the tri-view
already consumes. Companion RFC L-2 in docs/upstream-ledger.md asks
the runtime to emit these signals natively rather than deriving them
in the shell, so once L-2 lands the shell will consume upstream signals
and this detector becomes a fallback.

Files:
  src/renderer/trace-signal-detect.js       new  (loop/redundant/plan)
  src/renderer/trace-tri-view.js            +24  (wire detector output)
  src/renderer/trace-timeline.js            +39  (badge in gutter)
  src/renderer/trace-graph.js               +30  (colored ring)
  src/renderer/renderer.js                  +81  (finishTurnContainer chips)
  src/renderer/index.html                    +1  (one script tag)
  src/renderer/style.css                    +58  (badge/ring/chip rules)
  docs/upstream-ledger.md                  +121  (append L-2 RFC)
  scripts/qa-trace-signals-fixture.mjs     new  (headless SVG proof)
  scripts/qa-trace-signals-shoot.mjs       new  (CDP live shoot)
  docs/trace-signals-shoot/*.html          new  (3 fixture-driven shots)
  test/trace-signal-detect.test.js         new  (detector unit tests)
  test/trace-signal-overlay.test.js        new  (renderer overlay tests)

Test suite: 1668/1668 pass (17 new). Fixture-driven signals-01-timeline-
loop.html regenerated byte-identical from the merged HEAD.
2026-07-19 00:57:19 -07:00

142 lines
5.4 KiB
JavaScript

// trace-signal-overlay.test.js — Timeline + Graph rendering with a
// `signals` bySeq Map should drop badges/rings and expose the tooltip.
'use strict'
const test = require('node:test')
const assert = require('node:assert')
const T = require('../src/renderer/trace-timeline.js')
const G = require('../src/renderer/trace-graph.js')
const SD = require('../src/renderer/trace-signal-detect.js')
function makeDoc() {
function makeEl(nsOrTag, tag) {
const cls = { _s: new Set(),
add(c) { this._s.add(c) }, remove(c) { this._s.delete(c) },
toggle(c, on) { if (on) this._s.add(c); else this._s.delete(c) },
contains(c) { return this._s.has(c) },
}
const el = {
tagName: (tag || nsOrTag).toUpperCase(),
_children: [],
_attrs: {},
_listeners: {},
dataset: {},
style: {},
textContent: '',
hidden: false,
get className() { return Array.from(cls._s).join(' ') },
set className(v) {
cls._s.clear()
String(v || '').split(/\s+/).forEach(x => x && cls._s.add(x))
},
classList: cls,
appendChild(c) { this._children.push(c); return c },
append(...cs) { for (const c of cs) this._children.push(c) },
setAttribute(k, v) {
this._attrs[k] = String(v)
if (k === 'class') {
cls._s.clear()
String(v || '').split(/\s+/).forEach(x => x && cls._s.add(x))
}
},
getAttribute(k) { return this._attrs[k] },
addEventListener(name, fn) { (this._listeners[name] = this._listeners[name] || []).push(fn) },
querySelector(sel) { return null },
querySelectorAll() { return [] },
firstChild: null,
insertBefore(node) { this._children.unshift(node); return node },
outerHTML: '',
}
return el
}
return {
createElement(tag) { return makeEl(tag) },
createElementNS(ns, tag) { return makeEl(ns, tag) },
body: makeEl('body'),
}
}
function collectByClass(root, cls) {
const found = []
const stack = [root]
while (stack.length) {
const cur = stack.pop()
if (cur && cur.classList && cur.classList.contains(cls)) found.push(cur)
if (cur && cur._children) for (const c of cur._children) stack.push(c)
}
return found
}
test('renderTimeline drops one signal badge per row whose seq appears in the map', () => {
const doc = makeDoc()
const rec = {
turn: 1, step: 0, startSeq: 10, endSeq: 14,
startTime: 1000, endTime: 1500, durationMs: 500,
summary: 'read', inputs: [], outputs: [],
events: [
{ type: 'tool/call', seq: 11, time: 1050, data: { name: 'fs.read', arguments: '{"p":"a"}', callId: 'c1' } },
{ type: 'tool/call', seq: 12, time: 1100, data: { name: 'fs.read', arguments: '{"p":"a"}', callId: 'c2' } },
{ type: 'tool/call', seq: 13, time: 1150, data: { name: 'fs.read', arguments: '{"p":"a"}', callId: 'c3' } },
],
}
const { bySeq } = SD.detectSignalsFromRecords(rec, { loopN: 3 })
const el = T.renderTimeline(doc, rec, { signals: bySeq })
const badges = collectByClass(el, 'trace-timeline-signal-badge')
assert.ok(badges.length >= 1, 'at least one signal badge drawn for the loop-detected seq (12)')
// The badge for the loop-detected signal on seq 12 should carry the sig-loop class.
const loopBadges = badges.filter(b => b.classList.contains('sig-loop'))
assert.strictEqual(loopBadges.length, 1)
// Tooltip <title> child is present
const title = loopBadges[0]._children.find(c => c.tagName === 'TITLE')
assert.ok(title, 'badge has a <title> tooltip child')
assert.match(title.textContent, /Loop/)
})
test('renderTimeline draws no badges when signals map is empty', () => {
const doc = makeDoc()
const rec = {
turn: 1, step: 0, startSeq: 10, endSeq: 12,
startTime: 1000, endTime: 1200,
inputs: [], outputs: [],
events: [
{ type: 'tool/call', seq: 11, time: 1050, data: { name: 'fs.read', arguments: '{}', callId: 'c1' } },
],
}
const el = T.renderTimeline(doc, rec, { signals: new Map() })
const badges = collectByClass(el, 'trace-timeline-signal-badge')
assert.strictEqual(badges.length, 0)
})
test('renderGraph puts a colored ring around a node whose seq matches a signal', () => {
const doc = makeDoc()
const rec = {
turn: 1, step: 0, startSeq: 10, endSeq: 14,
startTime: 1000, endTime: 1500,
inputs: [], outputs: [],
events: [
{ type: 'tool/call', seq: 11, time: 1050, data: { name: 'fs.read', arguments: '{"p":"a"}', callId: 'c1' } },
{ type: 'tool/result', seq: 12, time: 1080, data: { callId: 'c1', ok: false, error: 'ENOENT' } },
],
}
const { bySeq } = SD.detectSignalsFromRecords(rec)
const el = G.renderGraph(doc, rec, { signals: bySeq })
const rings = collectByClass(el, 'trace-graph-signal-ring')
assert.ok(rings.length >= 1, 'at least one signal ring around the tool-error seq')
const errRings = rings.filter(r => r.classList.contains('sig-error'))
assert.strictEqual(errRings.length, 1, 'ring wears the sig-error class')
})
test('renderGraph draws no rings without a signals map', () => {
const doc = makeDoc()
const rec = {
turn: 1, step: 0, startSeq: 10, endSeq: 12,
startTime: 1000, endTime: 1100,
inputs: [], outputs: [],
events: [{ type: 'tool/call', seq: 11, time: 1050, data: { name: 'x', arguments: '{}', callId: 'c' } }],
}
const el = G.renderGraph(doc, rec, {})
assert.strictEqual(collectByClass(el, 'trace-graph-signal-ring').length, 0)
})