mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-30 19:35:18 +00:00
The before/after preview flagged text-heavy views (e.g. workspace-sidebar) as ~0.1-0.3% "changed" on PRs that do not touch them — #7204 is a live example (the two panels are pixel-for-pixel indistinguishable). Root cause: base and head render in SEPARATE CI jobs, so Linux font anti-aliasing is not bit-identical between them, and the naive per-pixel diff counts the scatter of isolated / 1px-wide glyph-edge pixels that leaves. At the tight 0.02% threshold that scatter crosses the line. Measure the changed fraction AFTER a cluster denoise: a differing pixel counts only when at least 4 of its 8 neighbours also differ. AA scatter (isolated = 0 neighbours, a 1px line = 2) erodes to ~zero, while a real change — a badge, chip, icon, panel — is a solid block whose interior keeps 5-8 and easily clears the threshold, so the threshold stays tight without raising it (which would miss small real changes like a workspace badge). The browser now returns a compact bit-mask; the denoise + count run in node against the unit-tested countDenoisedChanges, so there is one tested implementation of the metric. Co-authored-by: wenshao <wenshao@example.com>
173 lines
6 KiB
JavaScript
173 lines
6 KiB
JavaScript
/**
|
||
* @license
|
||
* Copyright 2025 Qwen
|
||
* SPDX-License-Identifier: Apache-2.0
|
||
*/
|
||
|
||
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
|
||
import {
|
||
CHANGED_PCT_THRESHOLD,
|
||
countDenoisedChanges,
|
||
isChanged,
|
||
parseShot,
|
||
planWork,
|
||
unpackBitMask,
|
||
} from './web-shell-visuals-compose.mjs';
|
||
|
||
/** Build a row-major 0/1 mask from a predicate. */
|
||
function mask(width, height, isSet) {
|
||
const m = new Uint8Array(width * height);
|
||
for (let y = 0; y < height; y++) {
|
||
for (let x = 0; x < width; x++) {
|
||
m[y * width + x] = isSet(x, y) ? 1 : 0;
|
||
}
|
||
}
|
||
return m;
|
||
}
|
||
|
||
test('parseShot extracts view + theme, is case-insensitive, and strips dirs', () => {
|
||
assert.deepEqual(parseShot('mermaid-diagram-light.png'), {
|
||
view: 'mermaid-diagram',
|
||
theme: 'light',
|
||
});
|
||
assert.deepEqual(parseShot('permission-panel-DARK.png'), {
|
||
view: 'permission-panel',
|
||
theme: 'dark',
|
||
});
|
||
// A view name may itself contain a dash — only the final -light/-dark splits.
|
||
assert.deepEqual(parseShot('a/b/session-transcript-dark.png'), {
|
||
view: 'session-transcript',
|
||
theme: 'dark',
|
||
});
|
||
});
|
||
|
||
test('parseShot rejects non-screenshot names (gifs, manifest, themeless)', () => {
|
||
assert.equal(parseShot('model-switch.gif'), null);
|
||
assert.equal(parseShot('manifest.json'), null);
|
||
assert.equal(parseShot('mermaid-diagram.png'), null); // no -light/-dark
|
||
assert.equal(parseShot('random.png'), null);
|
||
});
|
||
|
||
test('isChanged: a view with no baseline (PR-added) always counts as changed', () => {
|
||
assert.equal(isChanged({ hasBefore: false, changedPct: 0 }), true);
|
||
});
|
||
|
||
test('isChanged: with a baseline, only meets threshold counts as changed', () => {
|
||
assert.equal(isChanged({ hasBefore: true, changedPct: 0 }), false);
|
||
// Below the threshold (relative, so it survives a threshold retune).
|
||
assert.equal(
|
||
isChanged({ hasBefore: true, changedPct: CHANGED_PCT_THRESHOLD / 2 }),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
isChanged({ hasBefore: true, changedPct: CHANGED_PCT_THRESHOLD }),
|
||
true,
|
||
);
|
||
assert.equal(isChanged({ hasBefore: true, changedPct: 6.5 }), true);
|
||
});
|
||
|
||
test('planWork pairs after-shots with baseline presence, sorted, ignoring non-shots', () => {
|
||
const plan = planWork(
|
||
[
|
||
'mermaid-diagram-light.png',
|
||
'mermaid-diagram-dark.png',
|
||
'session-transcript-light.png',
|
||
'model-switch.gif', // ignored (not a shot)
|
||
'manifest.json', // ignored
|
||
],
|
||
['session-transcript-light.png', 'session-transcript-dark.png'],
|
||
);
|
||
assert.deepEqual(plan, [
|
||
{ name: 'mermaid-diagram-dark.png', hasBefore: false }, // PR-added → NEW
|
||
{ name: 'mermaid-diagram-light.png', hasBefore: false },
|
||
{ name: 'session-transcript-light.png', hasBefore: true }, // has baseline
|
||
]);
|
||
});
|
||
|
||
test('planWork is robust to a missing before set (first-ever PR → all NEW)', () => {
|
||
const plan = planWork(['home-light.png', 'home-dark.png'], []);
|
||
assert.deepEqual(
|
||
plan.map((p) => p.hasBefore),
|
||
[false, false],
|
||
);
|
||
});
|
||
|
||
test('planWork tolerates null/undefined args (exercises the ?? [] guards)', () => {
|
||
assert.deepEqual(planWork(null, null), []);
|
||
assert.deepEqual(planWork(undefined, undefined), []);
|
||
// after present, before nullish → every shot is NEW.
|
||
assert.deepEqual(planWork(['a-light.png'], null), [
|
||
{ name: 'a-light.png', hasBefore: false },
|
||
]);
|
||
});
|
||
|
||
test('countDenoisedChanges: isolated scatter (cross-job AA) erodes to zero', () => {
|
||
// Isolated single pixels, none adjacent — the shape of font-AA jitter.
|
||
const [w, h] = [20, 20];
|
||
const m = mask(w, h, (x, y) => x % 4 === 0 && y % 4 === 0);
|
||
// Every set pixel has 0 set neighbours → none survive the density cutoff.
|
||
assert.equal(countDenoisedChanges(m, w, h), 0);
|
||
});
|
||
|
||
test('countDenoisedChanges: a 1px-wide line (glyph edge) erodes to zero', () => {
|
||
const [w, h] = [20, 20];
|
||
const m = mask(w, h, (_x, y) => y === 5); // one horizontal line
|
||
// Interior line pixels have exactly 2 set neighbours (< 4) → none survive.
|
||
assert.equal(countDenoisedChanges(m, w, h), 0);
|
||
});
|
||
|
||
test('countDenoisedChanges: minNeighbors controls erosion strength on a solid block', () => {
|
||
const [w, h] = [3, 3];
|
||
const m = mask(w, h, () => true); // full 3×3
|
||
// Centre keeps all 8 neighbours; the 4 edges keep 5; the 4 corners keep 3.
|
||
assert.equal(countDenoisedChanges(m, w, h, 8), 1); // centre only
|
||
assert.equal(countDenoisedChanges(m, w, h, 4), 5); // centre + 4 edges
|
||
assert.equal(countDenoisedChanges(m, w, h, 3), 9); // corners survive too
|
||
});
|
||
|
||
test('countDenoisedChanges: a badge-shaped solid block mostly survives', () => {
|
||
const [w, h] = [40, 40];
|
||
// 15×12 solid block = 180 px.
|
||
const m = mask(w, h, (x, y) => x >= 10 && x < 25 && y >= 10 && y < 22);
|
||
const survivors = countDenoisedChanges(m, w, h);
|
||
// Only the 4 corners (3 neighbours) erode; the rest of the 180-px block keeps.
|
||
assert.ok(
|
||
survivors >= 170,
|
||
`expected most of the 180-px block to survive, got ${survivors}`,
|
||
);
|
||
});
|
||
|
||
test('at 1280×800: AA scatter erodes below the threshold, a real badge clears it', () => {
|
||
const [W, H] = [1280, 800];
|
||
const N = W * H;
|
||
// ~0.25% of pixels as an isolated scatter — well over the raw 0.02% line.
|
||
const aa = mask(W, H, (x, y) => x % 20 === 0 && y % 20 === 0);
|
||
const aaPct = (countDenoisedChanges(aa, W, H) / N) * 100;
|
||
assert.ok(
|
||
aaPct < CHANGED_PCT_THRESHOLD,
|
||
`AA scatter should denoise below ${CHANGED_PCT_THRESHOLD}%, got ${aaPct}`,
|
||
);
|
||
|
||
// A 55×18 badge-sized solid block (~0.075% of the frame).
|
||
const badge = mask(
|
||
W,
|
||
H,
|
||
(x, y) => x >= 100 && x < 155 && y >= 100 && y < 118,
|
||
);
|
||
const badgePct = (countDenoisedChanges(badge, W, H) / N) * 100;
|
||
assert.ok(
|
||
badgePct >= CHANGED_PCT_THRESHOLD,
|
||
`a real badge-sized change should stay above ${CHANGED_PCT_THRESHOLD}%, got ${badgePct}`,
|
||
);
|
||
});
|
||
|
||
test('unpackBitMask round-trips a packed mask (LSB-first)', () => {
|
||
// Pixels 0..9, set {0, 3, 8}: byte0 = 0b0000_1001, byte1 = 0b0000_0001.
|
||
const b64 = Buffer.from([0b00001001, 0b00000001]).toString('base64');
|
||
assert.deepEqual(
|
||
Array.from(unpackBitMask(b64, 10)),
|
||
[1, 0, 0, 1, 0, 0, 0, 0, 1, 0],
|
||
);
|
||
});
|