cc-haha/adapters/common/__tests__/config.test.ts
程序员阿江(Relakkes) f1c5f86b80 feat: keep IM permission approvals consistent
DingTalk can receive interactive card callbacks, but the card UI still depends on a published template, so the adapter now supports a template-backed card path with text commands as the reliable fallback. Telegram and WeChat use the same allow-once, allow-always, and deny semantics so manual authorization behaves the same across platforms.

Constraint: DingTalk button rendering requires an operator-provided interactive card template id
Rejected: Treat the existing AI streaming card template as a permission card | it cannot guarantee visible action buttons
Confidence: high
Scope-risk: moderate
Directive: Do not remove the text approval fallback unless DingTalk card template provisioning is guaranteed
Tested: bun test adapters/common/__tests__/permission.test.ts adapters/dingtalk/__tests__/permission-card.test.ts adapters/telegram/__tests__/telegram.test.ts adapters/common/__tests__/config.test.ts src/server/__tests__/adapters.test.ts
Tested: bunx tsc -p adapters/tsconfig.json --noEmit
Tested: bun run check:adapters
Tested: cd desktop && bun run build
Tested: bun run check:server
Tested: bun run check:docs
Tested: git diff --check
Not-tested: bun run quality:pr is blocked by existing CLI core approval policy; see artifacts/quality-runs/2026-05-03T13-19-56-232Z/report.md
2026-05-04 19:03:36 +08:00

114 lines
4.9 KiB
TypeScript

import { afterEach, describe, expect, it } from 'bun:test'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { getConfiguredWorkDir, loadConfig } from '../config.js'
describe('adapter config defaults', () => {
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR
const originalAdapterDefaultWorkDir = process.env.CLAUDE_ADAPTER_DEFAULT_WORK_DIR
const originalAdapterDefaultProjectDir = process.env.ADAPTER_DEFAULT_PROJECT_DIR
const originalDingtalkPermissionCardTemplateId = process.env.DINGTALK_PERMISSION_CARD_TEMPLATE_ID
const originalPwd = process.env.PWD
afterEach(() => {
restoreEnv('CLAUDE_CONFIG_DIR', originalConfigDir)
restoreEnv('CLAUDE_ADAPTER_DEFAULT_WORK_DIR', originalAdapterDefaultWorkDir)
restoreEnv('ADAPTER_DEFAULT_PROJECT_DIR', originalAdapterDefaultProjectDir)
restoreEnv('DINGTALK_PERMISSION_CARD_TEMPLATE_ID', originalDingtalkPermissionCardTemplateId)
restoreEnv('PWD', originalPwd)
})
it('uses the user shell working directory when no default project is configured', () => {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'adapter-config-'))
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'adapter-workdir-'))
try {
process.env.CLAUDE_CONFIG_DIR = configDir
delete process.env.CLAUDE_ADAPTER_DEFAULT_WORK_DIR
process.env.PWD = workDir
const config = loadConfig()
expect(config.telegram.defaultWorkDir).toBe(fs.realpathSync(workDir))
expect(config.feishu.defaultWorkDir).toBe(fs.realpathSync(workDir))
expect(config.wechat.defaultWorkDir).toBe(fs.realpathSync(workDir))
expect(config.dingtalk.defaultWorkDir).toBe(fs.realpathSync(workDir))
expect(getConfiguredWorkDir(config, config.wechat)).toBe(fs.realpathSync(workDir))
expect(getConfiguredWorkDir(config, config.dingtalk)).toBe(fs.realpathSync(workDir))
} finally {
fs.rmSync(configDir, { recursive: true, force: true })
fs.rmSync(workDir, { recursive: true, force: true })
}
})
it('keeps the explicit default project ahead of the platform default work dir', () => {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'adapter-config-'))
const defaultProjectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'adapter-project-'))
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'adapter-workdir-'))
try {
fs.writeFileSync(
path.join(configDir, 'adapters.json'),
JSON.stringify({ defaultProjectDir }),
)
process.env.CLAUDE_CONFIG_DIR = configDir
process.env.CLAUDE_ADAPTER_DEFAULT_WORK_DIR = workDir
const config = loadConfig()
expect(getConfiguredWorkDir(config, config.wechat)).toBe(defaultProjectDir)
expect(getConfiguredWorkDir(config, config.dingtalk)).toBe(defaultProjectDir)
expect(config.wechat.defaultWorkDir).toBe(fs.realpathSync(workDir))
expect(config.dingtalk.defaultWorkDir).toBe(fs.realpathSync(workDir))
} finally {
fs.rmSync(configDir, { recursive: true, force: true })
fs.rmSync(defaultProjectDir, { recursive: true, force: true })
fs.rmSync(workDir, { recursive: true, force: true })
}
})
it('accepts ADAPTER_DEFAULT_PROJECT_DIR as a sidecar-friendly default work dir override', () => {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'adapter-config-'))
const defaultProjectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'adapter-project-'))
try {
process.env.CLAUDE_CONFIG_DIR = configDir
process.env.ADAPTER_DEFAULT_PROJECT_DIR = defaultProjectDir
delete process.env.CLAUDE_ADAPTER_DEFAULT_WORK_DIR
delete process.env.PWD
const config = loadConfig()
expect(getConfiguredWorkDir(config, config.wechat)).toBe(fs.realpathSync(defaultProjectDir))
expect(getConfiguredWorkDir(config, config.dingtalk)).toBe(fs.realpathSync(defaultProjectDir))
} finally {
fs.rmSync(configDir, { recursive: true, force: true })
fs.rmSync(defaultProjectDir, { recursive: true, force: true })
}
})
it('loads DingTalk permission card template id from file or env', () => {
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'adapter-config-'))
try {
fs.writeFileSync(
path.join(configDir, 'adapters.json'),
JSON.stringify({ dingtalk: { permissionCardTemplateId: 'file-template' } }),
)
process.env.CLAUDE_CONFIG_DIR = configDir
delete process.env.DINGTALK_PERMISSION_CARD_TEMPLATE_ID
expect(loadConfig().dingtalk.permissionCardTemplateId).toBe('file-template')
process.env.DINGTALK_PERMISSION_CARD_TEMPLATE_ID = 'env-template'
expect(loadConfig().dingtalk.permissionCardTemplateId).toBe('env-template')
} finally {
fs.rmSync(configDir, { recursive: true, force: true })
}
})
})
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}