diff --git a/packages/cli/src/commands/checks/list.ts b/packages/cli/src/commands/checks/list.ts index a93ab762e..bbd2bbbec 100644 --- a/packages/cli/src/commands/checks/list.ts +++ b/packages/cli/src/commands/checks/list.ts @@ -61,8 +61,9 @@ export default class ChecksList extends AuthCommand { const { page, limit } = flags try { - // All filtering is server-side — single paginated API call - const [paginated, statuses] = await Promise.all([ + // Paginated + filtered for the table; full account fetch drives the + // account-wide summary bar, which never reacts to filters. + const [paginated, allChecks, statuses] = await Promise.all([ api.checks.getAllPaginated({ limit, page, @@ -71,6 +72,7 @@ export default class ChecksList extends AuthCommand { search: flags.search, status: flags.status, }), + api.checks.fetchAll(), api.checkStatuses.fetchAll().catch(() => []), ]) @@ -112,7 +114,7 @@ export default class ChecksList extends AuthCommand { // Table output const output: string[] = [] - output.push(formatSummaryBar(statuses, totalChecks)) + output.push(formatSummaryBar(allChecks, statuses)) output.push('') if (checks.length === 0) { diff --git a/packages/cli/src/commands/checks/stats.ts b/packages/cli/src/commands/checks/stats.ts index 4d3713846..bd87c4640 100644 --- a/packages/cli/src/commands/checks/stats.ts +++ b/packages/cli/src/commands/checks/stats.ts @@ -3,6 +3,8 @@ import { AuthCommand } from '../authCommand' import { outputFlag } from '../../helpers/flags' import * as api from '../../rest/api' import { batchQuickRangeValues, type BatchQuickRange } from '../../rest/batch-analytics' +import type { Check } from '../../rest/checks' +import type { CheckStatus } from '../../rest/check-statuses' import type { CheckWithStatus, PaginationInfo } from '../../formatters/checks' import { formatSummaryBar, formatPaginationInfo } from '../../formatters/checks' import type { OutputFormat } from '../../formatters/render' @@ -72,22 +74,25 @@ export default class ChecksStats extends AuthCommand { let checksWithStatus: CheckWithStatus[] let totalChecks: number + let allChecks: Check[] + let allStatuses: CheckStatus[] if (explicitIds.length > 0) { // Fetch all checks (paginate through all pages), filter to requested IDs - const [allChecks, statuses] = await Promise.all([ + ;[allChecks, allStatuses] = await Promise.all([ api.checks.fetchAll(), api.checkStatuses.fetchAll().catch(() => []), ]) - const statusMap = new Map(statuses.map(s => [s.checkId, s])) + const statusMap = new Map(allStatuses.map(s => [s.checkId, s])) const idSet = new Set(explicitIds) checksWithStatus = allChecks .filter(c => idSet.has(c.id)) .map(c => ({ ...c, status: statusMap.get(c.id) })) totalChecks = checksWithStatus.length } else { - // Paginated fetch with filters - const [paginated, statuses] = await Promise.all([ + // Paginated fetch with filters for the table; account-wide fetch drives + // the summary bar, which doesn't react to filters. + const paginatedResult = await Promise.all([ api.checks.getAllPaginated({ limit, page, @@ -95,9 +100,13 @@ export default class ChecksStats extends AuthCommand { checkType: flags.type, search: flags.search, }), + api.checks.fetchAll(), api.checkStatuses.fetchAll().catch(() => []), ]) - const statusMap = new Map(statuses.map(s => [s.checkId, s])) + const paginated = paginatedResult[0] + allChecks = paginatedResult[1] + allStatuses = paginatedResult[2] + const statusMap = new Map(allStatuses.map(s => [s.checkId, s])) checksWithStatus = paginated.checks.map(c => ({ ...c, status: statusMap.get(c.id) })) totalChecks = paginated.total } @@ -161,11 +170,7 @@ export default class ChecksStats extends AuthCommand { // Terminal output const output: string[] = [] - const statuses = checksWithStatus - .map(c => c.status) - .filter((s): s is NonNullable => s != null) - const activeCheckIds = new Set(checksWithStatus.map(c => c.id)) - output.push(formatSummaryBar(statuses, totalChecks, activeCheckIds)) + output.push(formatSummaryBar(allChecks, allStatuses)) output.push('') output.push(formatBatchStats(rows, range, fmt)) output.push('') diff --git a/packages/cli/src/formatters/__tests__/checks.spec.ts b/packages/cli/src/formatters/__tests__/checks.spec.ts index 79d2d7233..07f114082 100644 --- a/packages/cli/src/formatters/__tests__/checks.spec.ts +++ b/packages/cli/src/formatters/__tests__/checks.spec.ts @@ -9,7 +9,9 @@ import { formatCheckDetail, formatResults, formatErrorGroups, + getActivatedStatuses, } from '../checks' +import { scenario } from '../../rest/__tests__/__fixtures__/api' import { passingCheck, failingCheck, @@ -45,30 +47,53 @@ afterEach(() => { vi.useRealTimers() }) -describe('formatSummaryBar', () => { - it('shows counts for passing, degraded, and failing', () => { - const statuses = [passingStatus, failingStatus, degradedStatus] - const result = stripAnsi(formatSummaryBar(statuses, 10)) - expect(result).toContain('1 passing') - expect(result).toContain('1 degraded') - expect(result).toContain('1 failing') - expect(result).toContain('10 total checks') +describe('getActivatedStatuses', () => { + it('excludes statuses whose check is deactivated', () => { + const { checks, statuses } = scenario('mixed') + const deactivatedIds = new Set(checks.filter(c => !c.activated).map(c => c.id)) + const result = getActivatedStatuses(checks, statuses) + // mixed: 16 statuses (13 active + 3 stale on deactivated). Expected: 13. + expect(result).toHaveLength(13) + expect(result.every(s => !deactivatedIds.has(s.checkId))).toBe(true) }) - it('counts hasErrors status as failing', () => { - const statuses = [passingStatus, errorStatus] - const result = stripAnsi(formatSummaryBar(statuses, 2)) - expect(result).toContain('1 passing') - expect(result).toContain('1 failing') + it('returns empty for an all-deactivated account', () => { + const { checks, statuses } = scenario('all-deactivated') + expect(getActivatedStatuses(checks, statuses)).toEqual([]) }) +}) - it('filters by activeCheckIds when provided', () => { - const statuses = [passingStatus, failingStatus, degradedStatus] - const activeIds = new Set(['check-1']) - const result = stripAnsi(formatSummaryBar(statuses, 3, activeIds)) - expect(result).toContain('1 passing') - expect(result).not.toContain('failing') - expect(result).not.toContain('degraded') +describe('formatSummaryBar', () => { + it('renders account-wide 4-bucket bar with "Account wide:" prefix and no total label', () => { + const { checks, statuses } = scenario('mixed') + const result = stripAnsi(formatSummaryBar(checks, statuses)) + // mixed: 8 passing, 2 degraded, 3 failing, 1 no-status (not counted), + // 5 deactivated -> all in inactive bucket. + expect(result).toContain('Account wide:') + expect(result).toContain('8 passing') + expect(result).toContain('2 degraded') + expect(result).toContain('3 failing') + expect(result).toContain('5 inactive') + expect(result).not.toContain('total checks') + }) + + it('does not count stale passing statuses on deactivated checks as passing', () => { + // Regression: 3 of the 5 deactivated checks carry stale hasFailures=false statuses. + // Before the fix they leaked into the passing count. With all-deactivated only the + // inactive bucket should remain. + const { checks, statuses } = scenario('all-deactivated') + expect(stripAnsi(formatSummaryBar(checks, statuses))).toBe('Account wide: ⊘ 5 inactive') + }) + + it('hides zero-count buckets', () => { + const { checks, statuses } = scenario('all-passing') + const result = stripAnsi(formatSummaryBar(checks, statuses)) + expect(result).toContain('6 passing') + expect(result).not.toMatch(/degraded|failing|inactive/) + }) + + it('returns an empty string when the account has no checks', () => { + expect(formatSummaryBar([], [])).toBe('') }) }) diff --git a/packages/cli/src/formatters/checks.ts b/packages/cli/src/formatters/checks.ts index 61865a588..f861e7e9d 100644 --- a/packages/cli/src/formatters/checks.ts +++ b/packages/cli/src/formatters/checks.ts @@ -55,23 +55,26 @@ function boolSymbol (value: boolean, format: OutputFormat): string { // --- Summary bar (terminal only) --- -export function formatSummaryBar (statuses: CheckStatus[], totalChecks: number, activeCheckIds?: Set): string { - const counted = activeCheckIds - ? statuses.filter(s => activeCheckIds.has(s.checkId)) - : statuses - const passing = counted.filter(s => !s.hasFailures && !s.hasErrors && !s.isDegraded).length - const degraded = counted.filter(s => s.isDegraded && !s.hasFailures && !s.hasErrors).length - const failing = counted.filter(s => s.hasFailures || s.hasErrors).length +export function getActivatedStatuses (checks: Check[], statuses: CheckStatus[]): CheckStatus[] { + const activated = new Set(checks.filter(c => c.activated).map(c => c.id)) + return statuses.filter(s => activated.has(s.checkId)) +} + +export function formatSummaryBar (checks: Check[], statuses: CheckStatus[]): string { + const activated = getActivatedStatuses(checks, statuses) + const passing = activated.filter(s => !s.hasFailures && !s.hasErrors && !s.isDegraded).length + const degraded = activated.filter(s => s.isDegraded && !s.hasFailures && !s.hasErrors).length + const failing = activated.filter(s => s.hasFailures || s.hasErrors).length + const inactive = checks.filter(c => !c.activated).length const parts: string[] = [] if (passing > 0) parts.push(chalk.green(`${logSymbols.success} ${passing} passing`)) if (degraded > 0) parts.push(chalk.yellow(`${logSymbols.warning} ${degraded} degraded`)) if (failing > 0) parts.push(chalk.red(`${logSymbols.error} ${failing} failing`)) + if (inactive > 0) parts.push(chalk.dim(`⊘ ${inactive} inactive`)) - const displayTotal = activeCheckIds ? activeCheckIds.size : totalChecks - const total = chalk.dim(`(${displayTotal} total checks)`) - if (parts.length === 0) return total - return parts.join(' ') + ' ' + total + if (parts.length === 0) return '' + return chalk.dim('Account wide:') + ' ' + parts.join(' ') } // --- Type breakdown (terminal only) --- diff --git a/packages/cli/src/rest/__tests__/__fixtures__/api/README.md b/packages/cli/src/rest/__tests__/__fixtures__/api/README.md new file mode 100644 index 000000000..c22fe7384 --- /dev/null +++ b/packages/cli/src/rest/__tests__/__fixtures__/api/README.md @@ -0,0 +1,87 @@ +# API response fixtures + +Sanitized samples of `/v1/checks` and `/v1/check-statuses` responses, drawn +from a real Checkly production account and scrubbed of account-specific data. +Use via the typed helpers in `./index.ts`: + +```ts +import { scenario } from './' + +const { checks, statuses } = scenario('all-deactivated') +``` + +## Contents + +- `checks.json` — 19 checks covering a mix of active/deactivated states across + API, BROWSER, HEARTBEAT, MULTI_STEP, PLAYWRIGHT, AGENTIC check types. +- `check-statuses.json` — 16 statuses (3 of the 19 checks legitimately lack a + status entry — they're either freshly created or deactivated without a run). + +## Scenarios (via `scenario(name)`) + +| Name | Description | +| --------------------- | ------------------------------------------------------ | +| `mixed` | Full 19-check fixture. The canonical dataset. | +| `all-passing` | 6 active checks, all passing. Healthy-account baseline.| +| `all-deactivated` | 5 deactivated checks (3 with stale statuses). | +| `active-no-statuses` | 1 active check with no status entry. New-check case. | +| `empty` | `{ checks: [], statuses: [] }`. | + +For edge cases not represented in real data (pure `hasErrors`, +deactivated+failing), mutate the returned fixture in the test that needs it. + +## Sanitization + +The following were remapped or stripped: + +- `id` → `check-NNN` (sequential) +- `name` → `"Check NNN"` +- `description` → `null` +- `request.url` → `https://example.com/check-NNN` +- `tags`, `privateLocations` → sequential synthetic names +- `groupId` → sequential small integers +- `created_at` / `updated_at` → synthetic dates starting `2025-01-01` +- `longestRun` / `shortestRun` → rounded to nearest 100ms +- `script`, `environmentVariables`, `alertChannelSubscriptions`, + `alertSettings`, `localSetupScript`, `localTearDownScript`, + `setupSnippetId`, `tearDownSnippetId`, `sslCheckDomain` → dropped entirely + +Retained as-is (not sensitive): +- `checkType`, `activated`, `muted` +- `frequency`, `frequencyOffset` +- AWS region codes in `locations` +- Public `runtimeId` version strings +- `heartbeat` config on HEARTBEAT checks +- Status booleans (`hasFailures`, `hasErrors`, `isDegraded`) and + `sslDaysRemaining` + +## Empirical API quirks verified at capture time + +Documented here because they materially affect how the CLI should treat the +data — they're not obvious from the type declarations. + +1. **`/v1/check-statuses` ignores `limit` and `page`** query parameters. It + always returns all statuses that exist for the account, in one response. + The `content-range` header can be misleading if those params are passed. + +2. **`/v1/check-statuses` includes statuses for deactivated checks.** The + webapp filters these out client-side when rendering its summary bar; the + CLI must do the same. This fixture reproduces that: `check-015`, `-016`, + and `-017` are deactivated but appear in `check-statuses.json`. + +3. **Not every check has a status entry.** Brand-new checks that have never + run are absent from the status response (e.g. `check-013` here). + Deactivated checks that never ran are also absent. + +4. **`/v1/checks?status=X` server filters already exclude deactivated checks** + — confirmed empirically. No client-side `activated` check is needed when + applying `status=` to the checks endpoint; only the summary-bar needs + client-side activated filtering, because the status endpoint isn't + filter-aware. + +5. **Per-type shape varies.** HEARTBEAT checks have no `frequency`, + `locations`, `privateLocations`, or `groupId`; instead they carry a + `heartbeat` object with `period`/`periodUnit`/`grace`/`graceUnit`. Other + types carry `frequency` and `locations`. The declared `Check` interface in + `packages/cli/src/rest/checks.ts` does not currently reflect this + divergence — something to be aware of if tests run into missing fields. diff --git a/packages/cli/src/rest/__tests__/__fixtures__/api/check-statuses.json b/packages/cli/src/rest/__tests__/__fixtures__/api/check-statuses.json new file mode 100644 index 000000000..de6caee59 --- /dev/null +++ b/packages/cli/src/rest/__tests__/__fixtures__/api/check-statuses.json @@ -0,0 +1,226 @@ +[ + { + "name": "Check 001", + "checkId": "check-001", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": 6300, + "shortestRun": 0, + "lastRunLocation": "us-east-1", + "lastCheckRunId": "run-check-001", + "sslDaysRemaining": 34, + "created_at": "2025-01-01T00:00:00.000Z", + "updated_at": "2025-01-02T00:00:00.000Z" + }, + { + "name": "Check 002", + "checkId": "check-002", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": 5100, + "shortestRun": 0, + "lastRunLocation": "ap-southeast-1", + "lastCheckRunId": "run-check-002", + "sslDaysRemaining": 123, + "created_at": "2025-01-02T00:00:00.000Z", + "updated_at": "2025-01-03T00:00:00.000Z" + }, + { + "name": "Check 003", + "checkId": "check-003", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": 6600, + "shortestRun": 500, + "lastRunLocation": "eu-central-1", + "lastCheckRunId": "run-check-003", + "sslDaysRemaining": null, + "created_at": "2025-01-03T00:00:00.000Z", + "updated_at": "2025-01-04T00:00:00.000Z" + }, + { + "name": "Check 004", + "checkId": "check-004", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": 52300, + "shortestRun": 1300, + "lastRunLocation": "ap-south-1", + "lastCheckRunId": "run-check-004", + "sslDaysRemaining": null, + "created_at": "2025-01-04T00:00:00.000Z", + "updated_at": "2025-01-05T00:00:00.000Z" + }, + { + "name": "Check 005", + "checkId": "check-005", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": null, + "shortestRun": null, + "lastRunLocation": null, + "lastCheckRunId": "run-check-005", + "sslDaysRemaining": null, + "created_at": "2025-01-05T00:00:00.000Z", + "updated_at": "2025-01-06T00:00:00.000Z" + }, + { + "name": "Check 006", + "checkId": "check-006", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": 218600, + "shortestRun": 1000, + "lastRunLocation": "eu-north-1", + "lastCheckRunId": "run-check-006", + "sslDaysRemaining": null, + "created_at": "2025-01-06T00:00:00.000Z", + "updated_at": "2025-01-07T00:00:00.000Z" + }, + { + "name": "Check 007", + "checkId": "check-007", + "hasFailures": true, + "hasErrors": false, + "isDegraded": false, + "longestRun": 34100, + "shortestRun": 0, + "lastRunLocation": "ap-south-1", + "lastCheckRunId": "run-check-007", + "sslDaysRemaining": 123, + "created_at": "2025-01-07T00:00:00.000Z", + "updated_at": "2025-01-08T00:00:00.000Z" + }, + { + "name": "Check 008", + "checkId": "check-008", + "hasFailures": true, + "hasErrors": false, + "isDegraded": false, + "longestRun": 53500, + "shortestRun": 700, + "lastRunLocation": "ap-south-1", + "lastCheckRunId": "run-check-008", + "sslDaysRemaining": null, + "created_at": "2025-01-08T00:00:00.000Z", + "updated_at": "2025-01-09T00:00:00.000Z" + }, + { + "name": "Check 009", + "checkId": "check-009", + "hasFailures": true, + "hasErrors": true, + "isDegraded": false, + "longestRun": 36500, + "shortestRun": 1800, + "lastRunLocation": "us-east-1", + "lastCheckRunId": "run-check-009", + "sslDaysRemaining": null, + "created_at": "2025-01-09T00:00:00.000Z", + "updated_at": "2025-01-10T00:00:00.000Z" + }, + { + "name": "Check 010", + "checkId": "check-010", + "hasFailures": false, + "hasErrors": false, + "isDegraded": true, + "longestRun": 119400, + "shortestRun": 1800, + "lastRunLocation": "us-east-1", + "lastCheckRunId": "run-check-010", + "sslDaysRemaining": null, + "created_at": "2025-01-10T00:00:00.000Z", + "updated_at": "2025-01-11T00:00:00.000Z" + }, + { + "name": "Check 011", + "checkId": "check-011", + "hasFailures": false, + "hasErrors": false, + "isDegraded": true, + "longestRun": 151200, + "shortestRun": 1800, + "lastRunLocation": "us-east-1", + "lastCheckRunId": "run-check-011", + "sslDaysRemaining": null, + "created_at": "2025-01-11T00:00:00.000Z", + "updated_at": "2025-01-12T00:00:00.000Z" + }, + { + "name": "Check 012", + "checkId": "check-012", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": 470800, + "shortestRun": 33900, + "lastRunLocation": "us-east-1", + "lastCheckRunId": "run-check-012", + "sslDaysRemaining": null, + "created_at": "2025-01-12T00:00:00.000Z", + "updated_at": "2025-01-13T00:00:00.000Z" + }, + { + "name": "Check 014", + "checkId": "check-014", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": 1210000, + "shortestRun": 0, + "lastRunLocation": null, + "lastCheckRunId": "run-check-014", + "sslDaysRemaining": null, + "created_at": "2025-01-14T00:00:00.000Z", + "updated_at": "2025-01-15T00:00:00.000Z" + }, + { + "name": "Check 015", + "checkId": "check-015", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": 1179300, + "shortestRun": 87700, + "lastRunLocation": "us-east-1", + "lastCheckRunId": "run-check-015", + "sslDaysRemaining": null, + "created_at": "2025-01-15T00:00:00.000Z", + "updated_at": "2025-01-16T00:00:00.000Z" + }, + { + "name": "Check 016", + "checkId": "check-016", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": 30800, + "shortestRun": 0, + "lastRunLocation": null, + "lastCheckRunId": "run-check-016", + "sslDaysRemaining": 150, + "created_at": "2025-01-16T00:00:00.000Z", + "updated_at": "2025-01-17T00:00:00.000Z" + }, + { + "name": "Check 017", + "checkId": "check-017", + "hasFailures": false, + "hasErrors": false, + "isDegraded": false, + "longestRun": 2400, + "shortestRun": 1700, + "lastRunLocation": null, + "lastCheckRunId": "run-check-017", + "sslDaysRemaining": null, + "created_at": "2025-01-17T00:00:00.000Z", + "updated_at": "2025-01-18T00:00:00.000Z" + } +] \ No newline at end of file diff --git a/packages/cli/src/rest/__tests__/__fixtures__/api/checks.json b/packages/cli/src/rest/__tests__/__fixtures__/api/checks.json new file mode 100644 index 000000000..dcc2506b3 --- /dev/null +++ b/packages/cli/src/rest/__tests__/__fixtures__/api/checks.json @@ -0,0 +1,490 @@ +[ + { + "id": "check-001", + "name": "Check 001", + "description": null, + "checkType": "API", + "activated": true, + "muted": false, + "frequency": 5, + "frequencyOffset": 5, + "locations": [], + "privateLocations": [], + "tags": [ + "tag-1", + "tag-2" + ], + "groupId": 1, + "groupOrder": null, + "runtimeId": null, + "scriptPath": null, + "request": { + "url": "https://example.com/check-001", + "method": "GET" + }, + "created_at": "2025-01-01T00:00:00.000Z", + "updated_at": "2025-01-02T00:00:00.000Z" + }, + { + "id": "check-002", + "name": "Check 002", + "description": null, + "checkType": "API", + "activated": true, + "muted": false, + "frequency": 60, + "frequencyOffset": 412, + "locations": [], + "privateLocations": [], + "tags": [ + "tag-1", + "tag-2" + ], + "groupId": 2, + "groupOrder": 6, + "runtimeId": null, + "scriptPath": null, + "request": { + "url": "https://example.com/check-002", + "method": "GET" + }, + "created_at": "2025-01-02T00:00:00.000Z", + "updated_at": "2025-01-03T00:00:00.000Z" + }, + { + "id": "check-003", + "name": "Check 003", + "description": null, + "checkType": "BROWSER", + "activated": true, + "muted": false, + "frequency": 15, + "frequencyOffset": 108, + "locations": [], + "privateLocations": [], + "tags": [ + "tag-1", + "tag-2" + ], + "groupId": 3, + "groupOrder": null, + "runtimeId": null, + "scriptPath": "__checks__/check-003.check.ts", + "created_at": "2025-01-03T00:00:00.000Z", + "updated_at": "2025-01-04T00:00:00.000Z" + }, + { + "id": "check-004", + "name": "Check 004", + "description": null, + "checkType": "BROWSER", + "activated": true, + "muted": false, + "frequency": 10, + "frequencyOffset": 90, + "locations": [ + "us-east-1", + "us-west-1", + "eu-central-1", + "ap-south-1" + ], + "privateLocations": [], + "tags": [ + "tag-1" + ], + "groupId": 4, + "groupOrder": 3, + "runtimeId": "2024.02", + "scriptPath": "__checks__/check-004.check.ts", + "created_at": "2025-01-04T00:00:00.000Z", + "updated_at": "2025-01-05T00:00:00.000Z" + }, + { + "id": "check-005", + "name": "Check 005", + "description": null, + "checkType": "HEARTBEAT", + "activated": true, + "muted": false, + "tags": [ + "tag-1", + "tag-2" + ], + "heartbeat": { + "period": 1, + "periodUnit": "days", + "grace": 1, + "graceUnit": "hours" + }, + "created_at": "2025-01-05T00:00:00.000Z", + "updated_at": "2025-01-06T00:00:00.000Z" + }, + { + "id": "check-006", + "name": "Check 006", + "description": null, + "checkType": "MULTI_STEP", + "activated": true, + "muted": false, + "frequency": 5, + "frequencyOffset": 34, + "locations": [], + "privateLocations": [], + "tags": [ + "tag-1", + "tag-2", + "tag-3", + "tag-4" + ], + "groupId": 5, + "groupOrder": null, + "runtimeId": "2025.04", + "scriptPath": "__checks__/check-006.check.ts", + "created_at": "2025-01-06T00:00:00.000Z", + "updated_at": "2025-01-07T00:00:00.000Z" + }, + { + "id": "check-007", + "name": "Check 007", + "description": null, + "checkType": "API", + "activated": true, + "muted": false, + "frequency": 5, + "frequencyOffset": 27, + "locations": [], + "privateLocations": [], + "tags": [ + "tag-1", + "tag-2" + ], + "groupId": 6, + "groupOrder": 25, + "runtimeId": "2023.09", + "scriptPath": null, + "request": { + "url": "https://example.com/check-007", + "method": "GET" + }, + "created_at": "2025-01-07T00:00:00.000Z", + "updated_at": "2025-01-08T00:00:00.000Z" + }, + { + "id": "check-008", + "name": "Check 008", + "description": null, + "checkType": "BROWSER", + "activated": true, + "muted": false, + "frequency": 5, + "frequencyOffset": 42, + "locations": [], + "privateLocations": [], + "tags": [ + "tag-1", + "tag-2" + ], + "groupId": 6, + "groupOrder": 18, + "runtimeId": "2023.02", + "scriptPath": "__checks__/check-008.check.ts", + "created_at": "2025-01-08T00:00:00.000Z", + "updated_at": "2025-01-09T00:00:00.000Z" + }, + { + "id": "check-009", + "name": "Check 009", + "description": null, + "checkType": "PLAYWRIGHT", + "activated": true, + "muted": false, + "frequency": 30, + "locations": [ + "us-east-1", + "eu-west-1", + "ap-southeast-1", + "sa-east-1" + ], + "privateLocations": [], + "tags": [], + "groupId": 7, + "groupOrder": null, + "created_at": "2025-01-09T00:00:00.000Z", + "updated_at": "2025-01-10T00:00:00.000Z" + }, + { + "id": "check-010", + "name": "Check 010", + "description": null, + "checkType": "BROWSER", + "activated": true, + "muted": false, + "frequency": 10, + "frequencyOffset": 83, + "locations": [ + "us-east-1", + "eu-west-1" + ], + "privateLocations": [], + "tags": [ + "tag-1", + "tag-2" + ], + "groupId": 8, + "groupOrder": 5, + "runtimeId": "2025.04", + "scriptPath": "__checks__/check-010.check.ts", + "created_at": "2025-01-10T00:00:00.000Z", + "updated_at": "2025-01-11T00:00:00.000Z" + }, + { + "id": "check-011", + "name": "Check 011", + "description": null, + "checkType": "BROWSER", + "activated": true, + "muted": false, + "frequency": 10, + "frequencyOffset": 76, + "locations": [ + "us-east-1", + "eu-west-1" + ], + "privateLocations": [], + "tags": [ + "tag-1", + "tag-2" + ], + "groupId": 8, + "groupOrder": 9, + "runtimeId": "2025.04", + "scriptPath": "__checks__/check-011.check.ts", + "created_at": "2025-01-11T00:00:00.000Z", + "updated_at": "2025-01-12T00:00:00.000Z" + }, + { + "id": "check-012", + "name": "Check 012", + "description": null, + "checkType": "AGENTIC", + "activated": true, + "muted": true, + "frequency": 30, + "frequencyOffset": 178, + "locations": [ + "us-east-1" + ], + "privateLocations": [], + "tags": [ + "tag-1", + "tag-2", + "tag-3" + ], + "groupId": null, + "groupOrder": null, + "runtimeId": null, + "scriptPath": null, + "created_at": "2025-01-12T00:00:00.000Z", + "updated_at": "2025-01-13T00:00:00.000Z" + }, + { + "id": "check-013", + "name": "Check 013", + "description": null, + "checkType": "HEARTBEAT", + "activated": true, + "muted": false, + "tags": [], + "heartbeat": { + "period": 30, + "periodUnit": "seconds", + "grace": 30, + "graceUnit": "seconds" + }, + "created_at": "2025-01-13T00:00:00.000Z", + "updated_at": "2025-01-14T00:00:00.000Z" + }, + { + "id": "check-014", + "name": "Check 014", + "description": null, + "checkType": "PLAYWRIGHT", + "activated": true, + "muted": false, + "frequency": 120, + "locations": [ + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", + "ca-central-1", + "sa-east-1", + "eu-west-1", + "eu-central-1", + "eu-west-2", + "eu-west-3", + "eu-north-1", + "eu-south-1", + "ap-southeast-1", + "ap-northeast-1", + "ap-east-1", + "ap-southeast-2", + "ap-southeast-3", + "ap-northeast-2", + "ap-northeast-3", + "ap-south-1", + "af-south-1" + ], + "privateLocations": [ + "private-loc-1", + "private-loc-2" + ], + "tags": [ + "tag-1", + "tag-2", + "tag-3" + ], + "groupId": null, + "groupOrder": null, + "created_at": "2025-01-14T00:00:00.000Z", + "updated_at": "2025-01-15T00:00:00.000Z" + }, + { + "id": "check-015", + "name": "Check 015", + "description": null, + "checkType": "AGENTIC", + "activated": false, + "muted": true, + "frequency": 30, + "frequencyOffset": 183, + "locations": [ + "us-east-1" + ], + "privateLocations": [], + "tags": [ + "tag-1", + "tag-2", + "tag-3" + ], + "groupId": null, + "groupOrder": null, + "runtimeId": null, + "scriptPath": null, + "created_at": "2025-01-15T00:00:00.000Z", + "updated_at": "2025-01-16T00:00:00.000Z" + }, + { + "id": "check-016", + "name": "Check 016", + "description": null, + "checkType": "API", + "activated": false, + "muted": false, + "frequency": 0, + "frequencyOffset": 1, + "locations": [ + "us-east-1", + "eu-west-1", + "ap-southeast-1", + "us-east-2", + "us-west-1", + "us-west-2", + "ca-central-1", + "sa-east-1", + "eu-central-1", + "eu-west-2", + "eu-west-3", + "eu-north-1", + "eu-south-1", + "af-south-1", + "ap-south-1", + "ap-northeast-2", + "ap-southeast-3", + "ap-southeast-2", + "ap-east-1", + "ap-northeast-3", + "ap-northeast-1" + ], + "privateLocations": [], + "tags": [ + "tag-1" + ], + "groupId": null, + "groupOrder": null, + "runtimeId": null, + "scriptPath": null, + "request": { + "url": "https://example.com/check-016", + "method": "GET" + }, + "created_at": "2025-01-16T00:00:00.000Z", + "updated_at": "2025-01-17T00:00:00.000Z" + }, + { + "id": "check-017", + "name": "Check 017", + "description": null, + "checkType": "MULTI_STEP", + "activated": false, + "muted": false, + "frequency": 10, + "frequencyOffset": 69, + "locations": [ + "eu-central-1", + "eu-north-1" + ], + "privateLocations": [], + "tags": [], + "groupId": null, + "groupOrder": null, + "runtimeId": null, + "scriptPath": null, + "created_at": "2025-01-17T00:00:00.000Z", + "updated_at": "2025-01-18T00:00:00.000Z" + }, + { + "id": "check-018", + "name": "Check 018", + "description": null, + "checkType": "BROWSER", + "activated": false, + "muted": false, + "frequency": 10, + "frequencyOffset": 26, + "locations": [ + "us-east-1", + "us-west-1" + ], + "privateLocations": [], + "tags": [], + "groupId": null, + "groupOrder": null, + "runtimeId": null, + "scriptPath": null, + "created_at": "2025-01-18T00:00:00.000Z", + "updated_at": "2025-01-19T00:00:00.000Z" + }, + { + "id": "check-019", + "name": "Check 019", + "description": null, + "checkType": "BROWSER", + "activated": false, + "muted": false, + "frequency": 10, + "frequencyOffset": 65, + "locations": [ + "us-east-1", + "us-west-1" + ], + "privateLocations": [], + "tags": [], + "groupId": null, + "groupOrder": null, + "runtimeId": null, + "scriptPath": null, + "created_at": "2025-01-19T00:00:00.000Z", + "updated_at": "2025-01-20T00:00:00.000Z" + } +] \ No newline at end of file diff --git a/packages/cli/src/rest/__tests__/__fixtures__/api/index.ts b/packages/cli/src/rest/__tests__/__fixtures__/api/index.ts new file mode 100644 index 000000000..8a609a070 --- /dev/null +++ b/packages/cli/src/rest/__tests__/__fixtures__/api/index.ts @@ -0,0 +1,60 @@ +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import path from 'node:path' +import type { Check } from '../../../checks' +import type { CheckStatus } from '../../../check-statuses' + +const dir = path.dirname(fileURLToPath(import.meta.url)) + +export const allChecks = JSON.parse( + readFileSync(path.join(dir, 'checks.json'), 'utf8'), +) as Check[] + +export const allStatuses = JSON.parse( + readFileSync(path.join(dir, 'check-statuses.json'), 'utf8'), +) as CheckStatus[] + +export type ScenarioName = + | 'mixed' + | 'all-passing' + | 'all-deactivated' + | 'active-no-statuses' + | 'empty' + +export interface Scenario { + checks: Check[] + statuses: CheckStatus[] +} + +// Slices the canonical mixed fixture into scenario-specific subsets. +// For edge cases not present in real data (pure hasErrors, deactivated+failing), +// mutate the returned objects in the individual test that needs them. +export function scenario (name: ScenarioName): Scenario { + switch (name) { + case 'mixed': + return { checks: allChecks, statuses: allStatuses } + case 'all-passing': { + const ids = new Set(['check-001', 'check-002', 'check-003', 'check-004', 'check-005', 'check-006']) + return { + checks: allChecks.filter(c => ids.has(c.id)), + statuses: allStatuses.filter(s => ids.has(s.checkId)), + } + } + case 'all-deactivated': { + const ids = new Set(['check-015', 'check-016', 'check-017', 'check-018', 'check-019']) + return { + checks: allChecks.filter(c => ids.has(c.id)), + statuses: allStatuses.filter(s => ids.has(s.checkId)), + } + } + case 'active-no-statuses': { + const ids = new Set(['check-013']) + return { + checks: allChecks.filter(c => ids.has(c.id)), + statuses: [], + } + } + case 'empty': + return { checks: [], statuses: [] } + } +}