/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import assert from 'node:assert'; import child_process from 'node:child_process'; import type {Stats} from 'node:fs'; import fs from 'node:fs/promises'; import os from 'node:os'; import {afterEach, beforeEach, describe, it} from 'node:test'; import sinon from 'sinon'; import { checkForUpdates, resetUpdateCheckFlagForTesting, } from '../src/utils/check-for-updates.js'; import {VERSION} from '../src/version.js'; describe('checkForUpdates', () => { beforeEach(() => { sinon.stub(fs, 'mkdir').resolves(); sinon.stub(fs, 'utimes').resolves(); sinon.stub(fs, 'writeFile').resolves(); }); afterEach(() => { sinon.restore(); resetUpdateCheckFlagForTesting(); }); it('does nothing if CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS is set', async () => { process.env['CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS'] = 'true'; const warnStub = sinon.stub(console, 'warn'); const spawnStub = sinon.stub(child_process, 'spawn'); const readFileStub = sinon.stub(fs, 'readFile'); const statStub = sinon.stub(fs, 'stat'); await checkForUpdates('Run `npm update` to update.'); assert.ok(warnStub.notCalled); assert.ok(spawnStub.notCalled); assert.ok(readFileStub.notCalled); assert.ok(statStub.notCalled); delete process.env['CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS']; }); it('notifies if cache exists and version is different', async () => { sinon.stub(os, 'homedir').returns('/home/user'); sinon.stub(fs, 'stat').resolves({mtimeMs: Date.now()} as unknown as Stats); sinon.stub(fs, 'readFile').callsFake(async filePath => { if (filePath.toString().includes('latest.json')) { return JSON.stringify({ version: '99.9.9', }); } throw new Error(`File not found: ${filePath}`); }); const warnStub = sinon.stub(console, 'warn'); const spawnStub = sinon.stub(child_process, 'spawn'); await checkForUpdates('Run `npm update` to update.'); assert.ok( warnStub.calledWith( sinon.match('Update available: ' + VERSION + ' -> 99.9.9'), ), ); assert.ok(spawnStub.notCalled); }); it('does not notify if incoming version is older than current version', async () => { sinon.stub(os, 'homedir').returns('/home/user'); sinon.stub(fs, 'stat').resolves({mtimeMs: Date.now()} as unknown as Stats); sinon.stub(fs, 'readFile').callsFake(async filePath => { if (filePath.toString().includes('latest.json')) { return JSON.stringify({ version: '0.0.1', }); } throw new Error(`File not found: ${filePath}`); }); const warnStub = sinon.stub(console, 'warn'); const spawnStub = sinon.stub(child_process, 'spawn'); await checkForUpdates('Run `npm update` to update.'); assert.ok(warnStub.notCalled); assert.ok(spawnStub.notCalled); }); it('does not spawn fetch process if cache is fresh', async () => { sinon.stub(os, 'homedir').returns('/home/user'); sinon.stub(fs, 'stat').resolves({mtimeMs: Date.now()} as unknown as Stats); sinon.stub(fs, 'readFile').callsFake(async filePath => { if (filePath.toString().includes('latest.json')) { return JSON.stringify({ version: VERSION, }); } throw new Error(`File not found: ${filePath}`); }); const spawnStub = sinon.stub(child_process, 'spawn'); await checkForUpdates('Run `npm update` to update.'); assert.ok(spawnStub.notCalled); }); it('spawns detached process if cache is stale', async () => { sinon.stub(os, 'homedir').returns('/home/user'); sinon.stub(fs, 'stat').resolves({ mtimeMs: Date.now() - 25 * 60 * 60 * 1000, // 25 hours ago } as unknown as Stats); sinon.stub(fs, 'readFile').callsFake(async filePath => { if (filePath.toString().includes('latest.json')) { return JSON.stringify({ version: VERSION, }); } throw new Error(`File not found: ${filePath}`); }); const unrefSpy = sinon.spy(); const spawnStub = sinon.stub(child_process, 'spawn').returns({ unref: unrefSpy, } as unknown as child_process.ChildProcess); await checkForUpdates('Run `npm update` to update.'); assert.ok(spawnStub.calledOnce); assert.strictEqual(spawnStub.firstCall.args[0], process.execPath); assert.ok( spawnStub.firstCall.args[1][0]?.includes('check-latest-version.js'), ); assert.ok(spawnStub.firstCall.args[1][1]?.includes('latest.json')); assert.strictEqual(spawnStub.firstCall.args[2]?.detached, true); assert.strictEqual(spawnStub.firstCall.args[2]?.windowsHide, true); assert.ok(unrefSpy.calledOnce); }); it('spawns detached process if cache is missing', async () => { sinon.stub(os, 'homedir').returns('/home/user'); sinon.stub(fs, 'stat').rejects(new Error('File not found')); sinon.stub(fs, 'readFile').callsFake(async filePath => { throw new Error(`File not found: ${filePath}`); }); const unrefSpy = sinon.spy(); const spawnStub = sinon.stub(child_process, 'spawn').returns({ unref: unrefSpy, } as unknown as child_process.ChildProcess); await checkForUpdates('Run `npm update` to update.'); assert.ok(spawnStub.calledOnce); assert.strictEqual(spawnStub.firstCall.args[0], process.execPath); assert.ok( spawnStub.firstCall.args[1][0]?.includes('check-latest-version.js'), ); assert.ok(spawnStub.firstCall.args[1][1]?.includes('latest.json')); assert.strictEqual(spawnStub.firstCall.args[2]?.detached, true); assert.strictEqual(spawnStub.firstCall.args[2]?.windowsHide, true); assert.ok(unrefSpy.calledOnce); }); });