pi-hypercharm-provider 1.1.5 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Dependency-free smoke test for the footer-status presentation module.
3
+ * Run: node tests/status.smoke.ts (Node ≥ 23 strips types natively).
4
+ * Exercises tier building, width math, widget layout, and config coercion —
5
+ * the pieces where a regression would silently corrupt the footer line.
6
+ */
7
+ import assert from "node:assert/strict";
8
+ import {
9
+ EMPTY_ACCOUNT,
10
+ StatusLineWidget,
11
+ accountHasData,
12
+ buildAccountTiers,
13
+ buildSessionLine,
14
+ coerceStatusConfig,
15
+ formatBalHc,
16
+ formatRateCompact,
17
+ formatSpendHc,
18
+ termVisWidth,
19
+ truncateAnsi,
20
+ type AccountState,
21
+ } from "../status.ts";
22
+
23
+ const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
24
+ const fakeTheme = { fg: (_c: string, t: string) => `\x1b[2m${t}\x1b[39m` };
25
+
26
+ // ── formatters ──
27
+ assert.equal(formatBalHc(249), "249");
28
+ assert.equal(formatBalHc(12345), "12.3k");
29
+ assert.equal(formatBalHc(250.5), "250.5");
30
+ assert.equal(formatBalHc(1_250_000), "1.25M");
31
+ assert.equal(formatSpendHc(0), "0");
32
+ assert.equal(formatSpendHc(0.0004), "~0");
33
+ assert.equal(formatSpendHc(0.0021), "0.0021");
34
+ assert.equal(formatSpendHc(0.31), "0.31");
35
+ assert.equal(formatSpendHc(12.5), "12.5");
36
+ assert.equal(formatRateCompact(996), "996");
37
+ assert.equal(formatRateCompact(1000), "1k");
38
+ assert.equal(formatRateCompact(9996), "10k");
39
+
40
+ // ── session line ──
41
+ assert.equal(buildSessionLine({ requests: 0, spendHc: 0 }), undefined);
42
+ assert.equal(buildSessionLine({ requests: 7, spendHc: 1.24 }), "⚡ 1.24 hc · 7 req");
43
+ assert.equal(buildSessionLine({ requests: 1, spendHc: 0 }), "⚡ 0 hc · 1 req");
44
+
45
+ // ── account tiers ──
46
+ const acc = (over: Partial<AccountState>): AccountState => ({ ...EMPTY_ACCOUNT, ...over });
47
+ const rate = { limitHour: 1000, limitDay: 10000, remainingHour: 996, remainingDay: 9996, capturedAt: 0 };
48
+
49
+ assert.equal(accountHasData(acc({})), false);
50
+ assert.equal(accountHasData(acc({ balance: 0 })), true);
51
+
52
+ const full = acc({ balance: 249, teamName: "ACME Team", rate, authDaysLeft: 29 });
53
+ const tiers = buildAccountTiers(full, false);
54
+ assert.equal(tiers[0], "ACME Team ◆ 249 hc · 996/1k/h · 10k/10k/d · ⟳ 29d");
55
+ assert.ok(tiers.includes("ACME Team ◆ 249 hc"));
56
+ assert.ok(tiers.includes("◆ 249 hc · 996/1k/h"));
57
+ assert.ok(tiers.includes("249 hc"));
58
+ // tiers must be strictly non-increasing in width
59
+ for (let i = 1; i < tiers.length; i++) {
60
+ assert.ok(termVisWidth(tiers[i]) <= termVisWidth(tiers[i - 1]), `tier ${i} wider than previous`);
61
+ }
62
+ // dedupe of adjacent identical tiers when atoms are missing
63
+ assert.deepEqual(buildAccountTiers(acc({ teamName: "ACME" }), false), ["ACME"]);
64
+ const balOnly = buildAccountTiers(acc({ balance: 12 }), true);
65
+ assert.equal(balOnly[0], "⚠ ◆ 12 hc");
66
+ assert.ok(balOnly.includes("12 hc"));
67
+
68
+ // ── width math ──
69
+ assert.equal(termVisWidth("abc"), 3);
70
+ assert.equal(termVisWidth(""), 0);
71
+ assert.equal(termVisWidth(fakeTheme.fg("dim", "abc")), 3, "ANSI is zero-width");
72
+ assert.equal(termVisWidth("◆"), 1, "◆ counts as narrow in this terminal");
73
+ assert.equal(termVisWidth("⚡"), 2);
74
+ assert.equal(truncateAnsi("hello world", 8), "hello w…");
75
+ assert.equal(termVisWidth(truncateAnsi(fakeTheme.fg("x", "hello world"), 8)), 8);
76
+ assert.equal(truncateAnsi("abc", 5), "abc");
77
+ assert.equal(truncateAnsi("abc", 0), "");
78
+
79
+ // ── widget render ──
80
+ const left = buildSessionLine({ requests: 7, spendHc: 1.24 })!;
81
+ const widget = new StatusLineWidget(fakeTheme, left, tiers, false);
82
+
83
+ // Wide: full tier, left-right justified, exactly width columns
84
+ const wide = widget.render(80);
85
+ assert.equal(wide.length, 1);
86
+ assert.equal(termVisWidth(wide[0]), 80);
87
+ assert.ok(stripAnsi(wide[0]).startsWith("⚡ 1.24 hc"));
88
+ assert.ok(stripAnsi(wide[0]).endsWith("⟳ 29d"));
89
+
90
+ // Medium: drops to a compressed tier, still exactly width
91
+ const med = widget.render(52);
92
+ assert.equal(termVisWidth(med[0]), 52);
93
+ assert.ok(!stripAnsi(med[0]).includes("⟳"), "compressed tiers drop auth first");
94
+
95
+ // Narrow: no tier fits → left only, padded
96
+ const narrow = widget.render(termVisWidth(left) + 3);
97
+ assert.equal(termVisWidth(narrow[0]), termVisWidth(left) + 3);
98
+ assert.ok(stripAnsi(narrow[0]).startsWith("⚡"));
99
+ assert.ok(!stripAnsi(narrow[0]).includes("◆"));
100
+
101
+ // Narrower than left itself: truncation never overflows (crash guard)
102
+ const tiny = widget.render(10);
103
+ assert.equal(termVisWidth(tiny[0]), 10);
104
+
105
+ // Left empty (session gated) → right-aligned account line
106
+ const rightOnly = new StatusLineWidget(fakeTheme, "", tiers, false);
107
+ const ro = rightOnly.render(70);
108
+ assert.equal(termVisWidth(ro[0]), 70);
109
+ assert.ok(stripAnsi(ro[0]).endsWith("⟳ 29d"));
110
+
111
+ // No data at all
112
+ assert.deepEqual(new StatusLineWidget(fakeTheme, "", [], false).render(40), [fakeTheme.fg("dim", "") + " ".repeat(40)]);
113
+
114
+ // Warning color wired through
115
+ const warn = new StatusLineWidget(fakeTheme, "", buildAccountTiers(acc({ balance: 10 }), true), true);
116
+ const warnLine = warn.render(60)[0];
117
+ assert.ok(stripAnsi(warnLine).includes("⚠ ◆ 10 hc"));
118
+ assert.ok(warnLine.includes("warning") || true); // fakeTheme ignores color names
119
+ const markTheme = { fg: (c: string, t: string) => `<${c}>${t}</>` };
120
+ assert.ok(new StatusLineWidget(markTheme, "", buildAccountTiers(acc({ balance: 10 }), true), true).render(60)[0].includes("<warning>"));
121
+ assert.ok(new StatusLineWidget(markTheme, "", buildAccountTiers(acc({ balance: 10 }), true), false).render(60)[0].includes("<dim>"));
122
+
123
+ // ── config coercion ──
124
+ assert.deepEqual(coerceStatusConfig(undefined), {
125
+ session: "widget",
126
+ account: "widget",
127
+ hideOnOtherProvider: true,
128
+ lowBalanceHc: 25,
129
+ });
130
+ assert.deepEqual(coerceStatusConfig({ session: "bogus", lowBalanceHc: -3 }), {
131
+ session: "widget",
132
+ account: "widget",
133
+ hideOnOtherProvider: true,
134
+ lowBalanceHc: 25,
135
+ });
136
+ assert.deepEqual(coerceStatusConfig({ session: "statusbar", account: "off", hideOnOtherProvider: false, lowBalanceHc: null }), {
137
+ session: "statusbar",
138
+ account: "off",
139
+ hideOnOtherProvider: false,
140
+ lowBalanceHc: null,
141
+ });
142
+ assert.equal(coerceStatusConfig({ lowBalanceHc: 42 }).lowBalanceHc, 42);
143
+ assert.equal(coerceStatusConfig({ lowBalanceHc: false }).lowBalanceHc, null);
144
+ assert.deepEqual(coerceStatusConfig(null).session, "widget");
145
+
146
+ console.log("status.smoke: all assertions passed");
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "esModuleInterop": true,
8
+ "strict": false,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["./*.ts"],
14
+ "exclude": ["node_modules", "tests"]
15
+ }