Skip to content
Merged
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
6 changes: 5 additions & 1 deletion packages/cli/src/ai-context/references/communicate.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Open incidents and lead customer communications via status pages.

## Confirmation Protocol

Write commands (`incidents create`, `incidents update`, `incidents resolve`, `deploy`, `destroy`) require confirmation before executing. When the CLI detects an agent environment, it returns a JSON envelope with exit code 2 instead of executing:
Write commands (`incidents create`, `incidents update`, `incidents resolve`, `deploy`, `destroy`, `import commit`, `import cancel`) require confirmation before executing. When the CLI detects an agent environment, it returns a JSON envelope with exit code 2 instead of executing:

```json
{
Expand All @@ -27,6 +27,10 @@ Write commands (`incidents create`, `incidents update`, `incidents resolve`, `de

The `confirmCommand` omits flags left at their default, so a bare `npx checkly deploy` confirms as `checkly deploy --force` rather than echoing back every boolean the parser filled in. Treat every flag you see there as deliberate.

### Commands that pin a resolved target

A command that picks its own target before confirming writes that target back into the `confirmCommand`. `import commit` and `import cancel` do this: run without `--plan-id` they select the only candidate plan themselves, and confirm as `checkly import commit --plan-id="<resolved-id>" --force` — carrying a flag you never passed. That is deliberate: the pinned ID guarantees the approved run acts on the plan whose `changes` you showed the user, not on whatever happens to be pending by then. Run the `confirmCommand` exactly as returned; do not strip the flag or fall back to the bare command.

## Available Commands

Parse and read further reference documentation when tasked with any of the following:
Expand Down
144 changes: 144 additions & 0 deletions packages/cli/src/commands/import/__tests__/apply.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

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

vi.mock('../../../rest/api', () => ({
projects: {
findImportPlans: vi.fn(),
applyImportPlan: vi.fn(),
commitImportPlan: vi.fn(),
},
}))

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

import { detectCliMode } from '../../../helpers/cli-mode.js'
import * as api from '../../../rest/api.js'
import { loadChecklyConfig } from '../../../services/checkly-config-loader.js'
import { ImportPlan } from '../../../rest/projects.js'
import ImportApplyCommand from '../apply.js'

function plan (id: string): ImportPlan {
return { id, createdAt: new Date(0).toISOString() }
}

function createCommandContext (parsed: unknown) {
const logged: string[] = []
let exitCodeValue: number | undefined
return {
parse: vi.fn().mockResolvedValue(parsed),
log: vi.fn((msg?: string) => {
if (msg) logged.push(msg)
}),
exit: vi.fn((code: number) => {
exitCodeValue = code
throw new Error(`EXIT_${code}`)
}),
style: {
actionStart: vi.fn(),
actionSuccess: vi.fn(),
actionFailure: vi.fn(),
fatal: vi.fn(),
},
constructor: ImportApplyCommand,
logged,
get exitCodeValue () {
return exitCodeValue
},
}
}

describe('import apply command (non-interactive)', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(loadChecklyConfig).mockResolvedValue({
config: { logicalId: 'my-project' },
} as any)
vi.mocked(api.projects.applyImportPlan).mockResolvedValue({} as any)
vi.mocked(api.projects.commitImportPlan).mockResolvedValue({} as any)
})

it('auto-applies the single plan in agent mode and does not commit', async () => {
vi.mocked(detectCliMode).mockReturnValue('agent')
vi.mocked(api.projects.findImportPlans).mockResolvedValue({ data: [plan('only')] } as any)
const ctx = createCommandContext({ flags: { 'config': undefined, 'plan-id': undefined, 'no-commit': false } })

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

expect(api.projects.applyImportPlan).toHaveBeenCalledWith('only')
expect(api.projects.commitImportPlan).not.toHaveBeenCalled()
})

it('targets the plan named by --plan-id when several exist', async () => {
vi.mocked(detectCliMode).mockReturnValue('agent')
vi.mocked(api.projects.findImportPlans).mockResolvedValue({
data: [plan('a'), plan('b'), plan('c')],
} as any)
const ctx = createCommandContext({ flags: { 'config': undefined, 'plan-id': 'b', 'no-commit': false } })

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

expect(api.projects.applyImportPlan).toHaveBeenCalledWith('b')
expect(api.projects.commitImportPlan).not.toHaveBeenCalled()
})

it('fails with exit 1 when the selection is ambiguous and no --plan-id is given', async () => {
vi.mocked(detectCliMode).mockReturnValue('agent')
vi.mocked(api.projects.findImportPlans).mockResolvedValue({
data: [plan('a'), plan('b')],
} as any)
const ctx = createCommandContext({ flags: { 'config': undefined, 'plan-id': undefined, 'no-commit': false } })

await expect(ImportApplyCommand.prototype.run.call(ctx as any)).rejects.toThrow('EXIT_1')
expect(ctx.style.fatal).toHaveBeenCalledWith(expect.stringContaining('--plan-id'))
expect(api.projects.applyImportPlan).not.toHaveBeenCalled()
})

it('applies without committing in interactive mode when --no-commit is given', async () => {
vi.mocked(detectCliMode).mockReturnValue('interactive')
vi.mocked(api.projects.findImportPlans).mockResolvedValue({ data: [plan('only')] } as any)
const ctx = createCommandContext({ flags: { 'config': undefined, 'plan-id': 'only', 'no-commit': true } })

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

expect(api.projects.applyImportPlan).toHaveBeenCalledWith('only')
expect(api.projects.commitImportPlan).not.toHaveBeenCalled()
})

it('tells the caller how to commit the plan it left pending', async () => {
vi.mocked(detectCliMode).mockReturnValue('agent')
vi.mocked(api.projects.findImportPlans).mockResolvedValue({ data: [plan('abc123')] } as any)
const ctx = createCommandContext({ flags: { 'config': undefined, 'plan-id': undefined, 'no-commit': false } })

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

expect(ctx.logged.join('\n')).toContain('import commit --plan-id abc123')
})

it('fails with exit 1 when --plan-id is given but no plans exist', async () => {
vi.mocked(detectCliMode).mockReturnValue('agent')
vi.mocked(api.projects.findImportPlans).mockResolvedValue({ data: [] } as any)
const ctx = createCommandContext({ flags: { 'config': undefined, 'plan-id': 'abc123', 'no-commit': false } })

await expect(ImportApplyCommand.prototype.run.call(ctx as any)).rejects.toThrow('EXIT_1')
expect(ctx.style.fatal).toHaveBeenCalledWith(expect.stringContaining('abc123'))
expect(api.projects.applyImportPlan).not.toHaveBeenCalled()
})

it('reports nothing-to-do without failing when no plans exist and none was requested', async () => {
vi.mocked(detectCliMode).mockReturnValue('agent')
vi.mocked(api.projects.findImportPlans).mockResolvedValue({ data: [] } as any)
const ctx = createCommandContext({ flags: { 'config': undefined, 'plan-id': undefined, 'no-commit': false } })

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

expect(ctx.exit).not.toHaveBeenCalled()
expect(ctx.style.fatal).not.toHaveBeenCalled()
expect(ctx.logged.join('\n')).toContain('Nothing to apply')
expect(api.projects.applyImportPlan).not.toHaveBeenCalled()
})
})
Loading
Loading