Skip to content

Commit 62d9307

Browse files
committed
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
1 parent f6ff3f9 commit 62d9307

6 files changed

Lines changed: 88 additions & 81 deletions

File tree

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

Lines changed: 40 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,146 +1,128 @@
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 { rmSync } from 'fs';
5+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
36
import { mkdir, readFile, writeFile } from 'fs/promises';
7+
import { tmpdir } from 'os';
48
import { join } from 'path';
59
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
610

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-
});
15-
16-
vi.mock('../../lib/schemas/io/global-config.js', () => ({
17-
GLOBAL_CONFIG_DIR: tmpDir,
18-
}));
19-
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-
});
24-
25-
const { mockFetchLatestVersion } = vi.hoisted(() => ({
26-
mockFetchLatestVersion: vi.fn(),
27-
}));
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;
11+
const NOW = 1708646400000;
12+
const tmpDir = mkdtempSync(join(tmpdir(), 'update-notifier-test-'));
3513
const CACHE_FILE = join(tmpDir, 'update-check.json');
3614

3715
describe('checkForUpdate', () => {
16+
let originalConfigDir: string | undefined;
17+
3818
beforeEach(() => {
39-
vi.spyOn(Date, 'now').mockReturnValue(1708646400000);
40-
try {
41-
rmSync(CACHE_DIR, { recursive: true });
42-
} catch {}
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 });
4324
});
4425

4526
afterEach(() => {
4627
vi.restoreAllMocks();
47-
mockFetchLatestVersion.mockReset();
28+
if (originalConfigDir === undefined) {
29+
delete process.env.AGENTCORE_CONFIG_DIR;
30+
} else {
31+
process.env.AGENTCORE_CONFIG_DIR = originalConfigDir;
32+
}
4833
});
4934

5035
afterAll(() => {
51-
rmSync(tmpDir, { recursive: true });
36+
rmSync(tmpDir, { recursive: true, force: true });
5237
});
5338

5439
it('fetches from registry when no cache exists', async () => {
55-
mockFetchLatestVersion.mockResolvedValue('2.0.0');
40+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('2.0.0');
5641

5742
const result = await checkForUpdate();
5843

5944
expect(result).toEqual({ updateAvailable: true, latestVersion: '2.0.0' });
60-
expect(mockFetchLatestVersion).toHaveBeenCalled();
6145
});
6246

6347
it('uses cache when last check was less than 24 hours ago', async () => {
64-
await mkdir(CACHE_DIR, { recursive: true });
65-
await writeFile(CACHE_FILE, JSON.stringify({ lastCheck: 1708646400000 - 1000, latestVersion: '2.0.0' }), 'utf-8');
48+
await mkdir(tmpDir, { recursive: true });
49+
await writeFile(CACHE_FILE, JSON.stringify({ lastCheck: NOW - ONE_SECOND_MS, latestVersion: '2.0.0' }), 'utf-8');
6650

6751
const result = await checkForUpdate();
6852

6953
expect(result).toEqual({ updateAvailable: true, latestVersion: '2.0.0' });
70-
expect(mockFetchLatestVersion).not.toHaveBeenCalled();
7154
});
7255

7356
it('fetches from registry when cache is expired', async () => {
74-
await mkdir(CACHE_DIR, { recursive: true });
57+
await mkdir(tmpDir, { recursive: true });
7558
await writeFile(
7659
CACHE_FILE,
77-
JSON.stringify({ lastCheck: 1708646400000 - 25 * 60 * 60 * 1000, latestVersion: '1.5.0' }),
60+
JSON.stringify({ lastCheck: NOW - ONE_DAY_MS - ONE_HOUR_MS, latestVersion: '1.5.0' }),
7861
'utf-8'
7962
);
80-
mockFetchLatestVersion.mockResolvedValue('2.0.0');
63+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('2.0.0');
8164

8265
const result = await checkForUpdate();
8366

8467
expect(result).toEqual({ updateAvailable: true, latestVersion: '2.0.0' });
85-
expect(mockFetchLatestVersion).toHaveBeenCalled();
8668
});
8769

8870
it('writes cache after fetching', async () => {
89-
mockFetchLatestVersion.mockResolvedValue('2.0.0');
71+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('2.0.0');
9072

9173
await checkForUpdate();
9274

9375
const cached = JSON.parse(await readFile(CACHE_FILE, 'utf-8'));
94-
expect(cached).toEqual({ lastCheck: 1708646400000, latestVersion: '2.0.0' });
76+
expect(cached).toEqual({ lastCheck: NOW, latestVersion: '2.0.0' });
9577
});
9678

9779
it('returns updateAvailable: false when versions match', async () => {
98-
mockFetchLatestVersion.mockResolvedValue('1.0.0');
80+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('1.0.0');
9981

10082
const result = await checkForUpdate();
10183

10284
expect(result).toEqual({ updateAvailable: false, latestVersion: '1.0.0' });
10385
});
10486

10587
it('returns updateAvailable: false when current is newer', async () => {
106-
mockFetchLatestVersion.mockResolvedValue('0.9.0');
88+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('0.9.0');
10789

10890
const result = await checkForUpdate();
10991

11092
expect(result).toEqual({ updateAvailable: false, latestVersion: '0.9.0' });
11193
});
11294

11395
it('returns null on fetch error', async () => {
114-
mockFetchLatestVersion.mockRejectedValue(new Error('network error'));
96+
vi.spyOn(action, 'fetchLatestVersion').mockRejectedValue(new Error('network error'));
11597

11698
const result = await checkForUpdate();
11799

118100
expect(result).toBeNull();
119101
});
120102

121103
it('returns null on cache parse error and fetch error', async () => {
122-
await mkdir(CACHE_DIR, { recursive: true });
104+
await mkdir(tmpDir, { recursive: true });
123105
await writeFile(CACHE_FILE, 'invalid json', 'utf-8');
124-
mockFetchLatestVersion.mockRejectedValue(new Error('network error'));
106+
vi.spyOn(action, 'fetchLatestVersion').mockRejectedValue(new Error('network error'));
125107

126108
const result = await checkForUpdate();
127109

128110
expect(result).toBeNull();
129111
});
130112

131113
it('succeeds even when cache write fails', async () => {
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);
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;
136120

137-
mockFetchLatestVersion.mockResolvedValue('2.0.0');
121+
vi.spyOn(action, 'fetchLatestVersion').mockResolvedValue('2.0.0');
138122

139123
const result = await checkForUpdate();
140124

141125
expect(result).toEqual({ updateAvailable: true, latestVersion: '2.0.0' });
142-
143-
await chmod(CACHE_DIR, 0o755);
144126
});
145127
});
146128

@@ -153,9 +135,7 @@ describe('printUpdateNotification', () => {
153135

154136
const output = stderrSpy.mock.calls.map(c => c[0]).join('');
155137
expect(output).toContain('Update available:');
156-
expect(output).toContain('1.0.0');
157138
expect(output).toContain('2.0.0');
158-
expect(output).toContain('npm install -g @aws/agentcore@latest');
159139

160140
stderrSpy.mockRestore();
161141
});

src/cli/operations/dev/web-ui/__tests__/resolve-ui-dist-dir.test.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
11
import { resolveUIDistDir } from '../web-server.js';
2-
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
33
import { tmpdir } from 'node:os';
4-
import { join } from 'node:path';
4+
import { dirname, join } from 'node:path';
5+
import { fileURLToPath } from 'node:url';
56
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
67

8+
const testDir = dirname(fileURLToPath(import.meta.url));
9+
const bundledExists = existsSync(
10+
join(
11+
testDir,
12+
'..',
13+
'..',
14+
'..',
15+
'..',
16+
'..',
17+
'..',
18+
'node_modules',
19+
'@aws',
20+
'agent-inspector',
21+
'dist-assets',
22+
'index.html'
23+
)
24+
);
25+
726
describe('resolveUIDistDir', () => {
827
const originalEnv = process.env;
928
let tmpDir: string;
@@ -19,6 +38,13 @@ describe('resolveUIDistDir', () => {
1938
rmSync(tmpDir, { recursive: true, force: true });
2039
});
2140

41+
it.skipIf(bundledExists)('returns null when no candidate has index.html', () => {
42+
// AGENT_INSPECTOR_PATH points to empty dir, bundled candidates don't exist in test env
43+
process.env.AGENT_INSPECTOR_PATH = join(tmpDir, 'empty');
44+
45+
expect(resolveUIDistDir()).toBeNull();
46+
});
47+
2248
it('returns AGENT_INSPECTOR_PATH when env var is set and dir has index.html', () => {
2349
process.env.AGENT_INSPECTOR_PATH = tmpDir;
2450
writeFileSync(join(tmpDir, 'index.html'), '<html></html>');
@@ -39,12 +65,4 @@ describe('resolveUIDistDir', () => {
3965

4066
expect(resolveUIDistDir()).toBe(tmpDir);
4167
});
42-
43-
it('returns a bundled candidate when no env var is set and bundled path exists', () => {
44-
const result = resolveUIDistDir();
45-
// If a bundled candidate has index.html, it should be returned
46-
if (result) {
47-
expect(result).toContain('dist-assets');
48-
}
49-
});
5068
});

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { FetchTraceRecordsOptions } from '../types';
33
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
44
import { tmpdir } from 'node:os';
55
import { join } from 'node:path';
6-
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
77

88
const { mockSend } = vi.hoisted(() => ({
99
mockSend: vi.fn(),
@@ -181,16 +181,15 @@ describe('fetchTraceRecords', () => {
181181
describe('getTrace', () => {
182182
let tmpDir: string;
183183

184-
beforeAll(() => {
184+
beforeEach(() => {
185185
tmpDir = mkdtempSync(join(tmpdir(), 'get-trace-test-'));
186186
});
187187

188-
afterAll(() => {
188+
afterEach(() => {
189+
vi.clearAllMocks();
189190
rmSync(tmpDir, { recursive: true, force: true });
190191
});
191192

192-
afterEach(() => vi.clearAllMocks());
193-
194193
it('calls fetchTraceRecords and writes result to disk', async () => {
195194
mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({
196195
status: 'Complete',

src/cli/update-notifier.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { GLOBAL_CONFIG_DIR } from '../lib/schemas/io/global-config.js';
1+
import { ONE_DAY_MS } from '../lib/time-constants.js';
22
import { compareVersions, fetchLatestVersion } from './commands/update/action.js';
33
import { PACKAGE_VERSION } from './constants.js';
44
import { mkdir, readFile, writeFile } from 'fs/promises';
5+
import { homedir } from 'os';
56
import { join } from 'path';
67

7-
const CACHE_FILE = join(GLOBAL_CONFIG_DIR, 'update-check.json');
8-
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // every 24 hours
8+
function getConfigDir(): string {
9+
return process.env.AGENTCORE_CONFIG_DIR ?? join(homedir(), '.agentcore');
10+
}
11+
12+
const CHECK_INTERVAL_MS = ONE_DAY_MS;
913

1014
interface CacheData {
1115
lastCheck: number;
@@ -19,7 +23,7 @@ export interface UpdateCheckResult {
1923

2024
async function readCache(): Promise<CacheData | null> {
2125
try {
22-
const data = await readFile(CACHE_FILE, 'utf-8');
26+
const data = await readFile(join(getConfigDir(), 'update-check.json'), 'utf-8');
2327
return JSON.parse(data) as CacheData;
2428
} catch {
2529
return null;
@@ -28,8 +32,9 @@ async function readCache(): Promise<CacheData | null> {
2832

2933
async function writeCache(data: CacheData): Promise<void> {
3034
try {
31-
await mkdir(GLOBAL_CONFIG_DIR, { recursive: true });
32-
await writeFile(CACHE_FILE, JSON.stringify(data), 'utf-8');
35+
const dir = getConfigDir();
36+
await mkdir(dir, { recursive: true });
37+
await writeFile(join(dir, 'update-check.json'), JSON.stringify(data), 'utf-8');
3338
} catch {
3439
// Silently ignore cache write failures
3540
}

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ export * from './utils';
2727

2828
// Schema I/O utilities
2929
export * from './schemas/io';
30+
export * from './time-constants';

src/lib/time-constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const ONE_SECOND_MS = 1000;
2+
export const ONE_MINUTE_MS = ONE_SECOND_MS * 60;
3+
export const ONE_HOUR_MS = ONE_MINUTE_MS * 60;
4+
export const ONE_DAY_MS = ONE_HOUR_MS * 24;

0 commit comments

Comments
 (0)