Skip to content
Open
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
97 changes: 94 additions & 3 deletions packages/cli/src/commands/__tests__/test-sessions-get.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { CheckResult } from '../../rest/check-results.js'
import type { TestSessionErrorGroup } from '../../rest/test-session-error-groups.js'
import type { TestSessionDetail } from '../../rest/test-sessions.js'

vi.mock('../../rest/api.js', () => ({
testSessions: { get: vi.fn(), pollUntilComplete: vi.fn() },
testSessions: { get: vi.fn(), getResult: vi.fn(), pollUntilComplete: vi.fn() },
testSessionErrorGroups: { get: vi.fn() },
}))

Expand Down Expand Up @@ -51,9 +52,54 @@ const testSessionErrorGroup: TestSessionErrorGroup = {
archivedUntilNextEvent: false,
}

const testSessionResult: CheckResult = {
id: '42406a0f-5864-4a26-9884-7c5d1be15bc2',
checkId: 'check-1',
name: 'API smoke',
hasFailures: true,
hasErrors: false,
isDegraded: false,
overMaxResponseTime: false,
runLocation: 'eu-west-1',
startedAt: '2026-05-20T08:00:00.000Z',
stoppedAt: '2026-05-20T08:02:03.456Z',
created_at: '2026-05-20T08:02:03.456Z',
responseTime: 1234,
checkRunId: 42,
attempts: 1,
resultType: 'FINAL',
apiCheckResult: {
assertions: [
{ source: 'STATUS_CODE', comparison: 'EQUALS', target: 200 },
],
request: {
method: 'GET',
url: 'https://api.example.com/health',
data: '',
headers: {},
params: {},
},
response: {
status: 500,
statusText: '500 Internal Server Error',
body: '{"ok":false}',
headers: {},
timings: null,
timingPhases: null,
},
requestError: null,
jobLog: null,
jobAssets: null,
pcapDataUrl: null,
},
browserCheckResult: null,
multiStepCheckResult: null,
agenticCheckResult: null,
}

function createCommandContext (parsed: unknown) {
const logged: string[] = []
return {
return Object.assign(Object.create(TestSessionsGet.prototype), {
parse: vi.fn().mockResolvedValue(parsed),
log: vi.fn((msg?: string) => {
if (msg) logged.push(msg)
Expand All @@ -66,14 +112,15 @@ function createCommandContext (parsed: unknown) {
actionFailure: vi.fn(),
},
logged,
}
})
}

describe('test-sessions get command', () => {
beforeEach(() => {
vi.clearAllMocks()
process.exitCode = undefined
vi.mocked(api.testSessions.get).mockResolvedValue({ data: testSession } as any)
vi.mocked(api.testSessions.getResult).mockResolvedValue({ data: testSessionResult } as any)
vi.mocked(api.testSessions.pollUntilComplete).mockResolvedValue(testSession as any)
vi.mocked(api.testSessionErrorGroups.get).mockResolvedValue({ data: testSessionErrorGroup } as any)
})
Expand Down Expand Up @@ -128,6 +175,50 @@ describe('test-sessions get command', () => {
expect(ctx.style.actionStart).not.toHaveBeenCalled()
})

it('fetches and renders one test session result detail', async () => {
const ctx = createCommandContext({
args: { id: testSession.testSessionId },
flags: { output: 'detail', result: testSessionResult.id },
})

await TestSessionsGet.prototype.run.call(ctx as any)

expect(api.testSessions.get).not.toHaveBeenCalled()
expect(api.testSessions.getResult).toHaveBeenCalledWith(testSession.testSessionId, testSessionResult.id)
expect(ctx.logged[0]).toContain('API smoke')
expect(ctx.logged[0]).toContain('REQUEST')
expect(ctx.logged[0]).toContain('checkly test-sessions get 8166fa86-c9b4-4162-8541-d380c6c212d8')
expect(ctx.logged[0]).toContain('checkly test-sessions list')
})

it('watches completion before fetching one test session result detail', async () => {
const ctx = createCommandContext({
args: { id: testSession.testSessionId },
flags: { output: 'detail', result: testSessionResult.id, watch: true },
})

await TestSessionsGet.prototype.run.call(ctx as any)

expect(api.testSessions.get).not.toHaveBeenCalled()
expect(api.testSessions.pollUntilComplete).toHaveBeenCalledWith(testSession.testSessionId)
expect(api.testSessions.getResult).toHaveBeenCalledWith(testSession.testSessionId, testSessionResult.id)
expect(ctx.style.actionStart).toHaveBeenCalledWith('Watching test session until completion...')
expect(ctx.style.actionSuccess).toHaveBeenCalled()
expect(ctx.logged[0]).toContain('API smoke')
})

it('returns raw test session result response for json drilldown output', async () => {
const ctx = createCommandContext({
args: { id: testSession.testSessionId },
flags: { output: 'json', result: testSessionResult.id },
})

await TestSessionsGet.prototype.run.call(ctx as any)

expect(api.testSessions.get).not.toHaveBeenCalled()
expect(JSON.parse(ctx.logged[0])).toEqual(testSessionResult)
})

it('fetches and renders one test session error group detail', async () => {
const ctx = createCommandContext({
args: { id: testSession.testSessionId },
Expand Down
43 changes: 24 additions & 19 deletions packages/cli/src/commands/checks/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,19 @@ import { AuthCommand } from '../authCommand.js'
import { outputFlag } from '../../helpers/flags.js'
import * as api from '../../rest/api.js'
import type { CheckWithStatus } from '../../formatters/checks.js'
import { type OutputFormat, stripAnsi, formatDate } from '../../formatters/render.js'
import {
type OutputFormat,
type CommandHint,
stripAnsi,
formatDate,
renderCommandHints,
} from '../../formatters/render.js'
import {
formatCheckDetail,
formatResults,
formatErrorGroups,
} from '../../formatters/checks.js'
import { formatResultDetail } from '../../formatters/check-result-detail.js'
import { formatResultDetailWithNavigation } from '../../formatters/check-result-detail.js'
import { formatRcaDetail, formatRcaHint, transformErrorGroupForJson } from '../../formatters/rca.js'
import { quickRangeValues, type QuickRange, type GroupBy } from '../../rest/analytics.js'
import { formatAnalyticsSection } from '../../formatters/analytics.js'
Expand Down Expand Up @@ -162,21 +168,23 @@ export default class ChecksGet extends AuthCommand {

// Navigation hints
output.push('')
const hints: CommandHint[] = []
if (errorGroups.length > 0) {
const firstActive = errorGroups.find(eg => !eg.archivedUntilNextEvent)
if (firstActive) {
output.push(` ${chalk.dim('View error:')} checkly checks get ${args.id} --error-group ${firstActive.id}`)
hints.push({ label: 'View error', command: `checkly checks get ${args.id} --error-group ${firstActive.id}` })
}
}
if (results.length > 0) {
output.push(` ${chalk.dim('View result:')} checkly checks get ${args.id} --result ${results[0].id}`)
hints.push({ label: 'View result', command: `checkly checks get ${args.id} --result ${results[0].id}` })
}
if (nextId) {
output.push(` ${chalk.dim('More results:')} checkly checks get ${args.id} --results-cursor ${nextId}`)
hints.push({ label: 'More results', command: `checkly checks get ${args.id} --results-cursor ${nextId}` })
}
output.push(` ${chalk.dim('Change range:')} checkly checks get ${args.id} --stats-range last7Days`)
output.push(` ${chalk.dim('By region:')} checkly checks get ${args.id} --group-by location`)
output.push(` ${chalk.dim('Back to list:')} checkly checks list`)
hints.push({ label: 'Change range', command: `checkly checks get ${args.id} --stats-range last7Days` })
hints.push({ label: 'By region', command: `checkly checks get ${args.id} --group-by location` })
hints.push({ label: 'Back to list', command: 'checkly checks list' })
output.push(renderCommandHints(hints, { gap: 3 }))

this.log(output.join('\n'))
} catch (err: any) {
Expand Down Expand Up @@ -267,8 +275,10 @@ export default class ChecksGet extends AuthCommand {
}

output.push('')
output.push(` ${chalk.dim('Back to check:')} checkly checks get ${checkId}`)
output.push(` ${chalk.dim('Back to list:')} checkly checks list`)
output.push(renderCommandHints([
{ label: 'Back to check', command: `checkly checks get ${checkId}` },
{ label: 'Back to list', command: 'checkly checks list' },
]))

this.log(output.join('\n'))
}
Expand All @@ -283,14 +293,9 @@ export default class ChecksGet extends AuthCommand {

const fmt: OutputFormat = outputFormat === 'md' ? 'md' : 'terminal'

const output: string[] = []
output.push(formatResultDetail(result, fmt))

// Navigation hints
output.push('')
output.push(` ${chalk.dim('Back to check:')} checkly checks get ${checkId}`)
output.push(` ${chalk.dim('Back to list:')} checkly checks list`)

this.log(output.join('\n'))
this.log(formatResultDetailWithNavigation(result, fmt, [
{ label: 'Back to check', command: `checkly checks get ${checkId}` },
{ label: 'Back to list', command: 'checkly checks list' },
]))
}
}
78 changes: 57 additions & 21 deletions packages/cli/src/commands/test-sessions/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
formatTestSessionErrorGroupDetail,
uniqueErrorGroupIds,
} from '../../formatters/test-sessions.js'
import { formatResultDetailWithNavigation } from '../../formatters/check-result-detail.js'
import { type OutputFormat } from '../../formatters/render.js'
import type { TestSessionDetail } from '../../rest/test-sessions.js'

Expand All @@ -24,6 +25,10 @@ export default class TestSessionsGet extends AuthCommand {
}

static flags = {
'result': Flags.string({
char: 'r',
description: 'Show details for a specific test session result ID.',
}),
'error-group': Flags.string({
description: 'Show details for a test session error group ID from this session.',
}),
Expand All @@ -48,29 +53,12 @@ export default class TestSessionsGet extends AuthCommand {
this.style.outputFormat = flags.output

try {
let testSession: TestSessionDetail

if (flags.watch) {
const showAction = flags.output === 'detail'
if (showAction) {
this.style.actionStart('Watching test session until completion...')
}
try {
testSession = await api.testSessions.pollUntilComplete(args.id)
if (showAction) {
this.style.actionSuccess()
}
} catch (err) {
if (showAction) {
this.style.actionFailure()
}
throw err
}
} else {
const { data } = await api.testSessions.get(args.id)
testSession = data
if (flags.result) {
return await this.showResultDetail(args.id, flags.result, flags.output ?? 'detail', flags.watch)
}

const testSession = await this.getTestSession(args.id, flags.watch, flags.output === 'detail')

if (flags['error-group']) {
const errorGroupIds = uniqueErrorGroupIds(testSession)

Expand Down Expand Up @@ -113,4 +101,52 @@ export default class TestSessionsGet extends AuthCommand {
process.exitCode = 1
}
}

private async getTestSession (id: string, watch: boolean, showAction: boolean): Promise<TestSessionDetail> {
if (!watch) {
const { data } = await api.testSessions.get(id)
return data
}

if (showAction) {
this.style.actionStart('Watching test session until completion...')
}

try {
const testSession = await api.testSessions.pollUntilComplete(id)
if (showAction) {
this.style.actionSuccess()
}
return testSession
} catch (err) {
if (showAction) {
this.style.actionFailure()
}
throw err
}
}

private async showResultDetail (
testSessionId: string,
resultId: string,
outputFormat: string,
watch: boolean,
): Promise<void> {
if (watch) {
await this.getTestSession(testSessionId, true, outputFormat === 'detail')
}

const { data: result } = await api.testSessions.getResult(testSessionId, resultId)

if (outputFormat === 'json') {
this.log(JSON.stringify(result, null, 2))
return
}

const fmt: OutputFormat = outputFormat === 'md' ? 'md' : 'terminal'
this.log(formatResultDetailWithNavigation(result, fmt, [
{ label: 'Back to session', command: `checkly test-sessions get ${testSessionId}` },
{ label: 'Back to list', command: 'checkly test-sessions list' },
]))
}
}
5 changes: 4 additions & 1 deletion packages/cli/src/formatters/account-members.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type ColumnDef,
type OutputFormat,
formatDate,
renderCommandHints,
renderTable,
truncateToWidth,
} from './render.js'
Expand All @@ -22,7 +23,9 @@ export function formatCursorPaginationInfo (count: number, nextId: string | null

export function formatCursorNavigationHints (nextId: string | null): string {
if (!nextId) return ''
return ` ${chalk.dim('Next page:')} checkly account members --limit <limit> --next-id ${nextId}`
return renderCommandHints([
{ label: 'Next page', command: `checkly account members --limit <limit> --next-id ${nextId}` },
], { gap: 4 })
}

function boolSymbol (value: boolean | undefined, format: OutputFormat): string {
Expand Down
14 changes: 8 additions & 6 deletions packages/cli/src/formatters/alert-channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
type OutputFormat,
type DetailField,
type ColumnDef,
type CommandHint,
renderDetailFields,
renderCommandHints,
renderTable,
formatDate,
truncateError,
Expand Down Expand Up @@ -132,18 +134,18 @@ export function formatAlertChannelPaginationInfo (pagination: AlertChannelPagina
export function formatAlertChannelNavigationHints (pagination: AlertChannelPaginationInfo): string {
const { page, limit, total } = pagination
const totalPages = Math.ceil(total / limit)
const lines: string[] = []
const hints: CommandHint[] = []

if (page < totalPages) {
lines.push(` ${chalk.dim('Next page:')} checkly alert-channels list --page ${page + 1}`)
hints.push({ label: 'Next page', command: `checkly alert-channels list --page ${page + 1}` })
}
if (page > 1) {
lines.push(` ${chalk.dim('Prev page:')} checkly alert-channels list --page ${page - 1}`)
hints.push({ label: 'Prev page', command: `checkly alert-channels list --page ${page - 1}` })
}
lines.push(` ${chalk.dim('View channel:')} checkly alert-channels get <id>`)
lines.push(` ${chalk.dim('View logs:')} checkly alert-channels logs <id> --status failed`)
hints.push({ label: 'View channel', command: 'checkly alert-channels get <id>' })
hints.push({ label: 'View logs', command: 'checkly alert-channels logs <id> --status failed' })

return lines.join('\n')
return renderCommandHints(hints, { gap: 1 })
}

const alertChannelDetailFields: DetailField<AlertChannel>[] = [
Expand Down
Loading
Loading