logancyang_obsidian-copilot/src/core/MessageLifecycle.test.ts

304 lines
9.9 KiB
TypeScript

import { AI_SENDER, USER_SENDER } from "@/constants";
import { MessageContext } from "@/types/message";
import { TFile } from "obsidian";
import { MessageRepository } from "./MessageRepository";
import { mockTFile } from "@/__tests__/mockObsidian";
// Mock the settings module
jest.mock("@/settings/model", () => ({
getSettings: jest.fn(() => ({ debug: false })),
}));
/**
* This test file demonstrates the complete message lifecycle with context notes.
* It serves as both a test and documentation of how messages flow through the system.
*/
describe("Message Lifecycle with Context Notes - Complete Example", () => {
let messageRepository: MessageRepository;
beforeEach(() => {
messageRepository = new MessageRepository();
});
it("should demonstrate complete message lifecycle with context note", () => {
// Step 1: User types a message and attaches a note
const userDisplayText = "Please summarize the key points";
const attachedNote: TFile = mockTFile({
path: "meeting-notes-2024-01-15.md",
name: "meeting-notes-2024-01-15.md",
basename: "meeting-notes-2024-01-15",
extension: "md",
});
const context: MessageContext = {
notes: [attachedNote],
urls: [],
selectedTextContexts: [],
};
// Step 2: Message is stored with basic display text
const messageId = messageRepository.addMessage(
userDisplayText,
userDisplayText, // Initially same as display
USER_SENDER,
context
);
// Verify initial storage
let displayMessages = messageRepository.getDisplayMessages();
expect(displayMessages).toHaveLength(1);
expect(displayMessages[0]).toMatchObject({
message: "Please summarize the key points",
sender: USER_SENDER,
context: {
notes: [
expect.objectContaining({
basename: "meeting-notes-2024-01-15",
}),
],
},
});
// Step 3: Context Manager processes the note and updates processed text
const processedTextWithContext = `Please summarize the key points
<note_context>
<title>meeting-notes-2024-01-15</title>
<path>meeting-notes-2024-01-15.md</path>
<ctime>2024-01-15T10:00:00.000Z</ctime>
<mtime>2024-01-15T14:30:00.000Z</mtime>
<content>
# Team Meeting - January 15, 2024
## Attendees
- John (Product Manager)
- Sarah (Tech Lead)
- Mike (Designer)
## Key Decisions
1. Launch date moved to Q2 2024
2. MVP features: Auth, Dashboard, Analytics
3. Tech stack: React + Node.js + PostgreSQL
## Action Items
- Sarah: Set up CI/CD pipeline by Jan 20
- Mike: Complete dashboard mockups by Jan 22
- John: Finalize user stories by Jan 18
</content>
</note_context>`;
messageRepository.updateProcessedText(messageId, processedTextWithContext);
// Step 4: Verify different views for UI vs LLM
// UI View - shows only what user typed
displayMessages = messageRepository.getDisplayMessages();
expect(displayMessages[0].message).toBe("Please summarize the key points");
// LLM History View - returns display text only (no context)
// Context should come from envelope (L3 layer), not baked into history
const llmMessages = messageRepository.getLLMMessages();
expect(llmMessages[0].message).toBe("Please summarize the key points");
expect(llmMessages[0].message).not.toContain("Team Meeting - January 15, 2024");
// LLM Current Turn - use getLLMMessage(id) for message with context
const llmCurrentMessage = messageRepository.getLLMMessage(messageId);
expect(llmCurrentMessage?.message).toBe(processedTextWithContext);
expect(llmCurrentMessage?.message).toContain("Team Meeting - January 15, 2024");
expect(llmCurrentMessage?.message).toContain("Launch date moved to Q2 2024");
// Step 5: AI responds based on the context
const aiResponse = `Based on the meeting notes, here are the key points:
**Main Decisions:**
• Project launch postponed to Q2 2024
• MVP will include authentication, dashboard, and analytics features
• Technology choices: React frontend, Node.js backend, PostgreSQL database
**Team Responsibilities:**
• Sarah (Tech Lead): CI/CD pipeline setup - Due Jan 20
• Mike (Designer): Dashboard mockup designs - Due Jan 22
• John (Product Manager): User story finalization - Due Jan 18
The team appears to be taking a pragmatic approach with a focused MVP scope and clear task delegation.`;
messageRepository.addMessage(
aiResponse,
aiResponse, // AI messages have same display and processed text
AI_SENDER
);
// Step 6: Verify complete conversation
const finalDisplayMessages = messageRepository.getDisplayMessages();
expect(finalDisplayMessages).toHaveLength(2);
// User message with context badge
expect(finalDisplayMessages[0]).toMatchObject({
message: "Please summarize the key points",
sender: USER_SENDER,
context: {
notes: expect.arrayContaining([
expect.objectContaining({ basename: "meeting-notes-2024-01-15" }),
]),
},
});
// AI response
expect(finalDisplayMessages[1]).toMatchObject({
message: expect.stringContaining("Based on the meeting notes"),
sender: AI_SENDER,
});
// Step 7: Verify what LLM history contains (for chat memory)
const llmView = messageRepository.getLLMMessages();
expect(llmView).toHaveLength(2);
// LLM history contains raw messages only (no context)
expect(llmView[0].message).toBe("Please summarize the key points");
expect(llmView[0].message).not.toContain("Team Meeting - January 15, 2024");
// LLM history contains AI response
expect(llmView[1].message).toContain("Based on the meeting notes");
// Context metadata is preserved (envelope may or may not be built depending on flow)
expect(llmView[0].context).toBeDefined();
});
it("should handle message edit with context reprocessing", () => {
// Initial message with context
const initialText = "List the attendees";
const note: TFile = mockTFile({
path: "meeting.md",
name: "meeting.md",
basename: "meeting",
extension: "md",
});
const context: MessageContext = {
notes: [note],
urls: [],
selectedTextContexts: [],
};
// Add initial message with properly formatted context
const messageId = messageRepository.addMessage(
initialText,
`${initialText}
<note_context>
<title>meeting</title>
<path>meeting.md</path>
<ctime>2024-01-10T09:00:00.000Z</ctime>
<mtime>2024-01-10T10:00:00.000Z</mtime>
<content>
Attendees: Alice, Bob, Charlie
</content>
</note_context>`,
USER_SENDER,
context
);
// User edits the message
const editedText = "List the attendees and their roles";
messageRepository.editMessage(messageId, editedText);
// Context is reprocessed (simulated)
const reprocessedText = `${editedText}
<note_context>
<title>meeting</title>
<path>meeting.md</path>
<ctime>2024-01-10T09:00:00.000Z</ctime>
<mtime>2024-01-10T10:30:00.000Z</mtime>
<content>
Attendees: Alice (PM), Bob (Dev), Charlie (QA)
</content>
</note_context>`;
messageRepository.updateProcessedText(messageId, reprocessedText);
// Verify the edit
const displayMessages = messageRepository.getDisplayMessages();
expect(displayMessages[0].message).toBe("List the attendees and their roles");
// LLM history view contains display text only
const llmMessages = messageRepository.getLLMMessages();
expect(llmMessages[0].message).toBe("List the attendees and their roles");
expect(llmMessages[0].message).not.toContain("Alice (PM), Bob (Dev), Charlie (QA)");
// Full context available via getLLMMessage(id)
const llmMessage = messageRepository.getLLMMessage(messageId);
expect(llmMessage?.message).toContain("List the attendees and their roles");
expect(llmMessage?.message).toContain("Alice (PM), Bob (Dev), Charlie (QA)");
});
it("should maintain context through conversation", () => {
// User asks initial question with context
const context: MessageContext = {
notes: [
mockTFile({
path: "budget.md",
name: "budget.md",
basename: "budget",
extension: "md",
}),
],
urls: [],
selectedTextContexts: [],
};
messageRepository.addMessage(
"What is our total budget?",
`What is our total budget?
<note_context>
<title>budget</title>
<path>budget.md</path>
<ctime>2024-01-01T08:00:00.000Z</ctime>
<mtime>2024-01-05T16:00:00.000Z</mtime>
<content>
Q1: $100k
Q2: $150k
Q3: $200k
Q4: $250k
</content>
</note_context>`,
USER_SENDER,
context
);
// AI responds
messageRepository.addMessage(
"Based on the budget document, your total budget for the year is $700k ($100k + $150k + $200k + $250k).",
"Based on the budget document, your total budget for the year is $700k ($100k + $150k + $200k + $250k).",
AI_SENDER
);
// User asks follow-up (no new context needed)
messageRepository.addMessage(
"What percentage increase is Q4 over Q1?",
"What percentage increase is Q4 over Q1?",
USER_SENDER
);
// Verify conversation flow
const messages = messageRepository.getDisplayMessages();
expect(messages).toHaveLength(3);
// First message has context
expect(messages[0].context?.notes).toHaveLength(1);
// Follow-up messages don't need context repeated
expect(messages[1].context).toBeUndefined();
expect(messages[2].context).toBeUndefined();
// LLM history contains raw messages only (no context in history)
const llmMessages = messageRepository.getLLMMessages();
expect(llmMessages[0].message).not.toContain("<note_context>");
expect(llmMessages[0].message).not.toContain("<title>budget</title>");
// Context available via getLLMMessage(id) for current turn processing
const firstMessage = messageRepository.getLLMMessage(llmMessages[0].id!);
expect(firstMessage?.message).toContain("<note_context>");
expect(firstMessage?.message).toContain("<title>budget</title>");
});
});