@pi-archimedes/footer 1.6.0 → 1.7.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.
- package/package.json +2 -2
- package/src/cost-accumulator.test.ts +85 -0
- package/src/utils/format.test.ts +138 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/footer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"./config": "./src/config.ts"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@pi-archimedes/core": "1.
|
|
18
|
+
"@pi-archimedes/core": "1.7.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { CostAccumulator } from "./cost-accumulator.js";
|
|
3
|
+
import { getBus, Events } from "@pi-archimedes/core/bus";
|
|
4
|
+
|
|
5
|
+
// ── globalThis cleanup ──────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const BUS_KEY = Symbol.for("archimedes:bus");
|
|
8
|
+
const QUEUE_KEY = Symbol.for("archimedes:busQueue");
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
delete (globalThis as Record<symbol, unknown>)[BUS_KEY];
|
|
12
|
+
delete (globalThis as Record<symbol, unknown>)[QUEUE_KEY];
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// ── CostAccumulator ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
describe("CostAccumulator", () => {
|
|
18
|
+
let accumulator: CostAccumulator;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
accumulator = new CostAccumulator();
|
|
22
|
+
accumulator.subscribe();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("accumulates input tokens from cost events", () => {
|
|
26
|
+
getBus().emit(Events.COST_UPDATE, { inputTokens: 100, outputTokens: 50 });
|
|
27
|
+
expect(accumulator.inputTokens).toBe(100);
|
|
28
|
+
expect(accumulator.outputTokens).toBe(50);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("accumulates cache read/write tokens", () => {
|
|
32
|
+
getBus().emit(Events.COST_UPDATE, {
|
|
33
|
+
cacheReadTokens: 200,
|
|
34
|
+
cacheWriteTokens: 100,
|
|
35
|
+
});
|
|
36
|
+
expect(accumulator.cacheReadTokens).toBe(200);
|
|
37
|
+
expect(accumulator.cacheWriteTokens).toBe(100);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("accumulates cost", () => {
|
|
41
|
+
getBus().emit(Events.COST_UPDATE, { cost: 0.015 });
|
|
42
|
+
expect(accumulator.cost).toBe(0.015);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("multiple cost updates accumulate correctly", () => {
|
|
46
|
+
getBus().emit(Events.COST_UPDATE, { inputTokens: 100, cost: 0.01 });
|
|
47
|
+
getBus().emit(Events.COST_UPDATE, { inputTokens: 200, cost: 0.02 });
|
|
48
|
+
getBus().emit(Events.COST_UPDATE, { inputTokens: 300, cost: 0.03 });
|
|
49
|
+
expect(accumulator.inputTokens).toBe(600);
|
|
50
|
+
expect(accumulator.cost).toBe(0.06);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("missing fields in payload default to 0", () => {
|
|
54
|
+
getBus().emit(Events.COST_UPDATE, {});
|
|
55
|
+
expect(accumulator.inputTokens).toBe(0);
|
|
56
|
+
expect(accumulator.outputTokens).toBe(0);
|
|
57
|
+
expect(accumulator.cacheReadTokens).toBe(0);
|
|
58
|
+
expect(accumulator.cacheWriteTokens).toBe(0);
|
|
59
|
+
expect(accumulator.cost).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("reset zeroes all counters", () => {
|
|
63
|
+
getBus().emit(Events.COST_UPDATE, { inputTokens: 100, cost: 0.01 });
|
|
64
|
+
accumulator.reset();
|
|
65
|
+
expect(accumulator.inputTokens).toBe(0);
|
|
66
|
+
expect(accumulator.outputTokens).toBe(0);
|
|
67
|
+
expect(accumulator.cacheReadTokens).toBe(0);
|
|
68
|
+
expect(accumulator.cacheWriteTokens).toBe(0);
|
|
69
|
+
expect(accumulator.cost).toBe(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("dispose unsubscribes — subsequent events not accumulated", () => {
|
|
73
|
+
getBus().emit(Events.COST_UPDATE, { inputTokens: 100 });
|
|
74
|
+
accumulator.dispose();
|
|
75
|
+
getBus().emit(Events.COST_UPDATE, { inputTokens: 500 });
|
|
76
|
+
expect(accumulator.inputTokens).toBe(100);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("subsequent events not accumulated after dispose", () => {
|
|
80
|
+
getBus().emit(Events.COST_UPDATE, { cost: 0.01 });
|
|
81
|
+
accumulator.dispose();
|
|
82
|
+
getBus().emit(Events.COST_UPDATE, { cost: 0.05 });
|
|
83
|
+
expect(accumulator.cost).toBe(0.01);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
formatTokenCount,
|
|
4
|
+
formatContextBar,
|
|
5
|
+
formatGitStatusIndicators,
|
|
6
|
+
formatThinkingIndicator,
|
|
7
|
+
} from "./format.js";
|
|
8
|
+
import type { ColorFn } from "./icons.js";
|
|
9
|
+
|
|
10
|
+
// ── Mock ColorFn ────────────────────────────────────────────────────────────
|
|
11
|
+
// Passthrough — we test structure, not actual coloring.
|
|
12
|
+
const mockColor: ColorFn = (_token, text) => text;
|
|
13
|
+
|
|
14
|
+
// ── formatTokenCount ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
describe("formatTokenCount", () => {
|
|
17
|
+
it("returns '0' for zero", () => {
|
|
18
|
+
expect(formatTokenCount(0)).toBe("0");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns raw number below 1K", () => {
|
|
22
|
+
expect(formatTokenCount(500)).toBe("500");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns '1.0k' for exactly 1024", () => {
|
|
26
|
+
expect(formatTokenCount(1024)).toBe("1.0k");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns '10k' for 10240", () => {
|
|
30
|
+
expect(formatTokenCount(10240)).toBe("10k");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns '1.0M' for exactly 1M", () => {
|
|
34
|
+
expect(formatTokenCount(1048576)).toBe("1.0M");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns '10M' for 10M", () => {
|
|
38
|
+
expect(formatTokenCount(10485760)).toBe("10M");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ── formatContextBar ────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
describe("formatContextBar", () => {
|
|
45
|
+
it("returns empty string when availableSpace <= 2", () => {
|
|
46
|
+
expect(formatContextBar(mockColor, 50, 0)).toBe("");
|
|
47
|
+
expect(formatContextBar(mockColor, 50, 1)).toBe("");
|
|
48
|
+
expect(formatContextBar(mockColor, 50, 2)).toBe("");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("0% produces bar of empty segments (not empty string)", () => {
|
|
52
|
+
const result = formatContextBar(mockColor, 0, 10);
|
|
53
|
+
expect(result).not.toBe("");
|
|
54
|
+
// Should contain the context window icon and "0%"
|
|
55
|
+
expect(result).toContain("0%");
|
|
56
|
+
// Should contain bar segments (━) — all empty, no filled portion
|
|
57
|
+
expect(result).toContain("━━━");
|
|
58
|
+
// Must NOT contain higher percentage labels (50%, 100%, etc.)
|
|
59
|
+
expect(result).not.toContain("50%");
|
|
60
|
+
expect(result).not.toContain("100%");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("50% produces half-filled bar", () => {
|
|
64
|
+
const result = formatContextBar(mockColor, 50, 10);
|
|
65
|
+
expect(result).not.toBe("");
|
|
66
|
+
expect(result).toContain("50%");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("100% produces full bar", () => {
|
|
70
|
+
const result = formatContextBar(mockColor, 100, 10);
|
|
71
|
+
expect(result).not.toBe("");
|
|
72
|
+
expect(result).toContain("100%");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns bar with icon and percentage", () => {
|
|
76
|
+
const result = formatContextBar(mockColor, 75, 10);
|
|
77
|
+
expect(result).toContain("75%");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ── formatGitStatusIndicators ───────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
describe("formatGitStatusIndicators", () => {
|
|
84
|
+
const emptyStatus = {
|
|
85
|
+
staged: 0,
|
|
86
|
+
unstaged: 0,
|
|
87
|
+
untracked: 0,
|
|
88
|
+
ahead: 0,
|
|
89
|
+
behind: 0,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
it("zero counts returns empty string", () => {
|
|
93
|
+
expect(formatGitStatusIndicators(emptyStatus, mockColor)).toBe("");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("staged > 0 shows indicator", () => {
|
|
97
|
+
const result = formatGitStatusIndicators(
|
|
98
|
+
{ ...emptyStatus, staged: 3 },
|
|
99
|
+
mockColor,
|
|
100
|
+
);
|
|
101
|
+
expect(result).toContain("●3");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("multiple counts shows all indicators", () => {
|
|
105
|
+
const result = formatGitStatusIndicators(
|
|
106
|
+
{ ...emptyStatus, staged: 2, unstaged: 5, untracked: 1 },
|
|
107
|
+
mockColor,
|
|
108
|
+
);
|
|
109
|
+
expect(result).toContain("●2");
|
|
110
|
+
expect(result).toContain("~5");
|
|
111
|
+
expect(result).toContain("U1");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("ahead and behind show indicators", () => {
|
|
115
|
+
const result = formatGitStatusIndicators(
|
|
116
|
+
{ ...emptyStatus, ahead: 3, behind: 1 },
|
|
117
|
+
mockColor,
|
|
118
|
+
);
|
|
119
|
+
expect(result).toContain("↑3");
|
|
120
|
+
expect(result).toContain("↓1");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ── formatThinkingIndicator ─────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
describe("formatThinkingIndicator", () => {
|
|
127
|
+
it("off returns empty string", () => {
|
|
128
|
+
expect(formatThinkingIndicator("off", mockColor)).toBe("");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("other levels return indicator with level name", () => {
|
|
132
|
+
expect(formatThinkingIndicator("minimal", mockColor)).toBe("◐ minimal");
|
|
133
|
+
expect(formatThinkingIndicator("low", mockColor)).toBe("◐ low");
|
|
134
|
+
expect(formatThinkingIndicator("medium", mockColor)).toBe("◐ medium");
|
|
135
|
+
expect(formatThinkingIndicator("high", mockColor)).toBe("◐ high");
|
|
136
|
+
expect(formatThinkingIndicator("xhigh", mockColor)).toBe("◐ xhigh");
|
|
137
|
+
});
|
|
138
|
+
});
|