feat(image-analysis): make MCP-first provisioning self-healing

This commit is contained in:
Tam Nhu Tran committed 2026-04-08 13:36:05 -04:00
1 parent 15ef3ac9a8
commit 64ecd8b3dc
19 files changed
+818 -136

No files matched your search

@@ -384,7 +384,7 @@ describe('CLAUDECODE environment stripping', () => {
});
});
it('headless executor prepares image-analysis MCP and compatibility hook fallback', async () => {
it('headless executor prepares image-analysis MCP and suppresses the legacy hook on healthy launches', async () => {
writeConfigWithAutoUpdatePreference(false);
const ccsDir = path.join(process.env.CCS_HOME as string, '.ccs');
const settingsPath = path.join(ccsDir, 'glm.settings.json');
@@ -398,11 +398,16 @@ describe('CLAUDECODE environment stripping', () => {
expect(result.success).toBe(true);
expect(spawnCalls.length).toBeGreaterThan(0);
const launch = spawnCalls[0];
const env = launch.options?.env as NodeJS.ProcessEnv;
const persistedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')) as {
hooks?: { PreToolUse?: Array<{ matcher?: string }> };
};
expect(persistedSettings.hooks?.PreToolUse?.some((hook) => hook.matcher === 'Read')).toBe(true);
expect(
persistedSettings.hooks?.PreToolUse?.some((hook) => hook.matcher === 'Read') ?? false
).toBe(false);
expect(env.CCS_IMAGE_ANALYSIS_SKIP_HOOK).toBe('1');
const claudeUserConfig = JSON.parse(
fs.readFileSync(path.join(process.env.CCS_HOME as string, '.claude.json'), 'utf8')
@@ -415,8 +420,10 @@ describe('CLAUDECODE environment stripping', () => {
args: [path.join(ccsDir, 'mcp', 'ccs-image-analysis-server.cjs')],
env: {},
});
expect(fs.existsSync(path.join(ccsDir, 'hooks', 'image-analyzer-transformer.cjs'))).toBe(true);
expect(fs.existsSync(path.join(ccsDir, 'hooks', 'image-analysis-runtime.cjs'))).toBe(true);
expect(fs.existsSync(path.join(ccsDir, 'hooks', 'image-analyzer-transformer.cjs'))).toBe(
false
);
expect(fs.existsSync(path.join(ccsDir, 'hooks', 'image-analysis-runtime.cjs'))).toBe(false);
});
it('headless executor propagates a WebSearch trace launch id when tracing is enabled', async () => {
@@ -0,0 +1,135 @@
import { describe, expect, it } from 'bun:test';
import {
deduplicateCcsImageAnalyzerHooks,
isCcsImageAnalyzerHook,
removeCcsImageAnalyzerHooks,
} from '../../../../src/utils/hooks/image-analyzer-hook-utils';
describe('image-analyzer-hook-utils', () => {
it('detects CCS-managed image hooks across current and legacy path variants', () => {
expect(
isCcsImageAnalyzerHook({
matcher: 'Read',
hooks: [
{
type: 'command',
command: 'node "/Users/kaitran/.ccs/hooks/image-analyzer-transformer.cjs"',
},
],
})
).toBe(true);
expect(
isCcsImageAnalyzerHook({
matcher: 'Read',
hooks: [
{
type: 'command',
command: 'node "/home/kai/.ccs/hooks/image-analyzer-transformer.cjs"',
},
],
})
).toBe(true);
expect(
isCcsImageAnalyzerHook({
matcher: 'Read',
hooks: [{ type: 'command', command: 'node "/tmp/custom-read-hook.cjs"' }],
})
).toBe(false);
expect(
isCcsImageAnalyzerHook({
matcher: 'Read',
hooks: [
{
type: 'command',
command: 'node "/Users/kaitran/.ccs/hooks/image-analyzer-transformer-custom.cjs"',
},
],
})
).toBe(false);
});
it('deduplicates only CCS-managed image hooks', () => {
const settings = {
hooks: {
PreToolUse: [
{
matcher: 'Read',
hooks: [
{
type: 'command',
command: 'node "/Users/kaitran/.ccs/hooks/image-analyzer-transformer.cjs"',
},
],
},
{
matcher: 'Read',
hooks: [
{
type: 'command',
command: 'node "/home/kai/.ccs/hooks/image-analyzer-transformer.cjs"',
},
],
},
{
matcher: 'Read',
hooks: [{ type: 'command', command: 'node "/tmp/custom-read-hook.cjs"' }],
},
],
},
} satisfies Record<string, unknown>;
expect(deduplicateCcsImageAnalyzerHooks(settings)).toBe(true);
expect((settings.hooks.PreToolUse as unknown[])).toHaveLength(2);
});
it('removes only CCS-managed image hooks and preserves unrelated hooks', () => {
const settings = {
hooks: {
PreToolUse: [
{
matcher: 'Read',
hooks: [
{
type: 'command',
command: 'node "/Users/kaitran/.ccs/hooks/image-analyzer-transformer.cjs"',
},
],
},
{
matcher: 'Read',
hooks: [{ type: 'command', command: 'node "/tmp/custom-read-hook.cjs"' }],
},
{
matcher: 'WebSearch',
hooks: [
{
type: 'command',
command: 'node "/Users/kaitran/.ccs/hooks/websearch-transformer.cjs"',
},
],
},
],
},
} satisfies Record<string, unknown>;
expect(removeCcsImageAnalyzerHooks(settings)).toBe(true);
expect(settings.hooks.PreToolUse).toEqual([
{
matcher: 'Read',
hooks: [{ type: 'command', command: 'node "/tmp/custom-read-hook.cjs"' }],
},
{
matcher: 'WebSearch',
hooks: [
{
type: 'command',
command: 'node "/Users/kaitran/.ccs/hooks/websearch-transformer.cjs"',
},
],
},
]);
});
});