Initial commit: Brachnha Insight project setup
- Next.js 14+ with App Router and TypeScript - Tailwind CSS and ShadCN UI styling - Zustand state management - Dexie.js for IndexedDB (local-first data) - Auth.js v5 for authentication - BMAD framework integration Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
28
tests/unit/prompt-engine.test.ts
Normal file
28
tests/unit/prompt-engine.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
// @ts-ignore - Module likely not created yet
|
||||
import { PromptEngine } from '@/lib/llm/prompt-engine';
|
||||
|
||||
describe('PromptEngine (Ghostwriter)', () => {
|
||||
it('should construct a grounded prompt with user insight', () => {
|
||||
// GIVEN: A user insight and chat history
|
||||
const history = [
|
||||
{ role: 'user', content: 'I feel stupid.' },
|
||||
{ role: 'assistant', content: 'Why?' },
|
||||
{ role: 'user', content: 'I made a typo.' }
|
||||
];
|
||||
const insight = "I need a checklist for typos.";
|
||||
|
||||
// WHEN: Constructing the prompt
|
||||
// @ts-ignore
|
||||
const prompt = PromptEngine.constructGhostwriterPrompt(history, insight);
|
||||
|
||||
// THEN: The system prompt should enforce grounding
|
||||
expect(prompt[0].role).toBe('system');
|
||||
expect(prompt[0].content).toContain('You are an expert Ghostwriter');
|
||||
expect(prompt[0].content).toContain('Ground your answer in the user insight'); // R-2.1 Mitigation
|
||||
|
||||
// AND: The user insight should be clearly labeled
|
||||
const lastMessage = prompt[prompt.length - 1];
|
||||
expect(lastMessage.content).toContain(`INSIGHT: ${insight}`);
|
||||
});
|
||||
});
|
||||
52
tests/unit/services/secure-storage.test.ts
Normal file
52
tests/unit/services/secure-storage.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { SecureStorage } from '../../../src/services/secure-storage'; // Implementation doesn't exist yet
|
||||
|
||||
describe('SecureStorage Service', () => {
|
||||
const TEST_KEY = 'test_api_key_123';
|
||||
const STORAGE_KEY = 'llm_provider_config';
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('should save API key with basic encoding (not plain text)', () => {
|
||||
SecureStorage.saveKey(TEST_KEY);
|
||||
|
||||
// Direct access to localStorage to verify obfuscation
|
||||
const storedValue = localStorage.getItem(STORAGE_KEY);
|
||||
expect(storedValue).toBeDefined();
|
||||
expect(storedValue).not.toBe(TEST_KEY); // Should NOT be plain text
|
||||
expect(storedValue).not.toContain(TEST_KEY); // Should not contain the key
|
||||
});
|
||||
|
||||
it('should retrieve the original API key correctly', () => {
|
||||
SecureStorage.saveKey(TEST_KEY);
|
||||
|
||||
const retrievedKey = SecureStorage.getKey();
|
||||
expect(retrievedKey).toBe(TEST_KEY);
|
||||
});
|
||||
|
||||
it('should return null if no key is stored', () => {
|
||||
const retrievedKey = SecureStorage.getKey();
|
||||
expect(retrievedKey).toBeNull();
|
||||
});
|
||||
|
||||
it('should overwrite existing key when saving new one', () => {
|
||||
SecureStorage.saveKey('old_key');
|
||||
SecureStorage.saveKey('new_key');
|
||||
|
||||
expect(SecureStorage.getKey()).toBe('new_key');
|
||||
});
|
||||
|
||||
it('should clear key when requested', () => {
|
||||
SecureStorage.saveKey(TEST_KEY);
|
||||
SecureStorage.clearKey();
|
||||
|
||||
expect(SecureStorage.getKey()).toBeNull();
|
||||
expect(localStorage.getItem(STORAGE_KEY)).toBeNull();
|
||||
});
|
||||
});
|
||||
111
tests/unit/services/sync-manager.test.ts
Normal file
111
tests/unit/services/sync-manager.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { SyncManager } from '../../../src/services/sync-manager';
|
||||
|
||||
// Define hoisted mocks to be accessible inside vi.mock
|
||||
const { mockAdd, mockWhere, mockUpdate, mockDelete, mockSortBy } = vi.hoisted(() => {
|
||||
const mockSortBy = vi.fn();
|
||||
const mockEquals = vi.fn(() => ({ sortBy: mockSortBy }));
|
||||
const mockWhere = vi.fn(() => ({ equals: mockEquals }));
|
||||
return {
|
||||
mockAdd: vi.fn(),
|
||||
mockWhere,
|
||||
mockUpdate: vi.fn(),
|
||||
mockDelete: vi.fn(),
|
||||
mockSortBy
|
||||
};
|
||||
});
|
||||
|
||||
// Mock Dexie
|
||||
vi.mock('../../../src/lib/db', () => ({
|
||||
db: {
|
||||
syncQueue: {
|
||||
add: mockAdd,
|
||||
where: mockWhere,
|
||||
update: mockUpdate,
|
||||
delete: mockDelete,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('SyncManager Service', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Default return for sortBy (mockPendingItems)
|
||||
mockSortBy.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
describe('queueAction', () => {
|
||||
it('should add action to syncQueue with pending status', async () => {
|
||||
// GIVEN: A draft payload
|
||||
const payload = { draftData: { title: 'Test', content: 'Content' } as any };
|
||||
mockAdd.mockResolvedValue(1);
|
||||
|
||||
// WHEN: queueAction is called
|
||||
const id = await SyncManager.queueAction('saveDraft', payload);
|
||||
|
||||
// THEN: It returns the new ID
|
||||
expect(id).toBe(1);
|
||||
|
||||
// AND: It calls db.syncQueue.add with correct data
|
||||
expect(mockAdd).toHaveBeenCalledWith({
|
||||
action: 'saveDraft',
|
||||
payload,
|
||||
status: 'pending',
|
||||
createdAt: expect.any(Number),
|
||||
retries: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('processQueue', () => {
|
||||
it('should process pending items in order', async () => {
|
||||
// GIVEN: Pending items in queue
|
||||
const items = [
|
||||
{ id: 1, action: 'saveDraft', payload: {}, status: 'pending', retries: 0 },
|
||||
{ id: 2, action: 'deleteDraft', payload: { draftId: 123 }, status: 'pending', retries: 0 },
|
||||
];
|
||||
|
||||
mockSortBy.mockResolvedValue(items);
|
||||
|
||||
// WHEN: processQueue is called
|
||||
await SyncManager.processQueue();
|
||||
|
||||
// THEN: Items are processed
|
||||
// Verify update to 'processing'
|
||||
expect(mockUpdate).toHaveBeenCalledWith(1, { status: 'processing' });
|
||||
expect(mockUpdate).toHaveBeenCalledWith(2, { status: 'processing' });
|
||||
|
||||
// Verify deletion after success (assuming success path)
|
||||
expect(mockDelete).toHaveBeenCalledWith(1);
|
||||
expect(mockDelete).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('should handle execution errors and increment retries', async () => {
|
||||
// GIVEN: An item that will fail execution (invalid action)
|
||||
const item = {
|
||||
id: 1,
|
||||
action: 'invalidAction', // Will throw error in executeAction
|
||||
payload: {},
|
||||
status: 'pending',
|
||||
retries: 0
|
||||
};
|
||||
|
||||
mockSortBy.mockResolvedValue([item]);
|
||||
|
||||
// WHEN: processQueue is called
|
||||
await SyncManager.processQueue();
|
||||
|
||||
// THEN: Item should be updated to processing
|
||||
expect(mockUpdate).toHaveBeenCalledWith(1, { status: 'processing' });
|
||||
|
||||
// AND: Should be updated with incremented retry count (not deleted)
|
||||
expect(mockUpdate).toHaveBeenCalledWith(1, {
|
||||
status: 'pending',
|
||||
retries: 1
|
||||
});
|
||||
expect(mockDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user