Files
oh-my-pi/packages/coding-agent/test/debug/log-viewer.test.ts
T
julia 3b7c2336c4 feat(coding-agent/debug): Added inline filtering to debug log viewer
- Added case-insensitive substring filtering with inline query entry and visible count line in the debug log viewer.
- Preserved cursor and selection when filters change and show a no matches placeholder with adjusted scroll bounds.
- Updated filter sanitization and frame layout, removing Kitty-specific help text and aligning status counters.
- Expanded tests to cover filtering, selection anchoring, copy payloads, and session boundary warnings.
2026-02-15 09:53:17 +01:00

127 lines
4.5 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { buildLogCopyPayload, DebugLogViewerModel, SESSION_BOUNDARY_WARNING } from "../../src/debug/log-viewer";
describe("DebugLogViewerModel", () => {
it("inserts session boundary warning between older and current-session logs", () => {
const processStartMs = Date.parse("2026-02-14T12:00:00.000Z");
const logs = [
'{"timestamp":"2026-02-14T11:59:00.000Z","level":"info","message":"old"}',
'{"timestamp":"2026-02-14T11:59:30.000Z","level":"info","message":"old-2"}',
'{"timestamp":"2026-02-14T12:00:05.000Z","level":"info","message":"current"}',
].join("\n");
const model = new DebugLogViewerModel(logs, processStartMs);
const rowKinds = model.rows.map(row =>
row.kind === "warning" ? SESSION_BOUNDARY_WARNING : `log:${row.logIndex}`,
);
expect(rowKinds).toEqual(["log:0", "log:1", SESSION_BOUNDARY_WARNING, "log:2"]);
});
it("filters logs with case-insensitive substring matching", () => {
const logs = ["Alpha", "beta", "Gamma", "BETTER"].join("\n");
const model = new DebugLogViewerModel(logs, Date.now());
model.setFilterQuery("be");
const rowKinds = model.rows.map(row =>
row.kind === "warning" ? SESSION_BOUNDARY_WARNING : `log:${row.logIndex}`,
);
expect(rowKinds).toEqual(["log:1", "log:3"]);
expect(model.visibleLogCount).toBe(2);
});
it("clamps cursor when filtered list shrinks", () => {
const logs = ["alpha", "beta", "gamma"].join("\n");
const model = new DebugLogViewerModel(logs, Date.now());
model.moveCursor(2, false);
expect(model.cursorLogIndex).toBe(2);
model.setFilterQuery("alpha");
expect(model.cursorLogIndex).toBe(0);
expect(model.visibleLogCount).toBe(1);
});
it("resets selection anchor when filtered view drops the anchor log", () => {
const logs = ["alpha", "beta", "gamma", "delta"].join("\n");
const model = new DebugLogViewerModel(logs, Date.now());
model.moveCursor(2, true);
expect(model.getSelectedLogIndices()).toEqual([0, 1, 2]);
model.setFilterQuery("beta");
expect(model.getSelectedLogIndices()).toEqual([1]);
});
it("shows session boundary warning only when older and newer logs are visible", () => {
const processStartMs = Date.parse("2026-02-14T12:00:00.000Z");
const logs = [
'{"timestamp":"2026-02-14T11:59:00.000Z","level":"info","message":"old"}',
'{"timestamp":"2026-02-14T12:00:05.000Z","level":"info","message":"current"}',
'{"timestamp":"2026-02-14T12:00:10.000Z","level":"info","message":"current-2"}',
].join("\n");
const model = new DebugLogViewerModel(logs, processStartMs);
model.setFilterQuery("old");
expect(model.rows.map(row => row.kind)).toEqual(["log"]);
model.setFilterQuery("current-2");
expect(model.rows.map(row => row.kind)).toEqual(["log"]);
model.setFilterQuery("current");
const rowKinds = model.rows.map(row =>
row.kind === "warning" ? SESSION_BOUNDARY_WARNING : `log:${row.logIndex}`,
);
expect(rowKinds).toEqual(["log:1", "log:2"]);
model.setFilterQuery("");
const fullRowKinds = model.rows.map(row =>
row.kind === "warning" ? SESSION_BOUNDARY_WARNING : `log:${row.logIndex}`,
);
expect(fullRowKinds).toEqual(["log:0", SESSION_BOUNDARY_WARNING, "log:1", "log:2"]);
});
it("copies only selected visible entries", () => {
const logs = ["alpha", "bar", "baz"].join("\n");
const model = new DebugLogViewerModel(logs, Date.now());
model.setFilterQuery("ba");
model.moveCursor(1, true);
const payload = buildLogCopyPayload(model.getSelectedRawLines());
expect(payload).toBe("bar\nbaz");
});
it("supports shift-range selection and reset on plain movement", () => {
const logs = ["a", "b", "c", "d"].join("\n");
const model = new DebugLogViewerModel(logs, Date.now());
model.moveCursor(1, true);
model.moveCursor(1, true);
expect(model.getSelectedLogIndices()).toEqual([0, 1, 2]);
model.moveCursor(1, false);
expect(model.getSelectedLogIndices()).toEqual([3]);
});
it("expands and collapses all selected rows", () => {
const logs = ["a", "b", "c"].join("\n");
const model = new DebugLogViewerModel(logs, Date.now());
model.moveCursor(1, true);
model.expandSelected();
expect(model.isExpanded(0)).toBe(true);
expect(model.isExpanded(1)).toBe(true);
model.collapseSelected();
expect(model.isExpanded(0)).toBe(false);
expect(model.isExpanded(1)).toBe(false);
});
});
describe("buildLogCopyPayload", () => {
it("joins selected lines and strips control/ansi sequences", () => {
const payload = buildLogCopyPayload(["plain", "\u001b[31mred\u001b[0m", "ok\u0007", ""]);
expect(payload).toBe("plain\nred\nok");
});
});