Skip to content

Commit 9063a77

Browse files
authored
test: remove unnecessary mocks, use real filesystem (#1156)
* test(resolve-ui-dist-dir): replace fs mock with real temp directory * test(BaseRenderer): remove APP_DIR and fs mocks, use real temp directories * test(get-trace): remove fs mock, verify file output with real filesystem * 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) * test: address review feedback on mock removal changes - Remove redundant CACHE_DIR alias, use tmpDir directly - Add force:true to afterAll rmSync for robustness - Wrap chmod test in try/finally to prevent leaked read-only state - Remove vacuous conditional assertion in resolve-ui-dist-dir - Switch get-trace to beforeEach/afterEach for test isolation consistency * review.md: specify I/O boundary examples (network, AWS SDK, HTTP)
1 parent 715a5a2 commit 9063a77

8 files changed

Lines changed: 137 additions & 153 deletions

File tree

.github/harness/prompts/review.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,12 @@ choose. Skip style nits and minor suggestions — only flag things that actually
1616

1717
When finished, submit a formal PR review (approve or request changes) with individual and inline comments in it. Be
1818
specific with line numbers.
19+
20+
If all serious issues have already been raised in existing comments, or if you found no new issues, post a single
21+
comment on the PR saying it looks good to merge (or that all issues have already been flagged).
22+
23+
## Patterns to look out for
24+
25+
- **Excessive mocking** — Avoid excessive mocking; it couples tests to implementation details, provides weaker
26+
guarantees, and often points to mismanaged dependencies. Prefer real dependencies (e.g. temp directories over fs
27+
mocks) and only mock at true I/O boundaries (e.g., network calls, AWS SDK clients, HTTP requests).

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

Lines changed: 55 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,124 @@
1+
import { ONE_DAY_MS, ONE_HOUR_MS, ONE_SECOND_MS } from '../../lib/time-constants.js';
2+
import * as action from '../commands/update/action.js';
3+
import * as constants from '../constants.js';
14
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-
}));
9-
10-
vi.mock('fs/promises', () => ({
11-
readFile: mockReadFile,
12-
writeFile: mockWriteFile,
13-
mkdir: mockMkdir,
14-
}));
15-
16-
vi.mock('../constants.js', () => ({
17-
PACKAGE_VERSION: '1.0.0',
18-
}));
19-
20-
const { mockFetchLatestVersion, mockCompareVersions } = vi.hoisted(() => ({
21-
mockFetchLatestVersion: vi.fn(),
22-
mockCompareVersions: vi.fn(),
23-
}));
24-
25-
vi.mock('../commands/update/action.js', () => ({
26-
fetchLatestVersion: mockFetchLatestVersion,
27-
compareVersions: mockCompareVersions,
28-
}));
5+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
6+
import { mkdir, readFile, writeFile } from 'fs/promises';
7+
import { tmpdir } from 'os';
8+
import { join } from 'path';
9+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
10+
11+
const NOW = 1708646400000;
12+
const tmpDir = mkdtempSync(join(tmpdir(), 'update-notifier-test-'));
13+
const CACHE_FILE = join(tmpDir, 'update-check.json');
2914

3015
describe('checkForUpdate', () => {
16+
let originalConfigDir: string | undefined;
17+
3118
beforeEach(() => {
32-
vi.spyOn(Date, 'now').mockReturnValue(1708646400000);
33-
mockWriteFile.mockResolvedValue(undefined);
34-
mockMkdir.mockResolvedValue(undefined);
19+
originalConfigDir = process.env.AGENTCORE_CONFIG_DIR;
20+
process.env.AGENTCORE_CONFIG_DIR = tmpDir;
21+
vi.spyOn(Date, 'now').mockReturnValue(NOW);
22+
vi.spyOn(constants, 'PACKAGE_VERSION', 'get').mockReturnValue('1.0.0');
23+
rmSync(tmpDir, { recursive: true, force: true });
3524
});
3625

3726
afterEach(() => {
3827
vi.restoreAllMocks();
39-
mockReadFile.mockReset();
40-
mockWriteFile.mockReset();
41-
mockMkdir.mockReset();
42-
mockFetchLatestVersion.mockReset();
43-
mockCompareVersions.mockReset();
28+
if (originalConfigDir === undefined) {
29+
delete process.env.AGENTCORE_CONFIG_DIR;
30+
} else {
31+
process.env.AGENTCORE_CONFIG_DIR = originalConfigDir;
32+
}
33+
});
34+
35+
afterAll(() => {
36+
rmSync(tmpDir, { recursive: true, force: true });
4437
});
4538

4639
it('fetches from registry when no cache exists', async () => {
47-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
48-
mockFetchLatestVersion.mockResolvedValue('2.0.0');
49-
mockCompareVersions.mockReturnValue(1);
40+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('2.0.0');
5041

5142
const result = await checkForUpdate();
5243

5344
expect(result).toEqual({ updateAvailable: true, latestVersion: '2.0.0' });
54-
expect(mockFetchLatestVersion).toHaveBeenCalled();
5545
});
5646

5747
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);
48+
await mkdir(tmpDir, { recursive: true });
49+
await writeFile(CACHE_FILE, JSON.stringify({ lastCheck: NOW - ONE_SECOND_MS, latestVersion: '2.0.0' }), 'utf-8');
6450

6551
const result = await checkForUpdate();
6652

6753
expect(result).toEqual({ updateAvailable: true, latestVersion: '2.0.0' });
68-
expect(mockFetchLatestVersion).not.toHaveBeenCalled();
6954
});
7055

7156
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);
77-
mockFetchLatestVersion.mockResolvedValue('2.0.0');
78-
mockCompareVersions.mockReturnValue(1);
57+
await mkdir(tmpDir, { recursive: true });
58+
await writeFile(
59+
CACHE_FILE,
60+
JSON.stringify({ lastCheck: NOW - ONE_DAY_MS - ONE_HOUR_MS, latestVersion: '1.5.0' }),
61+
'utf-8'
62+
);
63+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('2.0.0');
7964

8065
const result = await checkForUpdate();
8166

8267
expect(result).toEqual({ updateAvailable: true, latestVersion: '2.0.0' });
83-
expect(mockFetchLatestVersion).toHaveBeenCalled();
8468
});
8569

8670
it('writes cache after fetching', async () => {
87-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
88-
mockFetchLatestVersion.mockResolvedValue('2.0.0');
89-
mockCompareVersions.mockReturnValue(1);
71+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('2.0.0');
9072

9173
await checkForUpdate();
9274

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-
);
75+
const cached = JSON.parse(await readFile(CACHE_FILE, 'utf-8'));
76+
expect(cached).toEqual({ lastCheck: NOW, latestVersion: '2.0.0' });
9977
});
10078

10179
it('returns updateAvailable: false when versions match', async () => {
102-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
103-
mockFetchLatestVersion.mockResolvedValue('1.0.0');
104-
mockCompareVersions.mockReturnValue(0);
80+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('1.0.0');
10581

10682
const result = await checkForUpdate();
10783

10884
expect(result).toEqual({ updateAvailable: false, latestVersion: '1.0.0' });
10985
});
11086

11187
it('returns updateAvailable: false when current is newer', async () => {
112-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
113-
mockFetchLatestVersion.mockResolvedValue('0.9.0');
114-
mockCompareVersions.mockReturnValue(-1);
88+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('0.9.0');
11589

11690
const result = await checkForUpdate();
11791

11892
expect(result).toEqual({ updateAvailable: false, latestVersion: '0.9.0' });
11993
});
12094

12195
it('returns null on fetch error', async () => {
122-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
123-
mockFetchLatestVersion.mockRejectedValue(new Error('network error'));
96+
vi.spyOn(action, 'fetchLatestVersion').mockRejectedValue(new Error('network error'));
12497

12598
const result = await checkForUpdate();
12699

127100
expect(result).toBeNull();
128101
});
129102

130103
it('returns null on cache parse error and fetch error', async () => {
131-
mockReadFile.mockResolvedValue('invalid json');
132-
mockFetchLatestVersion.mockRejectedValue(new Error('network error'));
104+
await mkdir(tmpDir, { recursive: true });
105+
await writeFile(CACHE_FILE, 'invalid json', 'utf-8');
106+
vi.spyOn(action, 'fetchLatestVersion').mockRejectedValue(new Error('network error'));
133107

134108
const result = await checkForUpdate();
135109

136110
expect(result).toBeNull();
137111
});
138112

139113
it('succeeds even when cache write fails', async () => {
140-
mockReadFile.mockRejectedValue(new Error('ENOENT'));
141-
mockFetchLatestVersion.mockResolvedValue('2.0.0');
142-
mockCompareVersions.mockReturnValue(1);
143-
mockWriteFile.mockRejectedValue(new Error('EACCES'));
114+
// Point config dir at a regular file — mkdir/writeFile will fail because
115+
// a file can't be used as a directory. Works cross-platform and as root.
116+
mkdirSync(tmpDir, { recursive: true });
117+
const blocker = join(tmpDir, 'not-a-dir');
118+
writeFileSync(blocker, '');
119+
process.env.AGENTCORE_CONFIG_DIR = blocker;
120+
121+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('2.0.0');
144122

145123
const result = await checkForUpdate();
146124

@@ -157,7 +135,6 @@ describe('printUpdateNotification', () => {
157135

158136
const output = stderrSpy.mock.calls.map(c => c[0]).join('');
159137
expect(output).toContain('Update available:');
160-
expect(output).toContain('1.0.0');
161138
expect(output).toContain('2.0.0');
162139
expect(output).toContain('npm install -g @aws/agentcore@latest');
163140

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,50 @@
11
import { resolveUIDistDir } from '../web-server.js';
22
import fs from 'node:fs';
3-
import path from 'node:path';
3+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4+
import { tmpdir } from 'node:os';
5+
import { join } from 'node:path';
46
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
57

6-
vi.mock('node:fs');
7-
8-
const existsSync = vi.mocked(fs.existsSync);
9-
108
describe('resolveUIDistDir', () => {
119
const originalEnv = process.env;
10+
let tmpDir: string;
1211

1312
beforeEach(() => {
1413
process.env = { ...originalEnv };
14+
tmpDir = mkdtempSync(join(tmpdir(), 'resolve-ui-test-'));
1515
delete process.env.AGENT_INSPECTOR_PATH;
16-
existsSync.mockReturnValue(false);
1716
});
1817

1918
afterEach(() => {
2019
process.env = originalEnv;
20+
rmSync(tmpDir, { recursive: true, force: true });
2121
vi.restoreAllMocks();
2222
});
2323

2424
it('returns null when no candidate has index.html', () => {
25+
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
26+
2527
expect(resolveUIDistDir()).toBeNull();
2628
});
2729

2830
it('returns AGENT_INSPECTOR_PATH when env var is set and dir has index.html', () => {
29-
const customPath = '/custom/inspector/dist';
30-
process.env.AGENT_INSPECTOR_PATH = customPath;
31-
32-
existsSync.mockImplementation(p => p === path.join(customPath, 'index.html'));
31+
process.env.AGENT_INSPECTOR_PATH = tmpDir;
32+
writeFileSync(join(tmpDir, 'index.html'), '');
3333

34-
expect(resolveUIDistDir()).toBe(customPath);
34+
expect(resolveUIDistDir()).toBe(tmpDir);
3535
});
3636

3737
it('skips AGENT_INSPECTOR_PATH when env var is set but dir lacks index.html', () => {
38-
process.env.AGENT_INSPECTOR_PATH = '/missing/inspector';
39-
existsSync.mockReturnValue(false);
38+
process.env.AGENT_INSPECTOR_PATH = tmpDir;
39+
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
4040

4141
expect(resolveUIDistDir()).toBeNull();
4242
});
4343

44-
it('returns the first candidate that has index.html', () => {
45-
existsSync.mockImplementation(p => {
46-
return String(p).endsWith(path.join('agent-inspector', 'index.html'));
47-
});
48-
49-
const result = resolveUIDistDir();
50-
expect(result).not.toBeNull();
51-
expect(result!).toMatch(/agent-inspector$/);
52-
});
53-
5444
it('prefers AGENT_INSPECTOR_PATH over bundled candidates', () => {
55-
const customPath = '/custom/path';
56-
process.env.AGENT_INSPECTOR_PATH = customPath;
57-
58-
existsSync.mockReturnValue(true);
45+
process.env.AGENT_INSPECTOR_PATH = tmpDir;
46+
writeFileSync(join(tmpDir, 'index.html'), '');
5947

60-
expect(resolveUIDistDir()).toBe(customPath);
48+
expect(resolveUIDistDir()).toBe(tmpDir);
6149
});
6250
});

src/cli/operations/traces/__tests__/get-trace.test.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { fetchTraceRecords, getTrace } from '../get-trace';
22
import type { FetchTraceRecordsOptions } from '../types';
3-
import { afterEach, describe, expect, it, vi } from 'vitest';
3+
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
4+
import { tmpdir } from 'node:os';
5+
import { join } from 'node:path';
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
47

58
const { mockSend } = vi.hoisted(() => ({
69
mockSend: vi.fn(),
@@ -22,13 +25,6 @@ vi.mock('../../../aws', () => ({
2225
getCredentialProvider: vi.fn().mockReturnValue({}),
2326
}));
2427

25-
vi.mock('node:fs', () => ({
26-
default: {
27-
mkdirSync: vi.fn(),
28-
writeFileSync: vi.fn(),
29-
},
30-
}));
31-
3228
const baseOptions: FetchTraceRecordsOptions = {
3329
region: 'us-west-2',
3430
runtimeId: 'runtime-123',
@@ -183,11 +179,18 @@ describe('fetchTraceRecords', () => {
183179
});
184180

185181
describe('getTrace', () => {
186-
afterEach(() => vi.clearAllMocks());
182+
let tmpDir: string;
187183

188-
it('calls fetchTraceRecords and writes result to disk', async () => {
189-
const fs = await import('node:fs');
184+
beforeEach(() => {
185+
tmpDir = mkdtempSync(join(tmpdir(), 'get-trace-test-'));
186+
});
187+
188+
afterEach(() => {
189+
vi.clearAllMocks();
190+
rmSync(tmpDir, { recursive: true, force: true });
191+
});
190192

193+
it('calls fetchTraceRecords and writes result to disk', async () => {
191194
mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({
192195
status: 'Complete',
193196
results: [
@@ -198,36 +201,38 @@ describe('getTrace', () => {
198201
],
199202
});
200203

204+
const outputPath = join(tmpDir, 'test-trace.json');
201205
const result = await getTrace({
202206
region: 'us-west-2',
203207
runtimeId: 'runtime-123',
204208
agentName: 'my-agent',
205209
traceId: 'abc123def456',
206-
outputPath: '/tmp/test-trace.json',
210+
outputPath,
207211
startTime: 1000000,
208212
endTime: 2000000,
209213
});
210214

211215
expect(result.success).toBe(true);
212216
expect(result.filePath).toContain('test-trace.json');
213-
expect(fs.default.mkdirSync).toHaveBeenCalled();
214-
expect(fs.default.writeFileSync).toHaveBeenCalledWith('/tmp/test-trace.json', expect.stringContaining('"traceId"'));
217+
const content = JSON.parse(readFileSync(outputPath, 'utf-8'));
218+
expect(content).toHaveLength(1);
219+
expect(content[0]['@message']).toEqual({ traceId: 'abc123' });
215220
});
216221

217222
it('returns error from fetchTraceRecords without writing file', async () => {
218-
const fs = await import('node:fs');
219-
223+
const outputPath = join(tmpDir, 'should-not-exist.json');
220224
const result = await getTrace({
221225
region: 'us-west-2',
222226
runtimeId: 'runtime-123',
223227
agentName: 'my-agent',
224228
traceId: 'invalid!@#$',
229+
outputPath,
225230
startTime: 1000000,
226231
endTime: 2000000,
227232
});
228233

229234
expect(result.success).toBe(false);
230235
expect(result.error).toContain('Invalid trace ID format');
231-
expect(fs.default.writeFileSync).not.toHaveBeenCalled();
236+
expect(existsSync(outputPath)).toBe(false);
232237
});
233238
});

0 commit comments

Comments
 (0)