mirror of
https://github.com/narthur/research-quest.git
synced 2026-07-22 11:40:25 +00:00
improve test coverage
This commit is contained in:
parent
60039b47ab
commit
aacd7d7d65
7 changed files with 547 additions and 0 deletions
22
src/lib/extractContext.spec.ts
Normal file
22
src/lib/extractContext.spec.ts
Normal 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("");
|
||||
});
|
||||
});
|
||||
23
src/lib/generateContextHash.spec.ts
Normal file
23
src/lib/generateContextHash.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
67
src/lib/validateQuestions.spec.ts
Normal file
67
src/lib/validateQuestions.spec.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
48
src/services/events/events.spec.ts
Normal file
48
src/services/events/events.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
195
src/services/openai/openai.spec.ts
Normal file
195
src/services/openai/openai.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
139
src/services/storage/storage.spec.ts
Normal file
139
src/services/storage/storage.spec.ts
Normal 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
53
test-coverage-plan.md
Normal 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
|
||||
Loading…
Reference in a new issue