Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/kit

src/smoke-test.ts

132 lines · 3.8 KB · TypeScript
import { accounts } from './accounts.ts'
import { getAccount } from './account.ts'
import { createBroadcastDraft, listBroadcasts } from './broadcasts.ts'
import { subscribeAndTag } from './subscribers.ts'
import { parseAuthInput } from './auth.ts'
import { API_KEY_SETUP_URL, KIT_CALLBACK_URL, KIT_REQUIRED_HOSTS, OAUTH_CONNECT_URL } from './setup.ts'
import { rejectBroadcastSend } from './safety.ts'
import type { KitAuthInput } from './types.ts'
import { requireRecord } from './types.ts'

export type SmokeTestInput = KitAuthInput & {
	/** When true, skip the live account query even if credentials exist. */
	setupOnly?: boolean
}

/**
 * Local dry-run self-check plus an optional live account + broadcast list.
 * Never creates subscribers, drafts, or sends email.
 * @example
 * import kit from 'kody:@kody/kit'
 * const result = await kit({ action: 'smoke-test' })
 */
export async function smokeTest(input: SmokeTestInput = {}) {
	const draftPreview = await createBroadcastDraft({
		subject: 'Kody Kit smoke',
		content: '<p>Dry run only</p>',
		dryRun: true,
	})
	if (!('dryRun' in draftPreview) || draftPreview.dryRun !== true) {
		throw new Error('createBroadcastDraft dryRun self-check failed.')
	}

	const subscribePreview = await subscribeAndTag({
		email_address: 'ada@example.com',
		tagName: 'kody-kit-smoke',
		dryRun: true,
	})
	if (!('dryRun' in subscribePreview) || subscribePreview.dryRun !== true) {
		throw new Error('subscribeAndTag dryRun self-check failed.')
	}

	let sendAtBlocked = false
	try {
		rejectBroadcastSend({ send_at: '2026-01-01T00:00:00Z' }, 'broadcast draft')
	} catch {
		sendAtBlocked = true
	}
	if (!sendAtBlocked) throw new Error('send_at rejection self-check failed.')

	const info = await accounts(input)
	const canLive = Boolean(info.preferredAuth) && input.setupOnly !== true
	const selfCheck = {
		dryRunCreateDraft: true,
		dryRunSubscribeAndTag: true,
		sendAtRejected: sendAtBlocked,
		connectUrlUsesKitHost: info.connectUrl.includes('api.kit.com'),
		apiKeyUrlUsesKitHost: info.apiKeyUrl.includes('api.kit.com'),
		callbackUrl: info.callbackUrl === KIT_CALLBACK_URL,
	}

	if (!canLive) {
		return {
			ok: true,
			live: false,
			selfCheck,
			authMode: info.preferredAuth,
			integrationName: info.integrationName,
			secretName: info.secretName,
			oauthConnected: info.oauthConnected,
			apiKeySaved: info.apiKeySaved,
			setup: {
				connectUrl: info.connectUrl,
				apiKeyUrl: info.apiKeyUrl,
				callbackUrl: info.callbackUrl,
				requiredHosts: info.requiredHosts,
				nextSteps: [
					`Save a v4 API key (do not paste it in chat): ${info.apiKeyUrl}`,
					`Or connect OAuth: ${info.connectUrl}`,
					`Approve host ${KIT_REQUIRED_HOSTS.join(', ')}.`,
				],
			},
		}
	}

	const [account, broadcasts] = await Promise.all([
		getAccount(input),
		listBroadcasts({ ...input, maxItems: 5 }),
	])

	return {
		ok: true,
		live: true,
		selfCheck,
		authMode: info.preferredAuth,
		integrationName: info.integrationName,
		secretName: info.secretName,
		oauthConnected: info.oauthConnected,
		apiKeySaved: info.apiKeySaved,
		account: {
			id: account.id,
			name: account.name,
			planType: account.planType,
		},
		broadcastCount: broadcasts.length,
		sampleBroadcast: broadcasts[0]
			? { id: broadcasts[0].id, subject: broadcasts[0].subject, status: broadcasts[0].status }
			: null,
		setup: {
			connectUrl: OAUTH_CONNECT_URL,
			apiKeyUrl: API_KEY_SETUP_URL,
			requiredHosts: [...KIT_REQUIRED_HOSTS],
		},
	}
}

/**
 * Local dry-run self-check plus an optional live account read.
 * @example
 * import smokeTest from 'kody:@kody/kit/smoke-test'
 * const result = await smokeTest()
 */
export default async function smokeTestEntrypoint(
	params: SmokeTestInput & Record<string, unknown> = {},
) {
	const input = requireRecord(params, 'smoke-test')
	return smokeTest({
		...parseAuthInput(input),
		setupOnly: input.setupOnly === true,
	})
}

export { rejectBroadcastSend }