import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { buildPrelaunchMaintenanceCacheKey, runCachedPrelaunchMaintenanceTask, } from '@electron/gateway/prelaunch-maintenance-cache'; describe('prelaunch maintenance cache', () => { let tempDir: string; let cachePath: string; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), 'clawx-prelaunch-cache-')); cachePath = join(tempDir, 'cache.json'); }); afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); it('runs a task on cache miss and skips it when the cache key is unchanged', () => { const task = vi.fn(); const cacheKey = buildPrelaunchMaintenanceCacheKey({ task: 'skills-symlink-cleanup', appVersion: '1.0.0', rootSignature: 'mtime-a', }); expect(runCachedPrelaunchMaintenanceTask( 'skills-symlink-cleanup', cacheKey, task, { cachePath }, )).toEqual({ executed: true, reason: 'cache-miss' }); expect(task).toHaveBeenCalledTimes(1); expect(runCachedPrelaunchMaintenanceTask( 'skills-symlink-cleanup', cacheKey, task, { cachePath }, )).toEqual({ executed: false, reason: 'cache-hit' }); expect(task).toHaveBeenCalledTimes(1); }); it('stores the post-task cache key when the task mutates signed inputs', () => { let rootSignature = 'dirty'; const cacheKey = () => buildPrelaunchMaintenanceCacheKey({ task: 'skills-symlink-cleanup', appVersion: '1.0.0', rootSignature, }); const task = vi.fn(() => { rootSignature = 'clean'; }); expect(runCachedPrelaunchMaintenanceTask( 'skills-symlink-cleanup', cacheKey, task, { cachePath }, )).toEqual({ executed: true, reason: 'cache-miss' }); expect(task).toHaveBeenCalledTimes(1); const writtenCache = JSON.parse(readFileSync(cachePath, 'utf-8')); expect(writtenCache.tasks['skills-symlink-cleanup'].key).toBe(cacheKey()); expect(runCachedPrelaunchMaintenanceTask( 'skills-symlink-cleanup', cacheKey, task, { cachePath }, )).toEqual({ executed: false, reason: 'cache-hit' }); expect(task).toHaveBeenCalledTimes(1); }); it('does not cache a task that reports maintenance failure', () => { const cacheKey = buildPrelaunchMaintenanceCacheKey({ task: 'plugin-maintenance', appVersion: '1.0.0', configuredChannels: ['feishu'], }); const task = vi.fn() .mockReturnValueOnce(false) .mockReturnValueOnce(true); expect(runCachedPrelaunchMaintenanceTask( 'plugin-maintenance', cacheKey, task, { cachePath }, )).toEqual({ executed: true, reason: 'task-failed' }); expect(task).toHaveBeenCalledTimes(1); expect(runCachedPrelaunchMaintenanceTask( 'plugin-maintenance', cacheKey, task, { cachePath }, )).toEqual({ executed: true, reason: 'cache-miss' }); expect(task).toHaveBeenCalledTimes(2); }); it('reruns a task when the cache key changes', () => { const task = vi.fn(); const firstKey = buildPrelaunchMaintenanceCacheKey({ task: 'runtime-deps-cleanup', openclawDir: '/old/openclaw', }); const secondKey = buildPrelaunchMaintenanceCacheKey({ task: 'runtime-deps-cleanup', openclawDir: '/new/openclaw', }); runCachedPrelaunchMaintenanceTask('runtime-deps-cleanup', firstKey, task, { cachePath }); const result = runCachedPrelaunchMaintenanceTask('runtime-deps-cleanup', secondKey, task, { cachePath }); expect(result).toEqual({ executed: true, reason: 'cache-miss' }); expect(task).toHaveBeenCalledTimes(2); }); it('treats cache schema changes as misses', () => { const task = vi.fn(); const cacheKey = buildPrelaunchMaintenanceCacheKey({ task: 'plugin-maintenance', configuredChannels: ['feishu'], }); writeFileSync(cachePath, JSON.stringify({ schemaVersion: 0, tasks: { 'plugin-maintenance': { key: cacheKey, updatedAt: new Date().toISOString(), }, }, }), 'utf-8'); const result = runCachedPrelaunchMaintenanceTask('plugin-maintenance', cacheKey, task, { cachePath }); expect(result).toEqual({ executed: true, reason: 'cache-miss' }); expect(task).toHaveBeenCalledTimes(1); expect(JSON.parse(readFileSync(cachePath, 'utf-8')).schemaVersion).toBe(1); }); it('runs conservatively when the cache file cannot be read', () => { const task = vi.fn(); const blockedCachePath = join(tempDir, 'blocked-cache'); mkdirSync(blockedCachePath); const cacheKey = buildPrelaunchMaintenanceCacheKey({ task: 'plugin-maintenance', configuredChannels: [], }); const result = runCachedPrelaunchMaintenanceTask( 'plugin-maintenance', cacheKey, task, { cachePath: blockedCachePath }, ); expect(result).toEqual({ executed: true, reason: 'cache-unavailable' }); expect(task).toHaveBeenCalledTimes(1); }); });