// @vitest-environment node import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { AgentBrowserModule } from '../../electron/agent-browser'; import { createCodingComposition } from '../../electron/api/coding-composition'; import { createProjectPluginService } from '../../electron/coding-plugins/project-service'; import { readCodingProjectConfigV2 } from '../../electron/coding-projects/project-config'; import { createCodingProjectStore, createLocalCodingProject, createMemoryCodingProjectStorage, } from '../../electron/coding-projects/project-store'; import { DATA_SERVICE_PLUGIN_ID } from '../../shared/coding-plugins'; const roots: string[] = []; afterEach(async () => { await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); describe('coding plugin lifecycle wiring', () => { it('marks managed inputs stale and invalidates/deactivates only after a real disable', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-lifecycle-')); roots.push(root); const stale = vi.fn(); const invalidate = vi.fn(); const deactivate = vi.fn().mockResolvedValue(undefined); const service = createProjectPluginService({ now: () => '2026-08-27T00:00:00.000Z', onManagedInputsChanged: stale, onAdapterDeactivated: async ({ projectPath }) => { invalidate('project_deactivated'); await deactivate(projectPath); }, }); await service.enable(root, DATA_SERVICE_PLUGIN_ID); await service.disable(root, DATA_SERVICE_PLUGIN_ID); await service.disable(root, DATA_SERVICE_PLUGIN_ID); expect(stale).toHaveBeenCalledTimes(2); expect(stale.mock.calls.map(([event]) => event.revision)).toEqual([1, 2]); expect(invalidate).toHaveBeenCalledOnce(); expect(deactivate).toHaveBeenCalledOnce(); expect(deactivate).toHaveBeenCalledWith(path.resolve(root)); }); it('deactivates before identity write and before runtime shutdown', async () => { const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-identity-')); const userDataDir = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-runtime-')); roots.push(projectPath, userDataDir); const storage = createMemoryCodingProjectStorage(); const store = createCodingProjectStore(storage); const legacy = await createLocalCodingProject({ projectPath }, store); const composition = createCodingComposition({ storage, projectStore: store, browser: { close: vi.fn().mockResolvedValue(undefined) } as unknown as AgentBrowserModule, paths: { executablePath: process.execPath, cliPath: path.join(projectPath, 'unused-cli.js'), userDataDir, bundledSkillsDir: path.resolve('resources/coding-skills'), }, }); const secondId = '22222222-2222-4222-8222-222222222222'; const order: string[] = []; vi.spyOn(composition.plugins, 'deactivate').mockImplementation(async () => { const snapshot = await readCodingProjectConfigV2(projectPath); order.push(`deactivate:${snapshot.status === 'valid' ? snapshot.config.projectId : 'invalid'}`); }); vi.spyOn(composition.runtime, 'shutdown').mockImplementation(async () => { order.push('runtime-shutdown'); }); await composition.projects.resolveProjectIdentity(legacy.project.id, { kind: 'bind', projectId: secondId, }); await composition.shutdown(); expect(order[0]).toBe('deactivate:undefined'); expect(order).toContain(`deactivate:${secondId}`); expect(order.at(-1)).toBe('runtime-shutdown'); }); });