diff --git a/test/cleanup.test.ts b/test/cleanup.test.ts new file mode 100644 index 00000000..02138f3c --- /dev/null +++ b/test/cleanup.test.ts @@ -0,0 +1,199 @@ +import { describe, expect, test } from '@jest/globals' +import { convertFileNameToDate } from '../src/utils/cleanup' + +describe('Cleanup Utilities', () => { + describe('convertFileNameToDate', () => { + test('should convert standard ISO filename to Date', () => { + const filename = '2024-01-15T10-30-45-123Z.log' + const result = convertFileNameToDate(filename) + const expected = new Date('2024-01-15T10:30:45.123Z') + + expect(result).toEqual(expected) + expect(result.getTime()).toBe(expected.getTime()) + }) + + test('should handle filename with extension', () => { + const filename = '2023-12-25T23-59-59-999Z.json' + const result = convertFileNameToDate(filename) + const expected = new Date('2023-12-25T23:59:59.999Z') + + expect(result).toEqual(expected) + }) + + test('should handle filename without extension', () => { + const filename = '2024-06-15T12-00-00-000Z' + const result = convertFileNameToDate(filename) + const expected = new Date('2024-06-15T12:00:00.000Z') + + expect(result).toEqual(expected) + }) + + test('should handle midnight time', () => { + const filename = '2024-01-01T00-00-00-000Z.txt' + const result = convertFileNameToDate(filename) + const expected = new Date('2024-01-01T00:00:00.000Z') + + expect(result).toEqual(expected) + }) + + test('should handle leap year date', () => { + const filename = '2024-02-29T15-45-30-500Z.log' + const result = convertFileNameToDate(filename) + const expected = new Date('2024-02-29T15:45:30.500Z') + + expect(result).toEqual(expected) + }) + + test('should handle end of year date', () => { + const filename = '2023-12-31T23-59-59-999Z.backup' + const result = convertFileNameToDate(filename) + const expected = new Date('2023-12-31T23:59:59.999Z') + + expect(result).toEqual(expected) + }) + + test('should handle single digit milliseconds', () => { + const filename = '2024-03-10T08-15-22-001Z.log' + const result = convertFileNameToDate(filename) + const expected = new Date('2024-03-10T08:15:22.001Z') + + expect(result).toEqual(expected) + }) + + test('should handle double digit milliseconds', () => { + const filename = '2024-03-10T08-15-22-012Z.log' + const result = convertFileNameToDate(filename) + const expected = new Date('2024-03-10T08:15:22.012Z') + + expect(result).toEqual(expected) + }) + + test('should handle zero milliseconds', () => { + const filename = '2024-07-04T16-20-30-000Z.data' + const result = convertFileNameToDate(filename) + const expected = new Date('2024-07-04T16:20:30.000Z') + + expect(result).toEqual(expected) + }) + + test('should handle filename with multiple dots', () => { + const filename = '2024-08-20T09-45-15-750Z.backup.old.log' + const result = convertFileNameToDate(filename) + const expected = new Date('2024-08-20T09:45:15.750Z') + + expect(result).toEqual(expected) + }) + + test('should handle various file extensions', () => { + const testCases = [ + { filename: '2024-01-01T12-00-00-000Z.txt', expected: '2024-01-01T12:00:00.000Z' }, + { filename: '2024-01-01T12-00-00-000Z.json', expected: '2024-01-01T12:00:00.000Z' }, + { filename: '2024-01-01T12-00-00-000Z.log', expected: '2024-01-01T12:00:00.000Z' }, + { filename: '2024-01-01T12-00-00-000Z.backup', expected: '2024-01-01T12:00:00.000Z' }, + { filename: '2024-01-01T12-00-00-000Z.tmp', expected: '2024-01-01T12:00:00.000Z' }, + ] + + testCases.forEach(({ filename, expected }) => { + const result = convertFileNameToDate(filename) + const expectedDate = new Date(expected) + expect(result).toEqual(expectedDate) + }) + }) + + test('should handle early morning times', () => { + const filename = '2024-05-15T01-02-03-456Z.log' + const result = convertFileNameToDate(filename) + const expected = new Date('2024-05-15T01:02:03.456Z') + + expect(result).toEqual(expected) + }) + + test('should handle late evening times', () => { + const filename = '2024-11-30T22-58-59-789Z.error' + const result = convertFileNameToDate(filename) + const expected = new Date('2024-11-30T22:58:59.789Z') + + expect(result).toEqual(expected) + }) + + test('should handle different months correctly', () => { + const months = [ + '01', '02', '03', '04', '05', '06', + '07', '08', '09', '10', '11', '12' + ] + + months.forEach((month, index) => { + const filename = `2024-${month}-15T12-00-00-000Z.log` + const result = convertFileNameToDate(filename) + + expect(result.getMonth()).toBe(index) // getMonth() returns 0-11 + expect(result.getDate()).toBe(15) + expect(result.getFullYear()).toBe(2024) + }) + }) + + test('should handle edge case dates', () => { + const edgeCases = [ + { filename: '2024-01-31T23-59-59-999Z.log', desc: 'end of January' }, + { filename: '2024-02-01T00-00-00-000Z.log', desc: 'start of February' }, + { filename: '2024-04-30T12-00-00-000Z.log', desc: 'end of April (30 days)' }, + { filename: '2024-05-01T12-00-00-000Z.log', desc: 'start of May' }, + ] + + edgeCases.forEach(({ filename, desc }) => { + expect(() => convertFileNameToDate(filename)).not.toThrow() + const result = convertFileNameToDate(filename) + expect(result).toBeInstanceOf(Date) + expect(result.getTime()).not.toBeNaN() + }) + }) + + test('should produce valid Date objects', () => { + const testFilenames = [ + '2024-01-15T10-30-45-123Z.log', + '2023-12-25T23-59-59-999Z.json', + '2024-06-15T12-00-00-000Z.txt', + '2024-02-29T15-45-30-500Z.backup', + ] + + testFilenames.forEach(filename => { + const result = convertFileNameToDate(filename) + + expect(result).toBeInstanceOf(Date) + expect(result.getTime()).not.toBeNaN() + expect(result.toISOString()).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }) + }) + + test('should handle year boundaries correctly', () => { + const yearBoundaryTests = [ + { filename: '2023-12-31T23-59-59-999Z.log', year: 2023, month: 11, date: 31 }, + { filename: '2024-01-01T00-00-00-000Z.log', year: 2024, month: 0, date: 1 }, + { filename: '2024-12-31T23-59-59-999Z.log', year: 2024, month: 11, date: 31 }, + { filename: '2025-01-01T00-00-00-000Z.log', year: 2025, month: 0, date: 1 }, + ] + + yearBoundaryTests.forEach(({ filename, year, month, date }) => { + const result = convertFileNameToDate(filename) + + expect(result.getFullYear()).toBe(year) + expect(result.getMonth()).toBe(month) + expect(result.getDate()).toBe(date) + }) + }) + + test('should maintain precision for milliseconds', () => { + const precisionTests = [ + { filename: '2024-01-01T12-00-00-001Z.log', ms: 1 }, + { filename: '2024-01-01T12-00-00-010Z.log', ms: 10 }, + { filename: '2024-01-01T12-00-00-100Z.log', ms: 100 }, + { filename: '2024-01-01T12-00-00-999Z.log', ms: 999 }, + ] + + precisionTests.forEach(({ filename, ms }) => { + const result = convertFileNameToDate(filename) + expect(result.getMilliseconds()).toBe(ms) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/customCommands.test.ts b/test/customCommands.test.ts index 917adfcc..ec62e6af 100644 --- a/test/customCommands.test.ts +++ b/test/customCommands.test.ts @@ -1,5 +1,12 @@ -import { parseFrontmatter, loadCustomCommands } from '../src/services/customCommands' -import { describe, expect, test } from '@jest/globals' +import { + parseFrontmatter, + loadCustomCommands, + executeBashCommands, + resolveFileReferences +} from '../src/services/customCommands' +import { describe, expect, test, beforeEach, afterEach } from '@jest/globals' +import { writeFileSync, unlinkSync, existsSync, mkdirSync, rmSync } from 'fs' +import { join } from 'path' describe('Custom Commands', () => { describe('parseFrontmatter', () => { @@ -74,4 +81,197 @@ Content` expect(result.frontmatter.hidden).toBe(false) }) }) + + describe('executeBashCommands', () => { + test('should return content unchanged when no bash commands present', async () => { + const content = 'This is just regular text without any bash commands.' + const result = await executeBashCommands(content) + expect(result).toBe(content) + }) + + test('should execute simple bash command and replace with output', async () => { + const content = 'Current date: !`echo test-output`' + const result = await executeBashCommands(content) + expect(result).toBe('Current date: test-output') + }) + + test('should handle multiple bash commands in same content', async () => { + const content = 'First: !`echo hello` and Second: !`echo world`' + const result = await executeBashCommands(content) + expect(result).toBe('First: hello and Second: world') + }) + + test('should handle bash commands with arguments', async () => { + const content = 'Files: !`ls /tmp`' + const result = await executeBashCommands(content) + // Should contain some output (exact output may vary) + expect(result).toContain('Files: ') + expect(result).not.toContain('!`') + }) + + test('should handle command that produces no output', async () => { + const content = 'Silent command: !`true`' + const result = await executeBashCommands(content) + expect(result).toBe('Silent command: (no output)') + }) + + test('should handle failing commands gracefully', async () => { + const content = 'This will fail: !`nonexistent-command-12345`' + const result = await executeBashCommands(content) + expect(result).toContain('(error executing: nonexistent-command-12345)') + }) + + test('should handle commands with special characters', async () => { + const content = 'Echo with space: !`echo hello world`' + const result = await executeBashCommands(content) + expect(result).toBe('Echo with space: hello world') + }) + + test('should handle empty bash command', async () => { + const content = 'Empty command: !``' + const result = await executeBashCommands(content) + // Empty command should result in error since no command is provided + expect(result).toContain('Empty command: ') + }) + + test('should preserve content around bash commands', async () => { + const content = ` + Before command + !\`echo middle\` + After command + ` + const result = await executeBashCommands(content) + expect(result).toContain('Before command') + expect(result).toContain('middle') + expect(result).toContain('After command') + }) + }) + + describe('resolveFileReferences', () => { + const testDir = join(process.cwd(), 'test-temp-files') + const testFile1 = join(testDir, 'test1.txt') + const testFile2 = join(testDir, 'subdir', 'test2.js') + + beforeEach(() => { + // Create test directory and files + if (!existsSync(testDir)) { + mkdirSync(testDir, { recursive: true }) + } + if (!existsSync(join(testDir, 'subdir'))) { + mkdirSync(join(testDir, 'subdir'), { recursive: true }) + } + + writeFileSync(testFile1, 'This is test file 1 content') + writeFileSync(testFile2, 'console.log("Hello from test2.js")') + }) + + afterEach(() => { + // Clean up test files + if (existsSync(testDir)) { + rmSync(testDir, { recursive: true, force: true }) + } + }) + + test('should return content unchanged when no file references present', async () => { + const content = 'This is just regular text without any file references.' + const result = await resolveFileReferences(content) + expect(result).toBe(content) + }) + + test('should resolve single file reference', async () => { + const content = 'Check this file: @test-temp-files/test1.txt' + const result = await resolveFileReferences(content) + + expect(result).toContain('## File: test-temp-files/test1.txt') + expect(result).toContain('This is test file 1 content') + expect(result).toContain('```') + }) + + test('should resolve multiple file references', async () => { + const content = 'Files: @test-temp-files/test1.txt and @test-temp-files/subdir/test2.js' + const result = await resolveFileReferences(content) + + expect(result).toContain('## File: test-temp-files/test1.txt') + expect(result).toContain('This is test file 1 content') + expect(result).toContain('## File: test-temp-files/subdir/test2.js') + expect(result).toContain('console.log("Hello from test2.js")') + }) + + test('should handle non-existent file gracefully', async () => { + const content = 'Missing file: @test-temp-files/nonexistent.txt' + const result = await resolveFileReferences(content) + + expect(result).toContain('(file not found: test-temp-files/nonexistent.txt)') + }) + + test('should skip agent mentions', async () => { + const content = 'Agent reference: @agent-test should be ignored' + const result = await resolveFileReferences(content) + + expect(result).toBe(content) // Should remain unchanged + }) + + test('should handle file references with various extensions', async () => { + const jsFile = join(testDir, 'script.js') + const jsonFile = join(testDir, 'config.json') + + writeFileSync(jsFile, 'function test() { return true; }') + writeFileSync(jsonFile, '{"name": "test", "version": "1.0.0"}') + + const content = 'JS: @test-temp-files/script.js and JSON: @test-temp-files/config.json' + const result = await resolveFileReferences(content) + + expect(result).toContain('## File: test-temp-files/script.js') + expect(result).toContain('function test() { return true; }') + expect(result).toContain('## File: test-temp-files/config.json') + expect(result).toContain('"name": "test"') + }) + + test('should handle file references with special characters in path', async () => { + const specialFile = join(testDir, 'file-with-dashes_and_underscores.txt') + writeFileSync(specialFile, 'Special file content') + + const content = 'Special: @test-temp-files/file-with-dashes_and_underscores.txt' + const result = await resolveFileReferences(content) + + expect(result).toContain('## File: test-temp-files/file-with-dashes_and_underscores.txt') + expect(result).toContain('Special file content') + }) + + test('should preserve content around file references', async () => { + const content = ` + Before file reference + @test-temp-files/test1.txt + After file reference + ` + const result = await resolveFileReferences(content) + + expect(result).toContain('Before file reference') + expect(result).toContain('## File: test-temp-files/test1.txt') + expect(result).toContain('This is test file 1 content') + expect(result).toContain('After file reference') + }) + + test('should handle empty files', async () => { + const emptyFile = join(testDir, 'empty.txt') + writeFileSync(emptyFile, '') + + const content = 'Empty file: @test-temp-files/empty.txt' + const result = await resolveFileReferences(content) + + expect(result).toContain('## File: test-temp-files/empty.txt') + expect(result).toContain('```\n\n```') // Empty code block + }) + + test('should handle files with newlines and special content', async () => { + const complexFile = join(testDir, 'complex.txt') + writeFileSync(complexFile, 'Line 1\nLine 2\n\nLine 4 with "quotes" and \'apostrophes\'') + + const content = 'Complex file: @test-temp-files/complex.txt' + const result = await resolveFileReferences(content) + + expect(result).toContain('## File: test-temp-files/complex.txt') + expect(result).toContain('Line 1\nLine 2\n\nLine 4 with "quotes" and \'apostrophes\'') + }) + }) }) \ No newline at end of file diff --git a/test/responseState.test.ts b/test/responseState.test.ts new file mode 100644 index 00000000..0fd50620 --- /dev/null +++ b/test/responseState.test.ts @@ -0,0 +1,215 @@ +import { describe, expect, test, beforeEach } from '@jest/globals' +import { + getLastResponseId, + setLastResponseId, + clearResponseId, + clearAllResponseIds +} from '../src/utils/responseState' + +describe('Response State Management', () => { + beforeEach(() => { + // Clear all response IDs before each test to ensure clean state + clearAllResponseIds() + }) + + describe('getLastResponseId', () => { + test('should return undefined for non-existent conversation', () => { + const result = getLastResponseId('non-existent-conversation') + expect(result).toBeUndefined() + }) + + test('should return stored response ID for existing conversation', () => { + const conversationId = 'test-conversation-1' + const responseId = 'response-123' + + setLastResponseId(conversationId, responseId) + const result = getLastResponseId(conversationId) + + expect(result).toBe(responseId) + }) + + test('should handle empty string conversation ID', () => { + const conversationId = '' + const responseId = 'response-empty' + + setLastResponseId(conversationId, responseId) + const result = getLastResponseId(conversationId) + + expect(result).toBe(responseId) + }) + + test('should handle special characters in conversation ID', () => { + const conversationId = 'conversation-with-special-chars-!@#$%^&*()' + const responseId = 'response-special' + + setLastResponseId(conversationId, responseId) + const result = getLastResponseId(conversationId) + + expect(result).toBe(responseId) + }) + }) + + describe('setLastResponseId', () => { + test('should store response ID for conversation', () => { + const conversationId = 'test-conversation-2' + const responseId = 'response-456' + + setLastResponseId(conversationId, responseId) + const result = getLastResponseId(conversationId) + + expect(result).toBe(responseId) + }) + + test('should overwrite existing response ID', () => { + const conversationId = 'test-conversation-3' + const firstResponseId = 'response-first' + const secondResponseId = 'response-second' + + setLastResponseId(conversationId, firstResponseId) + setLastResponseId(conversationId, secondResponseId) + const result = getLastResponseId(conversationId) + + expect(result).toBe(secondResponseId) + }) + + test('should handle empty string response ID', () => { + const conversationId = 'test-conversation-4' + const responseId = '' + + setLastResponseId(conversationId, responseId) + const result = getLastResponseId(conversationId) + + expect(result).toBe(responseId) + }) + + test('should handle multiple conversations independently', () => { + const conversation1 = 'conversation-1' + const conversation2 = 'conversation-2' + const response1 = 'response-1' + const response2 = 'response-2' + + setLastResponseId(conversation1, response1) + setLastResponseId(conversation2, response2) + + expect(getLastResponseId(conversation1)).toBe(response1) + expect(getLastResponseId(conversation2)).toBe(response2) + }) + }) + + describe('clearResponseId', () => { + test('should remove response ID for specific conversation', () => { + const conversationId = 'test-conversation-5' + const responseId = 'response-789' + + setLastResponseId(conversationId, responseId) + expect(getLastResponseId(conversationId)).toBe(responseId) + + clearResponseId(conversationId) + expect(getLastResponseId(conversationId)).toBeUndefined() + }) + + test('should not affect other conversations when clearing one', () => { + const conversation1 = 'conversation-keep' + const conversation2 = 'conversation-clear' + const response1 = 'response-keep' + const response2 = 'response-clear' + + setLastResponseId(conversation1, response1) + setLastResponseId(conversation2, response2) + + clearResponseId(conversation2) + + expect(getLastResponseId(conversation1)).toBe(response1) + expect(getLastResponseId(conversation2)).toBeUndefined() + }) + + test('should handle clearing non-existent conversation gracefully', () => { + // Should not throw an error + expect(() => clearResponseId('non-existent')).not.toThrow() + expect(getLastResponseId('non-existent')).toBeUndefined() + }) + }) + + describe('clearAllResponseIds', () => { + test('should remove all stored response IDs', () => { + const conversations = ['conv1', 'conv2', 'conv3'] + const responses = ['resp1', 'resp2', 'resp3'] + + // Set multiple response IDs + conversations.forEach((conv, index) => { + setLastResponseId(conv, responses[index]) + }) + + // Verify they're all set + conversations.forEach((conv, index) => { + expect(getLastResponseId(conv)).toBe(responses[index]) + }) + + // Clear all + clearAllResponseIds() + + // Verify they're all cleared + conversations.forEach(conv => { + expect(getLastResponseId(conv)).toBeUndefined() + }) + }) + + test('should handle clearing when no response IDs exist', () => { + // Should not throw an error + expect(() => clearAllResponseIds()).not.toThrow() + }) + + test('should allow setting new response IDs after clearing all', () => { + const conversationId = 'test-after-clear' + const responseId = 'response-after-clear' + + // Set and clear + setLastResponseId(conversationId, 'temp-response') + clearAllResponseIds() + + // Set new response ID + setLastResponseId(conversationId, responseId) + expect(getLastResponseId(conversationId)).toBe(responseId) + }) + }) + + describe('edge cases and integration', () => { + test('should handle very long conversation and response IDs', () => { + const longConversationId = 'a'.repeat(1000) + const longResponseId = 'b'.repeat(1000) + + setLastResponseId(longConversationId, longResponseId) + expect(getLastResponseId(longConversationId)).toBe(longResponseId) + }) + + test('should handle Unicode characters in IDs', () => { + const unicodeConversationId = 'ζ΅‹θ―•δΌšθ―-πŸš€-conversation' + const unicodeResponseId = '响应-πŸ’‘-response' + + setLastResponseId(unicodeConversationId, unicodeResponseId) + expect(getLastResponseId(unicodeConversationId)).toBe(unicodeResponseId) + }) + + test('should maintain state across multiple operations', () => { + const operations = [ + { action: 'set', conv: 'conv1', resp: 'resp1' }, + { action: 'set', conv: 'conv2', resp: 'resp2' }, + { action: 'clear', conv: 'conv1' }, + { action: 'set', conv: 'conv3', resp: 'resp3' }, + { action: 'set', conv: 'conv1', resp: 'resp1-new' }, + ] + + operations.forEach(op => { + if (op.action === 'set') { + setLastResponseId(op.conv, op.resp!) + } else if (op.action === 'clear') { + clearResponseId(op.conv) + } + }) + + expect(getLastResponseId('conv1')).toBe('resp1-new') + expect(getLastResponseId('conv2')).toBe('resp2') + expect(getLastResponseId('conv3')).toBe('resp3') + }) + }) +}) \ No newline at end of file