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' ;
14import { 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
3015describe ( '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
0 commit comments