Files
stack/packages/mosaic/__tests__/update-checker.test.ts
Jarvis c39fa065fc
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
fix: simplify updater to @mosaic/mosaic only, add explicit tea repo/login flags (#387)
BREAKING CHANGE: Remove legacy @mosaic/cli fallback from update-checker.
The updater now targets only @mosaic/mosaic — legacy package compatibility
is out of scope per approved breaking change.

- update-checker.ts: remove LEGACY_PKG, INSTALLED_PACKAGE_ORDER, candidate
  iteration, and cache key comparison. Single query for @mosaic/mosaic only.
- update-checker.test.ts: remove legacy fallback and cross-contamination
  tests. Add test asserting no @mosaic/cli queries are made.
- pr-merge.sh: add --repo and --login flags to tea merge command; resolve
  OWNER/REPO before case block.
- issue-close.sh: add --repo and --login flags to tea issue commands;
  resolve OWNER/REPO after detect_platform.
- pr-ci-wait.sh: no changes needed (uses curl, not tea).
- detect-platform.sh: no changes needed (provides get_repo_owner/name).

Ref: #387
2026-04-04 21:06:59 -05:00

170 lines
5.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { semverLt, formatUpdateNotice } from '../src/runtime/update-checker.js';
import type { UpdateCheckResult } from '../src/runtime/update-checker.js';
const { execSyncMock, cacheFiles } = vi.hoisted(() => ({
execSyncMock: vi.fn(),
cacheFiles: new Map<string, string>(),
}));
vi.mock('node:child_process', () => ({
execSync: execSyncMock,
}));
vi.mock('node:fs', () => ({
existsSync: vi.fn((path: string) => cacheFiles.has(path)),
mkdirSync: vi.fn(),
readFileSync: vi.fn((path: string) => {
const value = cacheFiles.get(path);
if (value === undefined) {
throw new Error(`ENOENT: ${path}`);
}
return value;
}),
writeFileSync: vi.fn((path: string, content: string) => {
cacheFiles.set(path, content);
}),
}));
vi.mock('node:os', () => ({
homedir: vi.fn(() => '/mock-home'),
}));
async function importUpdateChecker() {
vi.resetModules();
return import('../src/runtime/update-checker.js');
}
describe('semverLt', () => {
it('returns true when a < b', () => {
expect(semverLt('0.0.1', '0.0.2')).toBe(true);
expect(semverLt('0.1.0', '0.2.0')).toBe(true);
expect(semverLt('1.0.0', '2.0.0')).toBe(true);
expect(semverLt('0.0.1-alpha.1', '0.0.1-alpha.2')).toBe(true);
expect(semverLt('0.0.1-alpha.1', '0.0.1')).toBe(true);
});
it('returns false when a >= b', () => {
expect(semverLt('0.0.2', '0.0.1')).toBe(false);
expect(semverLt('1.0.0', '1.0.0')).toBe(false);
expect(semverLt('2.0.0', '1.0.0')).toBe(false);
});
it('returns false for empty strings', () => {
expect(semverLt('', '1.0.0')).toBe(false);
expect(semverLt('1.0.0', '')).toBe(false);
expect(semverLt('', '')).toBe(false);
});
});
describe('formatUpdateNotice', () => {
beforeEach(() => {
execSyncMock.mockReset();
cacheFiles.clear();
});
it('returns empty string when up to date', () => {
const result: UpdateCheckResult = {
current: '1.0.0',
latest: '1.0.0',
updateAvailable: false,
checkedAt: new Date().toISOString(),
registry: 'https://example.com',
};
expect(formatUpdateNotice(result)).toBe('');
});
it('returns a notice when update is available', () => {
const result: UpdateCheckResult = {
current: '0.0.1',
latest: '0.1.0',
updateAvailable: true,
checkedAt: new Date().toISOString(),
registry: 'https://example.com',
};
const notice = formatUpdateNotice(result);
expect(notice).toContain('0.0.1');
expect(notice).toContain('0.1.0');
expect(notice).toContain('Update available');
});
it('uses @mosaic/mosaic for installs', async () => {
execSyncMock.mockImplementation((command: string) => {
if (command.includes('ls -g --depth=0 --json')) {
return JSON.stringify({
dependencies: {
'@mosaic/mosaic': { version: '0.0.19' },
},
});
}
if (command.includes('view @mosaic/mosaic version')) {
return '0.0.20';
}
throw new Error(`Unexpected command: ${command}`);
});
const { checkForUpdate } = await importUpdateChecker();
const result = checkForUpdate({ skipCache: true });
const notice = formatUpdateNotice(result);
expect(result.current).toBe('0.0.19');
expect(result.latest).toBe('0.0.20');
expect(result.currentPackage).toBe('@mosaic/mosaic');
expect(result.targetPackage).toBe('@mosaic/mosaic');
expect(notice).toContain('@mosaic/mosaic@latest');
});
it('does not query legacy @mosaic/cli package', async () => {
execSyncMock.mockImplementation((command: string) => {
if (command.includes('view @mosaic/cli')) {
throw new Error('Should not query @mosaic/cli');
}
if (command.includes('ls -g --depth=0 --json')) {
return JSON.stringify({
dependencies: {
'@mosaic/mosaic': { version: '0.0.19' },
},
});
}
if (command.includes('view @mosaic/mosaic version')) {
return '0.0.20';
}
throw new Error(`Unexpected command: ${command}`);
});
const { checkForUpdate } = await importUpdateChecker();
const result = checkForUpdate({ skipCache: true });
expect(result.targetPackage).toBe('@mosaic/mosaic');
expect(result.latest).toBe('0.0.20');
// Verify no @mosaic/cli queries were made
const calls = execSyncMock.mock.calls.map((c: any[]) => c[0] as string);
expect(calls.some((c) => c.includes('@mosaic/cli'))).toBe(false);
});
it('returns empty result when package is not installed', async () => {
execSyncMock.mockImplementation((command: string) => {
if (command.includes('ls -g --depth=0 --json')) {
return JSON.stringify({ dependencies: {} });
}
if (command.includes('view @mosaic/mosaic version')) {
return '';
}
throw new Error(`Unexpected command: ${command}`);
});
const { checkForUpdate } = await importUpdateChecker();
const result = checkForUpdate({ skipCache: true });
expect(result.current).toBe('');
expect(result.updateAvailable).toBe(false);
});
});