Files
nanoclaw/.claude/skills/add-image-vision/tests/image-vision.test.ts
glifocat af937d6453 feat(skills): add image vision skill for WhatsApp (#770)
* chore: prepare image-vision skill for template regeneration

- Delete stale modify/*.ts templates (built against 1.1.2)
- Update core_version to 1.2.6
- Strip fork-specific details from intent docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(skills): regenerate image-vision modify/ templates against upstream

Templates regenerated against upstream 1.2.6:
- src/container-runner.ts: imageAttachments field in ContainerInput
- src/index.ts: parseImageReferences + threading to runAgent
- src/channels/whatsapp.ts: downloadMediaMessage + image handling block
- src/channels/whatsapp.test.ts: image mocks + 4 test cases
- container/agent-runner/src/index.ts: ContentBlock types, pushMultimodal, image loading

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: update image-vision tests for upstream templates

- Relax downloadMediaMessage import pattern check (multi-line import)
- Remove check for [Image - processing failed] (not in upstream template)
- Add vitest.skills.config.ts for skill package test runs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: update image-vision core_version to 1.2.8

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:52:59 +02:00

298 lines
10 KiB
TypeScript

import { describe, it, expect, beforeAll } from 'vitest';
import fs from 'fs';
import path from 'path';
const SKILL_DIR = path.resolve(__dirname, '..');
describe('add-image-vision skill package', () => {
describe('manifest', () => {
let content: string;
beforeAll(() => {
content = fs.readFileSync(path.join(SKILL_DIR, 'manifest.yaml'), 'utf-8');
});
it('has a valid manifest.yaml', () => {
expect(fs.existsSync(path.join(SKILL_DIR, 'manifest.yaml'))).toBe(true);
expect(content).toContain('skill: add-image-vision');
expect(content).toContain('version: 1.1.0');
});
it('declares sharp as npm dependency', () => {
expect(content).toContain('sharp:');
expect(content).toMatch(/sharp:\s*"\^0\.34/);
});
it('has no env_additions', () => {
expect(content).toContain('env_additions: []');
});
it('lists all add files', () => {
expect(content).toContain('src/image.ts');
expect(content).toContain('src/image.test.ts');
});
it('lists all modify files', () => {
expect(content).toContain('src/channels/whatsapp.ts');
expect(content).toContain('src/channels/whatsapp.test.ts');
expect(content).toContain('src/container-runner.ts');
expect(content).toContain('src/index.ts');
expect(content).toContain('container/agent-runner/src/index.ts');
});
it('has no dependencies', () => {
expect(content).toContain('depends: []');
});
});
describe('add/ files', () => {
it('includes src/image.ts with required exports', () => {
const filePath = path.join(SKILL_DIR, 'add', 'src', 'image.ts');
expect(fs.existsSync(filePath)).toBe(true);
const content = fs.readFileSync(filePath, 'utf-8');
expect(content).toContain('export function isImageMessage');
expect(content).toContain('export async function processImage');
expect(content).toContain('export function parseImageReferences');
expect(content).toContain('export interface ProcessedImage');
expect(content).toContain('export interface ImageAttachment');
expect(content).toContain("import sharp from 'sharp'");
});
it('includes src/image.test.ts with test cases', () => {
const filePath = path.join(SKILL_DIR, 'add', 'src', 'image.test.ts');
expect(fs.existsSync(filePath)).toBe(true);
const content = fs.readFileSync(filePath, 'utf-8');
expect(content).toContain('isImageMessage');
expect(content).toContain('processImage');
expect(content).toContain('parseImageReferences');
});
});
describe('modify/ files exist', () => {
const modifyFiles = [
'src/channels/whatsapp.ts',
'src/channels/whatsapp.test.ts',
'src/container-runner.ts',
'src/index.ts',
'container/agent-runner/src/index.ts',
];
for (const file of modifyFiles) {
it(`includes modify/${file}`, () => {
const filePath = path.join(SKILL_DIR, 'modify', file);
expect(fs.existsSync(filePath)).toBe(true);
});
}
});
describe('intent files exist', () => {
const intentFiles = [
'src/channels/whatsapp.ts.intent.md',
'src/channels/whatsapp.test.ts.intent.md',
'src/container-runner.ts.intent.md',
'src/index.ts.intent.md',
'container/agent-runner/src/index.ts.intent.md',
];
for (const file of intentFiles) {
it(`includes modify/${file}`, () => {
const filePath = path.join(SKILL_DIR, 'modify', file);
expect(fs.existsSync(filePath)).toBe(true);
});
}
});
describe('modify/src/channels/whatsapp.ts', () => {
let content: string;
beforeAll(() => {
content = fs.readFileSync(
path.join(SKILL_DIR, 'modify', 'src', 'channels', 'whatsapp.ts'),
'utf-8',
);
});
it('imports image utilities', () => {
expect(content).toContain("from '../image.js'");
expect(content).toContain('processImage');
});
it('imports downloadMediaMessage', () => {
expect(content).toContain('downloadMediaMessage');
expect(content).toContain("from '@whiskeysockets/baileys'");
});
it('imports GROUPS_DIR from config', () => {
expect(content).toContain('GROUPS_DIR');
});
it('uses let content for mutable assignment', () => {
expect(content).toMatch(/let content\s*=/);
});
it('includes image processing block', () => {
expect(content).toContain('processImage(buffer');
expect(content).toContain('Image - download failed');
});
it('preserves core WhatsAppChannel structure', () => {
expect(content).toContain('export class WhatsAppChannel implements Channel');
expect(content).toContain('async connect()');
expect(content).toContain('async sendMessage(');
expect(content).toContain('async syncGroupMetadata(');
expect(content).toContain('private async translateJid(');
expect(content).toContain('private async flushOutgoingQueue(');
});
});
describe('modify/src/channels/whatsapp.test.ts', () => {
let content: string;
beforeAll(() => {
content = fs.readFileSync(
path.join(SKILL_DIR, 'modify', 'src', 'channels', 'whatsapp.test.ts'),
'utf-8',
);
});
it('mocks image.js module', () => {
expect(content).toContain("vi.mock('../image.js'");
expect(content).toContain('isImageMessage');
expect(content).toContain('processImage');
});
it('mocks downloadMediaMessage', () => {
expect(content).toContain('downloadMediaMessage');
});
it('includes image test cases', () => {
expect(content).toContain('downloads and processes image attachments');
expect(content).toContain('handles image without caption');
expect(content).toContain('handles image download failure gracefully');
expect(content).toContain('falls back to caption when processImage returns null');
});
it('preserves all existing test sections', () => {
expect(content).toContain('connection lifecycle');
expect(content).toContain('authentication');
expect(content).toContain('reconnection');
expect(content).toContain('message handling');
expect(content).toContain('LID to JID translation');
expect(content).toContain('outgoing message queue');
expect(content).toContain('group metadata sync');
expect(content).toContain('ownsJid');
expect(content).toContain('setTyping');
expect(content).toContain('channel properties');
});
it('includes all media handling test sections', () => {
// Image tests present (core skill feature)
expect(content).toContain('downloads and processes image attachments');
expect(content).toContain('handles image without caption');
});
});
describe('modify/src/container-runner.ts', () => {
it('adds imageAttachments to ContainerInput', () => {
const content = fs.readFileSync(
path.join(SKILL_DIR, 'modify', 'src', 'container-runner.ts'),
'utf-8',
);
expect(content).toContain('imageAttachments?');
expect(content).toContain('relativePath: string');
expect(content).toContain('mediaType: string');
});
it('preserves core container-runner structure', () => {
const content = fs.readFileSync(
path.join(SKILL_DIR, 'modify', 'src', 'container-runner.ts'),
'utf-8',
);
expect(content).toContain('export async function runContainerAgent');
expect(content).toContain('ContainerInput');
});
});
describe('modify/src/index.ts', () => {
let content: string;
beforeAll(() => {
content = fs.readFileSync(
path.join(SKILL_DIR, 'modify', 'src', 'index.ts'),
'utf-8',
);
});
it('imports parseImageReferences', () => {
expect(content).toContain("import { parseImageReferences } from './image.js'");
});
it('calls parseImageReferences in processGroupMessages', () => {
expect(content).toContain('parseImageReferences(missedMessages)');
});
it('passes imageAttachments to runAgent', () => {
expect(content).toContain('imageAttachments');
expect(content).toMatch(/runAgent\(group,\s*prompt,\s*chatJid,\s*imageAttachments/);
});
it('spreads imageAttachments into container input', () => {
expect(content).toContain('...(imageAttachments.length > 0 && { imageAttachments })');
});
it('preserves core index.ts structure', () => {
expect(content).toContain('processGroupMessages');
expect(content).toContain('startMessageLoop');
expect(content).toContain('async function main()');
});
});
describe('modify/container/agent-runner/src/index.ts', () => {
let content: string;
beforeAll(() => {
content = fs.readFileSync(
path.join(SKILL_DIR, 'modify', 'container', 'agent-runner', 'src', 'index.ts'),
'utf-8',
);
});
it('defines ContentBlock types', () => {
expect(content).toContain('interface ImageContentBlock');
expect(content).toContain('interface TextContentBlock');
expect(content).toContain('type ContentBlock = ImageContentBlock | TextContentBlock');
});
it('adds imageAttachments to ContainerInput', () => {
expect(content).toContain('imageAttachments?');
});
it('adds pushMultimodal to MessageStream', () => {
expect(content).toContain('pushMultimodal(content: ContentBlock[])');
});
it('includes image loading logic in runQuery', () => {
expect(content).toContain('containerInput.imageAttachments');
expect(content).toContain("path.join('/workspace/group', img.relativePath)");
expect(content).toContain("toString('base64')");
expect(content).toContain('stream.pushMultimodal(blocks)');
});
it('preserves core structure', () => {
expect(content).toContain('async function runQuery');
expect(content).toContain('class MessageStream');
expect(content).toContain('function writeOutput');
expect(content).toContain('function createPreCompactHook');
expect(content).toContain('function createSanitizeBashHook');
expect(content).toContain('async function main');
});
it('preserves core agent-runner exports', () => {
expect(content).toContain('async function main');
expect(content).toContain('function writeOutput');
});
});
});