Add testing infrastructure with vitest (#67)
- Add vitest as dev dependency - Add test scripts: `npm test` (watch) and `npm run test:run` (CI) - Add initial unit tests for pure utility functions: - src/utils/phone.test.ts (10 tests) - src/utils/server.test.ts (10 tests) - src/channels/attachments.test.ts (6 tests) All 26 tests passing. Written by Cameron ◯ Letta Code
This commit is contained in:
1431
package-lock.json
generated
1431
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,8 @@
|
||||
"dev": "tsx src/main.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/main.js",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run",
|
||||
"skills": "tsx src/cli.ts skills",
|
||||
"skills:list": "tsx src/cli.ts skills list",
|
||||
"skills:status": "tsx src/cli.ts skills status",
|
||||
@@ -64,6 +66,7 @@
|
||||
"discord.js": "^14.25.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/update-notifier": "^6.0.8"
|
||||
"@types/update-notifier": "^6.0.8",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
|
||||
41
src/channels/attachments.test.ts
Normal file
41
src/channels/attachments.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { sanitizeFilename } from './attachments.js';
|
||||
|
||||
describe('sanitizeFilename', () => {
|
||||
it('preserves safe filenames', () => {
|
||||
expect(sanitizeFilename('photo.jpg')).toBe('photo.jpg');
|
||||
expect(sanitizeFilename('my-file_123.png')).toBe('my-file_123.png');
|
||||
});
|
||||
|
||||
it('replaces unsafe characters with underscores', () => {
|
||||
expect(sanitizeFilename('my file.jpg')).toBe('my_file.jpg');
|
||||
expect(sanitizeFilename('file (1).jpg')).toBe('file__1_.jpg');
|
||||
expect(sanitizeFilename('file<>:"/\\|?*.jpg')).toBe('file_________.jpg');
|
||||
});
|
||||
|
||||
it('strips leading/trailing underscores', () => {
|
||||
expect(sanitizeFilename('___file___')).toBe('file');
|
||||
expect(sanitizeFilename(' file ')).toBe('file');
|
||||
});
|
||||
|
||||
it('returns "attachment" for empty input', () => {
|
||||
expect(sanitizeFilename('')).toBe('attachment');
|
||||
expect(sanitizeFilename(' ')).toBe('attachment');
|
||||
expect(sanitizeFilename('___')).toBe('attachment');
|
||||
});
|
||||
|
||||
it('handles path traversal attempts', () => {
|
||||
const result = sanitizeFilename('../../../etc/passwd');
|
||||
// Note: dots are allowed, so '..' becomes '.._' - but slashes are stripped
|
||||
// Path traversal is prevented by buildAttachmentPath using join() on sanitized components
|
||||
expect(result).not.toContain('/');
|
||||
expect(result).toMatch(/^[A-Za-z0-9._-]+$/);
|
||||
});
|
||||
|
||||
it('handles unicode characters', () => {
|
||||
const result = sanitizeFilename('photo_日本語.jpg');
|
||||
expect(result).not.toContain('日');
|
||||
expect(result).toContain('photo_');
|
||||
expect(result).toContain('.jpg');
|
||||
});
|
||||
});
|
||||
47
src/utils/phone.test.ts
Normal file
47
src/utils/phone.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { normalizePhoneForStorage, isSameContact } from './phone.js';
|
||||
|
||||
describe('normalizePhoneForStorage', () => {
|
||||
it('strips WhatsApp DM suffix', () => {
|
||||
expect(normalizePhoneForStorage('12345678901@s.whatsapp.net')).toBe('+12345678901');
|
||||
});
|
||||
|
||||
it('strips WhatsApp group suffix', () => {
|
||||
expect(normalizePhoneForStorage('12345678901@g.us')).toBe('+12345678901');
|
||||
});
|
||||
|
||||
it('strips LID suffix', () => {
|
||||
expect(normalizePhoneForStorage('12345678901@lid')).toBe('+12345678901');
|
||||
});
|
||||
|
||||
it('strips port suffix', () => {
|
||||
expect(normalizePhoneForStorage('12345678901:2')).toBe('+12345678901');
|
||||
});
|
||||
|
||||
it('adds + prefix to raw numbers', () => {
|
||||
expect(normalizePhoneForStorage('12345678901')).toBe('+12345678901');
|
||||
});
|
||||
|
||||
it('preserves existing + prefix', () => {
|
||||
expect(normalizePhoneForStorage('+12345678901')).toBe('+12345678901');
|
||||
});
|
||||
|
||||
it('handles combined suffixes', () => {
|
||||
expect(normalizePhoneForStorage('12345678901@lid:2')).toBe('+12345678901');
|
||||
});
|
||||
|
||||
it('trims whitespace', () => {
|
||||
expect(normalizePhoneForStorage(' 12345678901 ')).toBe('+12345678901');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSameContact', () => {
|
||||
it('returns true for same number with different formats', () => {
|
||||
expect(isSameContact('123@lid', '+123')).toBe(true);
|
||||
expect(isSameContact('123@s.whatsapp.net', '123')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for different numbers', () => {
|
||||
expect(isSameContact('123', '456')).toBe(false);
|
||||
});
|
||||
});
|
||||
45
src/utils/server.test.ts
Normal file
45
src/utils/server.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { isLettaCloudUrl } from './server.js';
|
||||
|
||||
describe('isLettaCloudUrl', () => {
|
||||
it('returns true for undefined (default is cloud)', () => {
|
||||
expect(isLettaCloudUrl(undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for Letta Cloud URL', () => {
|
||||
expect(isLettaCloudUrl('https://api.letta.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for Letta Cloud URL with trailing slash', () => {
|
||||
expect(isLettaCloudUrl('https://api.letta.com/')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for Letta Cloud URL with path', () => {
|
||||
expect(isLettaCloudUrl('https://api.letta.com/v1/agents')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for localhost', () => {
|
||||
expect(isLettaCloudUrl('http://localhost:8283')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for 127.0.0.1', () => {
|
||||
expect(isLettaCloudUrl('http://127.0.0.1:8283')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for custom server', () => {
|
||||
expect(isLettaCloudUrl('https://custom.server.com')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for docker network URL', () => {
|
||||
expect(isLettaCloudUrl('http://letta:8283')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for invalid URL', () => {
|
||||
expect(isLettaCloudUrl('not-a-url')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for empty string (treated as default)', () => {
|
||||
// Empty string is falsy, so it's treated like undefined (default to cloud)
|
||||
expect(isLettaCloudUrl('')).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user