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
20 changes: 10 additions & 10 deletions packages/cli/e2e/__tests__/trigger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ import { runCheckly } from '../run-checkly'
describe('trigger', () => {
let fixt: FixtureSandbox
const executionId = uuid.v4()
// The fixture config derives its logicalId from EXECUTION_ID, and
// runCheckly runs with extendEnv: false — every invocation must pass the
// variable or the config is invalid, which aborts the command.
const runOptions = { env: { EXECUTION_ID: executionId } }

beforeAll(async () => {
fixt = await FixtureSandbox.create({
source: path.join(__dirname, 'fixtures', 'trigger-project'),
})
await runCheckly(fixt, ['deploy', '--force'], {
env: { EXECUTION_ID: executionId },
})
await runCheckly(fixt, ['deploy', '--force'], runOptions)
}, 180_000)

afterAll(async () => {
try {
await runCheckly(fixt, ['destroy', '--force'], {
env: { EXECUTION_ID: executionId },
})
await runCheckly(fixt, ['destroy', '--force'], runOptions)
} catch {
// cleanup best-effort
}
Expand All @@ -42,7 +42,7 @@ describe('trigger', () => {
`production,backend,${executionId}`,
'--tags',
`production,frontend,${executionId}`,
])
], runOptions)

expect(stdout).toContain(secretEnv)
expect(stdout).toContain('Prod Backend Check')
Expand All @@ -56,7 +56,7 @@ describe('trigger', () => {
'trigger',
'--tags',
'no-checks-match-this-tag',
])
], runOptions)
expect.unreachable('Expected command to fail')
} catch (err) {
if (err instanceof ExecaError) {
Expand All @@ -75,7 +75,7 @@ describe('trigger', () => {
'--tags',
'no-checks-match-this-tag',
'--fail-on-no-matching',
])
], runOptions)
expect.unreachable('Expected command to fail')
} catch (err) {
if (err instanceof ExecaError) {
Expand All @@ -93,7 +93,7 @@ describe('trigger', () => {
'--tags',
'no-checks-match-this-tag',
'--no-fail-on-no-matching',
])
], runOptions)

expect(stdout).toContain('No matching checks were found.')
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, it, expect, vi } from 'vitest'

import { BaseCommand } from '../baseCommand.js'
import { CommandStyle } from '../../helpers/command-style.js'
import { ConfigFileDiagnostics, InvalidConfigError } from '../../services/config-diagnostics.js'
import { Diagnostics, ErrorDiagnostic, NoticeDiagnostic, WarningDiagnostic } from '../../constructs/diagnostics.js'

function createCommandContext () {
let exitCodeValue: number | undefined
return {
style: {
diagnostics: CommandStyle.prototype.diagnostics,
actionFailure: vi.fn(),
longError: vi.fn(),
longWarning: vi.fn(),
longInfo: vi.fn(),
shortError: vi.fn(),
},
exit: vi.fn((code: number) => {
exitCodeValue = code
throw new Error(`EXIT_${code}`)
}),
catch: (BaseCommand.prototype as any).catch,
get exitCodeValue () {
return exitCodeValue
},
}
}

describe('CommandStyle', () => {
describe('diagnostics()', () => {
it('dispatches diagnostics by severity', () => {
const ctx = createCommandContext()

const diagnostics = new Diagnostics()
diagnostics.add(new ErrorDiagnostic({
title: 'error title',
message: 'error message',
error: new Error('error message'),
}))
diagnostics.add(new WarningDiagnostic({
title: 'warning title',
message: 'warning message',
}))
diagnostics.add(new NoticeDiagnostic({
title: 'notice title',
message: 'notice message',
}))

ctx.style.diagnostics(diagnostics)

expect(ctx.style.longError).toHaveBeenCalledWith('error title', 'error message')
expect(ctx.style.longWarning).toHaveBeenCalledWith('warning title', 'warning message')
expect(ctx.style.longInfo).toHaveBeenCalledWith('notice title', 'notice message')
})
})
})

describe('BaseCommand', () => {
describe('catch()', () => {
it('renders config diagnostics and exits 1 on InvalidConfigError', () => {
const ctx = createCommandContext()

const diagnostics = new ConfigFileDiagnostics('checkly.config.ts')
diagnostics.add(new ErrorDiagnostic({
title: 'Invalid property value',
message: 'The value is not valid.',
error: new Error('The value is not valid.'),
}))

expect(() => ctx.catch.call(ctx, new InvalidConfigError(diagnostics)))
.toThrow('EXIT_1')

expect(ctx.style.longError).toHaveBeenCalledWith(
'[checkly.config.ts] Invalid property value',
'The value is not valid.',
)
expect(ctx.style.shortError).toHaveBeenCalledWith('Your Checkly configuration file is not valid.')
expect(ctx.exitCodeValue).toBe(1)
})
})
})
26 changes: 15 additions & 11 deletions packages/cli/src/commands/__tests__/confirm-flow-deploy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,21 @@ vi.mock('../../rest/api', () => ({
validateAuthentication: vi.fn().mockResolvedValue({ name: 'Test Account' }),
}))

vi.mock('../../services/checkly-config-loader', () => ({
loadChecklyConfig: vi.fn().mockResolvedValue({
config: {
logicalId: 'my-project',
projectName: 'My Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
checks: {},
},
constructs: [],
}),
}))
vi.mock('../../services/checkly-config-loader', async () => {
const { Diagnostics } = await import('../../constructs/diagnostics.js')
return {
loadChecklyConfig: vi.fn().mockResolvedValue({
config: {
logicalId: 'my-project',
projectName: 'My Project',
repoUrl: 'https://github.com/checkly/checkly-cli',
checks: {},
},
constructs: [],
diagnostics: new Diagnostics(),
}),
}
})

vi.mock('../../services/project-parser', () => ({
parseProject: vi.fn(),
Expand Down
40 changes: 35 additions & 5 deletions packages/cli/src/commands/__tests__/confirm-flow-destroy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ vi.mock('../../rest/api', () => ({
validateAuthentication: vi.fn().mockResolvedValue({ name: 'Test Account' }),
}))

vi.mock('../../services/checkly-config-loader', () => ({
loadChecklyConfig: vi.fn().mockResolvedValue({
config: { logicalId: 'my-project', projectName: 'My Project' },
}),
}))
vi.mock('../../services/checkly-config-loader', async () => {
const { Diagnostics } = await import('../../constructs/diagnostics.js')
return {
loadChecklyConfig: vi.fn().mockResolvedValue({
config: { logicalId: 'my-project', projectName: 'My Project' },
diagnostics: new Diagnostics(),
}),
}
})

vi.mock('../../services/util', () => ({
splitConfigFilePath: vi.fn().mockReturnValue({
Expand All @@ -31,6 +35,8 @@ import { detectCliMode } from '../../helpers/cli-mode.js'
import prompts from 'prompts'
import * as api from '../../rest/api.js'
import { buildConfirmCommand } from '../../helpers/command-preview.js'
import { loadChecklyConfig } from '../../services/checkly-config-loader.js'
import { Diagnostics, WarningDiagnostic } from '../../constructs/diagnostics.js'
import { AuthCommand } from '../authCommand.js'
import Destroy from '../destroy.js'

Expand Down Expand Up @@ -60,6 +66,7 @@ function createCommandContext (parsed: { flags: Record<string, unknown>, metadat
confirmOrAbort: AuthCommand.prototype.confirmOrAbort,
style: {
outputFormat: undefined,
diagnostics: vi.fn(),
longError: vi.fn(),
actionStart: vi.fn(),
actionStatus: vi.fn(),
Expand Down Expand Up @@ -98,6 +105,29 @@ describe('destroy confirmation flow', () => {
expect(api.projects.deleteProject).not.toHaveBeenCalled()
})

it('renders non-fatal config diagnostics right after loading the config', async () => {
vi.mocked(detectCliMode).mockReturnValue('agent')
const configDiagnostics = new Diagnostics()
configDiagnostics.add(new WarningDiagnostic({
title: 'Config warning',
message: 'A config-level warning.',
}))
vi.mocked(loadChecklyConfig).mockResolvedValueOnce({
config: { logicalId: 'my-project', projectName: 'My Project' },
diagnostics: configDiagnostics,
} as any)
const ctx = createCommandContext({
flags: { force: false },
})

// The confirmation gate exits 2, but the diagnostics render before it.
await expect(
Destroy.prototype.run.call(ctx as any),
).rejects.toThrow('EXIT_2')

expect(ctx.style.diagnostics).toHaveBeenCalledWith(configDiagnostics)
})

it('executes with --force in agent mode', async () => {
vi.mocked(detectCliMode).mockReturnValue('agent')
const ctx = createCommandContext({
Expand Down
126 changes: 126 additions & 0 deletions packages/cli/src/commands/__tests__/deploy-config-diagnostics.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { describe, expect, it, vi } from 'vitest'

vi.mock('../../helpers/cli-mode', () => ({
detectCliMode: vi.fn(() => 'agent'),
}))

vi.mock('../../rest/api', () => ({
runtimes: { getAll: vi.fn().mockResolvedValue([]) },
validateAuthentication: vi.fn().mockResolvedValue({ name: 'Test Account' }),
}))

vi.mock('../../services/checkly-config-loader', () => ({
loadChecklyConfig: vi.fn(),
}))

vi.mock('../../services/project-parser', () => ({
parseProject: vi.fn(),
}))

vi.mock('../../services/util', () => ({
splitConfigFilePath: vi.fn().mockReturnValue({
configDirectory: '.',
configFilenames: ['checkly.config.ts'],
}),
getGitInformation: vi.fn(),
}))

import { loadChecklyConfig } from '../../services/checkly-config-loader.js'
import { parseProject } from '../../services/project-parser.js'
import { ConfigFileDiagnostics } from '../../services/config-diagnostics.js'
import { Diagnostics, ErrorDiagnostic, WarningDiagnostic } from '../../constructs/diagnostics.js'
import { CommandStyle } from '../../helpers/command-style.js'
import { AuthCommand } from '../authCommand.js'
import Deploy from '../deploy.js'

function createCommandContext (parsed: unknown) {
let exitCodeValue: number | undefined
return {
parse: vi.fn().mockResolvedValue(parsed),
exit: vi.fn((code: number) => {
exitCodeValue = code
throw new Error(`EXIT_${code}`)
}),
style: {
outputFormat: undefined,
diagnostics: CommandStyle.prototype.diagnostics,
actionStart: vi.fn(),
actionSuccess: vi.fn(),
actionFailure: vi.fn(),
longError: vi.fn(),
longWarning: vi.fn(),
longInfo: vi.fn(),
shortError: vi.fn(),
},
validateProject: (AuthCommand.prototype as any).validateProject,
constructor: Deploy,
account: { name: 'Test Account', runtimeId: 'runtime-default' },
get exitCodeValue () {
return exitCodeValue
},
}
}

const deployFlags = {
flags: {
'force': true,
'preview': true,
'output': false,
'verbose': false,
'config': undefined,
'schedule-on-deploy': true,
'verify-runtime-dependencies': true,
'debug-bundle': false,
'debug-bundle-output-file': './debug-bundle.json',
},
metadata: { flags: {} },
}

describe('deploy config diagnostics', () => {
it('renders config diagnostics ahead of project diagnostics', async () => {
const configDiagnostics = new ConfigFileDiagnostics('checkly.config.ts')
configDiagnostics.add(new WarningDiagnostic({
title: 'Config warning',
message: 'A config-level warning.',
}))
vi.mocked(loadChecklyConfig).mockResolvedValue({
config: {
logicalId: 'my-project',
projectName: 'My Project',
checks: {},
},
constructs: [],
diagnostics: configDiagnostics,
} as any)

const project = {
repoUrl: undefined,
validate: vi.fn((diagnostics: Diagnostics) => {
diagnostics.add(new ErrorDiagnostic({
title: 'Project error',
message: 'A project-level error.',
error: new Error('A project-level error.'),
}))
return Promise.resolve()
}),
}
vi.mocked(parseProject).mockResolvedValue(project as any)

const ctx = createCommandContext(deployFlags)

await expect(Deploy.prototype.run.call(ctx as any)).rejects.toThrow('EXIT_1')

expect(ctx.style.longWarning).toHaveBeenCalledWith(
'[checkly.config.ts] Config warning',
'A config-level warning.',
)
expect(ctx.style.longError).toHaveBeenCalledWith('Project error', 'A project-level error.')

// Config diagnostics must render before project diagnostics.
const warningOrder = ctx.style.longWarning.mock.invocationCallOrder[0]
const errorOrder = ctx.style.longError.mock.invocationCallOrder[0]
expect(warningOrder).toBeLessThan(errorOrder)

expect(ctx.exitCodeValue).toBe(1)
})
})
Loading
Loading