Skip to content

Commit 8989ec2

Browse files
Add checks run for ordinary check sessions (#1418)
* feat: add checks run command * test: update checks help expectation * fix: preserve JSON output for unmatched checks
1 parent 14d5b2d commit 8989ec2

13 files changed

Lines changed: 890 additions & 5 deletions

File tree

packages/cli/e2e/__tests__/help.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('help', () => {
5757
alert-channels List and inspect alert channels in your Checkly account.
5858
api Make an authenticated HTTP request to the Checkly API.
5959
assets List and download result assets.
60-
checks List and inspect checks in your Checkly account.
60+
checks List, inspect, and run checks in your Checkly account.
6161
destroy Destroy your project with all its related resources.
6262
env Manage Checkly environment variables.
6363
help Display help for checkly.

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"description": "List and download result assets."
7979
},
8080
"checks": {
81-
"description": "List and inspect checks in your Checkly account."
81+
"description": "List, inspect, and run checks in your Checkly account."
8282
},
8383
"env": {
8484
"description": "Manage Checkly environment variables."

packages/cli/src/ai-context/__tests__/references.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ describe('AI context investigation references', () => {
4747
it('documents check result attempts and asset retrieval', async () => {
4848
const content = normalizeLineEndings(await readReference('investigate-checks.md'))
4949

50+
expect(content).toContain('npx checkly checks run --tags production')
51+
expect(content).toContain('creates ordinary Check Sessions')
52+
expect(content).toContain('Test Sessions do not update normal check health')
5053
expect(content).toContain('npx checkly checks get <check-id> --result <result-id> --include-attempts --output json')
5154
expect(content).toContain('"result": {}')
5255
expect(content).toContain('"attempts": []')

packages/cli/src/ai-context/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export const REFERENCES = [
7272
export const INVESTIGATE_REFERENCES = [
7373
{
7474
id: 'investigate-checks',
75-
description: 'Inspect checks (`checks list`, `checks get`), retry attempts, result assets, and trigger on-demand runs',
75+
description: 'Inspect checks (`checks list`, `checks get`), retry attempts, result assets, and run on-demand monitoring or test sessions',
7676
},
7777
{
7878
id: 'investigate-alerting',

packages/cli/src/ai-context/references/investigate-checks.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,27 @@ Flags:
146146

147147
Pass one or more check IDs as positional arguments to get stats for specific checks only.
148148

149-
## Trigger checks
149+
## Run checks with normal monitoring behavior
150+
151+
```bash
152+
npx checkly checks run --tags production
153+
npx checkly checks run --check-id <check-id>
154+
npx checkly checks run --tags critical,api --detach
155+
```
156+
157+
`checks run` creates ordinary Check Sessions using the checks' deployed
158+
configuration and locations. These runs update check health and apply configured
159+
alerting rules. The command waits for completion by default; use `--detach` to
160+
schedule the runs and exit immediately.
161+
162+
## Trigger a recorded test session
150163

151164
```bash
152165
npx checkly trigger --tags production
153166
npx checkly trigger --tags critical,api
154167
```
155168

156-
Triggers existing deployed checks by tag. Useful for on-demand verification.
169+
`trigger` runs deployed checks as a Test Session. Use it for CI verification or
170+
when you need Test Session options such as location, environment, or retry
171+
overrides. Test Sessions do not update normal check health or send configured
172+
monitoring alerts.
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
import type { CheckSession, CompletedCheckSession } from '../../rest/check-sessions.js'
4+
5+
vi.mock('../../rest/api.js', () => ({
6+
checkSessions: {
7+
trigger: vi.fn(),
8+
pollUntilComplete: vi.fn(),
9+
},
10+
}))
11+
12+
import * as api from '../../rest/api.js'
13+
import {
14+
CheckSessionWaitTimeoutError,
15+
NoMatchingChecksError,
16+
} from '../../rest/check-sessions.js'
17+
import ChecksRun from '../checks/run.js'
18+
19+
const pendingSessions: CheckSession[] = [
20+
{
21+
checkSessionId: '8166fa86-c9b4-4162-8541-d380c6c212d8',
22+
checkSessionLink: 'https://app.checklyhq.com/check-sessions/8166fa86-c9b4-4162-8541-d380c6c212d8',
23+
checkId: 'a4cd4ad9-4815-4a9e-92d2-0a7c562ee69a',
24+
checkType: 'API',
25+
name: 'Production API',
26+
status: 'PROGRESS',
27+
startedAt: '2026-07-21T12:00:00.000Z',
28+
stoppedAt: null,
29+
timeElapsed: 0,
30+
runLocations: ['us-east-1'],
31+
runSource: 'TRIGGER_API',
32+
},
33+
{
34+
checkSessionId: '16e22c0d-22a5-4d03-93c5-91bc38ac3c85',
35+
checkSessionLink: 'https://app.checklyhq.com/check-sessions/16e22c0d-22a5-4d03-93c5-91bc38ac3c85',
36+
checkId: '22336fd1-c407-4bf7-a793-91c2246fd08f',
37+
checkType: 'BROWSER',
38+
name: 'Checkout journey',
39+
status: 'PROGRESS',
40+
startedAt: '2026-07-21T12:00:00.000Z',
41+
stoppedAt: null,
42+
timeElapsed: 0,
43+
runLocations: ['eu-west-1'],
44+
runSource: 'TRIGGER_API',
45+
},
46+
]
47+
48+
const completedSessions: CompletedCheckSession[] = pendingSessions.map((session, index) => ({
49+
...session,
50+
status: index === 0 ? 'PASSED' : 'DEGRADED',
51+
stoppedAt: '2026-07-21T12:00:01.000Z',
52+
timeElapsed: 1_000,
53+
results: [],
54+
}))
55+
56+
function defaultFlags (overrides: Record<string, unknown> = {}) {
57+
return {
58+
'tags': undefined,
59+
'check-id': undefined,
60+
'refresh-cache': false,
61+
'timeout': 600,
62+
'detach': false,
63+
'fail-on-no-matching': true,
64+
'output': 'table',
65+
...overrides,
66+
}
67+
}
68+
69+
function createCommandContext (flags: Record<string, unknown>) {
70+
const logged: string[] = []
71+
return {
72+
parse: vi.fn().mockResolvedValue({ flags }),
73+
error: vi.fn((message: string) => {
74+
throw new Error(message)
75+
}),
76+
log: vi.fn((message?: string) => {
77+
if (message) logged.push(message)
78+
}),
79+
style: {
80+
outputFormat: undefined,
81+
actionStart: vi.fn(),
82+
actionStatus: vi.fn(),
83+
actionSuccess: vi.fn(),
84+
actionFailure: vi.fn(),
85+
longError: vi.fn(),
86+
},
87+
logged,
88+
}
89+
}
90+
91+
describe('checks run command', () => {
92+
beforeEach(() => {
93+
vi.clearAllMocks()
94+
process.exitCode = undefined
95+
vi.mocked(api.checkSessions.trigger).mockResolvedValue({ sessions: pendingSessions })
96+
vi.mocked(api.checkSessions.pollUntilComplete)
97+
.mockImplementation(id => Promise.resolve(completedSessions.find(session => session.checkSessionId === id)!))
98+
})
99+
100+
it('maps selectors to the public check sessions API and waits by default', async () => {
101+
const ctx = createCommandContext(defaultFlags({
102+
'tags': ['production,api', 'critical'],
103+
'check-id': [pendingSessions[0].checkId],
104+
'refresh-cache': true,
105+
'timeout': 120,
106+
}))
107+
108+
await ChecksRun.prototype.run.call(ctx as any)
109+
110+
expect(api.checkSessions.trigger).toHaveBeenCalledWith({
111+
target: {
112+
matchTags: [['production', 'api'], ['critical']],
113+
checkId: [pendingSessions[0].checkId],
114+
},
115+
refreshCache: true,
116+
})
117+
expect(api.checkSessions.pollUntilComplete).toHaveBeenCalledTimes(2)
118+
expect(api.checkSessions.pollUntilComplete).toHaveBeenCalledWith(
119+
pendingSessions[0].checkSessionId,
120+
expect.any(Number),
121+
)
122+
expect(ctx.style.actionStart).toHaveBeenCalledWith('Waiting for check sessions to complete...')
123+
expect(ctx.style.actionSuccess).toHaveBeenCalled()
124+
expect(ctx.logged[0]).toContain('2 check sessions completed')
125+
expect(process.exitCode).toBeUndefined()
126+
})
127+
128+
it('omits the target when no selectors are provided', async () => {
129+
const ctx = createCommandContext(defaultFlags({ detach: true }))
130+
131+
await ChecksRun.prototype.run.call(ctx as any)
132+
133+
expect(api.checkSessions.trigger).toHaveBeenCalledWith({ refreshCache: false })
134+
})
135+
136+
it('detaches without requesting completion', async () => {
137+
const ctx = createCommandContext(defaultFlags({ detach: true }))
138+
139+
await ChecksRun.prototype.run.call(ctx as any)
140+
141+
expect(api.checkSessions.pollUntilComplete).not.toHaveBeenCalled()
142+
expect(ctx.style.actionStart).not.toHaveBeenCalled()
143+
expect(ctx.logged[0]).toContain('2 check sessions started.')
144+
})
145+
146+
it('prints a stable JSON envelope without spinner output', async () => {
147+
const ctx = createCommandContext(defaultFlags({ output: 'json' }))
148+
149+
await ChecksRun.prototype.run.call(ctx as any)
150+
151+
expect(JSON.parse(ctx.logged[0])).toEqual({ sessions: completedSessions })
152+
expect(ctx.style.actionStart).not.toHaveBeenCalled()
153+
})
154+
155+
it.each(['FAILED', 'TIMED_OUT', 'CANCELLED'] as const)('exits 1 when a session is %s', async status => {
156+
vi.mocked(api.checkSessions.pollUntilComplete).mockResolvedValue({
157+
...completedSessions[0],
158+
status,
159+
})
160+
vi.mocked(api.checkSessions.trigger).mockResolvedValue({ sessions: [pendingSessions[0]] })
161+
const ctx = createCommandContext(defaultFlags())
162+
163+
await ChecksRun.prototype.run.call(ctx as any)
164+
165+
expect(process.exitCode).toBe(1)
166+
})
167+
168+
it('fails by default when no checks match', async () => {
169+
vi.mocked(api.checkSessions.trigger).mockRejectedValue(new NoMatchingChecksError())
170+
const ctx = createCommandContext(defaultFlags())
171+
172+
await ChecksRun.prototype.run.call(ctx as any)
173+
174+
expect(ctx.logged).toContain('No matching checks were found.')
175+
expect(process.exitCode).toBe(1)
176+
})
177+
178+
it('can succeed when no checks match', async () => {
179+
vi.mocked(api.checkSessions.trigger).mockRejectedValue(new NoMatchingChecksError())
180+
const ctx = createCommandContext(defaultFlags({ 'fail-on-no-matching': false }))
181+
182+
await ChecksRun.prototype.run.call(ctx as any)
183+
184+
expect(process.exitCode).toBeUndefined()
185+
})
186+
187+
it('prints an empty JSON envelope when no checks match without failing', async () => {
188+
vi.mocked(api.checkSessions.trigger).mockRejectedValue(new NoMatchingChecksError())
189+
const ctx = createCommandContext(defaultFlags({
190+
'fail-on-no-matching': false,
191+
'output': 'json',
192+
}))
193+
194+
await ChecksRun.prototype.run.call(ctx as any)
195+
196+
expect(ctx.logged).toHaveLength(1)
197+
expect(JSON.parse(ctx.logged[0])).toEqual({ sessions: [] })
198+
expect(process.exitCode).toBeUndefined()
199+
})
200+
201+
it('reports an overall completion timeout and leaves sessions running', async () => {
202+
vi.mocked(api.checkSessions.trigger).mockResolvedValue({ sessions: [pendingSessions[0]] })
203+
vi.mocked(api.checkSessions.pollUntilComplete)
204+
.mockRejectedValue(new CheckSessionWaitTimeoutError(pendingSessions[0].checkSessionId))
205+
const ctx = createCommandContext(defaultFlags())
206+
207+
await ChecksRun.prototype.run.call(ctx as any)
208+
209+
expect(ctx.style.actionFailure).toHaveBeenCalled()
210+
expect(ctx.style.longError).toHaveBeenCalledWith(
211+
'Timed out waiting for check sessions.',
212+
expect.stringContaining('--detach'),
213+
)
214+
expect(process.exitCode).toBe(1)
215+
})
216+
217+
it('rejects a non-positive timeout before scheduling', async () => {
218+
const ctx = createCommandContext(defaultFlags({ timeout: 0 }))
219+
220+
await expect(ChecksRun.prototype.run.call(ctx as any))
221+
.rejects
222+
.toThrow('--timeout must be an integer greater than 0.')
223+
expect(api.checkSessions.trigger).not.toHaveBeenCalled()
224+
})
225+
226+
it('has intentional command metadata', () => {
227+
expect(ChecksRun.readOnly).toBe(false)
228+
expect(ChecksRun.destructive).toBe(false)
229+
expect(ChecksRun.idempotent).toBe(false)
230+
})
231+
})

packages/cli/src/commands/__tests__/command-metadata.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { BaseCommand } from '../baseCommand.js'
55
import ChecksList from '../checks/list.js'
66
import ChecksGet from '../checks/get.js'
77
import ChecksStats from '../checks/stats.js'
8+
import ChecksRun from '../checks/run.js'
89
import StatusPagesList from '../status-pages/list.js'
910
import StatusPagesGet from '../status-pages/get.js'
1011
import AlertChannelsList from '../alert-channels/list.js'
@@ -62,6 +63,7 @@ const commands: Array<[string, typeof BaseCommand]> = [
6263
['checks list', ChecksList],
6364
['checks get', ChecksGet],
6465
['checks stats', ChecksStats],
66+
['checks run', ChecksRun],
6567
['status-pages list', StatusPagesList],
6668
['status-pages get', StatusPagesGet],
6769
['test-sessions list', TestSessionsList],

0 commit comments

Comments
 (0)