* refactor: multi-channel infrastructure with explicit channel/is_group tracking - Add channels[] array and findChannel() routing in index.ts, replacing hardcoded whatsapp.* calls with channel-agnostic callbacks - Add channel TEXT and is_group INTEGER columns to chats table with COALESCE upsert to protect existing values from null overwrites - is_group defaults to 0 (safe: unknown chats excluded from groups) - WhatsApp passes explicit channel='whatsapp' and isGroup to onChatMetadata - getAvailableGroups filters on is_group instead of JID pattern matching - findChannel logs warnings instead of silently dropping unroutable JIDs - Migration backfills channel/is_group from JID patterns for existing DBs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: skills engine v0.1 — deterministic skill packages with rerere resolution Three-way merge engine for applying skill packages on top of a core codebase. Skills declare which files they add/modify, and the engine uses git merge-file for conflict detection with git rerere for automatic resolution of previously-seen conflicts. Key components: - apply: three-way merge with backup/rollback safety net - replay: clean-slate replay for uninstall and rebase - update: core version updates with deletion detection - rebase: bake applied skills into base (one-way) - manifest: validation with path traversal protection - resolution-cache: pre-computed rerere resolutions - structured: npm deps, env vars, docker-compose merging - CI: per-skill test matrix with conflict detection 151 unit tests covering merge, rerere, backup, replay, uninstall, update, rebase, structured ops, and edge cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Discord and Telegram skill packages Skill packages for adding Discord and Telegram channels to NanoClaw. Each package includes: - Channel implementation (add/src/channels/) - Three-way merge targets for index.ts, config.ts, routing.test.ts - Intent docs explaining merge invariants - Standalone integration tests - manifest.yaml with dependency/conflict declarations Applied via: npx tsx scripts/apply-skill.ts .claude/skills/add-discord These are inert until applied — no runtime impact. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * remove unused docs (skills-system-status, implementation-guide) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
134 lines
5.1 KiB
TypeScript
134 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
describe('discord skill package', () => {
|
|
const skillDir = path.resolve(__dirname, '..');
|
|
|
|
it('has a valid manifest', () => {
|
|
const manifestPath = path.join(skillDir, 'manifest.yaml');
|
|
expect(fs.existsSync(manifestPath)).toBe(true);
|
|
|
|
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
expect(content).toContain('skill: discord');
|
|
expect(content).toContain('version: 1.0.0');
|
|
expect(content).toContain('discord.js');
|
|
});
|
|
|
|
it('has all files declared in adds', () => {
|
|
const addFile = path.join(skillDir, 'add', 'src', 'channels', 'discord.ts');
|
|
expect(fs.existsSync(addFile)).toBe(true);
|
|
|
|
const content = fs.readFileSync(addFile, 'utf-8');
|
|
expect(content).toContain('class DiscordChannel');
|
|
expect(content).toContain('implements Channel');
|
|
|
|
// Test file for the channel
|
|
const testFile = path.join(skillDir, 'add', 'src', 'channels', 'discord.test.ts');
|
|
expect(fs.existsSync(testFile)).toBe(true);
|
|
|
|
const testContent = fs.readFileSync(testFile, 'utf-8');
|
|
expect(testContent).toContain("describe('DiscordChannel'");
|
|
});
|
|
|
|
it('has all files declared in modifies', () => {
|
|
const indexFile = path.join(skillDir, 'modify', 'src', 'index.ts');
|
|
const configFile = path.join(skillDir, 'modify', 'src', 'config.ts');
|
|
const routingTestFile = path.join(skillDir, 'modify', 'src', 'routing.test.ts');
|
|
|
|
expect(fs.existsSync(indexFile)).toBe(true);
|
|
expect(fs.existsSync(configFile)).toBe(true);
|
|
expect(fs.existsSync(routingTestFile)).toBe(true);
|
|
|
|
const indexContent = fs.readFileSync(indexFile, 'utf-8');
|
|
expect(indexContent).toContain('DiscordChannel');
|
|
expect(indexContent).toContain('DISCORD_BOT_TOKEN');
|
|
expect(indexContent).toContain('DISCORD_ONLY');
|
|
expect(indexContent).toContain('findChannel');
|
|
expect(indexContent).toContain('channels: Channel[]');
|
|
|
|
const configContent = fs.readFileSync(configFile, 'utf-8');
|
|
expect(configContent).toContain('DISCORD_BOT_TOKEN');
|
|
expect(configContent).toContain('DISCORD_ONLY');
|
|
});
|
|
|
|
it('has intent files for modified files', () => {
|
|
expect(fs.existsSync(path.join(skillDir, 'modify', 'src', 'index.ts.intent.md'))).toBe(true);
|
|
expect(fs.existsSync(path.join(skillDir, 'modify', 'src', 'config.ts.intent.md'))).toBe(true);
|
|
});
|
|
|
|
it('modified index.ts preserves core structure', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(skillDir, 'modify', 'src', 'index.ts'),
|
|
'utf-8',
|
|
);
|
|
|
|
// Core functions still present
|
|
expect(content).toContain('function loadState()');
|
|
expect(content).toContain('function saveState()');
|
|
expect(content).toContain('function registerGroup(');
|
|
expect(content).toContain('function getAvailableGroups()');
|
|
expect(content).toContain('function processGroupMessages(');
|
|
expect(content).toContain('function runAgent(');
|
|
expect(content).toContain('function startMessageLoop()');
|
|
expect(content).toContain('function recoverPendingMessages()');
|
|
expect(content).toContain('function ensureContainerSystemRunning()');
|
|
expect(content).toContain('async function main()');
|
|
|
|
// Test helper preserved
|
|
expect(content).toContain('_setRegisteredGroups');
|
|
|
|
// Direct-run guard preserved
|
|
expect(content).toContain('isDirectRun');
|
|
});
|
|
|
|
it('modified index.ts includes Discord channel creation', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(skillDir, 'modify', 'src', 'index.ts'),
|
|
'utf-8',
|
|
);
|
|
|
|
// Multi-channel architecture
|
|
expect(content).toContain('const channels: Channel[] = []');
|
|
expect(content).toContain('channels.push(whatsapp)');
|
|
expect(content).toContain('channels.push(discord)');
|
|
|
|
// Conditional channel creation
|
|
expect(content).toContain('if (!DISCORD_ONLY)');
|
|
expect(content).toContain('if (DISCORD_BOT_TOKEN)');
|
|
|
|
// Shutdown disconnects all channels
|
|
expect(content).toContain('for (const ch of channels) await ch.disconnect()');
|
|
});
|
|
|
|
it('modified config.ts preserves all existing exports', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(skillDir, 'modify', 'src', 'config.ts'),
|
|
'utf-8',
|
|
);
|
|
|
|
// All original exports preserved
|
|
expect(content).toContain('export const ASSISTANT_NAME');
|
|
expect(content).toContain('export const POLL_INTERVAL');
|
|
expect(content).toContain('export const TRIGGER_PATTERN');
|
|
expect(content).toContain('export const CONTAINER_IMAGE');
|
|
expect(content).toContain('export const DATA_DIR');
|
|
expect(content).toContain('export const TIMEZONE');
|
|
|
|
// Discord exports added
|
|
expect(content).toContain('export const DISCORD_BOT_TOKEN');
|
|
expect(content).toContain('export const DISCORD_ONLY');
|
|
});
|
|
|
|
it('modified routing.test.ts includes Discord JID tests', () => {
|
|
const content = fs.readFileSync(
|
|
path.join(skillDir, 'modify', 'src', 'routing.test.ts'),
|
|
'utf-8',
|
|
);
|
|
|
|
expect(content).toContain("Discord JID: starts with dc:");
|
|
expect(content).toContain("dc:1234567890123456");
|
|
expect(content).toContain("dc:");
|
|
});
|
|
});
|