@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 CHANGED
@@ -1,199 +1,106 @@
1
1
  # motel
2
2
 
3
- A local OpenTelemetry ingest + TUI viewer for development, backed by SQLite.
4
- Point your app's OTLP/HTTP exporters at the local motel server and browse
5
- traces, spans, and logs from a terminal or the built-in web UI.
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
- ## Install
8
+ ## For agents: install the motel-debug skill
8
9
 
9
- ```bash
10
- bun add -g @kitlangton/motel
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
- # Globally, available in every project
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
- The skill lives at [`skills/motel-debug/`](skills/motel-debug/) in this repo.
39
-
40
- ## Quick start
23
+ See the full skill at [`skills/motel-debug/SKILL.md`](skills/motel-debug/SKILL.md).
41
24
 
42
- ```bash
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
- If you just want the server without the TUI (for example, to run it in the
52
- background and browse the web UI):
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
- bun run server
56
- # then in another terminal
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
- ## Local ports
80
-
81
- This repo uses one local Bun server with SQLite storage. No Docker is required.
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
- ```bash
91
- http://127.0.0.1:27686/v1/traces
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
- Agents and scripts can query traces and logs from the local API:
42
+ Don't have Bun?
96
43
 
97
44
  ```bash
98
- http://127.0.0.1:27686/api/services
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
- ## TUI keys
115
-
116
- - `?`: show or hide keyboard shortcut help
117
- - `j` / `k` or `up` / `down`: move selection
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
- - correlated logs may include secrets, tokens, or PII if your app logs them
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
- The easiest flow is:
55
+ ## How your app connects
151
56
 
152
- 1. Run `bun run dev` here. That starts the local server if needed and then launches the TUI.
153
- 2. In `motel`, press `c`.
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
- ## For Agents
159
-
160
- An agent does not need to talk to the TUI.
60
+ ```
61
+ http://127.0.0.1:27686/v1/traces
62
+ http://127.0.0.1:27686/v1/logs
63
+ ```
161
64
 
162
- List and search endpoints now return a `meta` object with `limit`, `lookback`, `returned`, `truncated`, and `nextCursor` so callers can page safely instead of assuming they received all results.
65
+ Motel keeps everything in a local SQLite database at
66
+ `.motel-data/telemetry.sqlite`. No Docker, no cloud account.
163
67
 
164
- Use one of these:
68
+ ## How agents connect
165
69
 
166
- 1. motel HTTP API directly
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
- 2. The local CLI wrapper in this repo
175
-
176
- ```bash
177
- bun run cli services
178
- bun run cli traces my-service 20
179
- bun run cli span <span-id>
180
- bun run cli trace-spans <trace-id>
181
- bun run cli search-spans my-service Format.file parent=Tool.write attr.sessionID=sess_123
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
- Recommended shape going forward:
86
+ ## TUI keys
195
87
 
196
- 1. Keep motel as the single ingest point for apps.
197
- 2. Keep SQLite as the local source of truth.
198
- 3. Keep `motel` as the interactive viewer.
199
- 4. Keep the CLI and HTTP API as the agent/script interfaces.
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.0",
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": ["web"],
26
+ "workspaces": [
27
+ "web"
28
+ ],
27
29
  "engines": {
28
30
  "bun": ">=1.1.0"
29
31
  },
@@ -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, includeLogs = true) => {
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 = includeLogs ? 5 : 0
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, includeLogs = true) => {
181
- const { labelMaxWidth, durationWidth, logWidth, barWidth } = getWaterfallLayout(contentWidth, traceDurationMs, includeLogs)
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, includeLogs)
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, afterCells } = renderWaterfallBar(span, trace, barWidth, barColor, laneColor)
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
- {logCell.length > 0 ? <span fg={logFg}>{logCell}</span> : null}
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
- const includeLogs = filteredSpans.some((span) => (spanLogCounts.get(span.spanId) ?? 0) > 0)
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 `op #hash`.
64
- const rowMatch = leftHalf.match(/^\s+\u00b7\s+(\S+)\s+#/)
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
- })