import { resolve } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const files = vi.hoisted(() => ({ readFile: vi.fn() })); vi.mock('node:fs/promises', () => ({ readFile: files.readFile })); import { agentReleaseAsset } from '../../src/lib/server/hosts/releases.js'; import { GET } from '../../src/routes/api/agent/release/[file]/+server.js'; beforeEach(() => { files.readFile.mockReset(); vi.stubEnv('NODE_ENV', 'production'); }); afterEach(() => { vi.unstubAllEnvs(); }); describe('public agent release artifacts', () => { it('keeps every development artifact on the fresh static release when an older build also exists', async () => { vi.stubEnv('NODE_ENV', 'development'); files.readFile.mockImplementation(async (path: string) => path.startsWith(resolve('static/agent')) ? 'fresh-release' : 'old-build' ); for (const file of [ 'install.sh', 'ec-agent.py', 'ec-agent-update.py', 'manifest.json' ] as const) { expect(await agentReleaseAsset(file)).toBe('fresh-release'); } expect(files.readFile.mock.calls.map((call) => call[0])).toEqual([ resolve('static/agent/install.sh'), resolve('static/agent/ec-agent.py'), resolve('static/agent/ec-agent-update.py'), resolve('static/agent/manifest.json') ]); }); it('does not fill a missing development artifact from an unrelated older build', async () => { vi.stubEnv('NODE_ENV', 'development'); const missing = Object.assign(new Error('Not packaged yet'), { code: 'ENOENT' }); files.readFile.mockRejectedValueOnce(missing).mockResolvedValue('old-build'); await expect(agentReleaseAsset('ec-agent-update.py')).rejects.toBe(missing); expect(files.readFile.mock.calls).toEqual([ [resolve('static/agent/ec-agent-update.py'), 'utf8'] ]); }); it('uses the packaged production directory for every artifact even when source assets exist', async () => { files.readFile.mockImplementation(async (path: string) => path.startsWith(resolve('build/client/agent')) ? 'packaged-release' : 'source-release' ); for (const file of [ 'install.sh', 'ec-agent.py', 'ec-agent-update.py', 'manifest.json' ] as const) { expect(await agentReleaseAsset(file)).toBe('packaged-release'); } expect(files.readFile.mock.calls.map((call) => call[0])).toEqual([ resolve('build/client/agent/install.sh'), resolve('build/client/agent/ec-agent.py'), resolve('build/client/agent/ec-agent-update.py'), resolve('build/client/agent/manifest.json') ]); }); it.each(['ec-agent.py', 'ec-agent-update.py', 'manifest.json'])( 'serves the packaged %s without a browser session or host credential', async (file) => { const content = file.endsWith('.json') ? '{"version":"1.2.0","updaterProtocolVersion":1}' : '# verified release source\n'; files.readFile.mockResolvedValue(content); const response = await GET({ params: { file } }); expect(response.status).toBe(200); expect(await response.text()).toBe(content); expect(response.headers.get('cache-control')).toBe('no-cache'); expect(response.headers.get('content-type')).toContain( file.endsWith('.json') ? 'application/json' : 'text/plain' ); expect(files.readFile.mock.calls).toEqual([ [resolve('build/client/agent', file), 'utf8'] ]); } ); it('serves the updater from development assets when no packaged asset exists', async () => { files.readFile .mockRejectedValueOnce(Object.assign(new Error('missing'), { code: 'ENOENT' })) .mockResolvedValueOnce('# updater\n'); expect(await agentReleaseAsset('ec-agent-update.py')).toBe('# updater\n'); expect(files.readFile.mock.calls).toEqual([ [resolve('build/client/agent/ec-agent-update.py'), 'utf8'], [resolve('static/agent/ec-agent-update.py'), 'utf8'] ]); }); it('does not conceal a damaged packaged release by falling back to a different artifact', async () => { const failure = Object.assign(new Error('permission denied'), { code: 'EACCES' }); files.readFile.mockRejectedValue(failure); await expect(agentReleaseAsset('ec-agent-update.py')).rejects.toBe(failure); expect(files.readFile).toHaveBeenCalledTimes(1); }); it.each([ '../ec-agent.py', '%2e%2e%2fmanifest.json', 'agent-health.json', 'update-status.json', 'config.json', 'install.sh', 'ec-agent-update.py/anything' ])('rejects non-release file %s before reading disk', async (file) => { await expect(GET({ params: { file } })).rejects.toMatchObject({ status: 404 }); expect(files.readFile).not.toHaveBeenCalled(); }); });