Files
deepseek-harness/examples/desktop/test/style-css-static.test.js
T

378 lines
16 KiB
JavaScript
Raw Normal View History

// Static gate for the centered-card ban (density-spec §7; user re-flagged
// 2026-07-17, second occurrence). Every in-stream element is a full-width,
// left-aligned row that expands INLINE. The forbidden CSS smells are
// `margin: * auto` and `align-self: center` on the tool-card / trigger /
// steer families; both survived one dedicated sweep before drift caught
// them, so we lock the ban at the test layer instead of relying on eyes.
//
// Exemption list (must stay allowed to use auto/center):
// .empty-welcome — first-boot empty-state page chrome
// .empty-launcher — launcher grid page chrome
// .context-page-empty-* — Context page empty state (page chrome)
// .msg.user — canonical user-bubble right-anchor
// (`margin-left: auto` is the shape, not a smell)
'use strict'
const test = require('node:test')
const assert = require('node:assert')
const fs = require('node:fs')
const path = require('node:path')
const cssPath = path.resolve(__dirname, '..', 'src', 'renderer', 'style.css')
const css = fs.readFileSync(cssPath, 'utf8')
// Forbidden selectors: the 13-card family (density-spec inventory) + the
// steer-chip in-stream chip. Match the selector token at a rule boundary
// so `.card` never matches `.card-diff-tree-row` or similar longer names.
const CARD_FAMILY = [
'.card',
'.tool-block',
'.artifact-card',
'.card-terminal',
'.card-diff',
'.card-code-dispatch',
'.card-web-search',
'.card-skill',
'.card-workflow',
'.visibility-card',
'.todo-card',
'.recall-card',
'.compact-card',
'.steer-chip',
]
const EXEMPT = new Set([
'.empty-welcome',
'.empty-launcher',
'.context-page-empty',
'.context-page-empty-title',
'.context-page-empty-sub',
'.msg.user',
])
// Parse each top-level rule (selectorList { body }). Nested at-rules
// (@media, @supports) get their inner rules parsed the same way — we
// walk brace by brace and only surface leaf rules (bodies without `{`).
function iterRules(source) {
const rules = []
let i = 0
const n = source.length
while (i < n) {
// Skip whitespace and comments
while (i < n) {
if (source[i] === '/' && source[i + 1] === '*') {
const end = source.indexOf('*/', i + 2)
i = end === -1 ? n : end + 2
} else if (/\s/.test(source[i])) {
i++
} else break
}
if (i >= n) break
// Read up to '{' or ';'
let start = i
let depth = 0
while (i < n && source[i] !== '{' && source[i] !== ';') i++
if (i >= n) break
if (source[i] === ';') { i++; continue }
const selector = source.slice(start, i).trim()
i++ // consume {
// Read matching body
const bodyStart = i
depth = 1
while (i < n && depth > 0) {
if (source[i] === '{') depth++
else if (source[i] === '}') depth--
if (depth > 0) i++
}
const body = source.slice(bodyStart, i)
i++ // consume }
// If selector starts with @, recurse into body (media/supports/etc.)
if (selector.startsWith('@')) {
for (const r of iterRules(body)) rules.push(r)
} else {
rules.push({ selector, body })
}
}
return rules
}
function selectorTouchesFamily(selector) {
// A rule's selectorList is comma-separated; if ANY selector in the list
// ends in a token from the family (i.e., the LAST simple selector on
// the compound is a family member), the rule sets that family's box.
const groups = selector.split(',').map((s) => s.trim())
const matched = []
for (const g of groups) {
// Last simple selector = trailing chunk before any pseudo (:hover etc.)
// We match "the compound ends in .foo(.bar)*" by checking the whole
// string ends with one of the family names, allowing further class
// qualifiers (e.g. `.card.trigger-card`) but NOT a descendant chain
// (e.g. `.card .steer-title` — that styles a child, not the card box).
const bareTail = g.replace(/::?[a-z-]+(\([^)]*\))?$/i, '') // drop pseudo
// Reject exemptions first — a selector whose last simple selector is an
// exempt name is off-limits for the ban even if the compound also has
// a family class (e.g. `.card.empty-welcome-frame`).
for (const ex of EXEMPT) {
if (endsWithSelector(bareTail, ex)) return null
}
for (const fam of CARD_FAMILY) {
if (endsWithSelector(bareTail, fam)) {
matched.push({ group: g, family: fam })
break
}
}
}
return matched.length ? matched : null
}
// A compound `X.Y.Z` "ends with" `.Y` if `.Y` appears as one of the
// classes in the trailing compound (after the last combinator).
function endsWithSelector(compoundish, needle) {
// Last simple selector = substring after the last combinator (space, >, +, ~)
const lastSep = Math.max(
compoundish.lastIndexOf(' '),
compoundish.lastIndexOf('>'),
compoundish.lastIndexOf('+'),
compoundish.lastIndexOf('~'),
)
const tail = lastSep >= 0 ? compoundish.slice(lastSep + 1) : compoundish
// Split the tail on `.` (except the very first char which may be `.`
// itself). Tokens are the classes; needle without leading `.` is the
// class name we're looking for.
const need = needle.startsWith('.') ? needle.slice(1) : needle
// Include ANY class in the compound tail (e.g., `.card.trigger-card` matches
// both `.card` and `.trigger-card`).
const classes = tail.split('.').filter(Boolean)
return classes.includes(need)
}
const rules = iterRules(css)
test('centered-card ban — no `margin: * auto` on card family or steer-chip', () => {
const offenders = []
for (const { selector, body } of rules) {
const hit = selectorTouchesFamily(selector)
if (!hit) continue
// Match `margin: <anything with the token `auto` as a whole word> ;`
// Handles: `margin: auto`, `margin: 0 auto`, `margin: 6px auto`,
// `margin: var(--space-3) auto`, `margin: 12px auto 8px`.
// NOT matched: `margin-left: auto` (right-anchoring, legitimate),
// `margin-top: auto` (flex spacer, legitimate),
// `overflow: auto` (different property).
if (/(^|[\s;])margin\s*:[^;]*\bauto\b/i.test(body)) {
offenders.push(`${selector.replace(/\s+/g, ' ')} — body has \`margin: … auto\``)
}
}
assert.deepStrictEqual(offenders, [],
`centered-card ban violated (margin auto):\n ${offenders.join('\n ')}`)
})
test('centered-card ban — no `align-self: center` on card family or steer-chip', () => {
const offenders = []
for (const { selector, body } of rules) {
const hit = selectorTouchesFamily(selector)
if (!hit) continue
if (/(^|[\s;])align-self\s*:\s*center\b/i.test(body)) {
offenders.push(`${selector.replace(/\s+/g, ' ')} — body has \`align-self: center\``)
}
}
assert.deepStrictEqual(offenders, [],
`centered-card ban violated (align-self center):\n ${offenders.join('\n ')}`)
})
test('selector matcher: family-detection sanity (guards the guard)', () => {
// Positive cases — must be flagged as family.
assert.ok(selectorTouchesFamily('.card'), '.card')
assert.ok(selectorTouchesFamily('.card.steer'), '.card.steer')
assert.ok(selectorTouchesFamily('.card.trigger-card'), '.card.trigger-card')
assert.ok(selectorTouchesFamily('.tool-block'), '.tool-block')
assert.ok(selectorTouchesFamily('.steer-chip'), '.steer-chip')
// Compound classes get matched.
assert.ok(selectorTouchesFamily('.tool-block.family-code'), '.tool-block.family-code')
// Negative cases — descendant styles are NOT the card box.
assert.strictEqual(selectorTouchesFamily('.card .steer-title'), null,
'descendant of .card is not the card box')
assert.strictEqual(selectorTouchesFamily('.tool-block summary'), null,
'element inside .tool-block is not the block box')
// Longer names starting with the same chars are NOT the family.
assert.strictEqual(selectorTouchesFamily('.card-diff-tree-row'), null,
'longer name is not `.card`')
// Exemptions win over the family flag.
assert.strictEqual(selectorTouchesFamily('.msg.user'), null,
'.msg.user is exempt')
assert.strictEqual(selectorTouchesFamily('.empty-welcome'), null,
'.empty-welcome is exempt')
})
// Brace-balance machine lock. Third-time occurrence pattern — union-splicing
// style.css after a merge has three times swallowed later rules by leaving a
// block unclosed (Bench collapse f1efa1c was the most recent). Manual review
// missed it every time; add the machine so the eye never has to be right.
//
// Method: comment-aware char walk (strip `/* … */` first) that increments on
// `{`, decrements on `}`, and asserts (a) final depth === 0 (every open block
// is closed) and (b) depth never goes negative (no stray `}` before its `{`).
// Braces inside strings/URLs are theoretically possible; in practice CSS uses
// no string literals with `{`/`}` and url() bodies never contain them, so the
// raw walk after comment stripping is sound.
test('brace balance — style.css blocks close cleanly (union-splice lock)', () => {
const src = css
let depth = 0
let minDepth = 0
let firstNegativeAt = -1
let i = 0
const n = src.length
while (i < n) {
// Skip block comments so `/* } */` doesn't fool the counter.
if (src[i] === '/' && src[i + 1] === '*') {
const end = src.indexOf('*/', i + 2)
i = end === -1 ? n : end + 2
continue
}
const ch = src[i]
if (ch === '{') {
depth++
} else if (ch === '}') {
depth--
if (depth < minDepth) {
minDepth = depth
if (firstNegativeAt === -1 && depth < 0) firstNegativeAt = i
}
}
i++
}
// Turn a raw char index into a "line X" hint (best-effort; only used in
// the failure message, so an off-by-one on the line is not load-bearing).
const lineOf = (idx) => idx < 0 ? '?' : (src.slice(0, idx).match(/\n/g) || []).length + 1
assert.strictEqual(minDepth, 0,
`stray closing brace before its matching open at ~line ${lineOf(firstNegativeAt)} ` +
`(minDepth=${minDepth}) — union-splice likely dropped a rule head`)
assert.strictEqual(depth, 0,
`style.css ends with ${depth} unclosed block${depth === 1 ? '' : 's'} — ` +
`a rule head opened without its `+`}`+` and swallowed everything below. ` +
`See f1efa1c (Bench collapse) for the last occurrence.`)
})
// -----------------------------------------------------------------------
// payload-controls overlap lock (2026-07-18 P0 hotfix, da779ac / merge
// 9743db1). The button cluster (pretty ⇅ raw · copy · download) used to
// sit on tool-block args/result rows via `float: right` + a negative
// `margin-top: -18px` pull-up hack; when the label text or drawer meta
// string got long (e.g. drawer's "(content · meta · isError · error ·
// durationMs)"), the buttons landed on top of the label text. The fix
// wrapped label+controls in a flex row (`.tool-block-label-row`) and
// switched the drawer callsite to a right-aligned strip
// (`.tool-json-section-controls`) — see style.css §payload-controls +
// §tool-block-label-row + §tool-json-section-controls.
//
// This gate freezes the fix so it can't silently regress: any rule
// whose selector ends in `.payload-controls`, `.tool-block-label-row`,
// or `.tool-json-section-controls` (in any state — plain, hover,
// descendant of a family class, etc.) is forbidden from re-introducing
// `float: right|left` or a negative `margin-top`. The trace-detail-pane
// mount (mount D, `.trace-detail-json-panel`) also gets covered by the
// same body-scan below since payload-controls is where the buttons
// live regardless of host.
//
// Method: match rules whose LAST simple selector is in the payload
// vocabulary (using the same endsWithSelector matcher we already trust
// for the centered-card gate) and grep the body for float/negative
// margin-top. `margin-left: auto` on `.payload-controls` is EXPLICITLY
// allowed — that's how the cluster right-anchors inside its flex host.
const PAYLOAD_VOCAB = [
'.payload-controls',
'.tool-block-label-row',
'.tool-json-section-controls',
]
function selectorTouchesPayload(selector) {
const groups = selector.split(',').map((s) => s.trim())
const matched = []
for (const g of groups) {
const bareTail = g.replace(/::?[a-z-]+(\([^)]*\))?$/i, '')
// Last simple selector = substring after the last combinator
const lastSep = Math.max(
bareTail.lastIndexOf(' '),
bareTail.lastIndexOf('>'),
bareTail.lastIndexOf('+'),
bareTail.lastIndexOf('~'),
)
const tail = lastSep >= 0 ? bareTail.slice(lastSep + 1) : bareTail
const classes = tail.split('.').filter(Boolean)
for (const v of PAYLOAD_VOCAB) {
if (classes.includes(v.slice(1))) { matched.push({ group: g, vocab: v }); break }
}
}
return matched.length ? matched : null
}
test('payload-controls overlap lock — no `float: left|right` on payload vocabulary', () => {
const offenders = []
for (const { selector, body } of rules) {
if (!selectorTouchesPayload(selector)) continue
// `float: none` is a legitimate DE-fluke (drawer strip explicitly
// sets `float: none` to nuke a legacy inherited float). We only ban
// the two directional values that make things overlap.
if (/(^|[\s;])float\s*:\s*(right|left)\b/i.test(body)) {
offenders.push(`${selector.replace(/\s+/g, ' ')} — body has \`float: right|left\``)
}
}
assert.deepStrictEqual(offenders, [],
`payload-controls overlap lock violated (float:right|left reintroduced):\n ${offenders.join('\n ')}\n` +
'Root cause of the 2026-07-18 P0: float:right + margin-top:-18px pulled the button ' +
'cluster on top of the label text when the drawer meta string ran long. Use the ' +
'flex-row shape (`.tool-block-label-row` for inline / `.tool-json-section-controls` ' +
'for the drawer) instead — see style.css around the payload-controls block.')
})
test('payload-controls overlap lock — no negative `margin-top` on payload vocabulary', () => {
const offenders = []
for (const { selector, body } of rules) {
if (!selectorTouchesPayload(selector)) continue
// Match `margin-top: -Npx` (or `-N`, or `-N.Nrem`, etc.). The
// shorthand `margin: -Npx …` on a payload rule is equally bad; catch
// both. We DO allow `margin: 0`, `margin-top: 0`, and any positive
// value; we DO allow `margin-left: auto` (that's the right-anchor).
if (/(^|[\s;])margin-top\s*:\s*-\d/i.test(body) ||
/(^|[\s;])margin\s*:\s*-\d/i.test(body)) {
offenders.push(`${selector.replace(/\s+/g, ' ')} — body has negative margin-top`)
}
}
assert.deepStrictEqual(offenders, [],
`payload-controls overlap lock violated (negative margin-top reintroduced):\n ${offenders.join('\n ')}\n` +
'The negative-margin pull-up hack was half of the overlap bug (the other half was ' +
'`float: right`). Both are banned on `.payload-controls`, `.tool-block-label-row`, ' +
'and `.tool-json-section-controls` — the merged fix (da779ac) uses `margin-left: auto` ' +
'inside a flex row instead. See style.css around the payload-controls block.')
})
test('payload-controls overlap lock — the fix\'s flex row shape stays present', () => {
// Positive assertion: the two flex hosts that carry the fix must still
// exist as rules and still declare `display: flex`. A refactor that
// deletes them (thinking they\'re dead) would silently reintroduce
// the overlap.
function findRule(cls) {
for (const { selector, body } of rules) {
const groups = selector.split(',').map((s) => s.trim())
for (const g of groups) {
// Match `<cls>` as the whole selector, or `<cls>` followed by
// a `>` child combinator (both `.foo` and `.foo > .bar` count
// as "the rule that sets .foo").
if (g === cls || g.startsWith(cls + ' ') || g.startsWith(cls + '>') || g.startsWith(cls + '.')) {
return { selector, body }
}
}
}
return null
}
const row = findRule('.tool-block-label-row')
assert.ok(row, '.tool-block-label-row rule missing — the P0 flex fix has been deleted')
assert.match(row.body, /display\s*:\s*flex/i,
'.tool-block-label-row must stay `display: flex` — that is what pushes the button cluster to the right instead of stacking it via float.')
const strip = findRule('.tool-json-section-controls')
assert.ok(strip, '.tool-json-section-controls rule missing — the drawer overlap fix has been deleted')
assert.match(strip.body, /display\s*:\s*flex/i,
'.tool-json-section-controls must stay `display: flex` — the drawer callsite relies on it to right-align the controls strip.')
})