import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import {
chatUrl,
defaultSecretName,
modelsUrl,
providerSecretUrl,
requestHeaders,
} from './providers.ts'
import {
assertHttpsUrl,
isMutationDryRun,
isPreviewDryRun,
parseProviderId,
secretNameFor,
} from './validation.ts'
const here = dirname(fileURLToPath(import.meta.url))
const root = join(here, '..')
test('secret names default and suffix accounts', () => {
assert.equal(defaultSecretName('openai'), 'openaiApiKey')
assert.equal(defaultSecretName('anthropic', 'work'), 'anthropicApiKey-work')
assert.equal(defaultSecretName('groq'), 'groqApiKey')
assert.equal(defaultSecretName('cloudflare'), 'cloudflareApiToken')
assert.equal(
secretNameFor('openaiApiKey', undefined, 'openaiApiKey-live'),
'openaiApiKey-live',
)
})
test('secret names reject Kent-style aliases', () => {
assert.throws(
() => secretNameFor('openaiApiKey', undefined, 'OPENAI_API_KEY'),
/openaiApiKey/,
)
assert.throws(
() => secretNameFor('cloudflareApiToken', undefined, 'cfToken'),
/cloudflareApiToken/,
)
})
test('setup URLs are prefilled for each provider', () => {
assert.match(providerSecretUrl('openai'), /name=openaiApiKey/)
assert.match(providerSecretUrl('openai'), /allowedHosts=api\.openai\.com/)
assert.match(providerSecretUrl('openai'), /scope=user/)
assert.match(providerSecretUrl('anthropic'), /name=anthropicApiKey/)
assert.match(providerSecretUrl('anthropic'), /allowedHosts=api\.anthropic\.com/)
assert.match(providerSecretUrl('groq'), /allowedHosts=api\.groq\.com/)
assert.match(
providerSecretUrl('cloudflare'),
/allowedHosts=api\.cloudflare\.com%2Cgateway\.ai\.cloudflare\.com/,
)
assert.match(
providerSecretUrl('openai-compatible', 'openaiCompatibleApiKey', 'llm.example.com'),
/allowedHosts=llm\.example\.com/,
)
})
test('settings writes default to dry-run unless confirm is explicit', () => {
assert.equal(isMutationDryRun({}), true)
assert.equal(isMutationDryRun({ dryRun: true }), true)
assert.equal(isMutationDryRun({ confirm: true, dryRun: true }), true)
assert.equal(isMutationDryRun({ confirm: true }), false)
assert.equal(isMutationDryRun({ confirm: false }), true)
})
test('completion preview is opt-in via dryRun', () => {
assert.equal(isPreviewDryRun({}), false)
assert.equal(isPreviewDryRun({ dryRun: true }), true)
})
test('chat URLs stay on known provider hosts and never bake account ids', () => {
assert.equal(chatUrl({ provider: 'openai' }).href, 'https://api.openai.com/v1/chat/completions')
assert.equal(chatUrl({ provider: 'anthropic' }).host, 'api.anthropic.com')
assert.equal(chatUrl({ provider: 'groq' }).host, 'api.groq.com')
assert.throws(() => chatUrl({ provider: 'cloudflare' }), /cloudflareAccountId/)
const cf = chatUrl({
provider: 'cloudflare',
cloudflareAccountId: 'acct_example',
})
assert.equal(
cf.href,
'https://api.cloudflare.com/client/v4/accounts/acct_example/ai/v1/chat/completions',
)
const gateway = chatUrl({
provider: 'cloudflare',
cloudflareAccountId: 'acct_example',
cloudflareAiGatewayId: 'my-gateway',
})
assert.match(gateway.href, /gateway\.ai\.cloudflare\.com/)
assert.match(gateway.href, /my-gateway/)
assert.throws(() => chatUrl({ provider: 'openai-compatible' }), /baseUrl/)
const compat = chatUrl({
provider: 'openai-compatible',
baseUrl: 'https://llm.example.com/v1',
})
assert.equal(compat.href, 'https://llm.example.com/v1/chat/completions')
})
test('models list URLs are read-only GETs on the same hosts', () => {
assert.equal(modelsUrl({ provider: 'openai' })?.href, 'https://api.openai.com/v1/models')
assert.equal(modelsUrl({ provider: 'cloudflare' }), null)
assert.ok(modelsUrl({ provider: 'cloudflare', cloudflareAccountId: 'acct_example' }))
})
test('request headers use secret placeholders, never literal keys', () => {
const openai = requestHeaders('openai', 'openaiApiKey')
assert.equal(openai.Authorization, 'Bearer {{secret:openaiApiKey|scope=user}}')
const anthropic = requestHeaders('anthropic', 'anthropicApiKey')
assert.equal(anthropic['x-api-key'], '{{secret:anthropicApiKey|scope=user}}')
assert.equal(anthropic['anthropic-version'], '2023-06-01')
const blob = JSON.stringify({ openai, anthropic })
assert.doesNotMatch(blob, /sk-/)
assert.doesNotMatch(blob, /me@kentcdodds/)
})
test('https baseUrl rejects credentials and http', () => {
assert.throws(() => assertHttpsUrl('http://llm.example.com', 'baseUrl'), /https/)
assert.throws(
() => assertHttpsUrl('https://user:pass@llm.example.com', 'baseUrl'),
/credentials/,
)
assert.equal(assertHttpsUrl('https://llm.example.com/v1/', 'baseUrl'), 'https://llm.example.com/v1')
})
test('unknown providers fail closed', () => {
assert.throws(() => parseProviderId('kent-personal'), /openai/)
})
test('manifest is public MIT utilities with no jobs', () => {
const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
assert.equal(manifest.private, false)
assert.equal(manifest.license, 'MIT')
assert.equal(manifest.name, '@kody/ai')
assert.equal(manifest.kody.id, 'ai')
assert.equal(manifest.kody.category, 'utilities')
assert.ok(!manifest.kody.jobs)
assert.ok(!manifest.kody.subscriptions)
assert.ok((manifest.kody.description as string).length <= 200)
assert.ok(!JSON.stringify(manifest).includes('kentcdodds'))
})
test('README Intent and prefilled secret URLs are present', () => {
const readme = readFileSync(join(root, 'README.md'), 'utf8')
assert.match(readme, /^## Intent$/m)
assert.match(readme, /account\/secrets\/new\?name=openaiApiKey/)
assert.match(readme, /account\/secrets\/new\?name=anthropicApiKey/)
assert.match(readme, /account\/secrets\/new\?name=groqApiKey/)
assert.match(readme, /account\/secrets\/new\?name=cloudflareApiToken/)
assert.match(readme, /kody:@kody\/ai/)
assert.doesNotMatch(readme, /@kentcdodds\/ai/)
assert.doesNotMatch(readme, /cloudflareAiModel/)
assert.doesNotMatch(readme, /mistral-small-3\.1-24b-instruct/)
})
test('source does not bake personal keys or Kent package ids', () => {
const files = [
'src/complete.ts',
'src/providers.ts',
'src/settings.ts',
'src/index.ts',
'src/turn.ts',
'src/model-step.ts',
]
for (const file of files) {
const text = readFileSync(join(root, file), 'utf8')
assert.doesNotMatch(text, /sk-[a-zA-Z0-9]/)
assert.doesNotMatch(text, /@kentcdodds/)
assert.doesNotMatch(text, /value_get/)
assert.doesNotMatch(text, /migrateAiFromValues/)
assert.doesNotMatch(text, /ai-gateway\/gateways/)
}
})