feat: 注册所有新命令到命令系统和工具注册表

- commands.ts: 注册所有新命令(memory-stores、vault、schedule 等),
  移除 require() 动态加载,统一为 ESM import
- tools.ts: 注册 LocalMemoryRecallTool、VaultHttpFetchTool
- 补充命令测试(bridge-kick、commit、commit-push-pr、init-verifiers)
- 补充工具测试(AgentTool、RemoteTrigger、SkillTool、WebFetch、WebSearch)
- 集成测试:autonomy-lifecycle-user-flow 更新
- 探测脚本和功能文档

Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-05-09 23:04:39 +08:00
parent efaf4afd9c
commit 6a182e45b3
25 changed files with 3148 additions and 94 deletions

View File

@@ -15,9 +15,8 @@ import commitPushPr from './commands/commit-push-pr.js'
import compact from './commands/compact/index.js'
import config from './commands/config/index.js'
import { context, contextNonInteractive } from './commands/context/index.js'
import cost from './commands/cost/index.js'
// cost/index.ts re-exports usage — /cost is now an alias of /usage
import diff from './commands/diff/index.js'
import ctx_viz from './commands/ctx_viz/index.js'
import doctor from './commands/doctor/index.js'
import memory from './commands/memory/index.js'
import help from './commands/help/index.js'
@@ -30,7 +29,9 @@ import login from './commands/login/index.js'
import logout from './commands/logout/index.js'
import installGitHubApp from './commands/install-github-app/index.js'
import installSlackApp from './commands/install-slack-app/index.js'
import breakCache from './commands/break-cache/index.js'
import breakCache, {
breakCacheNonInteractive,
} from './commands/break-cache/index.js'
import mcp from './commands/mcp/index.js'
import mobile from './commands/mobile/index.js'
import onboarding from './commands/onboarding/index.js'
@@ -45,12 +46,13 @@ import skills from './commands/skills/index.js'
import status from './commands/status/index.js'
import tasks from './commands/tasks/index.js'
import teleport from './commands/teleport/index.js'
/* eslint-disable @typescript-eslint/no-require-imports */
const agentsPlatform =
process.env.USER_TYPE === 'ant'
? require('./commands/agents-platform/index.js').default
: null
/* eslint-enable @typescript-eslint/no-require-imports */
import agentsPlatform from './commands/agents-platform/index.js'
import scheduleCommand from './commands/schedule/index.js'
import memoryStoresCommand from './commands/memory-stores/index.js'
import skillStoreCommand from './commands/skill-store/index.js'
import vaultCommand from './commands/vault/index.js'
import localVaultCommand from './commands/local-vault/index.js'
import localMemoryCommand from './commands/local-memory/index.js'
import securityReview from './commands/security-review.js'
import bughunter from './commands/bughunter/index.js'
import terminalSetup from './commands/terminalSetup/index.js'
@@ -179,6 +181,7 @@ import mockLimits from './commands/mock-limits/index.js'
import bridgeKick from './commands/bridge-kick.js'
import version from './commands/version.js'
import summary from './commands/summary/index.js'
import recap from './commands/recap/index.js'
import skillLearning from './commands/skill-learning/index.js'
import skillSearch from './commands/skill-search/index.js'
import {
@@ -188,6 +191,7 @@ import {
import antTrace from './commands/ant-trace/index.js'
import perfIssue from './commands/perf-issue/index.js'
import sandboxToggle from './commands/sandbox-toggle/index.js'
import tui, { tuiNonInteractive } from './commands/tui/index.js'
import chrome from './commands/chrome/index.js'
import stickers from './commands/stickers/index.js'
import advisor from './commands/advisor.js'
@@ -227,7 +231,7 @@ import {
import rateLimitOptions from './commands/rate-limit-options/index.js'
import statusline from './commands/statusline.js'
import effort from './commands/effort/index.js'
import stats from './commands/stats/index.js'
// stats/index.ts re-exports usage — /stats is now an alias of /usage
// insights.ts is 113KB (3200 lines, includes diffLines/html rendering). Lazy
// shim defers the heavy module until /insights is actually invoked.
const usageReport: Command = {
@@ -265,32 +269,19 @@ export type {
export { getCommandName, isCommandEnabled } from './types/command.js'
// Commands that get eliminated from the external build
// Public-but-previously-locked commands moved to the main COMMANDS array below:
// commit, commitPushPr, bridgeKick, initVerifiers, autofixPr, onboarding
// Remaining items here are truly Anthropic-internal (admin/diagnostics endpoints
// with no fork backend), so they only show up under USER_TYPE=ant.
export const INTERNAL_ONLY_COMMANDS = [
backfillSessions,
breakCache,
bughunter,
commit,
commitPushPr,
ctx_viz,
goodClaude,
issue,
initVerifiers,
mockLimits,
bridgeKick,
version,
...(subscribePr ? [subscribePr] : []),
resetLimits,
resetLimitsNonInteractive,
onboarding,
share,
teleport,
antTrace,
perfIssue,
env,
oauthRefresh,
debugToolCall,
agentsPlatform,
autofixPr,
].filter(Boolean)
// Declared as a function so that we don't run this until getCommands is called,
@@ -298,6 +289,13 @@ export const INTERNAL_ONLY_COMMANDS = [
const COMMANDS = memoize((): Command[] => [
addDir,
advisor,
agentsPlatform,
scheduleCommand,
memoryStoresCommand,
skillStoreCommand,
vaultCommand,
localVaultCommand,
localMemoryCommand,
autonomy,
provider,
agents,
@@ -312,7 +310,6 @@ const COMMANDS = memoize((): Command[] => [
desktop,
context,
contextNonInteractive,
cost,
diff,
doctor,
effort,
@@ -341,7 +338,6 @@ const COMMANDS = memoize((): Command[] => [
resume,
session,
skills,
stats,
status,
statusline,
stickers,
@@ -398,8 +394,27 @@ const COMMANDS = memoize((): Command[] => [
...(jobCmd ? [jobCmd] : []),
...(forceSnip ? [forceSnip] : []),
summary,
recap,
skillLearning,
skillSearch,
autofixPr,
commit,
commitPushPr,
bridgeKick,
version,
...(subscribePr ? [subscribePr] : []),
initVerifiers,
env,
debugToolCall,
perfIssue,
breakCache,
breakCacheNonInteractive,
issue,
share,
teleport,
tui,
tuiNonInteractive,
onboarding,
...(process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO
? INTERNAL_ONLY_COMMANDS
: []),
@@ -684,8 +699,7 @@ export const REMOTE_SAFE_COMMANDS: Set<Command> = new Set([
theme, // Change terminal theme
color, // Change agent color
vim, // Toggle vim mode
cost, // Show session cost (local cost tracking)
usage, // Show usage info
usage, // Show session cost, plan usage, and activity stats (/cost and /stats are aliases)
copy, // Copy last message
btw, // Quick note
feedback, // Send feedback
@@ -713,7 +727,7 @@ export const BRIDGE_SAFE_COMMANDS: Set<Command> = new Set(
[
compact, // Shrink context — useful mid-session from a phone
clear, // Wipe transcript
cost, // Show session cost
usage, // Show session cost (/cost alias)
summary, // Summarize conversation
releaseNotes, // Show changelog
files, // List tracked files

View File

@@ -0,0 +1,246 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
mock.module('bun:bundle', () => ({
feature: (_name: string) => false,
}))
// Capture injected faults and handle calls for assertions
let mockHandle: any = null
let lastFault: any = null
let fireCloseCalled: number | null = null
let forceReconnectCalled = false
let wakePolled = false
let describeResult = 'bridge-status: ok'
mock.module('src/bridge/bridgeDebug.ts', () => ({
getBridgeDebugHandle: () => mockHandle,
registerBridgeDebugHandle: () => {},
clearBridgeDebugHandle: () => {},
injectBridgeFault: () => {},
wrapApiForFaultInjection: (api: any) => api,
}))
function makeMockHandle() {
return {
fireClose: (code: number) => {
fireCloseCalled = code
},
forceReconnect: () => {
forceReconnectCalled = true
},
injectFault: (fault: any) => {
lastFault = fault
},
wakePollLoop: () => {
wakePolled = true
},
describe: () => describeResult,
}
}
let bridgeKick: any
let callFn:
| ((args: string) => Promise<{ type: string; value: string }>)
| undefined
beforeEach(async () => {
mockHandle = null
lastFault = null
fireCloseCalled = null
forceReconnectCalled = false
wakePolled = false
const mod = await import('../bridge-kick.js')
bridgeKick = mod.default
const loaded = await bridgeKick.load()
callFn = loaded.call
})
afterEach(() => {
mockHandle = null
})
describe('bridge-kick command metadata', () => {
test('has correct name', () => {
expect(bridgeKick.name).toBe('bridge-kick')
})
test('has description', () => {
expect(bridgeKick.description).toBeTruthy()
})
test('type is local', () => {
expect(bridgeKick.type).toBe('local')
})
test('isEnabled returns true when USER_TYPE=ant', () => {
const originalUserType = process.env.USER_TYPE
process.env.USER_TYPE = 'ant'
expect(bridgeKick.isEnabled()).toBe(true)
if (originalUserType === undefined) delete process.env.USER_TYPE
else process.env.USER_TYPE = originalUserType
})
test('isEnabled returns false when USER_TYPE is not ant', () => {
const originalUserType = process.env.USER_TYPE
process.env.USER_TYPE = 'external'
expect(bridgeKick.isEnabled()).toBe(false)
if (originalUserType === undefined) delete process.env.USER_TYPE
else process.env.USER_TYPE = originalUserType
})
test('isEnabled returns false when USER_TYPE not set', () => {
const originalUserType = process.env.USER_TYPE
delete process.env.USER_TYPE
expect(bridgeKick.isEnabled()).toBe(false)
if (originalUserType !== undefined) process.env.USER_TYPE = originalUserType
})
test('supportsNonInteractive is false', () => {
expect(bridgeKick.supportsNonInteractive).toBe(false)
})
test('has load function', () => {
expect(typeof bridgeKick.load).toBe('function')
})
})
describe('bridge-kick call - no handle registered', () => {
test('returns error message when no handle registered', async () => {
mockHandle = null
const result = await callFn!('status')
expect(result.type).toBe('text')
expect(result.value).toContain('No bridge debug handle')
})
})
describe('bridge-kick call - with handle', () => {
beforeEach(() => {
mockHandle = makeMockHandle()
})
test('close with valid code fires close', async () => {
const result = await callFn!('close 1002')
expect(result.type).toBe('text')
expect(result.value).toContain('1002')
expect(fireCloseCalled).toBe(1002)
})
test('close with 1006 fires close(1006)', async () => {
await callFn!('close 1006')
expect(fireCloseCalled).toBe(1006)
})
test('close with non-numeric code returns error', async () => {
const result = await callFn!('close abc')
expect(result.type).toBe('text')
expect(result.value).toContain('need a numeric code')
})
test('poll transient injects transient fault and wakes poll loop', async () => {
const result = await callFn!('poll transient')
expect(result.type).toBe('text')
expect(result.value).toContain('transient')
expect(wakePolled).toBe(true)
expect(lastFault?.kind).toBe('transient')
expect(lastFault?.method).toBe('pollForWork')
})
test('poll 404 injects fatal fault with not_found_error', async () => {
const result = await callFn!('poll 404')
expect(result.type).toBe('text')
expect(lastFault?.kind).toBe('fatal')
expect(lastFault?.status).toBe(404)
expect(lastFault?.errorType).toBe('not_found_error')
expect(wakePolled).toBe(true)
})
test('poll 401 injects fatal fault with authentication_error default', async () => {
await callFn!('poll 401')
expect(lastFault?.status).toBe(401)
expect(lastFault?.errorType).toBe('authentication_error')
})
test('poll 404 with custom type uses provided type', async () => {
await callFn!('poll 404 custom_error')
expect(lastFault?.errorType).toBe('custom_error')
})
test('poll with non-numeric non-transient returns error', async () => {
const result = await callFn!('poll abc')
expect(result.type).toBe('text')
expect(result.value).toContain('need')
})
test('register fatal injects 403 fatal fault', async () => {
const result = await callFn!('register fatal')
expect(result.type).toBe('text')
expect(result.value).toContain('403')
expect(lastFault?.status).toBe(403)
expect(lastFault?.kind).toBe('fatal')
expect(lastFault?.method).toBe('registerBridgeEnvironment')
})
test('register fail injects transient fault with count 1', async () => {
const result = await callFn!('register fail')
expect(result.type).toBe('text')
expect(lastFault?.kind).toBe('transient')
expect(lastFault?.count).toBe(1)
})
test('register fail 3 injects transient fault with count 3', async () => {
await callFn!('register fail 3')
expect(lastFault?.count).toBe(3)
})
test('reconnect-session fail injects 404 fault for reconnectSession', async () => {
const result = await callFn!('reconnect-session fail')
expect(result.type).toBe('text')
expect(lastFault?.method).toBe('reconnectSession')
expect(lastFault?.status).toBe(404)
expect(lastFault?.count).toBe(2)
})
test('heartbeat 401 injects authentication_error', async () => {
await callFn!('heartbeat 401')
expect(lastFault?.method).toBe('heartbeatWork')
expect(lastFault?.status).toBe(401)
expect(lastFault?.errorType).toBe('authentication_error')
})
test('heartbeat with non-401 status uses not_found_error', async () => {
await callFn!('heartbeat 404')
expect(lastFault?.status).toBe(404)
expect(lastFault?.errorType).toBe('not_found_error')
})
test('heartbeat with no status defaults to 401', async () => {
await callFn!('heartbeat')
expect(lastFault?.status).toBe(401)
})
test('reconnect calls forceReconnect', async () => {
const result = await callFn!('reconnect')
expect(result.type).toBe('text')
expect(result.value).toContain('reconnect')
expect(forceReconnectCalled).toBe(true)
})
test('status returns bridge description', async () => {
const result = await callFn!('status')
expect(result.type).toBe('text')
expect(result.value).toBe(describeResult)
})
test('unknown subcommand returns usage info', async () => {
const result = await callFn!('unknown-cmd')
expect(result.type).toBe('text')
expect(result.value).toContain('bridge-kick')
})
test('empty args returns usage info', async () => {
const result = await callFn!('')
expect(result.type).toBe('text')
// empty trim → undefined sub → default case
expect(result.value).toBeTruthy()
})
})

View File

@@ -0,0 +1,330 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
import type { Command } from '../../commands.js'
mock.module('bun:bundle', () => ({
feature: (_name: string) => false,
}))
mock.module('src/utils/attribution.ts', () => ({
getAttributionTexts: () => ({ commit: '', pr: '' }),
getEnhancedPRAttribution: async () => undefined,
countUserPromptsInMessages: () => 0,
}))
mock.module('src/utils/undercover.ts', () => ({
isUndercover: () => false,
getUndercoverInstructions: () => '',
shouldShowUndercoverAutoNotice: () => false,
}))
mock.module('src/utils/promptShellExecution.ts', () => ({
executeShellCommandsInPrompt: async (content: string) => content,
}))
// IMPORTANT: mock.module is process-global. findGitRoot/findCanonicalGitRoot
// are SYNC in the real impl (returning string | null) — using async stubs
// here pollutes downstream callers (e.g. jobs/templates.ts) that consume the
// return value as a string. Match the real signatures (sync, string | null)
// so other test files in the same process keep working.
//
// Pure functions (normalizeGitRemoteUrl) are inlined with real semantics so
// git.test.ts and other consumers of this mock don't see null returns when
// the test runs in the full suite.
const isLocalHostForMock = (host: string): boolean => {
const lower = host.toLowerCase().split(':')[0] ?? ''
return lower === 'localhost' || lower === '127.0.0.1' || lower === '::1'
}
const realNormalizeGitRemoteUrl = (url: string): string | null => {
const trimmed = url.trim()
if (!trimmed) return null
const sshMatch = trimmed.match(/^git@([^:]+):(.+?)(?:\.git)?$/)
if (sshMatch && sshMatch[1] && sshMatch[2]) {
return `${sshMatch[1]}/${sshMatch[2]}`.toLowerCase()
}
const urlMatch = trimmed.match(
/^(?:https?|ssh):\/\/(?:[^@]+@)?([^/]+)\/(.+?)(?:\.git)?$/,
)
if (urlMatch && urlMatch[1] && urlMatch[2]) {
const host = urlMatch[1]
const p = urlMatch[2]
if (isLocalHostForMock(host) && p.startsWith('git/')) {
const proxyPath = p.slice(4)
const segments = proxyPath.split('/')
if (segments.length >= 3 && segments[0]!.includes('.')) {
return proxyPath.toLowerCase()
}
return `github.com/${proxyPath}`.toLowerCase()
}
return `${host}/${p}`.toLowerCase()
}
return null
}
mock.module('src/utils/git.ts', () => ({
getDefaultBranch: async () => 'main',
findGitRoot: (_startPath?: string) => '/fake/root',
findCanonicalGitRoot: (_startPath?: string) => '/fake/root',
gitExe: () => 'git',
getIsGit: async () => true,
getGitDir: async () => null,
isAtGitRoot: async () => true,
dirIsInGitRepo: async () => true,
getHead: async () => 'abc123',
getBranch: async () => 'main',
// The following exports are referenced by markdownConfigLoader (and other
// transitive consumers) — provide minimal stubs so the mock surface covers
// every real export and downstream callers don't see undefined.
getRemoteUrl: async () => null,
normalizeGitRemoteUrl: realNormalizeGitRemoteUrl,
getRepoRemoteHash: async () => null,
getIsHeadOnRemote: async () => false,
hasUnpushedCommits: async () => false,
getIsClean: async () => true,
getChangedFiles: async () => [] as string[],
getFileStatus: async () => ({
added: [],
modified: [],
deleted: [],
renamed: [],
untracked: [],
}),
getWorktreeCount: async () => 1,
stashToCleanState: async () => false,
getGitState: async () => null,
getGithubRepo: async () => null,
findRemoteBase: async () => null,
preserveGitStateForIssue: async () => null,
isCurrentDirectoryBareGitRepo: () => false,
}))
let commitPushPr: Command
let originalUserType: string | undefined
let originalSafeUser: string | undefined
let originalUser: string | undefined
beforeEach(async () => {
originalUserType = process.env.USER_TYPE
originalSafeUser = process.env.SAFEUSER
originalUser = process.env.USER
const mod = await import('../commit-push-pr.js')
commitPushPr = mod.default as Command
})
afterEach(() => {
if (originalUserType === undefined) delete process.env.USER_TYPE
else process.env.USER_TYPE = originalUserType
if (originalSafeUser === undefined) delete process.env.SAFEUSER
else process.env.SAFEUSER = originalSafeUser
if (originalUser === undefined) delete process.env.USER
else process.env.USER = originalUser
})
describe('commit-push-pr command metadata', () => {
test('has correct name', () => {
expect(commitPushPr.name).toBe('commit-push-pr')
})
test('has description', () => {
expect(commitPushPr.description).toBeTruthy()
expect(typeof commitPushPr.description).toBe('string')
})
test('type is prompt', () => {
expect(commitPushPr.type).toBe('prompt')
})
test('has progressMessage', () => {
expect((commitPushPr as any).progressMessage).toBeTruthy()
})
test('source is builtin', () => {
expect((commitPushPr as any).source).toBe('builtin')
})
test('has allowedTools array with git and gh tools', () => {
const tools = (commitPushPr as any).allowedTools as string[]
expect(Array.isArray(tools)).toBe(true)
expect(tools.some(t => t.includes('git push'))).toBe(true)
expect(tools.some(t => t.includes('gh pr create'))).toBe(true)
expect(tools.some(t => t.includes('git add'))).toBe(true)
expect(tools.some(t => t.includes('git commit'))).toBe(true)
})
test('contentLength getter returns a number', () => {
const len = (commitPushPr as any).contentLength
expect(typeof len).toBe('number')
expect(len).toBeGreaterThan(0)
})
})
describe('commit-push-pr getPromptForCommand', () => {
const makeContext = () => ({
getAppState: () => ({
toolPermissionContext: {
alwaysAllowRules: { command: [] },
},
}),
})
test('returns array with text type for empty args', async () => {
const result = await (commitPushPr as any).getPromptForCommand(
'',
makeContext(),
)
expect(Array.isArray(result)).toBe(true)
expect(result[0].type).toBe('text')
})
test('result text contains pull request instructions', async () => {
const result = await (commitPushPr as any).getPromptForCommand(
'',
makeContext(),
)
expect(result[0].text).toContain('PR')
})
test('result text contains default branch', async () => {
const result = await (commitPushPr as any).getPromptForCommand(
'',
makeContext(),
)
expect(result[0].text).toContain('main')
})
test('appends additional user instructions when args provided', async () => {
const result = await (commitPushPr as any).getPromptForCommand(
'Fix the bug',
makeContext(),
)
expect(result[0].text).toContain('Fix the bug')
expect(result[0].text).toContain('Additional instructions')
})
test('does not append additional instructions section for whitespace-only args', async () => {
const result = await (commitPushPr as any).getPromptForCommand(
' ',
makeContext(),
)
expect(result[0].text).not.toContain('Additional instructions')
})
test('handles null/undefined args gracefully', async () => {
const result = await (commitPushPr as any).getPromptForCommand(
undefined,
makeContext(),
)
expect(Array.isArray(result)).toBe(true)
expect(result[0].type).toBe('text')
})
test('with ant user type and not undercover, includes reviewer arg', async () => {
process.env.USER_TYPE = 'external'
const result = await (commitPushPr as any).getPromptForCommand(
'',
makeContext(),
)
expect(result[0].text).toContain('gh pr create')
})
test('with SAFEUSER env var set, text contains context', async () => {
process.env.SAFEUSER = 'testuser'
const result = await (commitPushPr as any).getPromptForCommand(
'',
makeContext(),
)
expect(result[0].text).toContain('SAFEUSER')
})
test('with ant user type and undercover, strips reviewer args', async () => {
process.env.USER_TYPE = 'ant'
// isUndercover is mocked as false, so no prefix should be added
const result = await (commitPushPr as any).getPromptForCommand(
'',
makeContext(),
)
expect(Array.isArray(result)).toBe(true)
})
test('with args containing newlines, appends full multi-line instructions', async () => {
const multiline = 'Line one\nLine two\nLine three'
const result = await (commitPushPr as any).getPromptForCommand(
multiline,
makeContext(),
)
expect(result[0].text).toContain('Line one')
expect(result[0].text).toContain('Line three')
})
test('getAppState override in context includes ALLOWED_TOOLS', async () => {
let capturedGetAppState: (() => any) | undefined
// Re-mock executeShellCommandsInPrompt to capture the context argument
mock.module('src/utils/promptShellExecution.ts', () => ({
executeShellCommandsInPrompt: async (content: string, ctx: any) => {
capturedGetAppState = ctx.getAppState.bind(ctx)
return content
},
}))
// Re-import to pick up the new mock
const { default: freshCmd } = await import('../commit-push-pr.js')
await (freshCmd as any).getPromptForCommand('', {
getAppState: () => ({
toolPermissionContext: {
alwaysAllowRules: { command: ['pre-existing'] },
extra: true,
},
someState: 'value',
}),
})
expect(capturedGetAppState).toBeDefined()
const resultState = capturedGetAppState!()
expect(
Array.isArray(resultState.toolPermissionContext.alwaysAllowRules.command),
).toBe(true)
// Should have replaced with ALLOWED_TOOLS
expect(
resultState.toolPermissionContext.alwaysAllowRules.command.length,
).toBeGreaterThan(0)
expect(resultState.someState).toBe('value')
})
test('ant undercover path strips reviewer/slack/changelog sections', async () => {
process.env.USER_TYPE = 'ant'
// Re-mock undercover to return true for this test
mock.module('src/utils/undercover.ts', () => ({
isUndercover: () => true,
getUndercoverInstructions: () => 'UNDERCOVER_INSTRUCTIONS',
shouldShowUndercoverAutoNotice: () => false,
}))
// Also re-mock attribution to return commit text
mock.module('src/utils/attribution.ts', () => ({
getAttributionTexts: () => ({
commit: 'Attribution text',
pr: 'PR Attribution',
}),
getEnhancedPRAttribution: async () => 'Enhanced PR Attribution',
countUserPromptsInMessages: () => 0,
}))
const { default: freshCmd } = await import('../commit-push-pr.js')
const result = await (freshCmd as any).getPromptForCommand(
'',
makeContext(),
)
expect(Array.isArray(result)).toBe(true)
// The undercover path removes slackStep, changelogSection, and reviewer args
// The prompt should not contain those sections
expect(result[0].text).not.toContain('CHANGELOG:START')
expect(result[0].text).not.toContain('Slack')
})
})

View File

@@ -0,0 +1,273 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
import type { Command } from '../../commands.js'
// Mock bun:bundle before any imports that use feature()
mock.module('bun:bundle', () => ({
feature: (_name: string) => false,
}))
// Mock dependencies to avoid side effects
mock.module('src/utils/attribution.ts', () => ({
getAttributionTexts: () => ({ commit: '', pr: '' }),
getEnhancedPRAttribution: async () => undefined,
countUserPromptsInMessages: () => 0,
}))
mock.module('src/utils/undercover.ts', () => ({
isUndercover: () => false,
getUndercoverInstructions: () => '',
shouldShowUndercoverAutoNotice: () => false,
}))
mock.module('src/utils/promptShellExecution.ts', () => ({
executeShellCommandsInPrompt: async (content: string) => content,
}))
let commit: Command
let originalUserType: string | undefined
beforeEach(async () => {
originalUserType = process.env.USER_TYPE
const mod = await import('../commit.js')
commit = mod.default as Command
})
afterEach(() => {
if (originalUserType === undefined) {
delete process.env.USER_TYPE
} else {
process.env.USER_TYPE = originalUserType
}
})
describe('commit command metadata', () => {
test('has correct name', () => {
expect(commit.name).toBe('commit')
})
test('has description', () => {
expect(commit.description).toBeTruthy()
expect(typeof commit.description).toBe('string')
})
test('type is prompt', () => {
expect(commit.type).toBe('prompt')
})
test('has progressMessage', () => {
expect((commit as any).progressMessage).toBeTruthy()
})
test('source is builtin', () => {
expect((commit as any).source).toBe('builtin')
})
test('has allowedTools array', () => {
const tools = (commit as any).allowedTools
expect(Array.isArray(tools)).toBe(true)
expect(tools.length).toBeGreaterThan(0)
})
test('allowedTools includes git add', () => {
const tools = (commit as any).allowedTools as string[]
expect(tools.some(t => t.includes('git add'))).toBe(true)
})
test('allowedTools includes git commit', () => {
const tools = (commit as any).allowedTools as string[]
expect(tools.some(t => t.includes('git commit'))).toBe(true)
})
test('allowedTools includes git status', () => {
const tools = (commit as any).allowedTools as string[]
expect(tools.some(t => t.includes('git status'))).toBe(true)
})
test('contentLength is 0 (dynamic)', () => {
expect((commit as any).contentLength).toBe(0)
})
})
describe('commit command getPromptForCommand', () => {
test('returns array with text type', async () => {
const mockContext = {
getAppState: () => ({
toolPermissionContext: {
alwaysAllowRules: { command: [] },
},
}),
}
const result = await (commit as any).getPromptForCommand('', mockContext)
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBeGreaterThan(0)
expect(result[0].type).toBe('text')
})
test('result text contains git instructions', async () => {
const mockContext = {
getAppState: () => ({
toolPermissionContext: {
alwaysAllowRules: { command: [] },
},
}),
}
const result = await (commit as any).getPromptForCommand('', mockContext)
expect(result[0].text).toContain('git')
})
test('result text contains git status', async () => {
const mockContext = {
getAppState: () => ({
toolPermissionContext: {
alwaysAllowRules: { command: [] },
},
}),
}
const result = await (commit as any).getPromptForCommand('', mockContext)
expect(result[0].text).toContain('git status')
})
test('result text contains commit message instructions', async () => {
const mockContext = {
getAppState: () => ({
toolPermissionContext: {
alwaysAllowRules: { command: [] },
},
}),
}
const result = await (commit as any).getPromptForCommand('', mockContext)
expect(result[0].text).toContain('commit')
})
test('getAppState override preserves alwaysAllowRules', async () => {
let capturedAppState: any
const mockContext = {
getAppState: () => ({
toolPermissionContext: {
alwaysAllowRules: { command: ['existing-rule'] },
otherProp: 'test',
},
otherState: 'value',
}),
}
// Wrap executeShellCommandsInPrompt to capture context
mock.module('src/utils/promptShellExecution.ts', () => ({
executeShellCommandsInPrompt: async (content: string, ctx: any) => {
capturedAppState = ctx.getAppState()
return content
},
}))
const mod = await import('../commit.js')
const freshCommit = mod.default as any
await freshCommit.getPromptForCommand('', mockContext)
// The override should include alwaysAllowRules with command tools
if (capturedAppState) {
expect(
capturedAppState.toolPermissionContext.alwaysAllowRules.command,
).toBeDefined()
}
})
test('getPromptForCommand with non-ant user_type does not include undercover prefix', async () => {
process.env.USER_TYPE = 'external'
const mockContext = {
getAppState: () => ({
toolPermissionContext: {
alwaysAllowRules: { command: [] },
},
}),
}
const result = await (commit as any).getPromptForCommand('', mockContext)
expect(Array.isArray(result)).toBe(true)
})
test('getPromptForCommand with ant user_type and undercover', async () => {
process.env.USER_TYPE = 'ant'
// isUndercover is mocked to return false, so prefix stays empty
const mockContext = {
getAppState: () => ({
toolPermissionContext: {
alwaysAllowRules: { command: [] },
},
}),
}
const result = await (commit as any).getPromptForCommand('', mockContext)
expect(Array.isArray(result)).toBe(true)
expect(result[0].type).toBe('text')
})
test('ant undercover path prepends undercover instructions', async () => {
process.env.USER_TYPE = 'ant'
mock.module('src/utils/undercover.ts', () => ({
isUndercover: () => true,
getUndercoverInstructions: () => 'SECRET_UNDERCOVER_PREFIX',
shouldShowUndercoverAutoNotice: () => false,
}))
mock.module('src/utils/attribution.ts', () => ({
getAttributionTexts: () => ({ commit: 'Co-Authored-By: Claude', pr: '' }),
getEnhancedPRAttribution: async () => undefined,
countUserPromptsInMessages: () => 0,
}))
const { default: freshCommit } = await import('../commit.js')
const mockContext = {
getAppState: () => ({
toolPermissionContext: {
alwaysAllowRules: { command: [] },
},
}),
}
const result = await (freshCommit as any).getPromptForCommand(
'',
mockContext,
)
expect(Array.isArray(result)).toBe(true)
expect(result[0].text).toContain('SECRET_UNDERCOVER_PREFIX')
expect(result[0].text).toContain('Co-Authored-By')
})
test('getAppState override in context passes ALLOWED_TOOLS', async () => {
let capturedCtx: any
mock.module('src/utils/promptShellExecution.ts', () => ({
executeShellCommandsInPrompt: async (content: string, ctx: any) => {
capturedCtx = ctx
return content
},
}))
const { default: freshCommit } = await import('../commit.js')
const baseAppState = {
toolPermissionContext: {
alwaysAllowRules: { command: ['old-rule'] },
otherProp: 'keep-this',
},
globalState: 'preserved',
}
const mockContext = {
getAppState: () => baseAppState,
}
await (freshCommit as any).getPromptForCommand('', mockContext)
expect(capturedCtx).toBeDefined()
const overriddenState = capturedCtx.getAppState()
expect(overriddenState.globalState).toBe('preserved')
expect(
Array.isArray(
overriddenState.toolPermissionContext.alwaysAllowRules.command,
),
).toBe(true)
expect(
overriddenState.toolPermissionContext.alwaysAllowRules.command.some(
(t: string) => t.includes('git add'),
),
).toBe(true)
})
})

View File

@@ -0,0 +1,113 @@
import { describe, expect, test } from 'bun:test'
// init-verifiers.ts has no external dependencies that need mocking
// It's a simple prompt-type command that returns a static text prompt
let initVerifiers: any
// Import once - no async deps
const mod = await import('../init-verifiers.js')
initVerifiers = mod.default
describe('init-verifiers command metadata', () => {
test('has correct name', () => {
expect(initVerifiers.name).toBe('init-verifiers')
})
test('has description', () => {
expect(initVerifiers.description).toBeTruthy()
expect(typeof initVerifiers.description).toBe('string')
})
test('type is prompt', () => {
expect(initVerifiers.type).toBe('prompt')
})
test('has progressMessage', () => {
expect(initVerifiers.progressMessage).toBeTruthy()
})
test('source is builtin', () => {
expect(initVerifiers.source).toBe('builtin')
})
test('contentLength is 0 (dynamic)', () => {
expect(initVerifiers.contentLength).toBe(0)
})
})
describe('init-verifiers getPromptForCommand', () => {
test('returns a non-empty array', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBeGreaterThan(0)
})
test('first element has type "text"', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].type).toBe('text')
})
test('text contains Phase 1 auto-detection instructions', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].text).toContain('Phase 1')
})
test('text contains Phase 2 verification tool setup', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].text).toContain('Phase 2')
})
test('text contains Phase 3 interactive Q&A', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].text).toContain('Phase 3')
})
test('text contains Phase 4 generate verifier skill', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].text).toContain('Phase 4')
})
test('text contains Phase 5 confirm creation', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].text).toContain('Phase 5')
})
test('text mentions Playwright', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].text).toContain('Playwright')
})
test('text mentions SKILL.md template', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].text).toContain('SKILL.md')
})
test('text mentions TodoWrite tool', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].text).toContain('TodoWrite')
})
test('text mentions verifier naming convention', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].text).toContain('verifier')
})
test('text mentions authentication handling', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(result[0].text).toContain('Authentication')
})
test('text is a non-empty string', async () => {
const result = await initVerifiers.getPromptForCommand()
expect(typeof result[0].text).toBe('string')
expect(result[0].text.length).toBeGreaterThan(100)
})
test('works with no arguments (no args parameter)', async () => {
// getPromptForCommand takes no required params
const result = await initVerifiers.getPromptForCommand(undefined, undefined)
expect(Array.isArray(result)).toBe(true)
expect(result.length).toBeGreaterThan(0)
})
})

View File

@@ -1,9 +1,26 @@
import { mock, describe, expect, test, afterEach } from 'bun:test'
import {
mock,
describe,
expect,
test,
afterEach,
beforeAll,
afterAll,
} from 'bun:test'
import { debugMock } from '../../../../tests/mocks/debug'
import { setupAxiosMock } from '../../../../tests/mocks/axios.js'
const axiosHandle = setupAxiosMock()
axiosHandle.stubs.get = async () => ({ data: { servers: [] } })
beforeAll(() => {
axiosHandle.useStubs = true
})
afterAll(() => {
axiosHandle.useStubs = false
})
mock.module('axios', () => ({
default: { get: async () => ({ data: { servers: [] } }) },
}))
mock.module('src/utils/debug.ts', debugMock)
const { isOfficialMcpUrl, resetOfficialMcpUrlsForTesting } = await import(

View File

@@ -87,6 +87,8 @@ import { EnterPlanModeTool } from '@claude-code-best/builtin-tools/tools/EnterPl
import { EnterWorktreeTool } from '@claude-code-best/builtin-tools/tools/EnterWorktreeTool/EnterWorktreeTool.js'
import { ExitWorktreeTool } from '@claude-code-best/builtin-tools/tools/ExitWorktreeTool/ExitWorktreeTool.js'
import { ConfigTool } from '@claude-code-best/builtin-tools/tools/ConfigTool/ConfigTool.js'
import { LocalMemoryRecallTool } from '@claude-code-best/builtin-tools/tools/LocalMemoryRecallTool/LocalMemoryRecallTool.js'
import { VaultHttpFetchTool } from '@claude-code-best/builtin-tools/tools/VaultHttpFetchTool/VaultHttpFetchTool.js'
import { TaskCreateTool } from '@claude-code-best/builtin-tools/tools/TaskCreateTool/TaskCreateTool.js'
import { TaskGetTool } from '@claude-code-best/builtin-tools/tools/TaskGetTool/TaskGetTool.js'
import { TaskUpdateTool } from '@claude-code-best/builtin-tools/tools/TaskUpdateTool/TaskUpdateTool.js'
@@ -233,6 +235,8 @@ export function getAllBaseTools(): Tools {
AskUserQuestionTool,
SkillTool,
EnterPlanModeTool,
LocalMemoryRecallTool,
VaultHttpFetchTool,
...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),
...(SuggestBackgroundPRTool ? [SuggestBackgroundPRTool] : []),