/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import * as os from 'node:os';
import { SESSION_FILE_PREFIX, } from './chatRecordingService.js';
import { coreEvents } from '../utils/events.js';
import { Storage } from '../config/storage.js';
// Mock external modules used by startMemoryService
vi.mock('../agents/local-executor.js', () => ({
    LocalAgentExecutor: {
        create: vi.fn().mockResolvedValue({
            run: vi.fn().mockResolvedValue(undefined),
        }),
    },
}));
vi.mock('../agents/skill-extraction-agent.js', () => ({
    SkillExtractionAgent: vi.fn().mockReturnValue({
        name: 'skill-extraction',
        promptConfig: { systemPrompt: 'test' },
        tools: [],
        outputSchema: {},
        modelConfig: { model: 'test-model' },
    }),
}));
vi.mock('./executionLifecycleService.js', () => ({
    ExecutionLifecycleService: {
        createExecution: vi.fn().mockReturnValue({ pid: 42, result: {} }),
        completeExecution: vi.fn(),
    },
}));
vi.mock('../tools/tool-registry.js', () => ({
    ToolRegistry: vi.fn(),
}));
vi.mock('../prompts/prompt-registry.js', () => ({
    PromptRegistry: vi.fn(),
}));
vi.mock('../resources/resource-registry.js', () => ({
    ResourceRegistry: vi.fn(),
}));
vi.mock('../policy/policy-engine.js', () => ({
    PolicyEngine: vi.fn(),
}));
vi.mock('../policy/types.js', () => ({
    PolicyDecision: { ALLOW: 'ALLOW' },
}));
vi.mock('../confirmation-bus/message-bus.js', () => ({
    MessageBus: vi.fn(),
}));
vi.mock('../agents/registry.js', () => ({
    getModelConfigAlias: vi.fn().mockReturnValue('skill-extraction-config'),
}));
vi.mock('../config/storage.js', () => ({
    Storage: {
        getUserSkillsDir: vi.fn().mockReturnValue('/tmp/fake-user-skills'),
    },
}));
vi.mock('../skills/skillLoader.js', () => ({
    FRONTMATTER_REGEX: /^---\n([\s\S]*?)\n---/,
    parseFrontmatter: vi.fn().mockReturnValue(null),
}));
vi.mock('../utils/debugLogger.js', () => ({
    debugLogger: {
        debug: vi.fn(),
        log: vi.fn(),
        warn: vi.fn(),
    },
}));
vi.mock('../utils/events.js', () => ({
    coreEvents: {
        emitFeedback: vi.fn(),
    },
}));
// Helper to create a minimal ConversationRecord
function createConversation(overrides = {}) {
    const { messageCount = 4, ...rest } = overrides;
    const messages = Array.from({ length: messageCount }, (_, i) => ({
        id: String(i + 1),
        timestamp: new Date().toISOString(),
        content: [{ text: `Message ${i + 1}` }],
        type: i % 2 === 0 ? 'user' : 'gemini',
    }));
    return {
        sessionId: rest.sessionId ?? `session-${Date.now()}`,
        projectHash: 'abc123',
        startTime: '2025-01-01T00:00:00Z',
        lastUpdated: '2025-01-01T01:00:00Z',
        messages,
        ...rest,
    };
}
async function writeConversationJsonl(filePath, conversation) {
    const metadata = {
        sessionId: conversation.sessionId,
        projectHash: conversation.projectHash,
        startTime: conversation.startTime,
        lastUpdated: conversation.lastUpdated,
        summary: conversation.summary,
        directories: conversation.directories,
        kind: conversation.kind,
    };
    const records = [metadata, ...conversation.messages];
    await fs.writeFile(filePath, records.map((record) => JSON.stringify(record)).join('\n') + '\n');
}
async function setSessionMtime(filePath, timestamp) {
    const date = new Date(timestamp);
    await fs.utimes(filePath, date, date);
}
describe('memoryService', () => {
    let tmpDir;
    beforeEach(async () => {
        tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'skill-extract-test-'));
    });
    afterEach(async () => {
        vi.clearAllMocks();
        vi.unstubAllEnvs();
        await fs.rm(tmpDir, { recursive: true, force: true });
    });
    describe('tryAcquireLock', () => {
        it('successfully acquires lock when none exists', async () => {
            const { tryAcquireLock } = await import('./memoryService.js');
            const lockPath = path.join(tmpDir, '.extraction.lock');
            const result = await tryAcquireLock(lockPath);
            expect(result).toBe(true);
            const content = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
            expect(content.pid).toBe(process.pid);
            expect(content.startedAt).toBeDefined();
        });
        it('returns false when lock is held by a live process', async () => {
            const { tryAcquireLock } = await import('./memoryService.js');
            const lockPath = path.join(tmpDir, '.extraction.lock');
            // Write a lock with the current PID (which is alive)
            const lockInfo = {
                pid: process.pid,
                startedAt: new Date().toISOString(),
            };
            await fs.writeFile(lockPath, JSON.stringify(lockInfo));
            const result = await tryAcquireLock(lockPath);
            expect(result).toBe(false);
        });
        it('cleans up and re-acquires stale lock (dead PID)', async () => {
            const { tryAcquireLock } = await import('./memoryService.js');
            const lockPath = path.join(tmpDir, '.extraction.lock');
            // Use a PID that almost certainly doesn't exist
            const lockInfo = {
                pid: 2147483646,
                startedAt: new Date().toISOString(),
            };
            await fs.writeFile(lockPath, JSON.stringify(lockInfo));
            const result = await tryAcquireLock(lockPath);
            expect(result).toBe(true);
            const content = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
            expect(content.pid).toBe(process.pid);
        });
        it('cleans up and re-acquires stale lock (too old)', async () => {
            const { tryAcquireLock } = await import('./memoryService.js');
            const lockPath = path.join(tmpDir, '.extraction.lock');
            // Lock from 40 minutes ago with current PID — old enough to be stale (>35min)
            const oldDate = new Date(Date.now() - 40 * 60 * 1000).toISOString();
            const lockInfo = {
                pid: process.pid,
                startedAt: oldDate,
            };
            await fs.writeFile(lockPath, JSON.stringify(lockInfo));
            const result = await tryAcquireLock(lockPath);
            expect(result).toBe(true);
            const content = JSON.parse(await fs.readFile(lockPath, 'utf-8'));
            expect(content.pid).toBe(process.pid);
            // The new lock should have a recent timestamp
            const newLockAge = Date.now() - new Date(content.startedAt).getTime();
            expect(newLockAge).toBeLessThan(5000);
        });
    });
    describe('isLockStale', () => {
        it('returns true when PID is dead', async () => {
            const { isLockStale } = await import('./memoryService.js');
            const lockPath = path.join(tmpDir, '.extraction.lock');
            const lockInfo = {
                pid: 2147483646,
                startedAt: new Date().toISOString(),
            };
            await fs.writeFile(lockPath, JSON.stringify(lockInfo));
            expect(await isLockStale(lockPath)).toBe(true);
        });
        it('returns true when lock is too old (>35 min)', async () => {
            const { isLockStale } = await import('./memoryService.js');
            const lockPath = path.join(tmpDir, '.extraction.lock');
            const oldDate = new Date(Date.now() - 40 * 60 * 1000).toISOString();
            const lockInfo = {
                pid: process.pid,
                startedAt: oldDate,
            };
            await fs.writeFile(lockPath, JSON.stringify(lockInfo));
            expect(await isLockStale(lockPath)).toBe(true);
        });
        it('returns false when PID is alive and lock is fresh', async () => {
            const { isLockStale } = await import('./memoryService.js');
            const lockPath = path.join(tmpDir, '.extraction.lock');
            const lockInfo = {
                pid: process.pid,
                startedAt: new Date().toISOString(),
            };
            await fs.writeFile(lockPath, JSON.stringify(lockInfo));
            expect(await isLockStale(lockPath)).toBe(false);
        });
        it('returns true when file cannot be read', async () => {
            const { isLockStale } = await import('./memoryService.js');
            const lockPath = path.join(tmpDir, 'nonexistent.lock');
            expect(await isLockStale(lockPath)).toBe(true);
        });
    });
    describe('releaseLock', () => {
        it('deletes the lock file', async () => {
            const { releaseLock } = await import('./memoryService.js');
            const lockPath = path.join(tmpDir, '.extraction.lock');
            await fs.writeFile(lockPath, '{}');
            await releaseLock(lockPath);
            await expect(fs.access(lockPath)).rejects.toThrow();
        });
        it('does not throw when file is already gone', async () => {
            const { releaseLock } = await import('./memoryService.js');
            const lockPath = path.join(tmpDir, 'nonexistent.lock');
            await expect(releaseLock(lockPath)).resolves.not.toThrow();
        });
    });
    describe('readExtractionState / writeExtractionState', () => {
        it('returns default state when file does not exist', async () => {
            const { readExtractionState } = await import('./memoryService.js');
            const statePath = path.join(tmpDir, 'nonexistent-state.json');
            const state = await readExtractionState(statePath);
            expect(state).toEqual({ runs: [] });
        });
        it('reads existing state file', async () => {
            const { readExtractionState } = await import('./memoryService.js');
            const statePath = path.join(tmpDir, '.extraction-state.json');
            const existingState = {
                runs: [
                    {
                        runAt: '2025-01-01T00:00:00Z',
                        sessionIds: ['session-1', 'session-2'],
                        skillsCreated: [],
                    },
                ],
            };
            await fs.writeFile(statePath, JSON.stringify(existingState));
            const state = await readExtractionState(statePath);
            expect(state).toEqual(existingState);
        });
        it('writes state atomically via temp file + rename', async () => {
            const { writeExtractionState, readExtractionState } = await import('./memoryService.js');
            const statePath = path.join(tmpDir, '.extraction-state.json');
            const state = {
                runs: [
                    {
                        runAt: '2025-01-01T00:00:00Z',
                        sessionIds: ['session-abc'],
                        skillsCreated: [],
                    },
                ],
            };
            await writeExtractionState(statePath, state);
            // Verify the temp file does not linger
            const files = await fs.readdir(tmpDir);
            expect(files).not.toContain('.extraction-state.json.tmp');
            // Verify the final file is readable
            const readBack = await readExtractionState(statePath);
            expect(readBack).toEqual(state);
        });
    });
    describe('startMemoryService', () => {
        it('skips when lock is held by another instance', async () => {
            const { startMemoryService } = await import('./memoryService.js');
            const { LocalAgentExecutor } = await import('../agents/local-executor.js');
            const memoryDir = path.join(tmpDir, 'memory');
            const skillsDir = path.join(tmpDir, 'skills');
            const projectTempDir = path.join(tmpDir, 'temp');
            await fs.mkdir(memoryDir, { recursive: true });
            // Pre-acquire the lock with current PID
            const lockPath = path.join(memoryDir, '.extraction.lock');
            await fs.writeFile(lockPath, JSON.stringify({
                pid: process.pid,
                startedAt: new Date().toISOString(),
            }));
            const mockConfig = {
                storage: {
                    getProjectMemoryDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectMemoryTempDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectSkillsMemoryDir: vi.fn().mockReturnValue(skillsDir),
                    getProjectTempDir: vi.fn().mockReturnValue(projectTempDir),
                },
                getToolRegistry: vi.fn(),
                getMessageBus: vi.fn(),
                getGeminiClient: vi.fn(),
                sandboxManager: undefined,
            };
            await startMemoryService(mockConfig);
            // Agent should never have been created
            expect(LocalAgentExecutor.create).not.toHaveBeenCalled();
        });
        it('skips when no unprocessed sessions exist', async () => {
            const { startMemoryService } = await import('./memoryService.js');
            const { LocalAgentExecutor } = await import('../agents/local-executor.js');
            const memoryDir = path.join(tmpDir, 'memory2');
            const skillsDir = path.join(tmpDir, 'skills2');
            const projectTempDir = path.join(tmpDir, 'temp2');
            await fs.mkdir(memoryDir, { recursive: true });
            // Create an empty chats directory
            await fs.mkdir(path.join(projectTempDir, 'chats'), { recursive: true });
            const mockConfig = {
                storage: {
                    getProjectMemoryDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectMemoryTempDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectSkillsMemoryDir: vi.fn().mockReturnValue(skillsDir),
                    getProjectTempDir: vi.fn().mockReturnValue(projectTempDir),
                },
                getToolRegistry: vi.fn(),
                getMessageBus: vi.fn(),
                getGeminiClient: vi.fn(),
                sandboxManager: undefined,
            };
            await startMemoryService(mockConfig);
            expect(LocalAgentExecutor.create).not.toHaveBeenCalled();
            // Lock should be released
            const lockPath = path.join(memoryDir, '.extraction.lock');
            await expect(fs.access(lockPath)).rejects.toThrow();
        });
        it('releases lock on error', async () => {
            const { startMemoryService } = await import('./memoryService.js');
            const { LocalAgentExecutor } = await import('../agents/local-executor.js');
            const { ExecutionLifecycleService } = await import('./executionLifecycleService.js');
            const memoryDir = path.join(tmpDir, 'memory3');
            const skillsDir = path.join(tmpDir, 'skills3');
            const projectTempDir = path.join(tmpDir, 'temp3');
            const chatsDir = path.join(projectTempDir, 'chats');
            await fs.mkdir(memoryDir, { recursive: true });
            await fs.mkdir(chatsDir, { recursive: true });
            // Write a valid session that will pass all filters
            const conversation = createConversation({
                sessionId: 'error-session',
                messageCount: 20,
            });
            await fs.writeFile(path.join(chatsDir, 'session-2025-01-01T00-00-err00001.json'), JSON.stringify(conversation));
            // Make LocalAgentExecutor.create throw
            vi.mocked(LocalAgentExecutor.create).mockRejectedValueOnce(new Error('Agent creation failed'));
            const mockConfig = {
                storage: {
                    getProjectMemoryDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectMemoryTempDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectSkillsMemoryDir: vi.fn().mockReturnValue(skillsDir),
                    getProjectTempDir: vi.fn().mockReturnValue(projectTempDir),
                },
                getToolRegistry: vi.fn(),
                getMessageBus: vi.fn(),
                getGeminiClient: vi.fn(),
                sandboxManager: undefined,
            };
            await startMemoryService(mockConfig);
            // Lock should be released despite the error
            const lockPath = path.join(memoryDir, '.extraction.lock');
            await expect(fs.access(lockPath)).rejects.toThrow();
            // ExecutionLifecycleService.completeExecution should have been called with error
            expect(ExecutionLifecycleService.completeExecution).toHaveBeenCalledWith(42, expect.objectContaining({
                error: expect.any(Error),
            }));
        });
        it('emits feedback when new skills are created during extraction', async () => {
            const { startMemoryService } = await import('./memoryService.js');
            const { LocalAgentExecutor } = await import('../agents/local-executor.js');
            // Reset mocks that may carry state from prior tests
            vi.mocked(coreEvents.emitFeedback).mockClear();
            vi.mocked(LocalAgentExecutor.create).mockReset();
            const memoryDir = path.join(tmpDir, 'memory4');
            const skillsDir = path.join(tmpDir, 'skills4');
            const projectTempDir = path.join(tmpDir, 'temp4');
            const chatsDir = path.join(projectTempDir, 'chats');
            await fs.mkdir(memoryDir, { recursive: true });
            await fs.mkdir(skillsDir, { recursive: true });
            await fs.mkdir(chatsDir, { recursive: true });
            // Write a valid session with enough messages to pass the filter
            const conversation = createConversation({
                sessionId: 'skill-session',
                messageCount: 20,
            });
            await fs.writeFile(path.join(chatsDir, 'session-2025-01-01T00-00-skill001.json'), JSON.stringify(conversation));
            // Override LocalAgentExecutor.create to return an executor whose run
            // creates a new skill directory with a SKILL.md in the skillsDir
            vi.mocked(LocalAgentExecutor.create).mockResolvedValueOnce({
                run: vi.fn().mockImplementation(async () => {
                    const newSkillDir = path.join(skillsDir, 'my-new-skill');
                    await fs.mkdir(newSkillDir, { recursive: true });
                    await fs.writeFile(path.join(newSkillDir, 'SKILL.md'), '# My New Skill');
                    return undefined;
                }),
            });
            const mockConfig = {
                storage: {
                    getProjectMemoryDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectMemoryTempDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectSkillsMemoryDir: vi.fn().mockReturnValue(skillsDir),
                    getProjectTempDir: vi.fn().mockReturnValue(projectTempDir),
                },
                getToolRegistry: vi.fn(),
                getMessageBus: vi.fn(),
                getGeminiClient: vi.fn(),
                getSkillManager: vi.fn().mockReturnValue({ getSkills: () => [] }),
                modelConfigService: {
                    registerRuntimeModelConfig: vi.fn(),
                },
                sandboxManager: undefined,
            };
            await startMemoryService(mockConfig);
            expect(coreEvents.emitFeedback).toHaveBeenCalledWith('info', expect.stringContaining('my-new-skill'));
            expect(coreEvents.emitFeedback).toHaveBeenCalledWith('info', expect.stringContaining('/memory inbox'));
        });
        it('records only sessions whose read_file calls succeed as processed', async () => {
            const { startMemoryService, readExtractionState } = await import('./memoryService.js');
            const { LocalAgentExecutor } = await import('../agents/local-executor.js');
            vi.mocked(LocalAgentExecutor.create).mockReset();
            const memoryDir = path.join(tmpDir, 'memory-read-tracking');
            const skillsDir = path.join(tmpDir, 'skills-read-tracking');
            const projectTempDir = path.join(tmpDir, 'temp-read-tracking');
            const chatsDir = path.join(projectTempDir, 'chats');
            await fs.mkdir(memoryDir, { recursive: true });
            await fs.mkdir(skillsDir, { recursive: true });
            await fs.mkdir(chatsDir, { recursive: true });
            const openedConversation = createConversation({
                sessionId: 'opened-session',
                summary: 'Read this one',
                messageCount: 20,
                lastUpdated: '2025-01-02T01:00:00Z',
            });
            const skippedConversation = createConversation({
                sessionId: 'skipped-session',
                summary: 'Do not read this one',
                messageCount: 20,
                lastUpdated: '2025-01-01T01:00:00Z',
            });
            const openedPath = path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-02T00-00-opened.jsonl`);
            const skippedPath = path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-01T00-00-skipped.jsonl`);
            await writeConversationJsonl(openedPath, openedConversation);
            await writeConversationJsonl(skippedPath, skippedConversation);
            vi.mocked(LocalAgentExecutor.create).mockImplementationOnce(async (_definition, _context, onActivity) => ({
                run: vi.fn().mockImplementation(async () => {
                    onActivity?.({
                        isSubagentActivityEvent: true,
                        agentName: 'Skill Extractor',
                        type: 'TOOL_CALL_START',
                        data: {
                            name: 'read_file',
                            args: { file_path: openedPath },
                            callId: 'call-opened',
                        },
                    });
                    onActivity?.({
                        isSubagentActivityEvent: true,
                        agentName: 'Skill Extractor',
                        type: 'TOOL_CALL_START',
                        data: {
                            name: 'read_file',
                            args: { file_path: skippedPath },
                            callId: 'call-skipped',
                        },
                    });
                    onActivity?.({
                        isSubagentActivityEvent: true,
                        agentName: 'Skill Extractor',
                        type: 'ERROR',
                        data: {
                            name: 'read_file',
                            callId: 'call-skipped',
                            error: 'access denied',
                        },
                    });
                    onActivity?.({
                        isSubagentActivityEvent: true,
                        agentName: 'Skill Extractor',
                        type: 'TOOL_CALL_END',
                        data: {
                            name: 'read_file',
                            id: 'call-opened',
                            data: { content: 'Read this one' },
                        },
                    });
                    onActivity?.({
                        isSubagentActivityEvent: true,
                        agentName: 'Skill Extractor',
                        type: 'TOOL_CALL_START',
                        data: {
                            name: 'read_file',
                            args: { file_path: path.join(chatsDir, 'unrelated.jsonl') },
                            callId: 'call-unrelated',
                        },
                    });
                    return undefined;
                }),
            }));
            const mockConfig = {
                storage: {
                    getProjectMemoryDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectMemoryTempDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectSkillsMemoryDir: vi.fn().mockReturnValue(skillsDir),
                    getProjectTempDir: vi.fn().mockReturnValue(projectTempDir),
                },
                getToolRegistry: vi.fn(),
                getMessageBus: vi.fn(),
                getGeminiClient: vi.fn(),
                getSkillManager: vi.fn().mockReturnValue({ getSkills: () => [] }),
                modelConfigService: {
                    registerRuntimeModelConfig: vi.fn(),
                },
                getTargetDir: vi.fn().mockReturnValue(tmpDir),
                sandboxManager: undefined,
            };
            await startMemoryService(mockConfig);
            const state = await readExtractionState(path.join(memoryDir, '.extraction-state.json'));
            expect(state.runs).toHaveLength(1);
            expect(state.runs[0].candidateSessions).toEqual([
                {
                    sessionId: 'opened-session',
                    lastUpdated: '2025-01-02T01:00:00Z',
                },
                {
                    sessionId: 'skipped-session',
                    lastUpdated: '2025-01-01T01:00:00Z',
                },
            ]);
            expect(state.runs[0].processedSessions).toEqual([
                {
                    sessionId: 'opened-session',
                    lastUpdated: '2025-01-02T01:00:00Z',
                },
            ]);
            expect(state.runs[0].sessionIds).toEqual(['opened-session']);
        });
    });
    describe('getProcessedSessionIds', () => {
        it('returns empty set for empty state', async () => {
            const { getProcessedSessionIds } = await import('./memoryService.js');
            const result = getProcessedSessionIds({ runs: [] });
            expect(result).toBeInstanceOf(Set);
            expect(result.size).toBe(0);
        });
        it('collects session IDs across multiple runs', async () => {
            const { getProcessedSessionIds } = await import('./memoryService.js');
            const state = {
                runs: [
                    {
                        runAt: '2025-01-01T00:00:00Z',
                        sessionIds: ['s1', 's2'],
                        skillsCreated: [],
                    },
                    {
                        runAt: '2025-01-02T00:00:00Z',
                        sessionIds: ['s3'],
                        skillsCreated: [],
                    },
                ],
            };
            const result = getProcessedSessionIds(state);
            expect(result).toEqual(new Set(['s1', 's2', 's3']));
        });
        it('deduplicates IDs that appear in multiple runs', async () => {
            const { getProcessedSessionIds } = await import('./memoryService.js');
            const state = {
                runs: [
                    {
                        runAt: '2025-01-01T00:00:00Z',
                        sessionIds: ['s1', 's2'],
                        skillsCreated: [],
                    },
                    {
                        runAt: '2025-01-02T00:00:00Z',
                        sessionIds: ['s2', 's3'],
                        skillsCreated: [],
                    },
                ],
            };
            const result = getProcessedSessionIds(state);
            expect(result.size).toBe(3);
            expect(result).toEqual(new Set(['s1', 's2', 's3']));
        });
    });
    describe('buildSessionIndex', () => {
        let chatsDir;
        beforeEach(async () => {
            chatsDir = path.join(tmpDir, 'chats');
            await fs.mkdir(chatsDir, { recursive: true });
        });
        it('returns empty index and no new IDs when chats dir is empty', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            const result = await buildSessionIndex(chatsDir, { runs: [] });
            expect(result.sessionIndex).toBe('');
            expect(result.newSessionIds).toEqual([]);
        });
        it('returns empty index when chats dir does not exist', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            const nonexistentDir = path.join(tmpDir, 'no-such-dir');
            const result = await buildSessionIndex(nonexistentDir, { runs: [] });
            expect(result.sessionIndex).toBe('');
            expect(result.newSessionIds).toEqual([]);
        });
        it('marks sessions as [NEW] when not in any previous run', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            const conversation = createConversation({
                sessionId: 'brand-new',
                summary: 'A brand new session',
                messageCount: 20,
            });
            await fs.writeFile(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-01T00-00-brandnew.json`), JSON.stringify(conversation));
            const result = await buildSessionIndex(chatsDir, { runs: [] });
            expect(result.sessionIndex).toContain('[NEW]');
            expect(result.sessionIndex).not.toContain('[old]');
        });
        it('marks sessions as [old] when already in a previous run', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            const conversation = createConversation({
                sessionId: 'old-session',
                summary: 'An old session',
                messageCount: 20,
            });
            await fs.writeFile(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-01T00-00-oldsess1.json`), JSON.stringify(conversation));
            const state = {
                runs: [
                    {
                        runAt: '2025-01-01T02:00:00Z',
                        sessionIds: ['old-session'],
                        skillsCreated: [],
                    },
                ],
            };
            const result = await buildSessionIndex(chatsDir, state);
            expect(result.sessionIndex).toContain('[old]');
            expect(result.sessionIndex).not.toContain('[NEW]');
        });
        it('treats resumed legacy sessions as [NEW] when lastUpdated moved past the old run', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            const conversation = createConversation({
                sessionId: 'resumed-session',
                summary: 'Resumed after extraction',
                messageCount: 20,
                lastUpdated: '2025-01-01T03:00:00Z',
            });
            await fs.writeFile(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-01T00-00-resumed01.json`), JSON.stringify(conversation));
            const state = {
                runs: [
                    {
                        runAt: '2025-01-01T02:00:00Z',
                        sessionIds: ['resumed-session'],
                        skillsCreated: [],
                    },
                ],
            };
            const result = await buildSessionIndex(chatsDir, state);
            expect(result.sessionIndex).toContain('[NEW]');
            expect(result.newSessionIds).toEqual(['resumed-session']);
        });
        it('includes file path and summary in each line', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            const conversation = createConversation({
                sessionId: 'detailed-session',
                summary: 'Debugging the login flow',
                messageCount: 20,
            });
            const fileName = `${SESSION_FILE_PREFIX}2025-01-01T00-00-detail01.json`;
            await fs.writeFile(path.join(chatsDir, fileName), JSON.stringify(conversation));
            const result = await buildSessionIndex(chatsDir, { runs: [] });
            expect(result.sessionIndex).toContain('Debugging the login flow');
            expect(result.sessionIndex).toContain(path.join(chatsDir, fileName));
        });
        it('filters out subagent sessions', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            const conversation = createConversation({
                sessionId: 'sub-session',
                kind: 'subagent',
                messageCount: 20,
            });
            await fs.writeFile(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-01T00-00-sub00001.json`), JSON.stringify(conversation));
            const result = await buildSessionIndex(chatsDir, { runs: [] });
            expect(result.sessionIndex).toBe('');
            expect(result.newSessionIds).toEqual([]);
        });
        it('filters out sessions with fewer than 10 user messages', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            // 2 messages total: 1 user (index 0) + 1 gemini (index 1)
            const conversation = createConversation({
                sessionId: 'short-session',
                messageCount: 2,
            });
            await fs.writeFile(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-01T00-00-short001.json`), JSON.stringify(conversation));
            const result = await buildSessionIndex(chatsDir, { runs: [] });
            expect(result.sessionIndex).toBe('');
            expect(result.newSessionIds).toEqual([]);
        });
        it('caps at MAX_SESSION_INDEX_SIZE (50)', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            // Create 3 eligible sessions, verify all 3 appear (well under cap)
            for (let i = 0; i < 3; i++) {
                const conversation = createConversation({
                    sessionId: `capped-session-${i}`,
                    summary: `Summary ${i}`,
                    messageCount: 20,
                });
                const paddedIndex = String(i).padStart(4, '0');
                await fs.writeFile(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-0${i + 1}T00-00-cap${paddedIndex}.json`), JSON.stringify(conversation));
            }
            const result = await buildSessionIndex(chatsDir, { runs: [] });
            const lines = result.sessionIndex.split('\n').filter((l) => l.length > 0);
            expect(lines).toHaveLength(3);
            expect(result.newSessionIds).toHaveLength(3);
        });
        it('returns newSessionIds only for unprocessed sessions', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            // Write two sessions: one already processed, one new
            const oldConv = createConversation({
                sessionId: 'processed-one',
                summary: 'Old',
                messageCount: 20,
            });
            await fs.writeFile(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-01T00-00-proc0001.json`), JSON.stringify(oldConv));
            const newConv = createConversation({
                sessionId: 'fresh-one',
                summary: 'New',
                messageCount: 20,
            });
            await fs.writeFile(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-02T00-00-fres0001.json`), JSON.stringify(newConv));
            const state = {
                runs: [
                    {
                        runAt: '2025-01-01T02:00:00Z',
                        sessionIds: ['processed-one'],
                        skillsCreated: [],
                    },
                ],
            };
            const result = await buildSessionIndex(chatsDir, state);
            expect(result.newSessionIds).toEqual(['fresh-one']);
            expect(result.newSessionIds).not.toContain('processed-one');
            // Both sessions should still appear in the index
            expect(result.sessionIndex).toContain('[NEW]');
            expect(result.sessionIndex).toContain('[old]');
        });
        it('reads JSONL sessions and sorts by actual lastUpdated instead of filename', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            const olderByName = createConversation({
                sessionId: 'older-by-name',
                summary: 'Filename looks newer',
                messageCount: 20,
                lastUpdated: '2025-01-01T01:00:00Z',
            });
            const newerByActivity = createConversation({
                sessionId: 'newer-by-activity',
                summary: 'Actually most recent',
                messageCount: 20,
                lastUpdated: '2025-02-01T01:00:00Z',
            });
            await writeConversationJsonl(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-02-01T00-00-oldername.jsonl`), olderByName);
            await writeConversationJsonl(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-01T00-00-neweractv.jsonl`), newerByActivity);
            const result = await buildSessionIndex(chatsDir, { runs: [] });
            const firstLine = result.sessionIndex.split('\n')[0];
            expect(firstLine).toContain('Actually most recent');
            expect(firstLine).not.toContain('Filename looks newer');
        });
        it('rotates in older unprocessed sessions instead of starving them behind retried recent ones', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            for (let i = 0; i < 11; i++) {
                const day = String(11 - i).padStart(2, '0');
                const conversation = createConversation({
                    sessionId: `backlog-${i}`,
                    summary: `Backlog ${i}`,
                    messageCount: 20,
                    lastUpdated: `2025-01-${day}T01:00:00Z`,
                });
                await fs.writeFile(path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-${day}T00-00-backlog${i}.json`), JSON.stringify(conversation));
            }
            const state = {
                runs: [
                    {
                        runAt: '2025-02-01T00:00:00Z',
                        sessionIds: [],
                        candidateSessions: Array.from({ length: 10 }, (_, i) => ({
                            sessionId: `backlog-${i}`,
                            lastUpdated: `2025-01-${String(11 - i).padStart(2, '0')}T01:00:00Z`,
                        })),
                        skillsCreated: [],
                    },
                ],
            };
            const result = await buildSessionIndex(chatsDir, state);
            expect(result.newSessionIds).toContain('backlog-10');
            expect(result.newSessionIds).not.toContain('backlog-9');
        });
        it('surfaces older unprocessed sessions even when the newest 100 files were already processed', async () => {
            const { buildSessionIndex } = await import('./memoryService.js');
            const processedSessions = [];
            for (let i = 0; i < 105; i++) {
                const timestamp = new Date(Date.UTC(2025, 0, 1, 0, 0, 105 - i)).toISOString();
                const conversation = createConversation({
                    sessionId: `backlog-${i}`,
                    summary: `Backlog ${i}`,
                    messageCount: 20,
                    lastUpdated: timestamp,
                });
                const filePath = path.join(chatsDir, `${SESSION_FILE_PREFIX}2025-01-01T00-00-backlog${String(i).padStart(3, '0')}.json`);
                await fs.writeFile(filePath, JSON.stringify(conversation));
                await setSessionMtime(filePath, timestamp);
                if (i < 100) {
                    processedSessions.push({
                        sessionId: conversation.sessionId,
                        lastUpdated: conversation.lastUpdated,
                    });
                }
            }
            const result = await buildSessionIndex(chatsDir, {
                runs: [
                    {
                        runAt: '2025-02-01T00:00:00Z',
                        sessionIds: processedSessions.map((session) => session.sessionId),
                        processedSessions,
                        skillsCreated: [],
                    },
                ],
            });
            expect(result.newSessionIds).toEqual([
                'backlog-100',
                'backlog-101',
                'backlog-102',
                'backlog-103',
                'backlog-104',
            ]);
            expect(result.sessionIndex).toContain('Backlog 100');
            expect(result.sessionIndex).toContain('Backlog 104');
        });
    });
    describe('ExtractionState runs tracking', () => {
        it('readExtractionState parses runs array with skillsCreated', async () => {
            const { readExtractionState } = await import('./memoryService.js');
            const statePath = path.join(tmpDir, 'state-with-skills.json');
            const state = {
                runs: [
                    {
                        runAt: '2025-06-01T00:00:00Z',
                        sessionIds: ['s1'],
                        candidateSessions: [
                            {
                                sessionId: 's1',
                                lastUpdated: '2025-05-31T12:00:00Z',
                            },
                        ],
                        processedSessions: [
                            {
                                sessionId: 's1',
                                lastUpdated: '2025-05-31T12:00:00Z',
                            },
                        ],
                        skillsCreated: ['debug-helper', 'test-gen'],
                    },
                ],
            };
            await fs.writeFile(statePath, JSON.stringify(state));
            const result = await readExtractionState(statePath);
            expect(result.runs).toHaveLength(1);
            expect(result.runs[0].skillsCreated).toEqual([
                'debug-helper',
                'test-gen',
            ]);
            expect(result.runs[0].candidateSessions).toEqual([
                {
                    sessionId: 's1',
                    lastUpdated: '2025-05-31T12:00:00Z',
                },
            ]);
            expect(result.runs[0].processedSessions).toEqual([
                {
                    sessionId: 's1',
                    lastUpdated: '2025-05-31T12:00:00Z',
                },
            ]);
            expect(result.runs[0].sessionIds).toEqual(['s1']);
            expect(result.runs[0].runAt).toBe('2025-06-01T00:00:00Z');
        });
        it('writeExtractionState + readExtractionState roundtrips runs correctly', async () => {
            const { writeExtractionState, readExtractionState } = await import('./memoryService.js');
            const statePath = path.join(tmpDir, 'roundtrip-state.json');
            const runs = [
                {
                    runAt: '2025-01-01T00:00:00Z',
                    sessionIds: ['a', 'b'],
                    candidateSessions: [
                        {
                            sessionId: 'a',
                            lastUpdated: '2024-12-31T23:00:00Z',
                        },
                        {
                            sessionId: 'b',
                            lastUpdated: '2024-12-31T22:00:00Z',
                        },
                    ],
                    processedSessions: [
                        {
                            sessionId: 'a',
                            lastUpdated: '2024-12-31T23:00:00Z',
                        },
                        {
                            sessionId: 'b',
                            lastUpdated: '2024-12-31T22:00:00Z',
                        },
                    ],
                    skillsCreated: ['skill-x'],
                },
                {
                    runAt: '2025-01-02T00:00:00Z',
                    sessionIds: ['c'],
                    skillsCreated: [],
                },
            ];
            const state = { runs };
            await writeExtractionState(statePath, state);
            const result = await readExtractionState(statePath);
            expect(result).toEqual(state);
        });
        it('readExtractionState handles old format without runs', async () => {
            const { readExtractionState } = await import('./memoryService.js');
            const statePath = path.join(tmpDir, 'old-format-state.json');
            // Old format: an object without a runs array
            await fs.writeFile(statePath, JSON.stringify({ lastProcessed: '2025-01-01' }));
            const result = await readExtractionState(statePath);
            expect(result).toEqual({ runs: [] });
        });
    });
    describe('validatePatches', () => {
        let skillsDir;
        let globalSkillsDir;
        let projectSkillsDir;
        let validateConfig;
        beforeEach(() => {
            skillsDir = path.join(tmpDir, 'skills');
            globalSkillsDir = path.join(tmpDir, 'global-skills');
            projectSkillsDir = path.join(tmpDir, 'project-skills');
            vi.mocked(Storage.getUserSkillsDir).mockReturnValue(globalSkillsDir);
            validateConfig = {
                storage: {
                    getProjectSkillsDir: () => projectSkillsDir,
                },
            };
        });
        it('returns empty array when no patch files exist', async () => {
            const { validatePatches } = await import('./memoryService.js');
            await fs.mkdir(skillsDir, { recursive: true });
            // Add a non-patch file to ensure it's ignored
            await fs.writeFile(path.join(skillsDir, 'some-file.txt'), 'hello');
            const result = await validatePatches(skillsDir, validateConfig);
            expect(result).toEqual([]);
        });
        it('returns empty array when directory does not exist', async () => {
            const { validatePatches } = await import('./memoryService.js');
            const result = await validatePatches(path.join(tmpDir, 'nonexistent-dir'), validateConfig);
            expect(result).toEqual([]);
        });
        it('removes invalid patch files', async () => {
            const { validatePatches } = await import('./memoryService.js');
            await fs.mkdir(skillsDir, { recursive: true });
            // Write a malformed patch
            const patchPath = path.join(skillsDir, 'bad-skill.patch');
            await fs.writeFile(patchPath, 'this is not a valid patch');
            const result = await validatePatches(skillsDir, validateConfig);
            expect(result).toEqual([]);
            // Verify the invalid patch was deleted
            await expect(fs.access(patchPath)).rejects.toThrow();
        });
        it('keeps valid patch files', async () => {
            const { validatePatches } = await import('./memoryService.js');
            await fs.mkdir(skillsDir, { recursive: true });
            await fs.mkdir(projectSkillsDir, { recursive: true });
            // Create a real target file to patch
            const targetFile = path.join(projectSkillsDir, 'target.md');
            await fs.writeFile(targetFile, 'line1\nline2\nline3\n');
            // Write a valid unified diff patch with absolute paths
            const patchContent = [
                `--- ${targetFile}`,
                `+++ ${targetFile}`,
                '@@ -1,3 +1,4 @@',
                ' line1',
                ' line2',
                '+line2.5',
                ' line3',
                '',
            ].join('\n');
            const patchPath = path.join(skillsDir, 'good-skill.patch');
            await fs.writeFile(patchPath, patchContent);
            const result = await validatePatches(skillsDir, validateConfig);
            expect(result).toEqual(['good-skill.patch']);
            // Verify the valid patch still exists
            await expect(fs.access(patchPath)).resolves.toBeUndefined();
        });
        it('keeps patches with repeated sections for the same file when hunks apply cumulatively', async () => {
            const { validatePatches } = await import('./memoryService.js');
            await fs.mkdir(skillsDir, { recursive: true });
            await fs.mkdir(projectSkillsDir, { recursive: true });
            const targetFile = path.join(projectSkillsDir, 'target.md');
            await fs.writeFile(targetFile, 'alpha\nbeta\ngamma\ndelta\n');
            const patchPath = path.join(skillsDir, 'multi-section.patch');
            await fs.writeFile(patchPath, [
                `--- ${targetFile}`,
                `+++ ${targetFile}`,
                '@@ -1,4 +1,5 @@',
                ' alpha',
                ' beta',
                '+beta2',
                ' gamma',
                ' delta',
                `--- ${targetFile}`,
                `+++ ${targetFile}`,
                '@@ -2,4 +2,5 @@',
                ' beta',
                ' beta2',
                ' gamma',
                '+gamma2',
                ' delta',
                '',
            ].join('\n'));
            const result = await validatePatches(skillsDir, validateConfig);
            expect(result).toEqual(['multi-section.patch']);
            await expect(fs.access(patchPath)).resolves.toBeUndefined();
        });
        it('removes /dev/null patches that target an existing skill file', async () => {
            const { validatePatches } = await import('./memoryService.js');
            await fs.mkdir(skillsDir, { recursive: true });
            await fs.mkdir(projectSkillsDir, { recursive: true });
            const targetFile = path.join(projectSkillsDir, 'existing-skill.md');
            await fs.writeFile(targetFile, 'original content\n');
            const patchPath = path.join(skillsDir, 'bad-new-file.patch');
            await fs.writeFile(patchPath, [
                '--- /dev/null',
                `+++ ${targetFile}`,
                '@@ -0,0 +1 @@',
                '+replacement content',
                '',
            ].join('\n'));
            const result = await validatePatches(skillsDir, validateConfig);
            expect(result).toEqual([]);
            await expect(fs.access(patchPath)).rejects.toThrow();
            expect(await fs.readFile(targetFile, 'utf-8')).toBe('original content\n');
        });
        it('removes patches with malformed diff headers', async () => {
            const { validatePatches } = await import('./memoryService.js');
            await fs.mkdir(skillsDir, { recursive: true });
            await fs.mkdir(projectSkillsDir, { recursive: true });
            const targetFile = path.join(projectSkillsDir, 'target.md');
            await fs.writeFile(targetFile, 'line1\nline2\nline3\n');
            const patchPath = path.join(skillsDir, 'bad-headers.patch');
            await fs.writeFile(patchPath, [
                `--- ${targetFile}`,
                '+++ .gemini/skills/foo/SKILL.md',
                '@@ -1,3 +1,4 @@',
                ' line1',
                ' line2',
                '+line2.5',
                ' line3',
                '',
            ].join('\n'));
            const result = await validatePatches(skillsDir, validateConfig);
            expect(result).toEqual([]);
            await expect(fs.access(patchPath)).rejects.toThrow();
            expect(await fs.readFile(targetFile, 'utf-8')).toBe('line1\nline2\nline3\n');
        });
        it('removes patches that contain no hunks', async () => {
            const { validatePatches } = await import('./memoryService.js');
            await fs.mkdir(skillsDir, { recursive: true });
            const patchPath = path.join(skillsDir, 'empty.patch');
            await fs.writeFile(patchPath, [
                `--- ${path.join(projectSkillsDir, 'target.md')}`,
                `+++ ${path.join(projectSkillsDir, 'target.md')}`,
                '',
            ].join('\n'));
            const result = await validatePatches(skillsDir, validateConfig);
            expect(result).toEqual([]);
            await expect(fs.access(patchPath)).rejects.toThrow();
        });
        it('removes patches that target files outside the allowed skill roots', async () => {
            const { validatePatches } = await import('./memoryService.js');
            await fs.mkdir(skillsDir, { recursive: true });
            const outsideFile = path.join(tmpDir, 'outside.md');
            await fs.writeFile(outsideFile, 'line1\nline2\nline3\n');
            const patchPath = path.join(skillsDir, 'outside.patch');
            await fs.writeFile(patchPath, [
                `--- ${outsideFile}`,
                `+++ ${outsideFile}`,
                '@@ -1,3 +1,4 @@',
                ' line1',
                ' line2',
                '+line2.5',
                ' line3',
                '',
            ].join('\n'));
            const result = await validatePatches(skillsDir, validateConfig);
            expect(result).toEqual([]);
            await expect(fs.access(patchPath)).rejects.toThrow();
        });
        it('removes patches that escape the allowed roots through a symlinked parent', async () => {
            const { validatePatches } = await import('./memoryService.js');
            await fs.mkdir(skillsDir, { recursive: true });
            await fs.mkdir(projectSkillsDir, { recursive: true });
            const outsideDir = path.join(tmpDir, 'outside-dir');
            const linkedDir = path.join(projectSkillsDir, 'linked');
            await fs.mkdir(outsideDir, { recursive: true });
            await fs.symlink(outsideDir, linkedDir, process.platform === 'win32' ? 'junction' : 'dir');
            const outsideFile = path.join(outsideDir, 'escaped.md');
            await fs.writeFile(outsideFile, 'line1\nline2\nline3\n');
            const patchPath = path.join(skillsDir, 'symlink.patch');
            await fs.writeFile(patchPath, [
                `--- ${path.join(linkedDir, 'escaped.md')}`,
                `+++ ${path.join(linkedDir, 'escaped.md')}`,
                '@@ -1,3 +1,4 @@',
                ' line1',
                ' line2',
                '+line2.5',
                ' line3',
                '',
            ].join('\n'));
            const result = await validatePatches(skillsDir, validateConfig);
            expect(result).toEqual([]);
            await expect(fs.access(patchPath)).rejects.toThrow();
            expect(await fs.readFile(outsideFile, 'utf-8')).not.toContain('line2.5');
        });
    });
    describe('startMemoryService feedback for patch-only runs', () => {
        it('emits feedback when extraction produces only patch suggestions', async () => {
            const { startMemoryService } = await import('./memoryService.js');
            const { LocalAgentExecutor } = await import('../agents/local-executor.js');
            vi.mocked(coreEvents.emitFeedback).mockClear();
            vi.mocked(LocalAgentExecutor.create).mockReset();
            const memoryDir = path.join(tmpDir, 'memory-patch-only');
            const skillsDir = path.join(tmpDir, 'skills-patch-only');
            const projectTempDir = path.join(tmpDir, 'temp-patch-only');
            const chatsDir = path.join(projectTempDir, 'chats');
            const projectSkillsDir = path.join(tmpDir, 'workspace-skills');
            await fs.mkdir(memoryDir, { recursive: true });
            await fs.mkdir(skillsDir, { recursive: true });
            await fs.mkdir(chatsDir, { recursive: true });
            await fs.mkdir(projectSkillsDir, { recursive: true });
            const existingSkill = path.join(projectSkillsDir, 'existing-skill.md');
            await fs.writeFile(existingSkill, 'line1\nline2\nline3\n');
            const conversation = createConversation({
                sessionId: 'patch-only-session',
                messageCount: 20,
            });
            await fs.writeFile(path.join(chatsDir, 'session-2025-01-01T00-00-patchonly.json'), JSON.stringify(conversation));
            vi.mocked(Storage.getUserSkillsDir).mockReturnValue(path.join(tmpDir, 'global-skills'));
            vi.mocked(LocalAgentExecutor.create).mockResolvedValueOnce({
                run: vi.fn().mockImplementation(async () => {
                    const patchPath = path.join(skillsDir, 'existing-skill.patch');
                    await fs.writeFile(patchPath, [
                        `--- ${existingSkill}`,
                        `+++ ${existingSkill}`,
                        '@@ -1,3 +1,4 @@',
                        ' line1',
                        ' line2',
                        '+line2.5',
                        ' line3',
                        '',
                    ].join('\n'));
                    return undefined;
                }),
            });
            const mockConfig = {
                storage: {
                    getProjectMemoryDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectMemoryTempDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectSkillsMemoryDir: vi.fn().mockReturnValue(skillsDir),
                    getProjectSkillsDir: vi.fn().mockReturnValue(projectSkillsDir),
                    getProjectTempDir: vi.fn().mockReturnValue(projectTempDir),
                },
                getToolRegistry: vi.fn(),
                getMessageBus: vi.fn(),
                getGeminiClient: vi.fn(),
                getSkillManager: vi.fn().mockReturnValue({ getSkills: () => [] }),
                modelConfigService: {
                    registerRuntimeModelConfig: vi.fn(),
                },
                sandboxManager: undefined,
            };
            await startMemoryService(mockConfig);
            expect(coreEvents.emitFeedback).toHaveBeenCalledWith('info', expect.stringContaining('skill update'));
            expect(coreEvents.emitFeedback).toHaveBeenCalledWith('info', expect.stringContaining('/memory inbox'));
        });
        it('does not emit feedback for old inbox patches when this run creates none', async () => {
            const { startMemoryService } = await import('./memoryService.js');
            const { LocalAgentExecutor } = await import('../agents/local-executor.js');
            vi.mocked(coreEvents.emitFeedback).mockClear();
            vi.mocked(LocalAgentExecutor.create).mockReset();
            const memoryDir = path.join(tmpDir, 'memory-old-patch');
            const skillsDir = path.join(tmpDir, 'skills-old-patch');
            const projectTempDir = path.join(tmpDir, 'temp-old-patch');
            const chatsDir = path.join(projectTempDir, 'chats');
            const projectSkillsDir = path.join(tmpDir, 'workspace-old-patch');
            await fs.mkdir(memoryDir, { recursive: true });
            await fs.mkdir(skillsDir, { recursive: true });
            await fs.mkdir(chatsDir, { recursive: true });
            await fs.mkdir(projectSkillsDir, { recursive: true });
            const existingSkill = path.join(projectSkillsDir, 'existing-skill.md');
            await fs.writeFile(existingSkill, 'line1\nline2\nline3\n');
            await fs.writeFile(path.join(skillsDir, 'existing-skill.patch'), [
                `--- ${existingSkill}`,
                `+++ ${existingSkill}`,
                '@@ -1,3 +1,4 @@',
                ' line1',
                ' line2',
                '+line2.5',
                ' line3',
                '',
            ].join('\n'));
            const conversation = createConversation({
                sessionId: 'old-patch-session',
                messageCount: 20,
            });
            await fs.writeFile(path.join(chatsDir, 'session-2025-01-01T00-00-oldpatch.json'), JSON.stringify(conversation));
            vi.mocked(Storage.getUserSkillsDir).mockReturnValue(path.join(tmpDir, 'global-skills'));
            vi.mocked(LocalAgentExecutor.create).mockResolvedValueOnce({
                run: vi.fn().mockResolvedValue(undefined),
            });
            const mockConfig = {
                storage: {
                    getProjectMemoryDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectMemoryTempDir: vi.fn().mockReturnValue(memoryDir),
                    getProjectSkillsMemoryDir: vi.fn().mockReturnValue(skillsDir),
                    getProjectSkillsDir: vi.fn().mockReturnValue(projectSkillsDir),
                    getProjectTempDir: vi.fn().mockReturnValue(projectTempDir),
                },
                getToolRegistry: vi.fn(),
                getMessageBus: vi.fn(),
                getGeminiClient: vi.fn(),
                getSkillManager: vi.fn().mockReturnValue({ getSkills: () => [] }),
                modelConfigService: {
                    registerRuntimeModelConfig: vi.fn(),
                },
                sandboxManager: undefined,
            };
            await startMemoryService(mockConfig);
            expect(coreEvents.emitFeedback).not.toHaveBeenCalled();
        });
    });
});
//# sourceMappingURL=memoryService.test.js.map