fix: better openai usage for big pickle
This commit is contained in:
@@ -2,7 +2,8 @@
|
|||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(npm run build:*)",
|
"Bash(npm run build:*)",
|
||||||
"Bash(npx tsc:*)"
|
"Bash(npx tsc:*)",
|
||||||
|
"Bash(node ./node_modules/typescript/bin/tsc:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ export class OpenCodeManager {
|
|||||||
const toolCallsCollected: Array<{ name: string; args: unknown }> = [];
|
const toolCallsCollected: Array<{ name: string; args: unknown }> = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
console.log('[OpenCodeManager] Sending to provider:', provider, 'model:', modelId);
|
||||||
if (provider === 'anthropic') {
|
if (provider === 'anthropic') {
|
||||||
const result = await this.sendAnthropicMessage(
|
const result = await this.sendAnthropicMessage(
|
||||||
modelId,
|
modelId,
|
||||||
@@ -288,7 +289,9 @@ export class OpenCodeManager {
|
|||||||
);
|
);
|
||||||
fullResponse = result.content;
|
fullResponse = result.content;
|
||||||
}
|
}
|
||||||
|
console.log('[OpenCodeManager] fullResponse length:', fullResponse.length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[OpenCodeManager] Request error:', (error as Error).message);
|
||||||
const isAborted = abortController.signal.aborted || (error as Error).message === 'Request cancelled';
|
const isAborted = abortController.signal.aborted || (error as Error).message === 'Request cancelled';
|
||||||
if (!isAborted) {
|
if (!isAborted) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -467,7 +470,7 @@ export class OpenCodeManager {
|
|||||||
callbacks: { onDelta?: (delta: string) => void }
|
callbacks: { onDelta?: (delta: string) => void }
|
||||||
): Promise<{ content: string }> {
|
): Promise<{ content: string }> {
|
||||||
// Build OpenAI-format messages
|
// Build OpenAI-format messages
|
||||||
const messages = [
|
const messages: Array<Record<string, unknown>> = [
|
||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
...dbMessages
|
...dbMessages
|
||||||
.filter(m => m.role === 'user' || m.role === 'assistant')
|
.filter(m => m.role === 'user' || m.role === 'assistant')
|
||||||
@@ -488,93 +491,90 @@ export class OpenCodeManager {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const body: Record<string, unknown> = {
|
let accumulatedText = '';
|
||||||
model: modelId,
|
const MAX_TOOL_ROUNDS = 10;
|
||||||
max_tokens: 4096,
|
let round = 0;
|
||||||
messages,
|
|
||||||
tools: openaiTools,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await this.httpRequest(ZEN_OPENAI_URL, {
|
while (round < MAX_TOOL_ROUNDS) {
|
||||||
method: 'POST',
|
round++;
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${this.apiKey}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.statusCode >= 400) {
|
const body: Record<string, unknown> = {
|
||||||
const errorMsg = this.parseErrorResponse(response);
|
|
||||||
throw new Error(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.parse(response.body);
|
|
||||||
const choice = data.choices?.[0];
|
|
||||||
|
|
||||||
if (!choice?.message) {
|
|
||||||
throw new Error('API response missing expected message content');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle tool calls in OpenAI format
|
|
||||||
if (choice.message.tool_calls && choice.message.tool_calls.length > 0) {
|
|
||||||
// Execute tools and do follow-up call
|
|
||||||
const toolMessages = [
|
|
||||||
...messages,
|
|
||||||
choice.message,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const toolCall of choice.message.tool_calls) {
|
|
||||||
const toolName = toolCall.function.name;
|
|
||||||
const toolArgs = JSON.parse(toolCall.function.arguments || '{}');
|
|
||||||
const result = await this.executeTool(toolName, toolArgs);
|
|
||||||
|
|
||||||
toolMessages.push({
|
|
||||||
role: 'tool',
|
|
||||||
content: JSON.stringify(result),
|
|
||||||
tool_call_id: toolCall.id,
|
|
||||||
} as Record<string, unknown> as typeof messages[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make follow-up call with tool results
|
|
||||||
const followUpBody: Record<string, unknown> = {
|
|
||||||
model: modelId,
|
model: modelId,
|
||||||
max_tokens: 4096,
|
max_tokens: 4096,
|
||||||
messages: toolMessages,
|
messages,
|
||||||
tools: openaiTools,
|
tools: openaiTools,
|
||||||
};
|
};
|
||||||
|
|
||||||
const followUpResponse = await this.httpRequest(ZEN_OPENAI_URL, {
|
const response = await this.httpRequest(ZEN_OPENAI_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${this.apiKey}`,
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(followUpBody),
|
body: JSON.stringify(body),
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (followUpResponse.statusCode >= 400) {
|
if (response.statusCode >= 400) {
|
||||||
throw new Error(this.parseErrorResponse(followUpResponse));
|
const errorMsg = this.parseErrorResponse(response);
|
||||||
|
throw new Error(errorMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const followUpData = JSON.parse(followUpResponse.body);
|
const data = JSON.parse(response.body);
|
||||||
const content = followUpData.choices?.[0]?.message?.content || '';
|
const choice = data.choices?.[0];
|
||||||
|
|
||||||
if (callbacks.onDelta) {
|
console.log('[OpenCodeManager:OpenAI] Round', round, 'status:', response.statusCode, 'content type:', typeof choice?.message?.content, 'content length:', choice?.message?.content?.length, 'tool_calls:', choice?.message?.tool_calls?.length);
|
||||||
callbacks.onDelta(content);
|
|
||||||
|
if (!choice?.message) {
|
||||||
|
throw new Error('API response missing expected message content');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { content };
|
// Handle content that might be a string or an array of content parts
|
||||||
|
let textContent = '';
|
||||||
|
const content = choice.message.content;
|
||||||
|
if (typeof content === 'string') {
|
||||||
|
textContent = content;
|
||||||
|
} else if (Array.isArray(content)) {
|
||||||
|
// Handle array of content parts (some models return this format)
|
||||||
|
textContent = content
|
||||||
|
.filter((part: { type?: string; text?: string }) => part.type === 'text' && part.text)
|
||||||
|
.map((part: { text: string }) => part.text)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textContent) {
|
||||||
|
accumulatedText += textContent;
|
||||||
|
if (callbacks.onDelta) {
|
||||||
|
callbacks.onDelta(textContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no tool calls, we're done
|
||||||
|
if (!choice.message.tool_calls || choice.message.tool_calls.length === 0) {
|
||||||
|
console.log('[OpenCodeManager:OpenAI] Done. Accumulated text length:', accumulatedText.length);
|
||||||
|
return { content: accumulatedText };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add assistant message (with tool_calls) to conversation
|
||||||
|
messages.push(choice.message);
|
||||||
|
|
||||||
|
// Execute tool calls and add results
|
||||||
|
for (const toolCall of choice.message.tool_calls) {
|
||||||
|
const toolName = toolCall.function.name;
|
||||||
|
const toolArgs = JSON.parse(toolCall.function.arguments || '{}');
|
||||||
|
const result = await this.executeTool(toolName, toolArgs);
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
role: 'tool',
|
||||||
|
content: JSON.stringify(result),
|
||||||
|
tool_call_id: toolCall.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = choice.message.content || '';
|
// Hit max rounds
|
||||||
if (callbacks.onDelta) {
|
const fallbackText = accumulatedText || 'I reached the maximum number of tool calls. Please try again.';
|
||||||
callbacks.onDelta(content);
|
return { content: fallbackText };
|
||||||
}
|
|
||||||
|
|
||||||
return { content };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
|||||||
const result = await window.electronAPI?.chat.sendMessage(conversationId, message);
|
const result = await window.electronAPI?.chat.sendMessage(conversationId, message);
|
||||||
|
|
||||||
// Use the streamed content we accumulated via onStreamDelta
|
// Use the streamed content we accumulated via onStreamDelta
|
||||||
const assistantContent = streamingRef.current;
|
// Fall back to the backend result message if streaming didn't capture the content
|
||||||
|
const assistantContent = streamingRef.current || (result?.success ? result.message : '');
|
||||||
|
|
||||||
if (assistantContent) {
|
if (assistantContent) {
|
||||||
const assistantMessage: ChatMessage = {
|
const assistantMessage: ChatMessage = {
|
||||||
@@ -156,6 +157,17 @@ export const ChatPanel: React.FC<ChatPanelProps> = ({ conversationId }) => {
|
|||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
setMessages(prev => [...prev, errorMessage]);
|
setMessages(prev => [...prev, errorMessage]);
|
||||||
|
} else {
|
||||||
|
// No content from streaming AND no error, but also no success message
|
||||||
|
// This can happen with some models that don't return content properly
|
||||||
|
const noContentMessage: ChatMessage = {
|
||||||
|
id: `empty-${Date.now()}`,
|
||||||
|
conversationId,
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'The model returned an empty response. Try a different model or rephrase your question.',
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
setMessages(prev => [...prev, noContentMessage]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send message:', error);
|
console.error('Failed to send message:', error);
|
||||||
|
|||||||
@@ -750,7 +750,7 @@ const SettingsNav: React.FC = () => {
|
|||||||
|
|
||||||
// Chat conversations list
|
// Chat conversations list
|
||||||
const ChatList: React.FC = () => {
|
const ChatList: React.FC = () => {
|
||||||
const { openTab } = useAppStore();
|
const { openTab, closeTab } = useAppStore();
|
||||||
const [conversations, setConversations] = useState<ChatConversation[]>([]);
|
const [conversations, setConversations] = useState<ChatConversation[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
@@ -819,6 +819,8 @@ const ChatList: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
await window.electronAPI?.chat.deleteConversation(conversationId);
|
await window.electronAPI?.chat.deleteConversation(conversationId);
|
||||||
setConversations(prev => prev.filter(c => c.id !== conversationId));
|
setConversations(prev => prev.filter(c => c.id !== conversationId));
|
||||||
|
// Close the tab for the deleted chat
|
||||||
|
closeTab(conversationId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete conversation:', error);
|
console.error('Failed to delete conversation:', error);
|
||||||
showToast.error('Failed to delete chat');
|
showToast.error('Failed to delete chat');
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|||||||
import { useAppStore, Tab } from '../../store';
|
import { useAppStore, Tab } from '../../store';
|
||||||
import './TabBar.css';
|
import './TabBar.css';
|
||||||
|
|
||||||
const getTabTitle = (tab: Tab, posts: { id: string; title: string }[], media: { id: string; originalName: string }[]): string => {
|
const MAX_CHAT_TITLE_LENGTH = 25;
|
||||||
|
|
||||||
|
const getTabTitle = (
|
||||||
|
tab: Tab,
|
||||||
|
posts: { id: string; title: string }[],
|
||||||
|
media: { id: string; originalName: string }[],
|
||||||
|
chatTitles: Map<string, string>
|
||||||
|
): string => {
|
||||||
if (tab.type === 'settings') {
|
if (tab.type === 'settings') {
|
||||||
return 'Settings';
|
return 'Settings';
|
||||||
}
|
}
|
||||||
@@ -21,6 +28,17 @@ const getTabTitle = (tab: Tab, posts: { id: string; title: string }[], media: {
|
|||||||
return mediaItem?.originalName || 'Media';
|
return mediaItem?.originalName || 'Media';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tab.type === 'chat') {
|
||||||
|
const title = chatTitles.get(tab.id);
|
||||||
|
if (title && title !== 'New Chat') {
|
||||||
|
// Truncate long titles for display
|
||||||
|
return title.length > MAX_CHAT_TITLE_LENGTH
|
||||||
|
? title.substring(0, MAX_CHAT_TITLE_LENGTH) + '…'
|
||||||
|
: title;
|
||||||
|
}
|
||||||
|
return 'New Chat';
|
||||||
|
}
|
||||||
|
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,6 +68,12 @@ const getTabIcon = (tab: Tab): React.ReactNode => {
|
|||||||
<path d="M14.28 7.72l-6-6A1 1 0 007.57 1.5H2.5A1 1 0 001.5 2.5v5.07a1 1 0 00.22.56l6 6a1 1 0 001.41 0l5.15-5a1 1 0 000-1.41zM4 5a1 1 0 110-2 1 1 0 010 2z"/>
|
<path d="M14.28 7.72l-6-6A1 1 0 007.57 1.5H2.5A1 1 0 001.5 2.5v5.07a1 1 0 00.22.56l6 6a1 1 0 001.41 0l5.15-5a1 1 0 000-1.41zM4 5a1 1 0 110-2 1 1 0 010 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
case 'chat':
|
||||||
|
return (
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M14 1H2a1 1 0 00-1 1v10a1 1 0 001 1h3v2.5l4-2.5h5a1 1 0 001-1V2a1 1 0 00-1-1zm0 11H8.5L5 14v-2H2V2h12v10z"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
@@ -94,6 +118,52 @@ export const TabBar: React.FC = () => {
|
|||||||
const tabsContainerRef = useRef<HTMLDivElement>(null);
|
const tabsContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
const [showLeftArrow, setShowLeftArrow] = useState(false);
|
||||||
const [showRightArrow, setShowRightArrow] = useState(false);
|
const [showRightArrow, setShowRightArrow] = useState(false);
|
||||||
|
const [chatTitles, setChatTitles] = useState<Map<string, string>>(new Map());
|
||||||
|
|
||||||
|
// Fetch chat titles for chat tabs
|
||||||
|
useEffect(() => {
|
||||||
|
const chatTabs = tabs.filter(t => t.type === 'chat');
|
||||||
|
if (chatTabs.length === 0) return;
|
||||||
|
|
||||||
|
// Fetch titles for chat tabs that don't have a title yet
|
||||||
|
const fetchTitles = async () => {
|
||||||
|
const newTitles = new Map(chatTitles);
|
||||||
|
|
||||||
|
for (const tab of chatTabs) {
|
||||||
|
if (!chatTitles.has(tab.id)) {
|
||||||
|
try {
|
||||||
|
const conversation = await window.electronAPI?.chat.getConversation(tab.id);
|
||||||
|
if (conversation) {
|
||||||
|
newTitles.set(tab.id, conversation.title);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch chat title:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTitles.size !== chatTitles.size) {
|
||||||
|
setChatTitles(newTitles);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchTitles();
|
||||||
|
}, [tabs]); // Note: intentionally not including chatTitles to avoid infinite loops
|
||||||
|
|
||||||
|
// Listen for chat title updates
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = window.electronAPI?.chat.onTitleUpdated((data) => {
|
||||||
|
setChatTitles(prev => {
|
||||||
|
const newTitles = new Map(prev);
|
||||||
|
newTitles.set(data.conversationId, data.title);
|
||||||
|
return newTitles;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsub?.();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Check if arrows are needed based on scroll position
|
// Check if arrows are needed based on scroll position
|
||||||
const updateArrowVisibility = useCallback(() => {
|
const updateArrowVisibility = useCallback(() => {
|
||||||
@@ -229,7 +299,7 @@ export const TabBar: React.FC = () => {
|
|||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const isActive = tab.id === activeTabId;
|
const isActive = tab.id === activeTabId;
|
||||||
const isDirty = tab.type === 'post' && dirtyPosts.has(tab.id);
|
const isDirty = tab.type === 'post' && dirtyPosts.has(tab.id);
|
||||||
const title = getTabTitle(tab, posts, media);
|
const title = getTabTitle(tab, posts, media, chatTitles);
|
||||||
const icon = getTabIcon(tab);
|
const icon = getTabIcon(tab);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,24 +17,31 @@ const mockDirs = new Set<string>();
|
|||||||
let mockPosts: any[] = [];
|
let mockPosts: any[] = [];
|
||||||
let mockProject: any = null;
|
let mockProject: any = null;
|
||||||
|
|
||||||
|
// Helper to normalize paths (handle both Windows and Unix separators)
|
||||||
|
const normalizePath = (p: string): string => p.replace(/\\/g, '/');
|
||||||
|
|
||||||
// Mock fs/promises
|
// Mock fs/promises
|
||||||
vi.mock('fs/promises', () => ({
|
vi.mock('fs/promises', () => ({
|
||||||
readFile: vi.fn(async (filePath: string) => {
|
readFile: vi.fn(async (filePath: string) => {
|
||||||
if (mockFiles.has(filePath)) {
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||||
return mockFiles.get(filePath);
|
if (mockFiles.has(normalizedPath)) {
|
||||||
|
return mockFiles.get(normalizedPath);
|
||||||
}
|
}
|
||||||
const err = new Error(`ENOENT: no such file or directory, open '${filePath}'`) as NodeJS.ErrnoException;
|
const err = new Error(`ENOENT: no such file or directory, open '${filePath}'`) as NodeJS.ErrnoException;
|
||||||
err.code = 'ENOENT';
|
err.code = 'ENOENT';
|
||||||
throw err;
|
throw err;
|
||||||
}),
|
}),
|
||||||
writeFile: vi.fn(async (filePath: string, content: string) => {
|
writeFile: vi.fn(async (filePath: string, content: string) => {
|
||||||
mockFiles.set(filePath, content);
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||||
|
mockFiles.set(normalizedPath, content);
|
||||||
}),
|
}),
|
||||||
mkdir: vi.fn(async (dirPath: string) => {
|
mkdir: vi.fn(async (dirPath: string) => {
|
||||||
mockDirs.add(dirPath);
|
const normalizedPath = dirPath.replace(/\\/g, '/');
|
||||||
|
mockDirs.add(normalizedPath);
|
||||||
}),
|
}),
|
||||||
access: vi.fn(async (filePath: string) => {
|
access: vi.fn(async (filePath: string) => {
|
||||||
if (!mockFiles.has(filePath) && !mockDirs.has(filePath)) {
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||||
|
if (!mockFiles.has(normalizedPath) && !mockDirs.has(normalizedPath)) {
|
||||||
const err = new Error(`ENOENT: no such file or directory, access '${filePath}'`) as NodeJS.ErrnoException;
|
const err = new Error(`ENOENT: no such file or directory, access '${filePath}'`) as NodeJS.ErrnoException;
|
||||||
err.code = 'ENOENT';
|
err.code = 'ENOENT';
|
||||||
throw err;
|
throw err;
|
||||||
@@ -165,13 +172,13 @@ describe('MetaEngine', () => {
|
|||||||
await metaEngine.saveTags();
|
await metaEngine.saveTags();
|
||||||
|
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
const tagsPath = `${metaDir}\\tags.json`;
|
const tagsPath = normalizePath(`${metaDir}/tags.json`);
|
||||||
expect(mockFiles.has(tagsPath) || mockFiles.has(tagsPath.replace(/\\/g, '/'))).toBe(true);
|
expect(mockFiles.has(tagsPath)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load tags from filesystem', async () => {
|
it('should load tags from filesystem', async () => {
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
const tagsPath = `${metaDir}\\tags.json`;
|
const tagsPath = normalizePath(`${metaDir}/tags.json`);
|
||||||
mockFiles.set(tagsPath, JSON.stringify(['saved-tag-1', 'saved-tag-2']));
|
mockFiles.set(tagsPath, JSON.stringify(['saved-tag-1', 'saved-tag-2']));
|
||||||
|
|
||||||
await metaEngine.loadTags();
|
await metaEngine.loadTags();
|
||||||
@@ -214,13 +221,13 @@ describe('MetaEngine', () => {
|
|||||||
await metaEngine.saveCategories();
|
await metaEngine.saveCategories();
|
||||||
|
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
const catPath = `${metaDir}\\categories.json`;
|
const catPath = normalizePath(`${metaDir}/categories.json`);
|
||||||
expect(mockFiles.has(catPath) || mockFiles.has(catPath.replace(/\\/g, '/'))).toBe(true);
|
expect(mockFiles.has(catPath)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load categories from filesystem', async () => {
|
it('should load categories from filesystem', async () => {
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
const catPath = `${metaDir}\\categories.json`;
|
const catPath = normalizePath(`${metaDir}/categories.json`);
|
||||||
mockFiles.set(catPath, JSON.stringify(['cat-1', 'cat-2']));
|
mockFiles.set(catPath, JSON.stringify(['cat-1', 'cat-2']));
|
||||||
|
|
||||||
await metaEngine.loadCategories();
|
await metaEngine.loadCategories();
|
||||||
@@ -263,7 +270,7 @@ describe('MetaEngine', () => {
|
|||||||
it('should merge file tags with database tags', async () => {
|
it('should merge file tags with database tags', async () => {
|
||||||
// File has some tags
|
// File has some tags
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
mockFiles.set(`${metaDir}\\tags.json`, JSON.stringify(['file-tag']));
|
mockFiles.set(normalizePath(`${metaDir}/tags.json`), JSON.stringify(['file-tag']));
|
||||||
|
|
||||||
// Posts have different tags
|
// Posts have different tags
|
||||||
mockPosts = [
|
mockPosts = [
|
||||||
@@ -279,7 +286,7 @@ describe('MetaEngine', () => {
|
|||||||
|
|
||||||
it('should merge file categories with database categories', async () => {
|
it('should merge file categories with database categories', async () => {
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
mockFiles.set(`${metaDir}\\categories.json`, JSON.stringify(['file-cat']));
|
mockFiles.set(normalizePath(`${metaDir}/categories.json`), JSON.stringify(['file-cat']));
|
||||||
|
|
||||||
mockPosts = [
|
mockPosts = [
|
||||||
{ categories: JSON.stringify(['db-cat']) },
|
{ categories: JSON.stringify(['db-cat']) },
|
||||||
@@ -302,7 +309,7 @@ describe('MetaEngine', () => {
|
|||||||
|
|
||||||
it('should save merged results back to file', async () => {
|
it('should save merged results back to file', async () => {
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
mockFiles.set(`${metaDir}\\tags.json`, JSON.stringify(['existing']));
|
mockFiles.set(normalizePath(`${metaDir}/tags.json`), JSON.stringify(['existing']));
|
||||||
mockPosts = [{ tags: JSON.stringify(['new-from-db']), categories: JSON.stringify([]) }];
|
mockPosts = [{ tags: JSON.stringify(['new-from-db']), categories: JSON.stringify([]) }];
|
||||||
|
|
||||||
await metaEngine.syncOnStartup();
|
await metaEngine.syncOnStartup();
|
||||||
@@ -428,11 +435,11 @@ describe('MetaEngine', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
const projectPath = `${metaDir}\\project.json`;
|
const projectPath = normalizePath(`${metaDir}/project.json`);
|
||||||
expect(mockFiles.has(projectPath) || mockFiles.has(projectPath.replace(/\\/g, '/'))).toBe(true);
|
expect(mockFiles.has(projectPath)).toBe(true);
|
||||||
|
|
||||||
// Verify content
|
// Verify content
|
||||||
const content = mockFiles.get(projectPath) || mockFiles.get(projectPath.replace(/\\/g, '/'));
|
const content = mockFiles.get(projectPath);
|
||||||
const parsed = JSON.parse(content!);
|
const parsed = JSON.parse(content!);
|
||||||
expect(parsed.name).toBe('Test Project');
|
expect(parsed.name).toBe('Test Project');
|
||||||
expect(parsed.description).toBe('Test description');
|
expect(parsed.description).toBe('Test description');
|
||||||
@@ -440,7 +447,7 @@ describe('MetaEngine', () => {
|
|||||||
|
|
||||||
it('should load project metadata from filesystem', async () => {
|
it('should load project metadata from filesystem', async () => {
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
const projectPath = `${metaDir}\\project.json`;
|
const projectPath = normalizePath(`${metaDir}/project.json`);
|
||||||
mockFiles.set(projectPath, JSON.stringify({
|
mockFiles.set(projectPath, JSON.stringify({
|
||||||
name: 'Loaded Project',
|
name: 'Loaded Project',
|
||||||
description: 'Loaded description',
|
description: 'Loaded description',
|
||||||
@@ -489,7 +496,7 @@ describe('MetaEngine', () => {
|
|||||||
|
|
||||||
it('should load project metadata during syncOnStartup if file exists', async () => {
|
it('should load project metadata during syncOnStartup if file exists', async () => {
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
mockFiles.set(`${metaDir}\\project.json`, JSON.stringify({
|
mockFiles.set(normalizePath(`${metaDir}/project.json`), JSON.stringify({
|
||||||
name: 'Synced Project',
|
name: 'Synced Project',
|
||||||
description: 'Synced description',
|
description: 'Synced description',
|
||||||
}));
|
}));
|
||||||
@@ -503,7 +510,7 @@ describe('MetaEngine', () => {
|
|||||||
|
|
||||||
it('should create project.json with data from database during syncOnStartup if file does not exist', async () => {
|
it('should create project.json with data from database during syncOnStartup if file does not exist', async () => {
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
const projectPath = `${metaDir}\\project.json`;
|
const projectPath = normalizePath(`${metaDir}/project.json`);
|
||||||
|
|
||||||
// Setup mock project in database
|
// Setup mock project in database
|
||||||
mockProject = {
|
mockProject = {
|
||||||
@@ -522,7 +529,7 @@ describe('MetaEngine', () => {
|
|||||||
await metaEngine.syncOnStartup();
|
await metaEngine.syncOnStartup();
|
||||||
|
|
||||||
// File should be created
|
// File should be created
|
||||||
expect(mockFiles.has(projectPath) || mockFiles.has(projectPath.replace(/\\/g, '/'))).toBe(true);
|
expect(mockFiles.has(projectPath)).toBe(true);
|
||||||
|
|
||||||
// Should have metadata from database
|
// Should have metadata from database
|
||||||
const metadata = await metaEngine.getProjectMetadata();
|
const metadata = await metaEngine.getProjectMetadata();
|
||||||
@@ -540,7 +547,7 @@ describe('MetaEngine', () => {
|
|||||||
|
|
||||||
it('should create categories.json with defaults for new project with no posts', async () => {
|
it('should create categories.json with defaults for new project with no posts', async () => {
|
||||||
const metaDir = metaEngine.getMetaDir();
|
const metaDir = metaEngine.getMetaDir();
|
||||||
const catPath = `${metaDir}\\categories.json`;
|
const catPath = normalizePath(`${metaDir}/categories.json`);
|
||||||
|
|
||||||
// Setup mock project in database
|
// Setup mock project in database
|
||||||
mockProject = {
|
mockProject = {
|
||||||
@@ -559,7 +566,7 @@ describe('MetaEngine', () => {
|
|||||||
await metaEngine.syncOnStartup();
|
await metaEngine.syncOnStartup();
|
||||||
|
|
||||||
// File should be created with default categories
|
// File should be created with default categories
|
||||||
expect(mockFiles.has(catPath) || mockFiles.has(catPath.replace(/\\/g, '/'))).toBe(true);
|
expect(mockFiles.has(catPath)).toBe(true);
|
||||||
|
|
||||||
const categories = await metaEngine.getCategories();
|
const categories = await metaEngine.getCategories();
|
||||||
expect(categories).toContain('article');
|
expect(categories).toContain('article');
|
||||||
|
|||||||
Reference in New Issue
Block a user