Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions packages/cli/src/commands/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Command } from "commander";
import { SqliteStore } from "@obyflow/core";
import { registerErrorsCommand } from "./errors.js";

function buildProgram(): Command {
const program = new Command();
program.exitOverride();
registerErrorsCommand(program);
return program;
}

function seedEvents(dbPath: string): void {
const store = new SqliteStore(dbPath);
const now = Date.now();
store.insert({
id: "evt-1",
type: "log",
trace_id: "trace-1",
request_id: null,
service: "checkout-service",
host: null,
container: null,
deployment_id: null,
timestamp: new Date(now).toISOString(),
duration_ms: null,
attributes: {},
severity: "info",
});
store.insert({
id: "evt-2",
type: "error",
trace_id: "trace-2",
request_id: null,
service: "checkout-service",
host: null,
container: null,
deployment_id: null,
timestamp: new Date(now + 1000).toISOString(),
duration_ms: null,
attributes: {},
severity: "error",
});
store.insert({
id: "evt-3",
type: "error",
trace_id: "trace-3",
request_id: null,
service: "billing-service",
host: null,
container: null,
deployment_id: null,
timestamp: new Date(now + 2000).toISOString(),
duration_ms: null,
attributes: {},
severity: "critical",
});
store.close();
}

describe("obyflow errors", () => {
let dir: string;

afterEach(() => {
if (dir) rmSync(dir, { recursive: true, force: true });
});

it("lists only error and critical severity events", async () => {
dir = mkdtempSync(join(tmpdir(), "obyflow-cli-errors-"));
const dbPath = join(dir, "test.db");
seedEvents(dbPath);

const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const program = buildProgram();

await program.parseAsync(["errors", "--db", dbPath], { from: "user" });

const output = logSpy.mock.calls.map((call) => call.join(" ")).join("\n");
expect(output).toContain("checkout-service");
expect(output).toContain("billing-service");
expect(output).toContain("2 error(s)");
logSpy.mockRestore();
});

it("filters by service", async () => {
dir = mkdtempSync(join(tmpdir(), "obyflow-cli-errors-"));
const dbPath = join(dir, "test.db");
seedEvents(dbPath);

const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const program = buildProgram();

await program.parseAsync(
["errors", "--db", dbPath, "--service", "billing-service"],
{ from: "user" },
);

const output = logSpy.mock.calls.map((call) => call.join(" ")).join("\n");
expect(output).toContain("billing-service");
expect(output).not.toContain("checkout-service");
logSpy.mockRestore();
});

it("renders detail cards when --detail is passed", async () => {
dir = mkdtempSync(join(tmpdir(), "obyflow-cli-errors-"));
const dbPath = join(dir, "test.db");
seedEvents(dbPath);

const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const program = buildProgram();

await program.parseAsync(["errors", "--db", dbPath, "--detail"], { from: "user" });

const output = logSpy.mock.calls.map((call) => call.join(" ")).join("\n");
expect(output).toContain("trace-2");
expect(output).toContain("trace-3");
logSpy.mockRestore();
});
});
111 changes: 111 additions & 0 deletions packages/cli/src/commands/prune.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Command } from "commander";
import { SqliteStore } from "@obyflow/core";
import { registerPruneCommand } from "./prune.js";

function buildProgram(): Command {
const program = new Command();
program.exitOverride();
registerPruneCommand(program);
return program;
}

function seedEvents(dbPath: string, timestamps: string[]): void {
const store = new SqliteStore(dbPath);
timestamps.forEach((timestamp, i) => {
store.insert({
id: `evt-${i}`,
type: "log",
trace_id: `trace-${i}`,
request_id: null,
service: "checkout-service",
host: null,
container: null,
deployment_id: null,
timestamp,
duration_ms: null,
attributes: {},
severity: "info",
});
});
store.close();
}

describe("obyflow prune", () => {
let dir: string;

afterEach(() => {
if (dir) rmSync(dir, { recursive: true, force: true });
});

it("rejects an invalid --older-than value", async () => {
dir = mkdtempSync(join(tmpdir(), "obyflow-cli-prune-"));
const dbPath = join(dir, "test.db");
seedEvents(dbPath, [new Date().toISOString()]);

const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const program = buildProgram();

await program.parseAsync(["prune", "--db", dbPath, "--older-than", "nonsense"], {
from: "user",
});

const output = logSpy.mock.calls.map((call) => call.join(" ")).join("\n");
expect(output).toContain('Invalid --older-than value "nonsense"');
logSpy.mockRestore();

const store = new SqliteStore(dbPath);
expect(store.countAll()).toBe(1);
store.close();
});

it("does not delete anything without --yes", async () => {
dir = mkdtempSync(join(tmpdir(), "obyflow-cli-prune-"));
const dbPath = join(dir, "test.db");
const old = new Date(Date.now() - 40 * 86_400_000).toISOString();
seedEvents(dbPath, [old]);

const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const program = buildProgram();

await program.parseAsync(["prune", "--db", dbPath, "--older-than", "30d"], {
from: "user",
});

const output = logSpy.mock.calls.map((call) => call.join(" ")).join("\n");
expect(output).toContain("This will permanently delete events older than");
expect(output).toContain("Re-run with --yes to confirm.");
logSpy.mockRestore();

const store = new SqliteStore(dbPath);
expect(store.countAll()).toBe(1);
store.close();
});

it("deletes only events older than the threshold when --yes is passed", async () => {
dir = mkdtempSync(join(tmpdir(), "obyflow-cli-prune-"));
const dbPath = join(dir, "test.db");
const old = new Date(Date.now() - 40 * 86_400_000).toISOString();
const recent = new Date().toISOString();
seedEvents(dbPath, [old, recent]);

const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const program = buildProgram();

await program.parseAsync(
["prune", "--db", dbPath, "--older-than", "30d", "--yes"],
{ from: "user" },
);

const output = logSpy.mock.calls.map((call) => call.join(" ")).join("\n");
expect(output).toContain("Deleted 1 event(s) older than");
logSpy.mockRestore();

const store = new SqliteStore(dbPath);
expect(store.countAll()).toBe(1);
store.close();
});
});
Loading