@cephalization/math 0.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,228 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import type { WebSocketMessage } from "./server";
3
+ import type { BufferLogEntry, BufferAgentOutput } from "./buffer";
4
+
5
+ /**
6
+ * Tests for the React app module.
7
+ * Since the app is primarily UI code that mounts to the DOM,
8
+ * we test that the module exports correctly and the types align.
9
+ */
10
+
11
+ describe("app.tsx", () => {
12
+ test("module exists and can be imported", async () => {
13
+ // The app module should exist at the expected path
14
+ const file = Bun.file("./src/ui/app.tsx");
15
+ const exists = await file.exists();
16
+ expect(exists).toBe(true);
17
+ });
18
+
19
+ test("imports react and react-dom", async () => {
20
+ const content = await Bun.file("./src/ui/app.tsx").text();
21
+
22
+ expect(content).toContain('from "react"');
23
+ expect(content).toContain('from "react-dom/client"');
24
+ });
25
+
26
+ test("uses createRoot for React 18", async () => {
27
+ const content = await Bun.file("./src/ui/app.tsx").text();
28
+
29
+ expect(content).toContain("createRoot");
30
+ expect(content).toContain('document.getElementById("root")');
31
+ });
32
+
33
+ test("connects to WebSocket at /ws", async () => {
34
+ const content = await Bun.file("./src/ui/app.tsx").text();
35
+
36
+ expect(content).toContain("WebSocket");
37
+ expect(content).toContain("/ws");
38
+ });
39
+
40
+ test("renders Loop Status section", async () => {
41
+ const content = await Bun.file("./src/ui/app.tsx").text();
42
+
43
+ expect(content).toContain("Loop Status");
44
+ });
45
+
46
+ test("renders Agent Output section", async () => {
47
+ const content = await Bun.file("./src/ui/app.tsx").text();
48
+
49
+ expect(content).toContain("Agent Output");
50
+ });
51
+
52
+ test("handles WebSocket message types", async () => {
53
+ const content = await Bun.file("./src/ui/app.tsx").text();
54
+
55
+ // Should handle all message types from server
56
+ expect(content).toContain('"connected"');
57
+ expect(content).toContain('"history"');
58
+ expect(content).toContain('"log"');
59
+ expect(content).toContain('"output"');
60
+ });
61
+
62
+ test("stores logs and output in state", async () => {
63
+ const content = await Bun.file("./src/ui/app.tsx").text();
64
+
65
+ // Should use useState for logs and output
66
+ expect(content).toContain("useState<BufferLogEntry[]>");
67
+ expect(content).toContain("useState<BufferAgentOutput[]>");
68
+ });
69
+
70
+ test("shows connection status", async () => {
71
+ const content = await Bun.file("./src/ui/app.tsx").text();
72
+
73
+ expect(content).toContain("Connected");
74
+ expect(content).toContain("Disconnected");
75
+ });
76
+ });
77
+
78
+ describe("stream-display features", () => {
79
+ test("defines category colors for all log types", async () => {
80
+ const content = await Bun.file("./src/ui/app.tsx").text();
81
+
82
+ // Should define colors for all categories
83
+ expect(content).toContain("categoryColors");
84
+ expect(content).toContain("info:");
85
+ expect(content).toContain("success:");
86
+ expect(content).toContain("warning:");
87
+ expect(content).toContain("error:");
88
+ });
89
+
90
+ test("uses correct terminal colors for categories", async () => {
91
+ const content = await Bun.file("./src/ui/app.tsx").text();
92
+
93
+ // Blue for info
94
+ expect(content).toMatch(/info.*#60a5fa|#60a5fa.*info/i);
95
+ // Green for success
96
+ expect(content).toMatch(/success.*#4ade80|#4ade80.*success/i);
97
+ // Yellow for warning
98
+ expect(content).toMatch(/warning.*#facc15|#facc15.*warning/i);
99
+ // Red for error
100
+ expect(content).toMatch(/error.*#f87171|#f87171.*error/i);
101
+ });
102
+
103
+ test("has refs for auto-scroll containers", async () => {
104
+ const content = await Bun.file("./src/ui/app.tsx").text();
105
+
106
+ // Should use refs for both containers
107
+ expect(content).toContain("logContainerRef");
108
+ expect(content).toContain("outputContainerRef");
109
+ expect(content).toContain("useRef<HTMLDivElement>");
110
+ });
111
+
112
+ test("implements auto-scroll on content changes", async () => {
113
+ const content = await Bun.file("./src/ui/app.tsx").text();
114
+
115
+ // Should scroll to bottom on logs and output changes
116
+ expect(content).toContain("scrollTop");
117
+ expect(content).toContain("scrollHeight");
118
+
119
+ // Should have useEffect hooks with appropriate dependencies
120
+ // The pattern: useEffect that uses logContainerRef and depends on [logs]
121
+ expect(content).toContain("logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight");
122
+ expect(content).toContain("}, [logs])");
123
+
124
+ // The pattern: useEffect that uses outputContainerRef and depends on [output]
125
+ expect(content).toContain("outputContainerRef.current.scrollTop = outputContainerRef.current.scrollHeight");
126
+ expect(content).toContain("}, [output])");
127
+ });
128
+
129
+ test("renders preformatted monospace agent output", async () => {
130
+ const content = await Bun.file("./src/ui/app.tsx").text();
131
+
132
+ // Should use <pre> tag for agent output
133
+ expect(content).toContain("<pre");
134
+ // Should have monospace font for output
135
+ expect(content).toContain("fontFamily: \"monospace\"");
136
+ // Should preserve whitespace
137
+ expect(content).toContain("pre-wrap");
138
+ });
139
+
140
+ test("has visual connection status indicator", async () => {
141
+ const content = await Bun.file("./src/ui/app.tsx").text();
142
+
143
+ // Should have a status dot element
144
+ expect(content).toContain("statusDot");
145
+ // Should use different colors based on connection
146
+ expect(content).toContain("backgroundColor:");
147
+ // Should have a container for status
148
+ expect(content).toContain("statusContainer");
149
+ });
150
+
151
+ test("applies category color to timestamp and category label", async () => {
152
+ const content = await Bun.file("./src/ui/app.tsx").text();
153
+
154
+ // Should apply color to timestamp
155
+ expect(content).toContain("getCategoryColor(log.category)");
156
+ // Should be used in style objects
157
+ expect(content).toMatch(/color:\s*getCategoryColor/);
158
+ });
159
+
160
+ test("imports LogCategory type", async () => {
161
+ const content = await Bun.file("./src/ui/app.tsx").text();
162
+
163
+ // Should import LogCategory from agent
164
+ expect(content).toContain('import type { LogCategory } from "../agent"');
165
+ });
166
+ });
167
+
168
+ describe("WebSocketMessage type compatibility", () => {
169
+ test("history message has correct structure", () => {
170
+ const logs: BufferLogEntry[] = [
171
+ { timestamp: new Date(), category: "info", message: "test" },
172
+ ];
173
+ const output: BufferAgentOutput[] = [
174
+ { timestamp: new Date(), text: "output" },
175
+ ];
176
+
177
+ const message: WebSocketMessage = {
178
+ type: "history",
179
+ logs,
180
+ output,
181
+ };
182
+
183
+ expect(message.type).toBe("history");
184
+ expect(message.logs).toHaveLength(1);
185
+ expect(message.output).toHaveLength(1);
186
+ });
187
+
188
+ test("log message has correct structure", () => {
189
+ const entry: BufferLogEntry = {
190
+ timestamp: new Date(),
191
+ category: "error",
192
+ message: "test error",
193
+ };
194
+
195
+ const message: WebSocketMessage = {
196
+ type: "log",
197
+ entry,
198
+ };
199
+
200
+ expect(message.type).toBe("log");
201
+ expect(message.entry.category).toBe("error");
202
+ });
203
+
204
+ test("output message has correct structure", () => {
205
+ const entry: BufferAgentOutput = {
206
+ timestamp: new Date(),
207
+ text: "agent text",
208
+ };
209
+
210
+ const message: WebSocketMessage = {
211
+ type: "output",
212
+ entry,
213
+ };
214
+
215
+ expect(message.type).toBe("output");
216
+ expect(message.entry.text).toBe("agent text");
217
+ });
218
+
219
+ test("connected message has correct structure", () => {
220
+ const message: WebSocketMessage = {
221
+ type: "connected",
222
+ id: "test-uuid",
223
+ };
224
+
225
+ expect(message.type).toBe("connected");
226
+ expect(message.id).toBe("test-uuid");
227
+ });
228
+ });
@@ -0,0 +1,222 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import {
3
+ OutputBuffer,
4
+ createOutputBuffer,
5
+ type BufferLogEntry,
6
+ type BufferAgentOutput,
7
+ } from "./buffer";
8
+
9
+ describe("OutputBuffer", () => {
10
+ describe("log entries", () => {
11
+ test("appendLog adds entry with timestamp", () => {
12
+ const buffer = createOutputBuffer();
13
+ const before = new Date();
14
+
15
+ const entry = buffer.appendLog("info", "Test message");
16
+
17
+ const after = new Date();
18
+ expect(entry.category).toBe("info");
19
+ expect(entry.message).toBe("Test message");
20
+ expect(entry.timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime());
21
+ expect(entry.timestamp.getTime()).toBeLessThanOrEqual(after.getTime());
22
+ });
23
+
24
+ test("getLogs returns all appended logs", () => {
25
+ const buffer = createOutputBuffer();
26
+
27
+ buffer.appendLog("info", "Message 1");
28
+ buffer.appendLog("success", "Message 2");
29
+ buffer.appendLog("warning", "Message 3");
30
+ buffer.appendLog("error", "Message 4");
31
+
32
+ const logs = buffer.getLogs();
33
+
34
+ expect(logs).toHaveLength(4);
35
+ expect(logs[0]!.category).toBe("info");
36
+ expect(logs[0]!.message).toBe("Message 1");
37
+ expect(logs[1]!.category).toBe("success");
38
+ expect(logs[1]!.message).toBe("Message 2");
39
+ expect(logs[2]!.category).toBe("warning");
40
+ expect(logs[2]!.message).toBe("Message 3");
41
+ expect(logs[3]!.category).toBe("error");
42
+ expect(logs[3]!.message).toBe("Message 4");
43
+ });
44
+
45
+ test("getLogs returns a copy, not the internal array", () => {
46
+ const buffer = createOutputBuffer();
47
+ buffer.appendLog("info", "Test");
48
+
49
+ const logs1 = buffer.getLogs();
50
+ const logs2 = buffer.getLogs();
51
+
52
+ expect(logs1).not.toBe(logs2);
53
+ expect(logs1).toEqual(logs2);
54
+ });
55
+ });
56
+
57
+ describe("agent output", () => {
58
+ test("appendOutput adds entry with timestamp", () => {
59
+ const buffer = createOutputBuffer();
60
+ const before = new Date();
61
+
62
+ const output = buffer.appendOutput("Test output");
63
+
64
+ const after = new Date();
65
+ expect(output.text).toBe("Test output");
66
+ expect(output.timestamp.getTime()).toBeGreaterThanOrEqual(before.getTime());
67
+ expect(output.timestamp.getTime()).toBeLessThanOrEqual(after.getTime());
68
+ });
69
+
70
+ test("getOutput returns all appended output", () => {
71
+ const buffer = createOutputBuffer();
72
+
73
+ buffer.appendOutput("Output 1\n");
74
+ buffer.appendOutput("Output 2\n");
75
+ buffer.appendOutput("Output 3\n");
76
+
77
+ const output = buffer.getOutput();
78
+
79
+ expect(output).toHaveLength(3);
80
+ expect(output[0]!.text).toBe("Output 1\n");
81
+ expect(output[1]!.text).toBe("Output 2\n");
82
+ expect(output[2]!.text).toBe("Output 3\n");
83
+ });
84
+
85
+ test("getOutput returns a copy, not the internal array", () => {
86
+ const buffer = createOutputBuffer();
87
+ buffer.appendOutput("Test");
88
+
89
+ const output1 = buffer.getOutput();
90
+ const output2 = buffer.getOutput();
91
+
92
+ expect(output1).not.toBe(output2);
93
+ expect(output1).toEqual(output2);
94
+ });
95
+ });
96
+
97
+ describe("subscriptions", () => {
98
+ test("subscribeLogs notifies on new log entries", () => {
99
+ const buffer = createOutputBuffer();
100
+ const received: BufferLogEntry[] = [];
101
+
102
+ buffer.subscribeLogs((entry) => received.push(entry));
103
+ buffer.appendLog("info", "Test 1");
104
+ buffer.appendLog("warning", "Test 2");
105
+
106
+ expect(received).toHaveLength(2);
107
+ expect(received[0]!.message).toBe("Test 1");
108
+ expect(received[1]!.message).toBe("Test 2");
109
+ });
110
+
111
+ test("subscribeOutput notifies on new output", () => {
112
+ const buffer = createOutputBuffer();
113
+ const received: BufferAgentOutput[] = [];
114
+
115
+ buffer.subscribeOutput((output) => received.push(output));
116
+ buffer.appendOutput("Output 1");
117
+ buffer.appendOutput("Output 2");
118
+
119
+ expect(received).toHaveLength(2);
120
+ expect(received[0]!.text).toBe("Output 1");
121
+ expect(received[1]!.text).toBe("Output 2");
122
+ });
123
+
124
+ test("unsubscribe from logs stops notifications", () => {
125
+ const buffer = createOutputBuffer();
126
+ const received: BufferLogEntry[] = [];
127
+
128
+ const unsubscribe = buffer.subscribeLogs((entry) => received.push(entry));
129
+ buffer.appendLog("info", "Before");
130
+
131
+ unsubscribe();
132
+ buffer.appendLog("info", "After");
133
+
134
+ expect(received).toHaveLength(1);
135
+ expect(received[0]!.message).toBe("Before");
136
+ });
137
+
138
+ test("unsubscribe from output stops notifications", () => {
139
+ const buffer = createOutputBuffer();
140
+ const received: BufferAgentOutput[] = [];
141
+
142
+ const unsubscribe = buffer.subscribeOutput((output) => received.push(output));
143
+ buffer.appendOutput("Before");
144
+
145
+ unsubscribe();
146
+ buffer.appendOutput("After");
147
+
148
+ expect(received).toHaveLength(1);
149
+ expect(received[0]!.text).toBe("Before");
150
+ });
151
+
152
+ test("multiple log subscribers receive notifications", () => {
153
+ const buffer = createOutputBuffer();
154
+ const received1: string[] = [];
155
+ const received2: string[] = [];
156
+
157
+ buffer.subscribeLogs((entry) => received1.push(entry.message));
158
+ buffer.subscribeLogs((entry) => received2.push(entry.message));
159
+ buffer.appendLog("info", "Test");
160
+
161
+ expect(received1).toEqual(["Test"]);
162
+ expect(received2).toEqual(["Test"]);
163
+ });
164
+
165
+ test("multiple output subscribers receive notifications", () => {
166
+ const buffer = createOutputBuffer();
167
+ const received1: string[] = [];
168
+ const received2: string[] = [];
169
+
170
+ buffer.subscribeOutput((output) => received1.push(output.text));
171
+ buffer.subscribeOutput((output) => received2.push(output.text));
172
+ buffer.appendOutput("Test");
173
+
174
+ expect(received1).toEqual(["Test"]);
175
+ expect(received2).toEqual(["Test"]);
176
+ });
177
+ });
178
+
179
+ describe("clear", () => {
180
+ test("clear removes all logs and output", () => {
181
+ const buffer = createOutputBuffer();
182
+
183
+ buffer.appendLog("info", "Log 1");
184
+ buffer.appendLog("success", "Log 2");
185
+ buffer.appendOutput("Output 1");
186
+ buffer.appendOutput("Output 2");
187
+
188
+ expect(buffer.getLogs()).toHaveLength(2);
189
+ expect(buffer.getOutput()).toHaveLength(2);
190
+
191
+ buffer.clear();
192
+
193
+ expect(buffer.getLogs()).toHaveLength(0);
194
+ expect(buffer.getOutput()).toHaveLength(0);
195
+ });
196
+
197
+ test("subscriptions still work after clear", () => {
198
+ const buffer = createOutputBuffer();
199
+ const receivedLogs: string[] = [];
200
+ const receivedOutput: string[] = [];
201
+
202
+ buffer.subscribeLogs((entry) => receivedLogs.push(entry.message));
203
+ buffer.subscribeOutput((output) => receivedOutput.push(output.text));
204
+
205
+ buffer.appendLog("info", "Before");
206
+ buffer.appendOutput("Before");
207
+ buffer.clear();
208
+ buffer.appendLog("info", "After");
209
+ buffer.appendOutput("After");
210
+
211
+ expect(receivedLogs).toEqual(["Before", "After"]);
212
+ expect(receivedOutput).toEqual(["Before", "After"]);
213
+ });
214
+ });
215
+ });
216
+
217
+ describe("createOutputBuffer", () => {
218
+ test("creates a new OutputBuffer instance", () => {
219
+ const buffer = createOutputBuffer();
220
+ expect(buffer).toBeInstanceOf(OutputBuffer);
221
+ });
222
+ });
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Shared output buffer module for storing loop logs and agent output.
3
+ * Provides append, get history, and subscribe functionality.
4
+ */
5
+
6
+ import type { LogCategory } from "../agent";
7
+
8
+ /**
9
+ * A log entry in the buffer with timestamp and category.
10
+ */
11
+ export interface BufferLogEntry {
12
+ timestamp: Date;
13
+ category: LogCategory;
14
+ message: string;
15
+ }
16
+
17
+ /**
18
+ * An agent output entry in the buffer.
19
+ */
20
+ export interface BufferAgentOutput {
21
+ timestamp: Date;
22
+ text: string;
23
+ }
24
+
25
+ /**
26
+ * Callback for log entry subscriptions.
27
+ */
28
+ export type LogSubscriber = (entry: BufferLogEntry) => void;
29
+
30
+ /**
31
+ * Callback for agent output subscriptions.
32
+ */
33
+ export type OutputSubscriber = (output: BufferAgentOutput) => void;
34
+
35
+ /**
36
+ * Output buffer that stores loop logs and agent output separately.
37
+ */
38
+ export class OutputBuffer {
39
+ private logs: BufferLogEntry[] = [];
40
+ private agentOutput: BufferAgentOutput[] = [];
41
+ private logSubscribers: Set<LogSubscriber> = new Set();
42
+ private outputSubscribers: Set<OutputSubscriber> = new Set();
43
+
44
+ /**
45
+ * Append a log entry to the buffer.
46
+ */
47
+ appendLog(category: LogCategory, message: string): BufferLogEntry {
48
+ const entry: BufferLogEntry = {
49
+ timestamp: new Date(),
50
+ category,
51
+ message,
52
+ };
53
+ this.logs.push(entry);
54
+
55
+ // Notify subscribers
56
+ for (const subscriber of this.logSubscribers) {
57
+ subscriber(entry);
58
+ }
59
+
60
+ return entry;
61
+ }
62
+
63
+ /**
64
+ * Append agent output to the buffer.
65
+ */
66
+ appendOutput(text: string): BufferAgentOutput {
67
+ const output: BufferAgentOutput = {
68
+ timestamp: new Date(),
69
+ text,
70
+ };
71
+ this.agentOutput.push(output);
72
+
73
+ // Notify subscribers
74
+ for (const subscriber of this.outputSubscribers) {
75
+ subscriber(output);
76
+ }
77
+
78
+ return output;
79
+ }
80
+
81
+ /**
82
+ * Get all log entries.
83
+ */
84
+ getLogs(): BufferLogEntry[] {
85
+ return [...this.logs];
86
+ }
87
+
88
+ /**
89
+ * Get all agent output entries.
90
+ */
91
+ getOutput(): BufferAgentOutput[] {
92
+ return [...this.agentOutput];
93
+ }
94
+
95
+ /**
96
+ * Subscribe to new log entries.
97
+ * Returns an unsubscribe function.
98
+ */
99
+ subscribeLogs(callback: LogSubscriber): () => void {
100
+ this.logSubscribers.add(callback);
101
+ return () => {
102
+ this.logSubscribers.delete(callback);
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Subscribe to new agent output.
108
+ * Returns an unsubscribe function.
109
+ */
110
+ subscribeOutput(callback: OutputSubscriber): () => void {
111
+ this.outputSubscribers.add(callback);
112
+ return () => {
113
+ this.outputSubscribers.delete(callback);
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Clear all logs and output.
119
+ */
120
+ clear(): void {
121
+ this.logs = [];
122
+ this.agentOutput = [];
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Create a new output buffer instance.
128
+ */
129
+ export function createOutputBuffer(): OutputBuffer {
130
+ return new OutputBuffer();
131
+ }