Repository navigation
Expand file tree
/
Copy pathusage-pace.test.ts
More file actions
288 lines (257 loc) · 13.8 KB
/
Copy pathusage-pace.test.ts
File metadata and controls
288 lines (257 loc) · 13.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
import { describe, expect, test } from "bun:test";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
// A limit bar at 86% means nothing on its own. The same card showed a solid red
// Weekly bar next to Burn Bar's "86% ON PACE", and it read as "out of Claude
// Code" when the number was identical in both: the cache, the collector and the
// live desk all said 0.86. What was missing is where EVEN PACE sits, so a bar
// can say "slightly ahead" instead of just "red". These tests pin the maths on
// the real numbers from that night, and render the real Meter component.
const view = readFileSync(join(import.meta.dir, "InfoView.qml"), "utf8");
// Lift one function out of the QML by counting braces, so a one-liner and a
// multi-line body both come out exactly as written.
function pickFunction(name: string): string {
const start = view.indexOf(`function ${name}(`);
if (start < 0) throw new Error(`InfoView.qml has no ${name}()`);
let depth = 0, i = view.indexOf("{", start);
for (; i < view.length; i++) {
if (view[i] === "{") depth++;
else if (view[i] === "}" && --depth === 0) break;
}
return view.slice(start, i + 1);
}
const usageWindowMs = new Function(`return (${pickFunction("usageWindowMs")})`)();
const usagePaceSource = pickFunction("usagePace");
const usagePace = (limit: any, snap: any) => new Function("usageWindowMs", "snap", `return (${usagePaceSource})`)(usageWindowMs, snap)(limit);
const HOUR = 3600_000;
const NOW = Date.parse("2026-10-10T03:50:00Z");
const usagePaceSrc = pickFunction("usagePace");
const usagePaceDeltaSrc = pickFunction("usagePaceDelta");
const usageLimitLabelSrc = pickFunction("usageLimitLabel");
// The three helpers call each other as bare names, as they do inside the QML
// object, so they are assembled the same way here.
const label = (limit: any, snap: any) =>
new Function("usageWindowMs", "snap", `
${usagePaceSrc}
${usagePaceDeltaSrc}
${usageLimitLabelSrc}
return usageLimitLabel(${JSON.stringify(limit)});
`)(usageWindowMs, snap);
describe("where even pace sits on a limit bar", () => {
test("the real Weekly reading: 30h of a 168h week left puts even pace at 82%", () => {
// 2026-10-09 23:47 EDT: Weekly 86%, resets Sunday 06:00 EDT. Burn Bar drew
// its tick at ~82% and so should this.
const pace = usagePace({ label: "Weekly (7-day)", resetsAt: new Date(NOW + 30 * HOUR).toISOString() }, { ts: NOW });
expect(pace).toBeCloseTo(1 - 30 / 168, 6);
expect(Math.round(pace * 100)).toBe(82);
});
test("the 5-hour session window and the Fable weekly window use their own lengths", () => {
expect(usagePace({ label: "Session (5-hour)", resetsAt: new Date(NOW + 1 * HOUR).toISOString() }, { ts: NOW })).toBeCloseTo(0.8, 6);
expect(usagePace({ title: "Fable Weekly", label: "Fable Weekly", resetsAt: new Date(NOW + 84 * HOUR).toISOString() }, { ts: NOW })).toBeCloseTo(0.5, 6);
});
test("no marker when the pace cannot be known", () => {
const at = (limit: any) => usagePace(limit, { ts: NOW });
// A window we do not know the length of must not get a guessed tick.
expect(at({ label: "Monthly credits", resetsAt: new Date(NOW + HOUR).toISOString() })).toBe(-1);
// No reset time, an unparseable one, a reset already in the past, and a
// reset further out than the window itself (a stale reading) all mean the
// elapsed fraction would be wrong, so there is no tick rather than a lie.
expect(at({ label: "Weekly (7-day)" })).toBe(-1);
expect(at({ label: "Weekly (7-day)", resetsAt: "tomorrow-ish" })).toBe(-1);
expect(at({ label: "Weekly (7-day)", resetsAt: new Date(NOW - HOUR).toISOString() })).toBe(-1);
expect(at({ label: "Weekly (7-day)", resetsAt: new Date(NOW + 200 * HOUR).toISOString() })).toBe(-1);
expect(usagePace(undefined, { ts: NOW })).toBe(-1);
});
test("it is read from the snapshot clock, so it moves with the data", () => {
const limit = { label: "Weekly (7-day)", resetsAt: new Date(NOW + 30 * HOUR).toISOString() };
const earlier = usagePace(limit, { ts: NOW - 12 * HOUR });
const later = usagePace(limit, { ts: NOW });
expect(later - earlier).toBeCloseTo(12 / 168, 6);
expect(usagePaceSource).toContain("snap.ts");
});
test("the limit rows hand it to the meter, and the legend explains the tick", () => {
expect(view).toContain("pace: view.usagePace(modelData)");
expect(view).toContain("omarchy agents · grok/opencode local · | = even pace");
});
});
// Rendering. The Meter is an inline component, so it cannot be instantiated on
// its own; instead the REAL source text is lifted out of InfoView.qml and
// dropped into a generated fixture with the few names it reaches for supplied
// as stand-ins. Testing a hand-written copy would prove the copy.
function meterSource(): string {
const start = view.indexOf("component Meter: Item {");
if (start < 0) throw new Error("InfoView.qml has no Meter component");
let depth = 0, i = view.indexOf("{", start);
for (; i < view.length; i++) {
if (view[i] === "{") depth++;
else if (view[i] === "}" && --depth === 0) break;
}
return view.slice(start, i + 1);
}
const RUNNER = ["/usr/lib/qt6/bin/qmltestrunner", "/usr/bin/qmltestrunner"].find(path => existsSync(path)) || "";
function writeFixture(): string {
const dir = mkdtempSync(join(tmpdir(), "infomarchy-pace-"));
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "qmldir"), "singleton Style Style.qml\nsingleton Util Util.qml\nsingleton Color Color.qml\n");
writeFileSync(join(dir, "Style.qml"), `pragma Singleton
import QtQuick
QtObject {
readonly property var spacing: ({ xs: 3, sm: 4 })
readonly property var font: ({ bodySmall: 12 })
readonly property real fontScale: 1
}
`);
writeFileSync(join(dir, "Util.qml"), `pragma Singleton
import QtQuick
QtObject { function alpha(c, a) { var k = Qt.color(c); return Qt.rgba(k.r, k.g, k.b, a) } }
`);
writeFileSync(join(dir, "Color.qml"), `pragma Singleton
import QtQuick
QtObject { readonly property color accent: "#00ffff" }
`);
const file = join(dir, "tst_pacetick.qml");
writeFileSync(file, `import QtQuick
import QtQuick.Layouts
import QtTest
import "."
Item {
id: view
width: 400; height: 200
property color textDim: "#999999"
property string mono: "monospace"
property var desk: ({ themeForeground: Qt.color("#ffffff") })
component PlainText: Text {}
${meterSource()}
// Same label and width everywhere, so the bar sits at the same y in each.
// Each Meter sits on a black cell and the CELL is what gets grabbed, so a
// pixel's brightness is read against black like on the real desk.
Column {
spacing: 8
Rectangle { color: "black"; width: 400; height: childrenRect.height
Meter { objectName: "behind"; label: "Weekly (7-day)"; value: "50%"; fraction: 0.50; pace: 0.90; tone: "#ff0000" } }
Rectangle { color: "black"; width: 400; height: childrenRect.height
Meter { objectName: "ahead"; label: "Weekly (7-day)"; value: "86%"; fraction: 0.86; pace: 0.82; tone: "#ff0000" } }
Rectangle { color: "black"; width: 400; height: childrenRect.height
Meter { objectName: "none"; label: "Weekly (7-day)"; value: "86%"; fraction: 0.86; pace: -1; tone: "#ff0000" } }
Rectangle { color: "black"; width: 400; height: childrenRect.height
Meter { objectName: "outside"; label: "Weekly (7-day)"; value: "86%"; fraction: 0.86; pace: 1.7; tone: "#ff0000" } }
Rectangle { color: "black"; width: 400; height: childrenRect.height
Meter { objectName: "start"; label: "Weekly (7-day)"; value: "10%"; fraction: 0.10; pace: 0.25; tone: "#ff0000" } }
}
TestCase {
name: "PaceTick"
when: windowShown
function barRow(item) {
var t = findChild(item, "paceTick")
return t.mapToItem(item, 0, t.height / 2).y
}
// Pixel probes on a grabbed Meter, in that Meter's own coordinates. The
// grab is opaque, so brightness (red/green) is what distinguishes a tick,
// never alpha. 'row' lists a run of pixels for the failure message.
function probe(objectName, x) {
var m = findChild(view, objectName)
wait(600)
var img = grabImage(m.parent)
var y = Math.round(barRow(m))
var row = ""
for (var dx = -8; dx <= 8; dx += 2) row += (x + dx) + ":" + img.red(x + dx, y) + "/" + img.green(x + dx, y) + " "
return { r: img.red(x, y), g: img.green(x, y), y: y, row: row }
}
function test_tick_over_the_fill_is_bright_against_the_red() {
// Bar is ahead of pace: the tick sits INSIDE the red fill. Red is
// (255,0,0); a white tick over it lifts green far above zero.
var on = probe("ahead", 328), beside = probe("ahead", 316)
verify(on.g > 180, "tick over fill should be near white, got g=" + on.g + " at y=" + on.y + " row " + on.row)
verify(beside.g < 40, "fill beside the tick should stay red, got g=" + beside.g)
}
function test_tick_beyond_the_fill_stands_out_of_the_empty_track() {
// Bar is behind pace: the tick sits past the fill on the faint track.
var on = probe("behind", 360), beside = probe("behind", 348)
verify(on.r > 200, "tick on the track should be near white, got r=" + on.r + " row " + on.row)
verify(beside.r < 80, "empty track beside it should stay dim, got r=" + beside.r)
}
function test_no_pace_draws_no_tick() {
var m = findChild(view, "none")
compare(findChild(m, "paceTick").visible, false)
var at = probe("none", 328)
verify(at.g < 40, "with no pace the fill must be plain red, got g=" + at.g)
}
function test_an_impossible_pace_draws_no_tick() {
// A pace past 1 is a bad reading, not a bar that is somehow beyond full.
compare(findChild(findChild(view, "outside"), "paceTick").visible, false)
}
function test_the_tick_lands_where_the_fraction_says() {
// Even pace 25% of a 400px track is x=100, whatever the fill is doing.
var m = findChild(view, "start")
var t = findChild(m, "paceTick")
var cx = t.mapToItem(m, t.width / 2, 0).x
verify(Math.abs(cx - 100) <= 1, "tick centre " + cx + " should be at 100")
compare(t.visible, true)
}
}
}
`);
return file;
}
describe("the real Meter draws the even-pace tick", () => {
test("qmltestrunner is available to gate this", () => {
expect(RUNNER, "install qt6-declarative for qmltestrunner").not.toBe("");
});
test("the Meter keeps the tick as an optional, off-by-default marker", () => {
const meter = meterSource();
expect(meter).toContain("property real pace: -1");
expect(meter).toContain('objectName: "paceTick"');
expect(meter).toContain("visible: pace >= 0 && pace <= 1");
// Every other Meter in the desk (model shares, machine gauges) passes no
// pace, so none of them grows a tick.
const uses = view.match(/\n\s+delegate: Meter \{|\n\s+Meter \{/g) || [];
const withPace = view.match(/pace: view\.usagePace\(modelData\)/g) || [];
expect(withPace.length).toBe(1);
expect(uses.length).toBeGreaterThan(withPace.length);
});
test("pixels: ahead of pace, behind pace, no pace, bad pace, and position", () => {
const file = writeFixture();
const result = Bun.spawnSync([RUNNER, "-input", file], {
env: { ...process.env, QT_QPA_PLATFORM: "offscreen", QT_QUICK_BACKEND: "software" },
});
const output = result.stdout.toString() + result.stderr.toString();
for (const name of [
"test_tick_over_the_fill_is_bright_against_the_red",
"test_tick_beyond_the_fill_stands_out_of_the_empty_track",
"test_no_pace_draws_no_tick",
"test_an_impossible_pace_draws_no_tick",
"test_the_tick_lands_where_the_fraction_says",
]) expect(output, output).toContain(`PASS : qmltestrunner::PaceTick::${name}()`);
expect(result.exitCode, output).toBe(0);
}, 60_000);
});
describe("the row keeps its numbers where a cropped or covered card still shows them", () => {
const weekly = { label: "Weekly (7-day)", percent: 0.86, resetsAt: new Date(NOW + 30 * HOUR).toISOString() };
test("the real Weekly row reads 86% and 4 points ahead of pace, in the label", () => {
// The card was screenshotted with its right end covered. The bar looked
// full because the number, the end of the fill and the tick all sit at the
// right. Name, percent and delta now sit together at the LEFT.
expect(label(weekly, { ts: NOW })).toBe("Weekly (7-day) 86% +4");
});
test("behind pace is negative, and an unknown pace adds no delta", () => {
expect(label({ label: "Fable Weekly", percent: 0.34, resetsAt: new Date(NOW + 30 * HOUR).toISOString() }, { ts: NOW })).toBe("Fable Weekly 34% -48");
expect(label({ label: "Monthly credits", percent: 0.5, resetsAt: new Date(NOW + HOUR).toISOString() }, { ts: NOW })).toBe("Monthly credits 50%");
expect(label({ label: "Weekly (7-day)", percent: 0.5 }, { ts: NOW })).toBe("Weekly (7-day) 50%");
});
test("exactly on pace shows a plain 0, not a sign", () => {
expect(label({ label: "Session (5-hour)", percent: 0.8, resetsAt: new Date(NOW + HOUR).toISOString() }, { ts: NOW })).toBe("Session (5-hour) 80% 0");
});
test("the row renders the label from the helper, and the right side no longer repeats the percent", () => {
expect(view).toContain("label: view.usageLimitLabel(modelData)");
// The right-hand value keeps the reset (and the forecast view), but the
// percent that used to lead it is gone, so it is not said twice.
const row = view.slice(view.indexOf("label: view.usageLimitLabel(modelData)"), view.indexOf("fraction: modelData.percent || 0", view.indexOf("label: view.usageLimitLabel(modelData)")));
expect(row).toContain('"% at reset"');
expect(row).toContain('"↻ "');
expect(row).not.toContain("Math.round((modelData.percent || 0) * 100)");
});
test("the legend explains both marks", () => {
expect(view).toContain("| = even pace · +n ahead, -n behind");
});
});