Skip to content

Commit c45ba96

Browse files
committed
test(update-notifier): remove fs and compareVersions mocks, use real filesystem
- Replace fs/promises mock with real temp directory via GLOBAL_CONFIG_DIR - Remove compareVersions mock, let real pure function run - Refactor source to use existing GLOBAL_CONFIG_DIR (supports AGENTCORE_CONFIG_DIR env var) - Only mock fetchLatestVersion (network) and PACKAGE_VERSION (constant)
1 parent ee54acf commit c45ba96

2 files changed

Lines changed: 56 additions & 61 deletions

File tree

src/cli/__tests__/update-notifier.test.ts

Lines changed: 53 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,58 @@
11
import { type UpdateCheckResult, checkForUpdate, printUpdateNotification } from '../update-notifier.js';
2-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3-
4-
const { mockReadFile, mockWriteFile, mockMkdir } = vi.hoisted(() => ({
5-
mockReadFile: vi.fn(),
6-
mockWriteFile: vi.fn(),
7-
mockMkdir: vi.fn(),
8-
}));
2+
import { rmSync } from 'fs';
3+
import { mkdir, readFile, writeFile } from 'fs/promises';
4+
import { join } from 'path';
5+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6+
7+
const tmpDir = vi.hoisted(() => {
8+
/* eslint-disable @typescript-eslint/no-require-imports */
9+
const fs = require('fs');
10+
const os = require('os');
11+
const path = require('path');
12+
/* eslint-enable @typescript-eslint/no-require-imports */
13+
return fs.mkdtempSync(path.join(os.tmpdir(), 'update-notifier-test-'));
14+
});
915

10-
vi.mock('fs/promises', () => ({
11-
readFile: mockReadFile,
12-
writeFile: mockWriteFile,
13-
mkdir: mockMkdir,
16+
vi.mock('../../lib/schemas/io/global-config.js', () => ({
17+
GLOBAL_CONFIG_DIR: tmpDir,
1418
}));
1519

16-
vi.mock('../constants.js', () => ({
17-
PACKAGE_VERSION: '1.0.0',
18-
}));
20+
vi.mock('../constants.js', async importOriginal => {
21+
const actual = await importOriginal<typeof import('../constants.js')>();
22+
return { ...actual, PACKAGE_VERSION: '1.0.0' };
23+
});
1924

20-
const { mockFetchLatestVersion, mockCompareVersions } = vi.hoisted(() => ({
25+
const { mockFetchLatestVersion } = vi.hoisted(() => ({
2126
mockFetchLatestVersion: vi.fn(),
22-
mockCompareVersions: vi.fn(),
2327
}));
2428

25-
vi.mock('../commands/update/action.js', () => ({
26-
fetchLatestVersion: mockFetchLatestVersion,
27-
compareVersions: mockCompareVersions,
28-
}));
29+
vi.mock('../commands/update/action.js', async importOriginal => {
30+
const actual = await importOriginal<typeof import('../commands/update/action.js')>();
31+
return { ...actual, fetchLatestVersion: mockFetchLatestVersion };
32+
});
33+
34+
const CACHE_DIR = tmpDir;
35+
const CACHE_FILE = join(tmpDir, 'update-check.json');
2936

3037
describe('checkForUpdate', () => {
3138
beforeEach(() => {
3239
vi.spyOn(Date, 'now').mockReturnValue(1708646400000);
33-
mockWriteFile.mockResolvedValue(undefined);
34-
mockMkdir.mockResolvedValue(undefined);
40+
try {
41+
rmSync(CACHE_DIR, { recursive: true });
42+
} catch {}
3543
});
3644

3745
afterEach(() => {
3846
vi.restoreAllMocks();
39-
mockReadFile.mockReset();
40-
mockWriteFile.mockReset();
41-
mockMkdir.mockReset();
4247
mockFetchLatestVersion.mockReset();
43-
mockCompareVersions.mockReset();
48+
});
49+
50+
afterAll(() => {
51+
rmSync(tmpDir, { recursive: true });
4452
});
4553

4654
it('fetches from registry when no cache exists', async () => {
47-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
4855
mockFetchLatestVersion.mockResolvedValue('2.0.0');
49-
mockCompareVersions.mockReturnValue(1);
5056

5157
const result = await checkForUpdate();
5258

@@ -55,12 +61,8 @@ describe('checkForUpdate', () => {
5561
});
5662

5763
it('uses cache when last check was less than 24 hours ago', async () => {
58-
const cache = JSON.stringify({
59-
lastCheck: 1708646400000 - 1000, // 1 second ago
60-
latestVersion: '2.0.0',
61-
});
62-
mockReadFile.mockResolvedValue(cache);
63-
mockCompareVersions.mockReturnValue(1);
64+
await mkdir(CACHE_DIR, { recursive: true });
65+
await writeFile(CACHE_FILE, JSON.stringify({ lastCheck: 1708646400000 - 1000, latestVersion: '2.0.0' }), 'utf-8');
6466

6567
const result = await checkForUpdate();
6668

@@ -69,13 +71,13 @@ describe('checkForUpdate', () => {
6971
});
7072

7173
it('fetches from registry when cache is expired', async () => {
72-
const cache = JSON.stringify({
73-
lastCheck: 1708646400000 - 25 * 60 * 60 * 1000, // 25 hours ago
74-
latestVersion: '1.5.0',
75-
});
76-
mockReadFile.mockResolvedValue(cache);
74+
await mkdir(CACHE_DIR, { recursive: true });
75+
await writeFile(
76+
CACHE_FILE,
77+
JSON.stringify({ lastCheck: 1708646400000 - 25 * 60 * 60 * 1000, latestVersion: '1.5.0' }),
78+
'utf-8'
79+
);
7780
mockFetchLatestVersion.mockResolvedValue('2.0.0');
78-
mockCompareVersions.mockReturnValue(1);
7981

8082
const result = await checkForUpdate();
8183

@@ -84,42 +86,31 @@ describe('checkForUpdate', () => {
8486
});
8587

8688
it('writes cache after fetching', async () => {
87-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
8889
mockFetchLatestVersion.mockResolvedValue('2.0.0');
89-
mockCompareVersions.mockReturnValue(1);
9090

9191
await checkForUpdate();
9292

93-
expect(mockMkdir).toHaveBeenCalled();
94-
expect(mockWriteFile).toHaveBeenCalledWith(
95-
expect.stringContaining('update-check.json'),
96-
JSON.stringify({ lastCheck: 1708646400000, latestVersion: '2.0.0' }),
97-
'utf-8'
98-
);
93+
const cached = JSON.parse(await readFile(CACHE_FILE, 'utf-8'));
94+
expect(cached).toEqual({ lastCheck: 1708646400000, latestVersion: '2.0.0' });
9995
});
10096

10197
it('returns updateAvailable: false when versions match', async () => {
102-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
10398
mockFetchLatestVersion.mockResolvedValue('1.0.0');
104-
mockCompareVersions.mockReturnValue(0);
10599

106100
const result = await checkForUpdate();
107101

108102
expect(result).toEqual({ updateAvailable: false, latestVersion: '1.0.0' });
109103
});
110104

111105
it('returns updateAvailable: false when current is newer', async () => {
112-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
113106
mockFetchLatestVersion.mockResolvedValue('0.9.0');
114-
mockCompareVersions.mockReturnValue(-1);
115107

116108
const result = await checkForUpdate();
117109

118110
expect(result).toEqual({ updateAvailable: false, latestVersion: '0.9.0' });
119111
});
120112

121113
it('returns null on fetch error', async () => {
122-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
123114
mockFetchLatestVersion.mockRejectedValue(new Error('network error'));
124115

125116
const result = await checkForUpdate();
@@ -128,7 +119,8 @@ describe('checkForUpdate', () => {
128119
});
129120

130121
it('returns null on cache parse error and fetch error', async () => {
131-
mockReadFile.mockResolvedValue('invalid json');
122+
await mkdir(CACHE_DIR, { recursive: true });
123+
await writeFile(CACHE_FILE, 'invalid json', 'utf-8');
132124
mockFetchLatestVersion.mockRejectedValue(new Error('network error'));
133125

134126
const result = await checkForUpdate();
@@ -137,14 +129,18 @@ describe('checkForUpdate', () => {
137129
});
138130

139131
it('succeeds even when cache write fails', async () => {
140-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
132+
await mkdir(CACHE_DIR, { recursive: true });
133+
await writeFile(CACHE_FILE, '', 'utf-8');
134+
const { chmod } = await import('fs/promises');
135+
await chmod(CACHE_DIR, 0o444);
136+
141137
mockFetchLatestVersion.mockResolvedValue('2.0.0');
142-
mockCompareVersions.mockReturnValue(1);
143-
mockWriteFile.mockRejectedValue(new Error('EACCES'));
144138

145139
const result = await checkForUpdate();
146140

147141
expect(result).toEqual({ updateAvailable: true, latestVersion: '2.0.0' });
142+
143+
await chmod(CACHE_DIR, 0o755);
148144
});
149145
});
150146

src/cli/update-notifier.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1+
import { GLOBAL_CONFIG_DIR } from '../lib/schemas/io/global-config.js';
12
import { compareVersions, fetchLatestVersion } from './commands/update/action.js';
23
import { PACKAGE_VERSION } from './constants.js';
34
import { mkdir, readFile, writeFile } from 'fs/promises';
4-
import { homedir } from 'os';
55
import { join } from 'path';
66

7-
const CACHE_DIR = join(homedir(), '.agentcore');
8-
const CACHE_FILE = join(CACHE_DIR, 'update-check.json');
7+
const CACHE_FILE = join(GLOBAL_CONFIG_DIR, 'update-check.json');
98
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // every 24 hours
109

1110
interface CacheData {
@@ -29,7 +28,7 @@ async function readCache(): Promise<CacheData | null> {
2928

3029
async function writeCache(data: CacheData): Promise<void> {
3130
try {
32-
await mkdir(CACHE_DIR, { recursive: true });
31+
await mkdir(GLOBAL_CONFIG_DIR, { recursive: true });
3332
await writeFile(CACHE_FILE, JSON.stringify(data), 'utf-8');
3433
} catch {
3534
// Silently ignore cache write failures

0 commit comments

Comments
 (0)