improve test coverage

This commit is contained in:
Nathan Arthur 2025-02-12 16:30:41 -05:00
parent 60039b47ab
commit aacd7d7d65
7 changed files with 547 additions and 0 deletions

View file

@ -0,0 +1,22 @@
import { describe, it, expect } from "vitest";
import { extractContext } from "./extractContext";
describe("extractContext", () => {
it("should return full text if shorter than context size", () => {
const text = "Short text";
const result = extractContext(text, "question", 500);
expect(result).toBe(text);
});
it("should extract context around middle of text", () => {
const longText = Array(1000).fill("word").join(" ");
const result = extractContext(longText, "question", 100);
const words = result.split(/\s+/);
expect(words.length).toBeLessThanOrEqual(100);
});
it("should handle empty text", () => {
const result = extractContext("", "question");
expect(result).toBe("");
});
});

View file

@ -0,0 +1,23 @@
import { describe, it, expect } from "vitest";
import { generateContextHash } from "./generateContextHash";
describe("generateContextHash", () => {
it("should generate consistent hash for same content", async () => {
const content = "Test content";
const hash1 = await generateContextHash(content);
const hash2 = await generateContextHash(content);
expect(hash1).toBe(hash2);
});
it("should generate different hashes for different content", async () => {
const hash1 = await generateContextHash("Content 1");
const hash2 = await generateContextHash("Content 2");
expect(hash1).not.toBe(hash2);
});
it("should handle empty content", async () => {
const hash = await generateContextHash("");
expect(typeof hash).toBe("string");
expect(hash.length).toBeGreaterThan(0);
});
});

View file

@ -0,0 +1,67 @@
import { describe, it, expect, vi } from "vitest";
import { validateQuestions } from "./validateQuestions";
import type { Quest } from "../services/storage";
import { generateContextHash } from "./generateContextHash";
describe("validateQuestions", () => {
const mockPlugin: any = {};
it("should mark questions as obsolete when content hash changes", async () => {
const quests: Quest[] = [{
id: "1",
question: "Test question",
isCompleted: false,
isDismissed: false,
createdAt: Date.now(),
documentId: "test.md",
documentPath: "test.md",
contextHash: "old-hash",
contextSnapshot: "old content",
}];
const newContent = "new content";
const validatedQuests = await validateQuestions(mockPlugin, quests, newContent);
expect(validatedQuests[0].isObsolete).toBe(true);
expect(validatedQuests[0].lastValidated).toBeDefined();
expect(validatedQuests[0].obsoleteReason).toBeDefined();
});
it("should not mark questions as obsolete when content hash matches", async () => {
const content = "test content";
// First generate the actual hash
const hash = await generateContextHash(content);
const quests: Quest[] = [{
id: "1",
question: "Test question",
isCompleted: false,
isDismissed: false,
createdAt: Date.now(),
documentId: "test.md",
documentPath: "test.md",
contextHash: hash,
contextSnapshot: content,
}];
const validatedQuests = await validateQuestions(mockPlugin, quests, content);
expect(validatedQuests[0].isObsolete).toBeUndefined();
});
it("should skip validation for questions without context hash", async () => {
const quests: Quest[] = [{
id: "1",
question: "Test question",
isCompleted: false,
isDismissed: false,
createdAt: Date.now(),
documentId: "test.md",
documentPath: "test.md",
}];
const validatedQuests = await validateQuestions(mockPlugin, quests, "content");
expect(validatedQuests[0]).toEqual(quests[0]);
});
});

View file

@ -0,0 +1,48 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { EventEmitter } from "./index";
describe("EventEmitter", () => {
let events: EventEmitter;
beforeEach(() => {
events = new EventEmitter();
});
it("should register and trigger event handlers", () => {
const handler = vi.fn();
events.on("test-event", handler);
events.emit("test-event", { data: "test" });
expect(handler).toHaveBeenCalledWith({ data: "test" });
});
it("should allow multiple handlers for same event", () => {
const handler1 = vi.fn();
const handler2 = vi.fn();
events.on("test-event", handler1);
events.on("test-event", handler2);
events.emit("test-event");
expect(handler1).toHaveBeenCalled();
expect(handler2).toHaveBeenCalled();
});
it("should remove event handler", () => {
const handler = vi.fn();
events.on("test-event", handler);
events.off("test-event", handler);
events.emit("test-event");
expect(handler).not.toHaveBeenCalled();
});
it("should handle events with no registered handlers", () => {
expect(() => {
events.emit("non-existent-event");
}).not.toThrow();
});
it("should handle removing non-existent handler", () => {
const handler = vi.fn();
expect(() => {
events.off("test-event", handler);
}).not.toThrow();
});
});

View file

@ -0,0 +1,195 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { OpenAIService } from "./index";
import type OpenAI from "openai";
describe("OpenAIService", () => {
const mockSettings = {
generatePrompt: "Test generate prompt",
evaluatePrompt: "Test evaluate prompt",
breakdownPrompt: "Test breakdown prompt",
};
let service: OpenAIService;
let mockClient: any;
beforeEach(() => {
mockClient = {
completions: {
create: vi.fn(),
},
chat: {
completions: {
create: vi.fn(),
},
},
};
service = new OpenAIService("test-key", "gpt-4", mockSettings);
service.client = mockClient;
});
describe("generateQuestions", () => {
it("should generate questions using chat completion", async () => {
mockClient.chat.completions.create.mockResolvedValue({
choices: [{
message: {
tool_calls: [{
function: {
arguments: JSON.stringify({
questions: ["Question 1", "Question 2"]
})
}
}]
},
finish_reason: "tool_calls"
}]
});
const questions = await service.generateQuestions("Test content", 2);
expect(questions).toEqual(["Question 1", "Question 2"]);
expect(mockClient.chat.completions.create).toHaveBeenCalledWith({
model: "gpt-4",
messages: [
{ role: "system", content: mockSettings.generatePrompt },
{ role: "user", content: expect.stringContaining("Test content") }
],
tools: expect.arrayContaining([
expect.objectContaining({
type: "function",
function: expect.objectContaining({
name: "generate_questions"
})
})
]),
temperature: 0.7
});
});
it("should throw error when no questions are generated", async () => {
mockClient.chat.completions.create.mockResolvedValue({
choices: [{
message: {},
finish_reason: "stop"
}]
});
await expect(service.generateQuestions("Test content", 2))
.rejects.toThrow("No questions generated");
});
});
describe("evaluateQuestions", () => {
it("should evaluate questions using chat completion", async () => {
const mockEvaluations = {
evaluations: [{
questionId: "1",
isAnswered: true,
explanation: "Found answer"
}]
};
mockClient.chat.completions.create.mockResolvedValue({
choices: [{
message: {
tool_calls: [{
function: {
arguments: JSON.stringify(mockEvaluations)
}
}]
},
finish_reason: "tool_calls"
}]
});
const result = await service.evaluateQuestions("Test content", [
{ id: "1", question: "Test question" }
]);
expect(result).toEqual(mockEvaluations);
expect(mockClient.chat.completions.create).toHaveBeenCalledWith({
model: "gpt-4",
messages: [
{ role: "system", content: mockSettings.evaluatePrompt },
{ role: "user", content: expect.stringContaining("Test content") }
],
tools: expect.arrayContaining([
expect.objectContaining({
type: "function",
function: expect.objectContaining({
name: "evaluate_questions"
})
})
]),
temperature: 0.7
});
});
it("should throw error when no evaluations are generated", async () => {
mockClient.chat.completions.create.mockResolvedValue({
choices: [{
message: {},
finish_reason: "stop"
}]
});
await expect(service.evaluateQuestions("Test content", [
{ id: "1", question: "Test question" }
])).rejects.toThrow("No evaluations generated");
});
});
describe("breakdownQuestion", () => {
it("should break down question into sub-questions", async () => {
mockClient.chat.completions.create.mockResolvedValue({
choices: [{
message: {
tool_calls: [{
function: {
arguments: JSON.stringify({
questions: ["Sub-question 1", "Sub-question 2"]
})
}
}]
},
finish_reason: "tool_calls"
}]
});
const subQuestions = await service.breakdownQuestion(
"Test content",
"Main question"
);
expect(subQuestions).toEqual(["Sub-question 1", "Sub-question 2"]);
expect(mockClient.chat.completions.create).toHaveBeenCalledWith({
model: "gpt-4",
messages: [
{ role: "system", content: mockSettings.breakdownPrompt },
{ role: "user", content: expect.stringContaining("Main question") }
],
tools: expect.arrayContaining([
expect.objectContaining({
type: "function",
function: expect.objectContaining({
name: "generate_sub_questions"
})
})
]),
temperature: 0.7
});
});
it("should throw error when no sub-questions are generated", async () => {
mockClient.chat.completions.create.mockResolvedValue({
choices: [{
message: {},
finish_reason: "stop"
}]
});
await expect(service.breakdownQuestion("Test content", "Main question"))
.rejects.toThrow("No questions generated");
});
});
});

View file

@ -0,0 +1,139 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { StorageService } from "./index";
import type { Quest } from "./index";
describe("StorageService", () => {
const mockPlugin: any = {
loadData: vi.fn(),
saveData: vi.fn(),
};
let storage: StorageService;
beforeEach(() => {
vi.clearAllMocks();
storage = new StorageService(mockPlugin);
});
describe("getQuests", () => {
it("should return empty array when no data exists", async () => {
mockPlugin.loadData.mockResolvedValue(null);
const quests = await storage.getQuests();
expect(quests).toEqual([]);
});
it("should return quests from plugin data", async () => {
const mockQuests = [
{
id: "1",
question: "Test question",
isCompleted: false,
isDismissed: false,
createdAt: Date.now(),
documentId: "test.md",
documentPath: "test.md",
},
];
mockPlugin.loadData.mockResolvedValue({ quests: mockQuests });
const quests = await storage.getQuests();
expect(quests).toEqual(mockQuests);
});
});
describe("getQuestsForDocument", () => {
it("should return only quests for specified document", async () => {
const mockQuests = [
{
id: "1",
question: "Doc 1 question",
documentId: "doc1.md",
documentPath: "doc1.md",
isCompleted: false,
isDismissed: false,
createdAt: Date.now(),
},
{
id: "2",
question: "Doc 2 question",
documentId: "doc2.md",
documentPath: "doc2.md",
isCompleted: false,
isDismissed: false,
createdAt: Date.now(),
},
];
mockPlugin.loadData.mockResolvedValue({ quests: mockQuests });
const doc1Quests = await storage.getQuestsForDocument("doc1.md");
expect(doc1Quests).toHaveLength(1);
expect(doc1Quests[0].documentId).toBe("doc1.md");
});
it("should return empty array when no quests exist for document", async () => {
const mockQuests = [
{
id: "1",
question: "Doc 1 question",
documentId: "doc1.md",
documentPath: "doc1.md",
isCompleted: false,
isDismissed: false,
createdAt: Date.now(),
},
];
mockPlugin.loadData.mockResolvedValue({ quests: mockQuests });
const doc2Quests = await storage.getQuestsForDocument("doc2.md");
expect(doc2Quests).toEqual([]);
});
});
describe("saveQuests", () => {
it("should save quests to plugin data", async () => {
const questsToSave = [
{
id: "1",
question: "Test question",
isCompleted: false,
isDismissed: false,
createdAt: Date.now(),
documentId: "test.md",
documentPath: "test.md",
},
];
await storage.saveQuests(questsToSave);
expect(mockPlugin.saveData).toHaveBeenCalledWith({
quests: questsToSave,
});
});
it("should preserve existing data when saving quests", async () => {
const existingData = {
someOtherKey: "value",
quests: [],
};
mockPlugin.loadData.mockResolvedValue(existingData);
const questsToSave = [
{
id: "1",
question: "Test question",
isCompleted: false,
isDismissed: false,
createdAt: Date.now(),
documentId: "test.md",
documentPath: "test.md",
},
];
await storage.saveQuests(questsToSave);
expect(mockPlugin.saveData).toHaveBeenCalledWith({
someOtherKey: "value",
quests: questsToSave,
});
});
});
});

53
test-coverage-plan.md Normal file
View file

@ -0,0 +1,53 @@
# Test Coverage Plan
## ✓ Completed
1. Storage Service
- Quest saving/loading
- Document-specific quest filtering
- Data persistence
2. OpenAI Service
- Question generation
- Question evaluation
- Question breakdown
- Error handling
3. Context Management
- Context extraction
- Context hash generation
- Context validation
- Obsolescence detection
4. Event System
- Event emission
- Event subscription
- Event cleanup
## 🚧 In Progress
5. Integration Tests
- Full workflow scenarios
- Plugin initialization/cleanup
- Settings management
- View interactions
6. UI Components
- QuestList rendering
- Settings view
- Clear confirmation modal
- User interactions
## Requirements
- Focus on core functionality first
- Ensure critical paths are tested
- Add tests incrementally
- Use proper mocking and test isolation
## Next Steps
1. Set up Svelte component testing infrastructure
2. Add integration tests for full workflows
3. Add UI component tests
4. Add end-to-end tests for critical user journeys