Skip to content
Draft
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ src/
│ ├── datastream/ # Datastream commands (create, list, view, update)
│ ├── ingest-token/ # Ingest token commands (create, list, view, update)
│ ├── metric/ # Metric commands (list, view)
│ ├── monitor/ # Monitor V2 commands (list, get, preview-query, preview, alarms)
│ ├── skill/ # AI agent skill commands (list, view)
│ ├── tag-key/ # Tag key commands (list)
│ ├── tag-value/ # Tag value commands (list)
Expand All @@ -57,6 +58,7 @@ src/
│ ├── datastream/ # Datastream queries/mutations
│ ├── ingest-token/ # Ingest token queries/mutations
│ ├── metric/ # Metric queries
│ ├── monitor/ # Monitor V2 queries
│ ├── workspace/ # Workspace queries
│ ├── gql-request.ts # GraphQL client/executor
│ └── gql-codegen.config.ts # Codegen configuration
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ To update installed skills after edits in this repo, run `npx skills update`.
| `observe datastream view` | View a datastream by ID |
| `observe datastream update` | Update a datastream |
| `observe datastream-token check-status` | Poll a datastream token until ingest data arrives |
| `observe monitor list` | List Monitor V2 resources |
| `observe monitor get` | Get a Monitor V2 by ID (includes its definition) |
| `observe monitor preview-query` | Compile a monitor input into its OPAL pipeline |
| `observe monitor preview` | Preview whether a monitor would fire against recent data |
| `observe monitor alarms` | Search Monitor V2 alarms |
| `observe cli install` | Configure shell integration (PATH, completions) |
| `observe cli uninstall` | Remove shell integration |
| `observe cli upgrade` | Upgrade to the latest version |
Expand Down
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { datastreamRoutes } from "./commands/datastream/index.js";
import { helpCommand } from "./commands/help.js";
import { ingestTokenRoutes } from "./commands/ingest-token/index.js";
import { metricRoutes } from "./commands/metric/index.js";
import { monitorRoutes } from "./commands/monitor/index.js";
import { queryCommand } from "./commands/query.js";
import { skillRoutes } from "./commands/skill/index.js";
import { tagKeyRoutes } from "./commands/tag-key/index.js";
Expand All @@ -36,6 +37,7 @@ export const routes = buildRouteMap({
"data-connection": dataConnectionRoutes,
datastream: datastreamRoutes,
"datastream-token": datastreamTokenRoutes,
monitor: monitorRoutes,
cli: cliRoutes,
},
defaultCommand: "help",
Expand Down
162 changes: 162 additions & 0 deletions src/commands/monitor/alarms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
mock,
test,
} from "bun:test";
import type { LocalContext } from "../../context";
import { createWriter } from "../../lib/writer";

const loadConfigFn = mock(() => ({
customerId: "test-customer",
token: "test-token",
domain: "observeinc.com",
}));

const searchAlarmsFn = mock((_config: unknown, _variables: unknown) =>
Promise.resolve([
{
id: "alarm-1",
level: "Critical",
isActive: true,
start: "2024-01-01T00:00:00Z",
end: null,
detectedStart: "2024-01-01T00:00:00Z",
detectedEnd: null,
monitor: { id: "mon-1", name: "CPU high" },
},
{
id: "alarm-2",
level: "Warning",
isActive: false,
start: "2024-01-01T00:00:00Z",
end: "2024-01-01T01:00:00Z",
detectedStart: "2024-01-01T00:00:00Z",
detectedEnd: "2024-01-01T01:00:00Z",
monitor: { id: "mon-2", name: "Disk" },
},
]),
);

const fixedNow = () => new Date("2024-01-02T00:00:00Z");

const deps = {
loadConfig: loadConfigFn,
searchMonitorAlarms: searchAlarmsFn,
now: fixedNow,
} as unknown as Parameters<(typeof import("./alarms"))["alarms"]>[1];

let previousNoColor: string | undefined;
let previousForceColor: string | undefined;

beforeAll(() => {
previousNoColor = process.env.NO_COLOR;
previousForceColor = process.env.FORCE_COLOR;
process.env.NO_COLOR = "1";
process.env.FORCE_COLOR = "0";
});

afterAll(() => {
if (previousNoColor === undefined) delete process.env.NO_COLOR;
else process.env.NO_COLOR = previousNoColor;
if (previousForceColor === undefined) delete process.env.FORCE_COLOR;
else process.env.FORCE_COLOR = previousForceColor;
});

function createMockContext() {
const stdout: string[] = [];
const stderr: string[] = [];
let exitCode: number | undefined;
const processMock = {
stdout: {
write: (msg: string) => {
stdout.push(msg);
return true;
},
},
stderr: {
write: (msg: string) => {
stderr.push(msg);
return true;
},
},
exit: (code?: number) => {
exitCode = code ?? 0;
throw new Error("process.exit");
},
};
const context = {
process: processMock,
writer: createWriter({ process: processMock }),
} as unknown as LocalContext;
return { context, stdout, stderr, getExitCode: () => exitCode };
}

describe("monitor alarms", () => {
let alarms: (typeof import("./alarms"))["alarms"];

beforeAll(async () => {
alarms = (await import("./alarms")).alarms;
});

beforeEach(() => {
loadConfigFn.mockClear();
searchAlarmsFn.mockClear();
});

test("projects rows with derived monitorId/status/startTime/endTime (JSON)", async () => {
const { context, stdout } = createMockContext();
await alarms.call(context, { json: true, since: 86_400_000 }, deps);

const output = JSON.parse(stdout.join(""));
expect(output).toHaveLength(2);
expect(output[0]).toEqual({
id: "alarm-1",
monitorId: "mon-1",
level: "Critical",
status: "Active",
startTime: "2024-01-01T00:00:00Z",
endTime: null,
});
expect(output[1].status).toBe("Ended");
});

test("computes minTime from since and forwards monitorId + level", async () => {
const { context } = createMockContext();
await alarms.call(
context,
{ json: true, since: 86_400_000, monitorId: "mon-9", level: "Critical" },
deps,
);
const [, variables] = searchAlarmsFn.mock.calls[0]!;
expect(variables).toMatchObject({
monitorIds: ["mon-9"],
levels: ["Critical"],
minTime: "2024-01-01T00:00:00.000Z",
});
});

test("warns when no alarms (table mode)", async () => {
searchAlarmsFn.mockImplementationOnce(() => Promise.resolve([]));
const { context, stdout } = createMockContext();
await alarms.call(context, { since: 86_400_000 }, deps);
expect(stdout.join("")).toContain("No alarms found");
});

test("exits 1 on error", async () => {
searchAlarmsFn.mockImplementationOnce(() => {
throw new Error("boom");
});
const { context, getExitCode } = createMockContext();
try {
await alarms.call(context, { since: 86_400_000 }, deps);
throw new Error("expected exit");
} catch (e) {
expect((e as Error).message).toBe("process.exit");
}
expect(getExitCode()).toBe(1);
});
});
Loading