@kitlangton/motel 0.1.0 → 0.1.1
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/README.md +70 -163
- package/package.json +4 -2
- package/src/ui/Waterfall.tsx +9 -12
- package/src/ui/traceSortNav.repro.test.ts +3 -2
- package/src/ui/traceDetailsWidth.repro.test.ts +0 -115
package/README.md
CHANGED
|
@@ -1,199 +1,106 @@
|
|
|
1
1
|
# motel
|
|
2
2
|
|
|
3
|
-
A local OpenTelemetry ingest + TUI viewer for development, backed by
|
|
4
|
-
Point your app's OTLP/HTTP exporters at the local motel server
|
|
5
|
-
|
|
3
|
+
A local OpenTelemetry ingest + TUI viewer for development, backed by
|
|
4
|
+
SQLite. Point your app's OTLP/HTTP exporters at the local motel server
|
|
5
|
+
and debug with real runtime evidence — from a terminal, the built-in web
|
|
6
|
+
UI, or directly from an AI coding agent.
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
+
## For agents: install the motel-debug skill
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
motel
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
(or `bunx @kitlangton/motel` for a one-off run without installing.)
|
|
15
|
-
|
|
16
|
-
## Requirements
|
|
17
|
-
|
|
18
|
-
- [Bun](https://bun.sh/) — v1.1 or newer
|
|
19
|
-
|
|
20
|
-
## Install the motel-debug skill
|
|
21
|
-
|
|
22
|
-
`motel` ships a companion skill that teaches agents (Claude Code, OpenCode,
|
|
23
|
-
Cursor, Codex, and 40+ others) how to debug with runtime evidence by
|
|
24
|
-
querying motel's local OTLP store. Install it with a one-liner via
|
|
25
|
-
[`npx skills`](https://github.com/vercel-labs/skills):
|
|
10
|
+
`motel` ships a companion skill that teaches Claude Code, OpenCode,
|
|
11
|
+
Cursor, Codex, and 40+ other agents how to debug with runtime evidence
|
|
12
|
+
by querying motel's local OTLP store. Install it once and any future
|
|
13
|
+
agent session in the project will know how to use it.
|
|
26
14
|
|
|
27
15
|
```bash
|
|
28
16
|
# Project-local (adds to .claude/skills, .agents/skills, etc.)
|
|
29
17
|
npx skills add kitlangton/motel --skill motel-debug
|
|
30
18
|
|
|
31
|
-
#
|
|
19
|
+
# Or globally, for every project
|
|
32
20
|
npx skills add kitlangton/motel --skill motel-debug -g
|
|
33
|
-
|
|
34
|
-
# Target a specific agent only
|
|
35
|
-
npx skills add kitlangton/motel --skill motel-debug -a claude-code
|
|
36
21
|
```
|
|
37
22
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## Quick start
|
|
23
|
+
See the full skill at [`skills/motel-debug/SKILL.md`](skills/motel-debug/SKILL.md).
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
bun install
|
|
44
|
-
bun run dev
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
`bun run dev` starts the local OTLP ingest server (on `http://127.0.0.1:27686`)
|
|
48
|
-
and launches the TUI. Press `?` once inside for the keyboard cheat sheet, or
|
|
49
|
-
`c` to copy paste-ready setup instructions for another Effect/OTEL app.
|
|
25
|
+
## For humans: install and run the TUI
|
|
50
26
|
|
|
51
|
-
|
|
52
|
-
|
|
27
|
+
motel is distributed on npm as `@kitlangton/motel`. The binary is a Bun
|
|
28
|
+
script, so Bun must be on your `PATH` at runtime:
|
|
53
29
|
|
|
54
30
|
```bash
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
bun run web:dev
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Commands
|
|
61
|
-
|
|
62
|
-
- `bun install`
|
|
63
|
-
- `bun run server`
|
|
64
|
-
- `bun run dev`
|
|
65
|
-
- `bun run test`
|
|
66
|
-
- `bun run cli services`
|
|
67
|
-
- `bun run cli traces <service>`
|
|
68
|
-
- `bun run cli span <span-id>`
|
|
69
|
-
- `bun run cli search-traces <service> [operation]`
|
|
70
|
-
- `bun run cli trace-stats <groupBy> <agg> [service]`
|
|
71
|
-
- `bun run cli logs <service>`
|
|
72
|
-
- `bun run cli search-logs <service> [body]`
|
|
73
|
-
- `bun run cli log-stats <groupBy> [service]`
|
|
74
|
-
- `bun run cli trace-logs <trace-id>`
|
|
75
|
-
- `bun run cli facets <traces|logs> <field>`
|
|
76
|
-
- `bun run instructions`
|
|
77
|
-
- `bun run typecheck`
|
|
31
|
+
# one-off (no install)
|
|
32
|
+
bunx @kitlangton/motel
|
|
78
33
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- motel local API / UI base: `http://127.0.0.1:27686`
|
|
84
|
-
- OTLP HTTP traces: `http://127.0.0.1:27686/v1/traces`
|
|
85
|
-
- OTLP HTTP logs: `http://127.0.0.1:27686/v1/logs`
|
|
86
|
-
- health: `http://127.0.0.1:27686/api/health`
|
|
87
|
-
|
|
88
|
-
Other local apps can send telemetry to:
|
|
34
|
+
# or install globally
|
|
35
|
+
bun add -g @kitlangton/motel
|
|
36
|
+
motel
|
|
89
37
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
http://127.0.0.1:27686/v1/logs
|
|
38
|
+
# npm also works (Bun still required to run it)
|
|
39
|
+
npm install -g @kitlangton/motel
|
|
93
40
|
```
|
|
94
41
|
|
|
95
|
-
|
|
42
|
+
Don't have Bun?
|
|
96
43
|
|
|
97
44
|
```bash
|
|
98
|
-
|
|
99
|
-
http://127.0.0.1:27686/api/traces?service=<service>&limit=20&lookback=1h
|
|
100
|
-
http://127.0.0.1:27686/api/traces/search?service=<service>&operation=proxy&status=error&attr.sessionID=<session-id>
|
|
101
|
-
http://127.0.0.1:27686/api/traces/stats?groupBy=operation&agg=p95_duration&service=<service>
|
|
102
|
-
http://127.0.0.1:27686/api/spans/<span-id>
|
|
103
|
-
http://127.0.0.1:27686/api/spans/<span-id>/logs
|
|
104
|
-
http://127.0.0.1:27686/api/spans/search?service=<service>&operation=Format.file&parentOperation=Tool.write&attr.sessionID=<session-id>
|
|
105
|
-
http://127.0.0.1:27686/api/traces/<trace-id>/spans
|
|
106
|
-
http://127.0.0.1:27686/api/logs?service=<service>&body=proxy_request
|
|
107
|
-
http://127.0.0.1:27686/api/logs?service=<service>&attr.service.name=<service>
|
|
108
|
-
http://127.0.0.1:27686/api/logs/stats?groupBy=severity&agg=count&service=<service>
|
|
109
|
-
http://127.0.0.1:27686/api/facets?type=logs&field=severity
|
|
110
|
-
http://127.0.0.1:27686/openapi.json
|
|
111
|
-
http://127.0.0.1:27686/docs
|
|
45
|
+
curl -fsSL https://bun.sh/install | bash
|
|
112
46
|
```
|
|
113
47
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
- `ctrl-n` / `ctrl-p`: switch traces even while in trace details
|
|
119
|
-
- `gg` or `home`: jump to the first trace or first span
|
|
120
|
-
- `G` or `end`: jump to the last trace or last span
|
|
121
|
-
- `ctrl-u` / `pageup`: move up by one page
|
|
122
|
-
- `ctrl-d` / `pagedown`: move down by one page
|
|
123
|
-
- `l`: toggle service logs mode
|
|
124
|
-
- `[` / `]`: switch service
|
|
125
|
-
- `enter`: enter span navigation or open selected span detail
|
|
126
|
-
- `esc`: leave span detail or span navigation
|
|
127
|
-
- `r`: refresh
|
|
128
|
-
- `c`: copy a paste-ready Effect setup prompt for another app
|
|
129
|
-
- `o`: open selected trace in browser
|
|
130
|
-
- `q`: quit
|
|
131
|
-
|
|
132
|
-
## How It Works
|
|
133
|
-
|
|
134
|
-
`motel` now has one local service process:
|
|
135
|
-
|
|
136
|
-
- the local Bun server receives OTLP traces and logs on `http://127.0.0.1:27686`
|
|
137
|
-
- it stores telemetry in SQLite at `.motel-data/telemetry.sqlite`
|
|
138
|
-
- it exposes query endpoints on the same base URL
|
|
139
|
-
|
|
140
|
-
So yes: another service has to point its OTEL exporters at this local motel instance.
|
|
141
|
-
|
|
142
|
-
## Privacy Note
|
|
143
|
-
|
|
144
|
-
`motel` is a local observability tool, and it can store sensitive telemetry content if the upstream app emits it.
|
|
48
|
+
`motel` starts the local OTLP ingest server on
|
|
49
|
+
`http://127.0.0.1:27686` and launches the TUI. Press `?` once inside for
|
|
50
|
+
the keyboard cheat sheet, or `c` to copy paste-ready setup instructions
|
|
51
|
+
for any Effect/OTEL app you want to trace.
|
|
145
52
|
|
|
146
|
-
|
|
147
|
-
- AI call data may include prompt previews, response previews, full prompt content, response text, tool metadata, and provider metadata
|
|
148
|
-
- treat the local SQLite store as sensitive development data when using motel against real workloads
|
|
53
|
+
Requirements: [Bun](https://bun.sh/) v1.1 or newer.
|
|
149
54
|
|
|
150
|
-
|
|
55
|
+
## How your app connects
|
|
151
56
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
3. Paste the copied instructions into an agent working in the other service.
|
|
155
|
-
4. Have that service export OTEL traces to `http://127.0.0.1:27686/v1/traces` and OTEL logs to `http://127.0.0.1:27686/v1/logs`.
|
|
156
|
-
5. Refresh `motel`, switch to that service with `[` / `]`, and use `l` or `enter` to inspect logs under a trace or span.
|
|
57
|
+
Once motel is running, point your app's OTLP/HTTP exporters at these
|
|
58
|
+
local endpoints:
|
|
157
59
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
60
|
+
```
|
|
61
|
+
http://127.0.0.1:27686/v1/traces
|
|
62
|
+
http://127.0.0.1:27686/v1/logs
|
|
63
|
+
```
|
|
161
64
|
|
|
162
|
-
|
|
65
|
+
Motel keeps everything in a local SQLite database at
|
|
66
|
+
`.motel-data/telemetry.sqlite`. No Docker, no cloud account.
|
|
163
67
|
|
|
164
|
-
|
|
68
|
+
## How agents connect
|
|
165
69
|
|
|
166
|
-
|
|
70
|
+
Agents with the `motel-debug` skill installed will automatically use
|
|
71
|
+
motel's HTTP API. The full OpenAPI spec is at
|
|
72
|
+
`http://127.0.0.1:27686/openapi.json` — the key endpoints are:
|
|
167
73
|
|
|
168
|
-
```bash
|
|
169
|
-
curl http://127.0.0.1:27686/api/services
|
|
170
|
-
curl "http://127.0.0.1:27686/api/traces?service=my-service&limit=20&lookback=1h"
|
|
171
|
-
curl http://127.0.0.1:27686/api/traces/<trace-id>
|
|
172
74
|
```
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
bun run cli search-traces my-service proxy attr.sessionID=sess_123
|
|
183
|
-
bun run cli trace-stats operation p95_duration my-service attr.modelID=gpt-5.4
|
|
184
|
-
bun run cli trace <trace-id>
|
|
185
|
-
bun run cli logs my-service
|
|
186
|
-
bun run cli search-logs my-service timeout attr.tool=search
|
|
187
|
-
bun run cli log-stats severity my-service attr.tool=search
|
|
188
|
-
bun run cli trace-logs <trace-id>
|
|
189
|
-
bun run cli span-logs <span-id>
|
|
190
|
-
bun run cli facets logs severity
|
|
191
|
-
bun run instructions
|
|
75
|
+
GET /api/health liveness check
|
|
76
|
+
GET /api/services services reporting telemetry
|
|
77
|
+
GET /api/traces?service=<service> recent traces for a service
|
|
78
|
+
GET /api/traces/<trace-id> full trace tree
|
|
79
|
+
GET /api/spans/<span-id> single span + logs
|
|
80
|
+
GET /api/logs?service=<service> recent logs
|
|
81
|
+
GET /api/traces/search?... structured trace search
|
|
82
|
+
GET /api/logs/search?... structured log search
|
|
83
|
+
GET /api/ai/calls AI SDK call inspector
|
|
192
84
|
```
|
|
193
85
|
|
|
194
|
-
|
|
86
|
+
## TUI keys
|
|
195
87
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
88
|
+
- `?` — keyboard cheat sheet
|
|
89
|
+
- `j` / `k` or `↑` / `↓` — move selection
|
|
90
|
+
- `enter` / `esc` — drill in / back out (trace → waterfall → span detail)
|
|
91
|
+
- `[` / `]` — switch service
|
|
92
|
+
- `tab` — toggle service logs
|
|
93
|
+
- `/` — filter traces
|
|
94
|
+
- `s` — cycle sort (recent → slowest → errors)
|
|
95
|
+
- `t` — cycle theme
|
|
96
|
+
- `c` — copy paste-ready setup instructions for another app
|
|
97
|
+
- `o` — open selected trace in the browser
|
|
98
|
+
- `q` — quit
|
|
99
|
+
|
|
100
|
+
## Privacy note
|
|
101
|
+
|
|
102
|
+
motel is a local development tool, but your app can emit sensitive
|
|
103
|
+
telemetry. Correlated logs may include secrets, tokens, or PII if your
|
|
104
|
+
app logs them; AI call traces may include full prompt content and
|
|
105
|
+
response text. Treat the local SQLite store as sensitive development
|
|
106
|
+
data when pointing motel at real workloads.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitlangton/motel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"effect",
|
|
24
24
|
"opentui"
|
|
25
25
|
],
|
|
26
|
-
"workspaces": [
|
|
26
|
+
"workspaces": [
|
|
27
|
+
"web"
|
|
28
|
+
],
|
|
27
29
|
"engines": {
|
|
28
30
|
"bun": ">=1.1.0"
|
|
29
31
|
},
|
package/src/ui/Waterfall.tsx
CHANGED
|
@@ -169,16 +169,16 @@ const durationColor = (durationMs: number) => {
|
|
|
169
169
|
return colors.muted
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
export const getWaterfallLayout = (contentWidth: number, traceDurationMs: number
|
|
172
|
+
export const getWaterfallLayout = (contentWidth: number, traceDurationMs: number) => {
|
|
173
173
|
const labelMaxWidth = Math.min(Math.floor(contentWidth * 0.4), 32)
|
|
174
174
|
const durationWidth = Math.max(8, formatDuration(traceDurationMs).length + 1)
|
|
175
|
-
const logWidth =
|
|
175
|
+
const logWidth = 5
|
|
176
176
|
const barWidth = Math.max(6, contentWidth - labelMaxWidth - durationWidth - logWidth - 2)
|
|
177
177
|
return { labelMaxWidth, durationWidth, logWidth, barWidth } as const
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
export const getWaterfallColumns = (contentWidth: number, traceDurationMs: number, durationMs: number, logCount: number
|
|
181
|
-
const { labelMaxWidth, durationWidth, logWidth, barWidth } = getWaterfallLayout(contentWidth, traceDurationMs
|
|
180
|
+
export const getWaterfallColumns = (contentWidth: number, traceDurationMs: number, durationMs: number, logCount: number) => {
|
|
181
|
+
const { labelMaxWidth, durationWidth, logWidth, barWidth } = getWaterfallLayout(contentWidth, traceDurationMs)
|
|
182
182
|
const durationCell = formatDuration(Math.max(0, durationMs)).padStart(durationWidth)
|
|
183
183
|
const logText = logCount > 0 ? `${logCount}lg` : ""
|
|
184
184
|
const logCell = logText.padStart(logWidth)
|
|
@@ -219,7 +219,6 @@ const WaterfallRow = memo(({
|
|
|
219
219
|
collapsed,
|
|
220
220
|
hasChildSpans,
|
|
221
221
|
onSelect,
|
|
222
|
-
includeLogs,
|
|
223
222
|
}: {
|
|
224
223
|
span: TraceSpanItem
|
|
225
224
|
logCount: number
|
|
@@ -231,14 +230,13 @@ const WaterfallRow = memo(({
|
|
|
231
230
|
collapsed: boolean
|
|
232
231
|
hasChildSpans: boolean
|
|
233
232
|
onSelect: () => void
|
|
234
|
-
includeLogs: boolean
|
|
235
233
|
}) => {
|
|
236
234
|
const prefix = buildTreePrefix(spans, index)
|
|
237
235
|
// Match the trace list indicator: `!` on error, chevron on collapsible parents, `·` on leaves.
|
|
238
236
|
const indicator = span.status === "error" ? "!" : hasChildSpans ? (collapsed ? "\u25b8" : "\u25be") : "\u00b7"
|
|
239
237
|
const opName = span.isRunning ? `${span.operationName} [${lifecycleLabel(span)}]` : span.operationName
|
|
240
238
|
|
|
241
|
-
const { labelMaxWidth, barWidth, durationCell, logCell } = getWaterfallColumns(contentWidth, trace.durationMs, span.durationMs, logCount
|
|
239
|
+
const { labelMaxWidth, barWidth, durationCell, logCell } = getWaterfallColumns(contentWidth, trace.durationMs, span.durationMs, logCount)
|
|
242
240
|
|
|
243
241
|
const opMaxWidth = Math.max(4, labelMaxWidth - prefix.length - 2)
|
|
244
242
|
const opTruncated = opName.length > opMaxWidth ? `${opName.slice(0, opMaxWidth - 1)}\u2026` : opName
|
|
@@ -248,7 +246,7 @@ const WaterfallRow = memo(({
|
|
|
248
246
|
const isError = span.status === "error"
|
|
249
247
|
const barColor = selected ? (isError ? waterfallColors.barSelectedError : waterfallColors.barSelected) : isError ? waterfallColors.barError : waterfallColors.bar
|
|
250
248
|
const laneColor = selected ? waterfallColors.barLane : waterfallColors.barBg
|
|
251
|
-
const { segments
|
|
249
|
+
const { segments } = renderWaterfallBar(span, trace, barWidth, barColor, laneColor)
|
|
252
250
|
const bg = selected ? colors.selectedBg : undefined
|
|
253
251
|
const treeColor = selected ? colors.separator : colors.treeLine
|
|
254
252
|
const indicatorColor = isError ? colors.error : hasChildSpans ? (selected ? colors.selectedText : colors.muted) : colors.passing
|
|
@@ -268,10 +266,9 @@ const WaterfallRow = memo(({
|
|
|
268
266
|
{segments.map((segment, index) => (
|
|
269
267
|
<span key={`${span.spanId}-bar-${index}`} fg={segment.fg} bg={segment.bg}>{segment.text}</span>
|
|
270
268
|
))}
|
|
271
|
-
{afterCells > 0 ? <span fg={laneColor} bg={laneColor}>{" ".repeat(afterCells)}</span> : null}
|
|
272
269
|
<span> </span>
|
|
273
270
|
<span fg={durationFg}>{durationCell}</span>
|
|
274
|
-
|
|
271
|
+
<span fg={logFg}>{logCell}</span>
|
|
275
272
|
</TextLine>
|
|
276
273
|
</box>
|
|
277
274
|
)
|
|
@@ -349,7 +346,8 @@ export const WaterfallTimeline = ({
|
|
|
349
346
|
onSelectSpan: (index: number) => void
|
|
350
347
|
}) => {
|
|
351
348
|
const selectedSpan = selectedSpanIndex !== null ? filteredSpans[selectedSpanIndex] ?? null : null
|
|
352
|
-
|
|
349
|
+
|
|
350
|
+
const { labelMaxWidth, durationWidth, barWidth } = getWaterfallLayout(contentWidth, trace.durationMs)
|
|
353
351
|
|
|
354
352
|
const spanIndexById = new Map<string, number>()
|
|
355
353
|
for (let i = 0; i < trace.spans.length; i++) {
|
|
@@ -399,7 +397,6 @@ export const WaterfallTimeline = ({
|
|
|
399
397
|
selected={selectedSpanIndex === actualIndex}
|
|
400
398
|
collapsed={collapsedSpanIds.has(span.spanId)}
|
|
401
399
|
hasChildSpans={fullIndex >= 0 && findFirstChildIndex(trace.spans, fullIndex) !== null}
|
|
402
|
-
includeLogs={includeLogs}
|
|
403
400
|
onSelect={() => onSelectSpan(actualIndex)}
|
|
404
401
|
/>
|
|
405
402
|
)
|
|
@@ -60,8 +60,9 @@ const listRows = (snap: string): { readonly rows: readonly string[]; readonly se
|
|
|
60
60
|
const leftHalf = raw.split("\u2502")[0] ?? raw
|
|
61
61
|
const rightHalf = raw.includes("\u2502") ? raw.split("\u2502").slice(1).join("\u2502") : ""
|
|
62
62
|
|
|
63
|
-
// Trace rows: left pane, `·` then
|
|
64
|
-
|
|
63
|
+
// Trace rows: left pane, `·` then the operation name as the first
|
|
64
|
+
// token. (Earlier versions appended `#<hash>`; that's been removed.)
|
|
65
|
+
const rowMatch = leftHalf.match(/^\s+\u00b7\s+(op[A-Z])\b/)
|
|
65
66
|
if (rowMatch) rows.push(rowMatch[1]!)
|
|
66
67
|
|
|
67
68
|
// Selected trace: right pane, line immediately after `TRACE DETAILS`
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* End-to-end reproducer for waterfall underfilling the trace-details pane.
|
|
3
|
-
*
|
|
4
|
-
* Strategy:
|
|
5
|
-
* 1. Seed a deterministic trace into a fresh SQLite database.
|
|
6
|
-
* 2. Launch the motel TUI under tuistory in narrow mode so trace details take
|
|
7
|
-
* the full screen width.
|
|
8
|
-
* 3. Drill into the trace details view.
|
|
9
|
-
* 4. Assert the root waterfall row reaches the right-side duration column
|
|
10
|
-
* instead of stopping several cells early.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { afterAll, beforeAll, describe, expect, it } from "bun:test"
|
|
14
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
15
|
-
import { tmpdir } from "node:os"
|
|
16
|
-
import { join } from "node:path"
|
|
17
|
-
|
|
18
|
-
const TUISTORY_BIN = "tuistory"
|
|
19
|
-
const SESSION = `motel-trace-width-${Date.now()}`
|
|
20
|
-
|
|
21
|
-
const hasTuistory = async () => {
|
|
22
|
-
try {
|
|
23
|
-
const proc = Bun.spawn({ cmd: ["which", TUISTORY_BIN], stdout: "pipe", stderr: "ignore" })
|
|
24
|
-
return (await proc.exited) === 0
|
|
25
|
-
} catch {
|
|
26
|
-
return false
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const tui = async (args: readonly string[]) => {
|
|
31
|
-
const proc = Bun.spawn({ cmd: [TUISTORY_BIN, ...args], stdout: "pipe", stderr: "pipe" })
|
|
32
|
-
const [stdout, stderr] = await Promise.all([
|
|
33
|
-
new Response(proc.stdout).text(),
|
|
34
|
-
new Response(proc.stderr).text(),
|
|
35
|
-
])
|
|
36
|
-
return { code: await proc.exited, stdout, stderr }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const snapshot = async () => (await tui(["snapshot", "--session", SESSION])).stdout
|
|
40
|
-
|
|
41
|
-
const press = async (...keys: string[]) => {
|
|
42
|
-
await tui(["press", "--session", SESSION, ...keys])
|
|
43
|
-
await Bun.sleep(120)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const dividerWidth = (snap: string) =>
|
|
47
|
-
snap.split("\n").find((line) => line.startsWith("─"))?.length ?? 0
|
|
48
|
-
|
|
49
|
-
const rootWaterfallRow = (snap: string) =>
|
|
50
|
-
snap.split("\n").find((line) => line.startsWith(" ▾ root.op") || line.startsWith(" ▸ root.op") || line.startsWith(" · root.op")) ?? null
|
|
51
|
-
|
|
52
|
-
describe("trace details waterfall width (end-to-end TUI)", () => {
|
|
53
|
-
const tempDir = mkdtempSync(join(tmpdir(), "motel-trace-width-"))
|
|
54
|
-
const dbPath = join(tempDir, "telemetry.sqlite")
|
|
55
|
-
const lastServicePath = join(tempDir, "last-service.txt")
|
|
56
|
-
let canRun = false
|
|
57
|
-
|
|
58
|
-
beforeAll(async () => {
|
|
59
|
-
canRun = await hasTuistory()
|
|
60
|
-
if (!canRun) return
|
|
61
|
-
|
|
62
|
-
writeFileSync(lastServicePath, "waterfall-repro")
|
|
63
|
-
|
|
64
|
-
const seed = Bun.spawn({
|
|
65
|
-
cmd: ["bun", "run", "src/ui/waterfallNav.repro.seed.ts"],
|
|
66
|
-
cwd: process.cwd(),
|
|
67
|
-
env: {
|
|
68
|
-
...process.env,
|
|
69
|
-
MOTEL_OTEL_DB_PATH: dbPath,
|
|
70
|
-
MOTEL_OTEL_ENABLED: "false",
|
|
71
|
-
},
|
|
72
|
-
stdout: "pipe",
|
|
73
|
-
stderr: "pipe",
|
|
74
|
-
})
|
|
75
|
-
const seedCode = await seed.exited
|
|
76
|
-
if (seedCode !== 0) {
|
|
77
|
-
const err = await new Response(seed.stderr).text()
|
|
78
|
-
throw new Error(`seed failed: ${err}`)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
await tui(["close", "--session", SESSION])
|
|
82
|
-
const launch = await tui([
|
|
83
|
-
"launch",
|
|
84
|
-
"bun run src/index.tsx",
|
|
85
|
-
"--session", SESSION,
|
|
86
|
-
"--cols", "96",
|
|
87
|
-
"--rows", "40",
|
|
88
|
-
"--cwd", process.cwd(),
|
|
89
|
-
"--env", `MOTEL_OTEL_DB_PATH=${dbPath}`,
|
|
90
|
-
"--env", "MOTEL_OTEL_ENABLED=false",
|
|
91
|
-
"--timeout", "15000",
|
|
92
|
-
])
|
|
93
|
-
if (launch.code !== 0) throw new Error(`launch failed: ${launch.stderr}`)
|
|
94
|
-
await tui(["wait", "root.op", "--session", SESSION, "--timeout", "10000"])
|
|
95
|
-
await tui(["wait-idle", "--session", SESSION, "--timeout", "5000"])
|
|
96
|
-
}, 60_000)
|
|
97
|
-
|
|
98
|
-
afterAll(async () => {
|
|
99
|
-
if (canRun) await tui(["close", "--session", SESSION])
|
|
100
|
-
try { rmSync(tempDir, { recursive: true, force: true }) } catch {}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it("fills the full-width trace details pane in narrow mode", async () => {
|
|
104
|
-
if (!canRun) return
|
|
105
|
-
|
|
106
|
-
await press("return")
|
|
107
|
-
const snap = await snapshot()
|
|
108
|
-
const divider = dividerWidth(snap)
|
|
109
|
-
const row = rootWaterfallRow(snap)
|
|
110
|
-
|
|
111
|
-
expect(divider).toBe(96)
|
|
112
|
-
expect(row).not.toBeNull()
|
|
113
|
-
expect(row!.length).toBeGreaterThanOrEqual(divider - 1)
|
|
114
|
-
}, 60_000)
|
|
115
|
-
})
|