From 9a3dd52e55dfc83e07eba2673760b653c078f79e Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Wed, 28 Oct 2026 22:22:07 +0900 Subject: [PATCH 1/4] feat(cli): refuse a SlackAppAlertChannel without the Checkly Slack app [RED-1289] validate, test and deploy now fail with an error for a SlackAppAlertChannel with Slack destinations when the account reports, through the account's new connectedIntegrations, that the Checkly Slack app is not connected. Its alerts cannot be delivered, including for a channel deployed before the app was disconnected, and a warning in CI output goes unseen. The check is skipped when the API does not report connected integrations. The advanced project templates no longer construct a Slack app channel, so a freshly created project validates on an account without the app, and the agent guidance that recommends the construct says it needs one. Co-Authored-By: Claude Opus 5.5 --- .../src/__checks__/utils/alert-channels.js | 13 ++-- .../src/__checks__/utils/alert-channels.ts | 12 ++-- .../references/configure-alert-channels.md | 2 +- packages/cli/src/commands/authCommand.ts | 2 + .../__tests__/slack-app-alert-channel.spec.ts | 70 +++++++++++++++++++ packages/cli/src/constructs/session.ts | 8 +++ .../src/constructs/slack-app-alert-channel.ts | 30 ++++++++ packages/cli/src/rest/accounts.ts | 5 ++ 8 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts diff --git a/examples/advanced-project-js/src/__checks__/utils/alert-channels.js b/examples/advanced-project-js/src/__checks__/utils/alert-channels.js index 251cfdabe..4002a88ae 100644 --- a/examples/advanced-project-js/src/__checks__/utils/alert-channels.js +++ b/examples/advanced-project-js/src/__checks__/utils/alert-channels.js @@ -2,7 +2,6 @@ const { URL } = require('node:url') const { EmailAlertChannel, - SlackAppAlertChannel, WebhookAlertChannel, } = require('checkly/constructs') @@ -25,10 +24,13 @@ const emailChannel = new EmailAlertChannel('email-channel-1', { ...sendDefaults, }) -const slackChannel = new SlackAppAlertChannel('slack-channel-1', { - slackChannels: ['#ops'], - ...sendDefaults, -}) +// A Slack channel needs the Checkly Slack app connected to your account first +// (https://www.checklyhq.com/docs/integrations/alerts/slack/). Once it is, import +// SlackAppAlertChannel from 'checkly/constructs' and uncomment: +// const slackChannel = new SlackAppAlertChannel('slack-channel-1', { +// slackChannels: ['#ops'], +// ...sendDefaults, +// }) const webhookChannel = new WebhookAlertChannel('webhook-channel-1', { name: 'Pushover webhook', @@ -49,6 +51,5 @@ const webhookChannel = new WebhookAlertChannel('webhook-channel-1', { module.exports = { emailChannel, - slackChannel, webhookChannel, } diff --git a/examples/advanced-project/src/__checks__/utils/alert-channels.ts b/examples/advanced-project/src/__checks__/utils/alert-channels.ts index 730f41733..d33cd06a5 100644 --- a/examples/advanced-project/src/__checks__/utils/alert-channels.ts +++ b/examples/advanced-project/src/__checks__/utils/alert-channels.ts @@ -1,7 +1,6 @@ import { URL } from 'node:url' import { EmailAlertChannel, - SlackAppAlertChannel, WebhookAlertChannel } from 'checkly/constructs' @@ -25,10 +24,13 @@ export const emailChannel = new EmailAlertChannel('email-channel-1', { ...sendDefaults }) -export const slackChannel = new SlackAppAlertChannel('slack-channel-1', { - slackChannels: ['#ops'], - ...sendDefaults -}) +// A Slack channel needs the Checkly Slack app connected to your account first +// (https://www.checklyhq.com/docs/integrations/alerts/slack/). Once it is, import +// SlackAppAlertChannel from 'checkly/constructs' and uncomment: +// export const slackChannel = new SlackAppAlertChannel('slack-channel-1', { +// slackChannels: ['#ops'], +// ...sendDefaults +// }) export const webhookChannel = new WebhookAlertChannel('webhook-channel-1', { name: 'Pushover webhook', diff --git a/packages/cli/src/ai-context/references/configure-alert-channels.md b/packages/cli/src/ai-context/references/configure-alert-channels.md index 287c0efb7..cc04c2c43 100644 --- a/packages/cli/src/ai-context/references/configure-alert-channels.md +++ b/packages/cli/src/ai-context/references/configure-alert-channels.md @@ -9,7 +9,7 @@ All available alerts are described in the [Checkly docs](https://www.checklyhq.c *Important*: Don't make up email addresses, phone numbers, Slack channel names or similar static values. Scan the project to discover a valid configuration or ask what the values should be. -For new Slack notifications, prefer `SlackAppAlertChannel`. It uses the Checkly Slack App and only needs Slack `#channel` names or `@user` handles in `slackChannels`. +For new Slack notifications, prefer `SlackAppAlertChannel`. It uses the Checkly Slack App and only needs Slack `#channel` names or `@user` handles in `slackChannels`. It needs the Checkly Slack App connected to the Checkly account: without it, `npx checkly validate`, `test` and `deploy` fail with "Checkly Slack app not connected". If you can't confirm the app is connected, ask before adding one. ## Email Alert Channel diff --git a/packages/cli/src/commands/authCommand.ts b/packages/cli/src/commands/authCommand.ts index 123c7fa06..4a93e4b2f 100644 --- a/packages/cli/src/commands/authCommand.ts +++ b/packages/cli/src/commands/authCommand.ts @@ -112,6 +112,8 @@ export abstract class AuthCommand extends BaseCommand { // Constructs validate against account-specific limits and have no access to // the command instance. Session.accountFeatures = this.#account?.features ?? [] + Session.accountId = this.#account?.id + Session.accountConnectedIntegrations = this.#account?.connectedIntegrations } protected async validateProject ( diff --git a/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts b/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts new file mode 100644 index 000000000..ae59d5857 --- /dev/null +++ b/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, afterEach, beforeEach } from 'vitest' + +import { SlackAppAlertChannel } from '../index.js' +import { Diagnostics } from '../diagnostics.js' +import { Project } from '../project.js' +import { Session } from '../session.js' + +const validate = async (slackChannels: string[]) => { + const diagnostics = new Diagnostics() + await new SlackAppAlertChannel('ops-slack', { slackChannels }).validate(diagnostics) + return diagnostics +} + +describe('SlackAppAlertChannel', () => { + beforeEach(() => { + Session.project = new Project('project-id', { + name: 'Test Project', + repoUrl: 'https://github.com/checkly/checkly-cli', + }) + Session.accountId = 'acc-1' + }) + + afterEach(() => { + Session.accountId = undefined + Session.accountConnectedIntegrations = undefined + }) + + it('refuses the channel when the account has no Checkly Slack app', async () => { + Session.accountConnectedIntegrations = [] + + const diagnostics = await validate(['#ops']) + + expect(diagnostics.isFatal()).toBe(true) + expect(diagnostics.observations).toEqual([ + expect.objectContaining({ + title: 'Checkly Slack app not connected', + message: expect.stringContaining( + `SlackAppAlertChannel 'ops-slack' needs the Checkly Slack app, which is not connected to this account, so its alerts cannot be delivered.`, + ), + }), + ]) + expect(diagnostics.observations[0].message).toContain( + 'https://app.checklyhq.com/accounts/acc-1/settings/account/integrations', + ) + }) + + it('accepts the channel when the Checkly Slack app is connected', async () => { + Session.accountConnectedIntegrations = ['SLACK'] + + const diagnostics = await validate(['#ops']) + + expect(diagnostics.isFatal()).toBe(false) + }) + + it('skips the check when the API does not report connected integrations', async () => { + Session.accountConnectedIntegrations = undefined + + const diagnostics = await validate(['#ops']) + + expect(diagnostics.isFatal()).toBe(false) + }) + + it('accepts a channel without Slack destinations, which needs no Slack app', async () => { + Session.accountConnectedIntegrations = [] + + const diagnostics = await validate([]) + + expect(diagnostics.isFatal()).toBe(false) + }) +}) diff --git a/packages/cli/src/constructs/session.ts b/packages/cli/src/constructs/session.ts index a918d9d65..113152e02 100644 --- a/packages/cli/src/constructs/session.ts +++ b/packages/cli/src/constructs/session.ts @@ -90,6 +90,12 @@ export class Session { static loadingChecklyConfigFile: boolean static checklyConfigFileConstructs?: Construct[] static accountFeatures: string[] = [] + static accountId?: string + /** + * The integrations the authenticated account has connected, or undefined + * when unknown (not authenticated, or an API that does not report them). + */ + static accountConnectedIntegrations?: string[] static privateLocations: PrivateLocationApi[] static parsers = new Map() static playwrightProjectBundler?: PlaywrightProjectBundler @@ -124,6 +130,8 @@ export class Session { this.loadingChecklyConfigFile = false this.checklyConfigFileConstructs = undefined this.accountFeatures = [] + this.accountId = undefined + this.accountConnectedIntegrations = undefined this.privateLocations = [] this.parsers = new Map() this.playwrightProjectBundler = undefined diff --git a/packages/cli/src/constructs/slack-app-alert-channel.ts b/packages/cli/src/constructs/slack-app-alert-channel.ts index bcd9240d9..dd10554c0 100644 --- a/packages/cli/src/constructs/slack-app-alert-channel.ts +++ b/packages/cli/src/constructs/slack-app-alert-channel.ts @@ -1,6 +1,9 @@ import { AlertChannel, AlertChannelProps } from './alert-channel.js' +import { Diagnostics, ErrorDiagnostic } from './diagnostics.js' import { Session } from './session.js' +const SLACK_INTEGRATION = 'SLACK' + export interface SlackAppAlertChannelProps extends AlertChannelProps { /** * List of Slack #channels or @handles to notify. @@ -38,6 +41,33 @@ export class SlackAppAlertChannel extends AlertChannel { return `SlackAppAlertChannel:${this.logicalId}` } + /** + * Refuses the channel when the account has no Checkly Slack app: its alerts + * cannot be delivered, whether the channel is new or was deployed before the + * app was disconnected. Skipped when the account's integrations are unknown. + */ + async validate (diagnostics: Diagnostics): Promise { + await super.validate(diagnostics) + + const connected = Session.accountConnectedIntegrations + if (connected === undefined || connected.includes(SLACK_INTEGRATION) || !this.slackChannels?.length) { + return + } + const error = new Error( + `SlackAppAlertChannel '${this.logicalId}' needs the Checkly Slack app, which is not connected to this account,` + + ` so its alerts cannot be delivered.`, + ) + diagnostics.add(new ErrorDiagnostic({ + title: 'Checkly Slack app not connected', + message: + `${error.message}\n\n` + + `Hint: Connect the Checkly Slack app at` + + ` https://app.checklyhq.com/accounts/${Session.accountId}/settings/account/integrations,` + + ` or remove the channel.`, + error, + })) + } + synthesize () { return { ...super.synthesize(), diff --git a/packages/cli/src/rest/accounts.ts b/packages/cli/src/rest/accounts.ts index 13326d1ef..3e2431894 100644 --- a/packages/cli/src/rest/accounts.ts +++ b/packages/cli/src/rest/accounts.ts @@ -17,6 +17,11 @@ export interface Account { * in which case standard limits apply. */ features?: string[] + /** + * The integrations the account has connected, e.g. `SLACK` for the Checkly + * Slack app. Absent when the API predates it. + */ + connectedIntegrations?: string[] } class Accounts { From aab96c910c2ac4297c2ae3c468b6aaa6c8e0f391 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Tue, 3 Nov 2026 00:48:15 +0900 Subject: [PATCH 2/4] refactor(cli): read the account's integrations list with a connected flag [RED-1289] The account response reports `integrations: [{ type, connected }]`, every type the API reports with whether it is connected, instead of a list of connected types. SlackAppAlertChannel errors only when the SLACK entry says it is not connected, and skips the check when the API lists no SLACK entry. Co-Authored-By: Claude Opus 5.5 --- packages/cli/src/commands/authCommand.ts | 2 +- .../__tests__/slack-app-alert-channel.spec.ts | 20 +++++++++++++------ packages/cli/src/constructs/session.ts | 9 +++++---- .../src/constructs/slack-app-alert-channel.ts | 6 +++--- packages/cli/src/rest/accounts.ts | 12 ++++++++--- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/commands/authCommand.ts b/packages/cli/src/commands/authCommand.ts index 4a93e4b2f..5e634cb38 100644 --- a/packages/cli/src/commands/authCommand.ts +++ b/packages/cli/src/commands/authCommand.ts @@ -113,7 +113,7 @@ export abstract class AuthCommand extends BaseCommand { // the command instance. Session.accountFeatures = this.#account?.features ?? [] Session.accountId = this.#account?.id - Session.accountConnectedIntegrations = this.#account?.connectedIntegrations + Session.accountIntegrations = this.#account?.integrations } protected async validateProject ( diff --git a/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts b/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts index ae59d5857..4b77d8159 100644 --- a/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts +++ b/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts @@ -22,11 +22,11 @@ describe('SlackAppAlertChannel', () => { afterEach(() => { Session.accountId = undefined - Session.accountConnectedIntegrations = undefined + Session.accountIntegrations = undefined }) it('refuses the channel when the account has no Checkly Slack app', async () => { - Session.accountConnectedIntegrations = [] + Session.accountIntegrations = [{ type: 'SLACK', connected: false }] const diagnostics = await validate(['#ops']) @@ -45,15 +45,23 @@ describe('SlackAppAlertChannel', () => { }) it('accepts the channel when the Checkly Slack app is connected', async () => { - Session.accountConnectedIntegrations = ['SLACK'] + Session.accountIntegrations = [{ type: 'SLACK', connected: true }] const diagnostics = await validate(['#ops']) expect(diagnostics.isFatal()).toBe(false) }) - it('skips the check when the API does not report connected integrations', async () => { - Session.accountConnectedIntegrations = undefined + it('skips the check when the API does not report integrations', async () => { + Session.accountIntegrations = undefined + + const diagnostics = await validate(['#ops']) + + expect(diagnostics.isFatal()).toBe(false) + }) + + it('skips the check when the API does not report the Slack app', async () => { + Session.accountIntegrations = [{ type: 'OTHER', connected: false }] const diagnostics = await validate(['#ops']) @@ -61,7 +69,7 @@ describe('SlackAppAlertChannel', () => { }) it('accepts a channel without Slack destinations, which needs no Slack app', async () => { - Session.accountConnectedIntegrations = [] + Session.accountIntegrations = [{ type: 'SLACK', connected: false }] const diagnostics = await validate([]) diff --git a/packages/cli/src/constructs/session.ts b/packages/cli/src/constructs/session.ts index 113152e02..59b897bf5 100644 --- a/packages/cli/src/constructs/session.ts +++ b/packages/cli/src/constructs/session.ts @@ -6,6 +6,7 @@ import { type EngineDetectionResult } from '../services/engine-detector.js' import { Parser } from '../services/check-parser/parser.js' import { Construct } from './construct.js' import { PrivateLocationApi } from '../rest/private-locations.js' +import type { AccountIntegration } from '../rest/accounts.js' import { FileLoader, JitiFileLoader, @@ -92,10 +93,10 @@ export class Session { static accountFeatures: string[] = [] static accountId?: string /** - * The integrations the authenticated account has connected, or undefined - * when unknown (not authenticated, or an API that does not report them). + * The integrations the API reports for the authenticated account, or + * undefined when not authenticated or the API does not report them. */ - static accountConnectedIntegrations?: string[] + static accountIntegrations?: AccountIntegration[] static privateLocations: PrivateLocationApi[] static parsers = new Map() static playwrightProjectBundler?: PlaywrightProjectBundler @@ -131,7 +132,7 @@ export class Session { this.checklyConfigFileConstructs = undefined this.accountFeatures = [] this.accountId = undefined - this.accountConnectedIntegrations = undefined + this.accountIntegrations = undefined this.privateLocations = [] this.parsers = new Map() this.playwrightProjectBundler = undefined diff --git a/packages/cli/src/constructs/slack-app-alert-channel.ts b/packages/cli/src/constructs/slack-app-alert-channel.ts index dd10554c0..f087c30c4 100644 --- a/packages/cli/src/constructs/slack-app-alert-channel.ts +++ b/packages/cli/src/constructs/slack-app-alert-channel.ts @@ -44,13 +44,13 @@ export class SlackAppAlertChannel extends AlertChannel { /** * Refuses the channel when the account has no Checkly Slack app: its alerts * cannot be delivered, whether the channel is new or was deployed before the - * app was disconnected. Skipped when the account's integrations are unknown. + * app was disconnected. Skipped when the API does not report the Slack app. */ async validate (diagnostics: Diagnostics): Promise { await super.validate(diagnostics) - const connected = Session.accountConnectedIntegrations - if (connected === undefined || connected.includes(SLACK_INTEGRATION) || !this.slackChannels?.length) { + const slack = Session.accountIntegrations?.find(({ type }) => type === SLACK_INTEGRATION) + if (slack === undefined || slack.connected || !this.slackChannels?.length) { return } const error = new Error( diff --git a/packages/cli/src/rest/accounts.ts b/packages/cli/src/rest/accounts.ts index 3e2431894..c3a180469 100644 --- a/packages/cli/src/rest/accounts.ts +++ b/packages/cli/src/rest/accounts.ts @@ -18,10 +18,16 @@ export interface Account { */ features?: string[] /** - * The integrations the account has connected, e.g. `SLACK` for the Checkly - * Slack app. Absent when the API predates it. + * Every integration type the API reports, each marked connected or not, e.g. + * `SLACK` for the Checkly Slack app. A type missing from the list is one the + * API does not report. Absent when the API predates it. */ - connectedIntegrations?: string[] + integrations?: AccountIntegration[] +} + +export interface AccountIntegration { + type: string + connected: boolean } class Accounts { From da8ac9817f2690c6fbef13b15b317065965ca6c2 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Tue, 3 Nov 2026 01:22:20 +0900 Subject: [PATCH 3/4] refactor(cli): keep the authenticated account on Session [RED-1289] Session.account holds the account AuthCommand loads, replacing the separate Session.accountId and Session.accountIntegrations copies the SlackAppAlertChannel check read. Co-Authored-By: Claude Opus 5.5 --- packages/cli/src/commands/authCommand.ts | 3 +-- .../__tests__/slack-app-alert-channel.spec.ts | 22 ++++++++++++------- packages/cli/src/constructs/session.ts | 12 +++++----- .../src/constructs/slack-app-alert-channel.ts | 4 ++-- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/packages/cli/src/commands/authCommand.ts b/packages/cli/src/commands/authCommand.ts index 5e634cb38..da47110e2 100644 --- a/packages/cli/src/commands/authCommand.ts +++ b/packages/cli/src/commands/authCommand.ts @@ -112,8 +112,7 @@ export abstract class AuthCommand extends BaseCommand { // Constructs validate against account-specific limits and have no access to // the command instance. Session.accountFeatures = this.#account?.features ?? [] - Session.accountId = this.#account?.id - Session.accountIntegrations = this.#account?.integrations + Session.account = this.#account } protected async validateProject ( diff --git a/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts b/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts index 4b77d8159..e86fa714b 100644 --- a/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts +++ b/packages/cli/src/constructs/__tests__/slack-app-alert-channel.spec.ts @@ -4,6 +4,14 @@ import { SlackAppAlertChannel } from '../index.js' import { Diagnostics } from '../diagnostics.js' import { Project } from '../project.js' import { Session } from '../session.js' +import type { Account, AccountIntegration } from '../../rest/accounts.js' + +const withIntegrations = (integrations?: AccountIntegration[]): Account => ({ + id: 'acc-1', + name: 'Test Account', + runtimeId: '2025.04', + integrations, +}) const validate = async (slackChannels: string[]) => { const diagnostics = new Diagnostics() @@ -17,16 +25,14 @@ describe('SlackAppAlertChannel', () => { name: 'Test Project', repoUrl: 'https://github.com/checkly/checkly-cli', }) - Session.accountId = 'acc-1' }) afterEach(() => { - Session.accountId = undefined - Session.accountIntegrations = undefined + Session.account = undefined }) it('refuses the channel when the account has no Checkly Slack app', async () => { - Session.accountIntegrations = [{ type: 'SLACK', connected: false }] + Session.account = withIntegrations([{ type: 'SLACK', connected: false }]) const diagnostics = await validate(['#ops']) @@ -45,7 +51,7 @@ describe('SlackAppAlertChannel', () => { }) it('accepts the channel when the Checkly Slack app is connected', async () => { - Session.accountIntegrations = [{ type: 'SLACK', connected: true }] + Session.account = withIntegrations([{ type: 'SLACK', connected: true }]) const diagnostics = await validate(['#ops']) @@ -53,7 +59,7 @@ describe('SlackAppAlertChannel', () => { }) it('skips the check when the API does not report integrations', async () => { - Session.accountIntegrations = undefined + Session.account = withIntegrations(undefined) const diagnostics = await validate(['#ops']) @@ -61,7 +67,7 @@ describe('SlackAppAlertChannel', () => { }) it('skips the check when the API does not report the Slack app', async () => { - Session.accountIntegrations = [{ type: 'OTHER', connected: false }] + Session.account = withIntegrations([{ type: 'OTHER', connected: false }]) const diagnostics = await validate(['#ops']) @@ -69,7 +75,7 @@ describe('SlackAppAlertChannel', () => { }) it('accepts a channel without Slack destinations, which needs no Slack app', async () => { - Session.accountIntegrations = [{ type: 'SLACK', connected: false }] + Session.account = withIntegrations([{ type: 'SLACK', connected: false }]) const diagnostics = await validate([]) diff --git a/packages/cli/src/constructs/session.ts b/packages/cli/src/constructs/session.ts index 59b897bf5..ad44c3d47 100644 --- a/packages/cli/src/constructs/session.ts +++ b/packages/cli/src/constructs/session.ts @@ -6,7 +6,7 @@ import { type EngineDetectionResult } from '../services/engine-detector.js' import { Parser } from '../services/check-parser/parser.js' import { Construct } from './construct.js' import { PrivateLocationApi } from '../rest/private-locations.js' -import type { AccountIntegration } from '../rest/accounts.js' +import type { Account } from '../rest/accounts.js' import { FileLoader, JitiFileLoader, @@ -91,12 +91,11 @@ export class Session { static loadingChecklyConfigFile: boolean static checklyConfigFileConstructs?: Construct[] static accountFeatures: string[] = [] - static accountId?: string /** - * The integrations the API reports for the authenticated account, or - * undefined when not authenticated or the API does not report them. + * The authenticated account, for constructs that validate against it. + * Undefined when the command does not authenticate. */ - static accountIntegrations?: AccountIntegration[] + static account?: Account static privateLocations: PrivateLocationApi[] static parsers = new Map() static playwrightProjectBundler?: PlaywrightProjectBundler @@ -131,8 +130,7 @@ export class Session { this.loadingChecklyConfigFile = false this.checklyConfigFileConstructs = undefined this.accountFeatures = [] - this.accountId = undefined - this.accountIntegrations = undefined + this.account = undefined this.privateLocations = [] this.parsers = new Map() this.playwrightProjectBundler = undefined diff --git a/packages/cli/src/constructs/slack-app-alert-channel.ts b/packages/cli/src/constructs/slack-app-alert-channel.ts index f087c30c4..60451b5fa 100644 --- a/packages/cli/src/constructs/slack-app-alert-channel.ts +++ b/packages/cli/src/constructs/slack-app-alert-channel.ts @@ -49,7 +49,7 @@ export class SlackAppAlertChannel extends AlertChannel { async validate (diagnostics: Diagnostics): Promise { await super.validate(diagnostics) - const slack = Session.accountIntegrations?.find(({ type }) => type === SLACK_INTEGRATION) + const slack = Session.account?.integrations?.find(({ type }) => type === SLACK_INTEGRATION) if (slack === undefined || slack.connected || !this.slackChannels?.length) { return } @@ -62,7 +62,7 @@ export class SlackAppAlertChannel extends AlertChannel { message: `${error.message}\n\n` + `Hint: Connect the Checkly Slack app at` - + ` https://app.checklyhq.com/accounts/${Session.accountId}/settings/account/integrations,` + + ` https://app.checklyhq.com/accounts/${Session.account?.id}/settings/account/integrations,` + ` or remove the channel.`, error, })) From dbd22984775d282a0c117116443b6d3830a0fb92 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Tue, 3 Nov 2026 01:22:33 +0900 Subject: [PATCH 4/4] refactor(cli): read account features from Session.account responseTimeLimits reads the features of Session.account instead of a separate Session.accountFeatures copy. An unauthenticated session or an API without features still gets the standard limits. Co-Authored-By: Claude Opus 5.5 --- packages/cli/src/commands/authCommand.ts | 1 - .../__tests__/account-features.spec.ts | 22 +++++++++++++------ .../constructs/internal/account-features.ts | 2 +- packages/cli/src/constructs/session.ts | 2 -- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/cli/src/commands/authCommand.ts b/packages/cli/src/commands/authCommand.ts index da47110e2..434377e88 100644 --- a/packages/cli/src/commands/authCommand.ts +++ b/packages/cli/src/commands/authCommand.ts @@ -111,7 +111,6 @@ export abstract class AuthCommand extends BaseCommand { config.pinEnvironment() // Constructs validate against account-specific limits and have no access to // the command instance. - Session.accountFeatures = this.#account?.features ?? [] Session.account = this.#account } diff --git a/packages/cli/src/constructs/__tests__/account-features.spec.ts b/packages/cli/src/constructs/__tests__/account-features.spec.ts index 5aa6375f5..e9288fc1f 100644 --- a/packages/cli/src/constructs/__tests__/account-features.spec.ts +++ b/packages/cli/src/constructs/__tests__/account-features.spec.ts @@ -4,6 +4,7 @@ import { ApiCheck, TcpMonitor, TcpRequest, UrlMonitor } from '../index.js' import { Diagnostics } from '../diagnostics.js' import { Project } from '../project.js' import { Session } from '../session.js' +import type { Account } from '../../rest/accounts.js' const EXTENDED_RESPONSE_TIME_LIMITS = 'EXTENDED_RESPONSE_TIME_LIMITS' @@ -19,6 +20,13 @@ const newProject = () => { }) } +const withFeatures = (features: string[]): Account => ({ + id: 'acc-1', + name: 'Test Account', + runtimeId: '2025.04', + features, +}) + const validate = async (construct: { validate: (diagnostics: Diagnostics) => Promise }) => { const diagnostics = new Diagnostics() await construct.validate(diagnostics) @@ -27,12 +35,12 @@ const validate = async (construct: { validate: (diagnostics: Diagnostics) => Pro describe('account-aware response time limits', () => { afterEach(() => { - Session.accountFeatures = [] + Session.account = undefined }) it('rejects an extended response time without the feature', async () => { newProject() - Session.accountFeatures = [] + Session.account = withFeatures([]) const diagnostics = await validate(new TcpMonitor('tcp-standard', { name: 'Test Check', @@ -50,7 +58,7 @@ describe('account-aware response time limits', () => { it('accepts an extended response time with the feature', async () => { newProject() - Session.accountFeatures = [EXTENDED_RESPONSE_TIME_LIMITS] + Session.account = withFeatures([EXTENDED_RESPONSE_TIME_LIMITS]) const diagnostics = await validate(new TcpMonitor('tcp-extended', { name: 'Test Check', @@ -64,7 +72,7 @@ describe('account-aware response time limits', () => { it('defers values above 45s to the API with the feature', async () => { newProject() - Session.accountFeatures = [EXTENDED_RESPONSE_TIME_LIMITS] + Session.account = withFeatures([EXTENDED_RESPONSE_TIME_LIMITS]) const diagnostics = await validate(new TcpMonitor('tcp-over-extended', { name: 'Test Check', @@ -77,7 +85,7 @@ describe('account-aware response time limits', () => { it('keeps the degraded <= max cross-check with the feature', async () => { newProject() - Session.accountFeatures = [EXTENDED_RESPONSE_TIME_LIMITS] + Session.account = withFeatures([EXTENDED_RESPONSE_TIME_LIMITS]) const diagnostics = await validate(new TcpMonitor('tcp-degraded-above-max', { name: 'Test Check', @@ -96,7 +104,7 @@ describe('account-aware response time limits', () => { it('raises the API check limit above its 30s standard', async () => { newProject() - Session.accountFeatures = [EXTENDED_RESPONSE_TIME_LIMITS] + Session.account = withFeatures([EXTENDED_RESPONSE_TIME_LIMITS]) const diagnostics = await validate(new ApiCheck('api-extended', { name: 'Test Check', @@ -109,7 +117,7 @@ describe('account-aware response time limits', () => { it('raises the URL monitor limit above its 30s standard', async () => { newProject() - Session.accountFeatures = [EXTENDED_RESPONSE_TIME_LIMITS] + Session.account = withFeatures([EXTENDED_RESPONSE_TIME_LIMITS]) const diagnostics = await validate(new UrlMonitor('url-extended', { name: 'Test Check', diff --git a/packages/cli/src/constructs/internal/account-features.ts b/packages/cli/src/constructs/internal/account-features.ts index 03961b671..7f2dd9a4b 100644 --- a/packages/cli/src/constructs/internal/account-features.ts +++ b/packages/cli/src/constructs/internal/account-features.ts @@ -16,7 +16,7 @@ const EXTENDED_RESPONSE_TIME_LIMITS = 'EXTENDED_RESPONSE_TIME_LIMITS' * the API does not report features at all (older or self-hosted deployments). */ export function responseTimeLimits (standard: number): ResponseTimeLimits { - const limit = Session.accountFeatures.includes(EXTENDED_RESPONSE_TIME_LIMITS) + const limit = Session.account?.features?.includes(EXTENDED_RESPONSE_TIME_LIMITS) ? Number.MAX_SAFE_INTEGER : standard diff --git a/packages/cli/src/constructs/session.ts b/packages/cli/src/constructs/session.ts index ad44c3d47..764719c27 100644 --- a/packages/cli/src/constructs/session.ts +++ b/packages/cli/src/constructs/session.ts @@ -90,7 +90,6 @@ export class Session { static verifyRuntimeDependencies = true static loadingChecklyConfigFile: boolean static checklyConfigFileConstructs?: Construct[] - static accountFeatures: string[] = [] /** * The authenticated account, for constructs that validate against it. * Undefined when the command does not authenticate. @@ -129,7 +128,6 @@ export class Session { this.verifyRuntimeDependencies = true this.loadingChecklyConfigFile = false this.checklyConfigFileConstructs = undefined - this.accountFeatures = [] this.account = undefined this.privateLocations = [] this.parsers = new Map()