/** * Tests for `ccs bar` command surface — Phase 3 TDD. * * Tests run FIRST per TDD mandate. * Covers: subcommand routing, bar.json contract, floating-tag install, * Info.plist version extraction, capability handshake compat, port-discovery * fallback, uninstall, version, and verified review findings #8-#13. * * All network I/O and filesystem-home operations are mocked. * Uses CCS_HOME env var for isolation — never touches real ~/.ccs. */ import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { BAR_AUTH_NONCE_HEADER, BAR_AUTH_TOKEN_HEADER, createBarAuthProof, getOrCreateBarAuthToken, } from '../../../src/utils/bar-auth-token'; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- let calls: string[] = []; let consoleOutput: string[] = []; let tempHome: string; let originalCcsHome: string | undefined; let originalConsoleLog: typeof console.log; let originalConsoleError: typeof console.error; const FAKE_SHA256 = 'a'.repeat(64); function captureConsole(): void { originalConsoleLog = console.log; originalConsoleError = console.error; console.log = (...args: unknown[]) => { consoleOutput.push(args.map(String).join(' ')); }; console.error = (...args: unknown[]) => { consoleOutput.push(args.map(String).join(' ')); }; } function restoreConsole(): void { console.log = originalConsoleLog; console.error = originalConsoleError; } // Unique module cache-buster so bun:test picks up fresh mocks each describe block. let moduleSeq = 0; async function loadHandleBarCommand() { moduleSeq++; const mod = await import(`../../../src/commands/bar/index?test=${Date.now()}-${moduleSeq}`); return mod.handleBarCommand as (args: string[]) => Promise; } async function loadInstallSubcommand() { moduleSeq++; const mod = await import( `../../../src/commands/bar/install-subcommand?test=${Date.now()}-${moduleSeq}` ); return mod as { handleBarInstall: (args: string[], deps?: Record) => Promise; validateDownloadUrl: (url: string) => void; }; } async function loadLaunchSubcommand() { moduleSeq++; const mod = await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` ); return mod as { handleBarLaunch: (args: string[], deps?: Record) => Promise; }; } async function loadUninstallSubcommand() { moduleSeq++; const mod = await import( `../../../src/commands/bar/uninstall-subcommand?test=${Date.now()}-${moduleSeq}` ); return mod as { handleBarUninstall: (args: string[], deps?: Record) => Promise; }; } // --------------------------------------------------------------------------- // Setup / teardown // --------------------------------------------------------------------------- beforeEach(() => { calls = []; consoleOutput = []; captureConsole(); tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-bar-test-')); originalCcsHome = process.env.CCS_HOME; process.env.CCS_HOME = tempHome; }); afterEach(() => { restoreConsole(); mock.restore(); process.exitCode = 0; if (originalCcsHome === undefined) { delete process.env.CCS_HOME; } else { process.env.CCS_HOME = originalCcsHome; } // Clean up temp dir try { fs.rmSync(tempHome, { recursive: true, force: true }); } catch { // ignore } }); // --------------------------------------------------------------------------- // 1. Subcommand routing via handleBarCommand dispatcher // --------------------------------------------------------------------------- describe('bar command dispatcher (index.ts)', () => { beforeEach(() => { mock.module('../../../src/commands/bar/launch-subcommand', () => ({ handleBarLaunch: async (args: string[]) => { calls.push(`launch:${args.join(' ')}`); }, })); mock.module('../../../src/commands/bar/install-subcommand', () => ({ handleBarInstall: async (args: string[]) => { calls.push(`install:${args.join(' ')}`); }, })); mock.module('../../../src/commands/bar/uninstall-subcommand', () => ({ handleBarUninstall: async (args: string[]) => { calls.push(`uninstall:${args.join(' ')}`); }, })); mock.module('../../../src/commands/bar/version-subcommand', () => ({ handleBarVersion: async () => { calls.push(`version:`); }, })); mock.module('../../../src/commands/bar/help-subcommand', () => ({ showHelp: async () => { calls.push(`help:`); }, })); }); it('dispatches bare `ccs bar` to launch', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand([]); expect(calls).toEqual(['launch:']); }); it('dispatches `ccs bar launch` to launch subcommand', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['launch']); expect(calls).toEqual(['launch:']); }); it('rejects an unknown bare launch flag without dispatching launch', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['--porrt', '3999']); expect(calls).toEqual([]); expect(process.exitCode).toBe(1); }); it('dispatches `ccs bar install` to install subcommand', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['install']); expect(calls).toEqual(['install:']); }); it('dispatches `ccs bar uninstall` to uninstall subcommand', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['uninstall']); expect(calls).toEqual(['uninstall:']); }); it('dispatches `ccs bar --version` to version subcommand', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['--version']); expect(calls).toEqual(['version:']); }); it('dispatches `ccs bar version` to version subcommand', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['version']); expect(calls).toEqual(['version:']); }); it('passes remaining args to install subcommand', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['install', '--force']); expect(calls).toEqual(['install:--force']); }); it('treats unknown subcommands as an error and does not throw', async () => { const handleBarCommand = await loadHandleBarCommand(); // Should print help or error but not crash await expect(handleBarCommand(['unknown-subcommand'])).resolves.toBeUndefined(); expect(process.exitCode).toBe(1); process.exitCode = 0; }); it('dispatches `ccs bar --help` to help subcommand and does not launch', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['--help']); expect(calls).toEqual(['help:']); expect(calls).not.toContain(expect.stringMatching(/^launch:/)); }); it('dispatches `ccs bar -h` to help subcommand and does not launch', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['-h']); expect(calls).toEqual(['help:']); expect(calls).not.toContain(expect.stringMatching(/^launch:/)); }); it('dispatches `ccs bar help` to help subcommand and does not hit unknown-subcommand error', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['help']); expect(calls).toEqual(['help:']); const allOutput = consoleOutput.join('\n'); expect(allOutput).not.toMatch(/Unknown bar subcommand/); }); }); // --------------------------------------------------------------------------- // 2. root-command-router registers `bar` // --------------------------------------------------------------------------- describe('root-command-router registers bar', () => { beforeEach(() => { mock.module('../../../src/commands/bar/index', () => ({ handleBarCommand: async (args: string[]) => { calls.push(`bar:${args.join(' ')}`); }, })); }); it('routes `ccs bar` through the root router', async () => { moduleSeq++; const mod = await import( `../../../src/commands/root-command-router?test=${Date.now()}-${moduleSeq}` ); const { tryHandleRootCommand } = mod; const handled = await tryHandleRootCommand(['bar']); expect(handled).toBe(true); expect(calls).toEqual(['bar:']); }); it('routes `ccs bar install` with args preserved', async () => { moduleSeq++; const mod = await import( `../../../src/commands/root-command-router?test=${Date.now()}-${moduleSeq}` ); const { tryHandleRootCommand } = mod; await tryHandleRootCommand(['bar', 'install', '--force']); expect(calls).toContain('bar:install --force'); }); }); // --------------------------------------------------------------------------- // 3. bar.json contract written by launch // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // Detached-model launch dep helpers (shared by tests in this describe block) // --------------------------------------------------------------------------- /** * Build a minimal set of detached-model deps that behave like a successful * launch for most tests. Individual tests override specific deps as needed. */ function makeDetachedDeps(ccsDir: string, port = 4242) { return { findRunningServer: async () => null, getPort: async () => port, spawnDetachedServer: (_p: number, _log: string) => { /* noop */ }, waitForServerLive: async (_url: string) => { /* live immediately */ }, writeLaunchDescriptor: () => { /* noop */ }, openApp: async (_appPath: string) => { /* noop */ }, getCcsDir: () => ccsDir, appInstallPath: path.join(tempHome, 'Applications', 'CCS Bar.app'), }; } describe('bar.json contract (launch subcommand)', () => { it('writes ~/.ccs/bar.json with correct shape when server starts', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); await handleBarLaunch([], { ...makeDetachedDeps(ccsDir, 4242), openApp: async (_appPath: string) => { calls.push(`open:${_appPath}`); }, }); const barJsonPath = path.join(ccsDir, 'bar.json'); expect(fs.existsSync(barJsonPath)).toBe(true); const barJson = JSON.parse(fs.readFileSync(barJsonPath, 'utf8')) as unknown; expect(barJson).toMatchObject({ baseUrl: 'http://127.0.0.1:4242', port: 4242, authMode: 'loopback', }); }); it('bar.json authMode is always "loopback" in v1', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); await handleBarLaunch([], makeDetachedDeps(ccsDir, 9000)); const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as { authMode: string; }; expect(barJson.authMode).toBe('loopback'); }); it('prints guidance when app is not installed', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); // App doesn't exist at appInstallPath — openApp throws so degraded path runs. const nonExistentApp = path.join(tempHome, 'Applications', 'CCS Bar.app'); await handleBarLaunch([], { ...makeDetachedDeps(ccsDir, 3000), openApp: async () => { throw new Error('App not found'); }, appInstallPath: nonExistentApp, }); const allOutput = consoleOutput.join('\n'); // Should suggest installation expect(allOutput.toLowerCase()).toMatch(/install|not found|ccs bar install/i); }); it('writes bar.json even when open fails (degraded path)', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); await handleBarLaunch([], { ...makeDetachedDeps(ccsDir, 3001), openApp: async () => { throw new Error('open failed'); }, }); // bar.json should still be written despite open failure const barJsonPath = path.join(ccsDir, 'bar.json'); expect(fs.existsSync(barJsonPath)).toBe(true); }); it('prints degraded-path warning when spawnDetachedServer throws', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); await handleBarLaunch([], { ...makeDetachedDeps(ccsDir, 3000), spawnDetachedServer: () => { throw new Error('spawn failed'); }, }); const allOutput = consoleOutput.join('\n'); expect(allOutput.toLowerCase()).toMatch(/error|failed|could not|unable/i); }); }); // --------------------------------------------------------------------------- // 4. install subcommand — floating tag + Info.plist version + compat handshake // --------------------------------------------------------------------------- describe('bar install subcommand', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; const FAKE_VERSION = '1.2.3'; /** Create the fake CCS Bar.app in appsDir so the post-extract assertion passes. */ function fakeExtract(appsDir: string) { return async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); calls.push('download'); }; } it('resolves the floating ccs-bar-latest tag (not exact CLI version)', async () => { const fetchedUrls: string[] = []; const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async (tag: string, _asset: string) => { fetchedUrls.push(tag); return { downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }; }, downloadAndExtract: fakeExtract(appsDir), verifyCompat: async (_baseUrl: string) => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => FAKE_VERSION, isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); // Must use the floating tag, NOT the CLI version expect(fetchedUrls).toContain('ccs-bar-latest'); expect(fetchedUrls).not.toContain(expect.stringMatching(/^\d+\.\d+\.\d+$/)); }); it('passes the release asset sha256 digest to the downloader before install', async () => { const appsDir = path.join(tempHome, 'Applications'); const seenDigests: string[] = []; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: async (_url: string, dest: string, expectedSha256: string) => { seenDigests.push(expectedSha256); fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => FAKE_VERSION, isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); expect(seenDigests).toEqual([FAKE_SHA256]); }); it('pins the Info.plist version (not tag name) to ~/.ccs/bar/.version', async () => { const ccsDir = path.join(tempHome, '.ccs'); const appsDir = path.join(tempHome, 'Applications'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => FAKE_VERSION, isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => ccsDir, getAppsDir: () => appsDir, }); const versionFile = path.join(ccsDir, 'bar', '.version'); expect(fs.existsSync(versionFile)).toBe(true); expect(fs.readFileSync(versionFile, 'utf8').trim()).toBe(FAKE_VERSION); }); it('calls /api/bar/summary for compat handshake after install', async () => { const compatCalls: string[] = []; const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async (baseUrl: string) => { compatCalls.push(baseUrl); return { compatible: true, reason: 'ok' }; }, readAppBundleVersion: (_appPath: string) => FAKE_VERSION, isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); expect(compatCalls.length).toBeGreaterThan(0); }); it('does not hard-fail when compat returns no-bar-api', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await expect( handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: false, reason: 'no-bar-api' }), readAppBundleVersion: (_appPath: string) => FAKE_VERSION, isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }) ).resolves.toBeUndefined(); // does not throw const allOutput = consoleOutput.join('\n'); expect(allOutput.toLowerCase()).toMatch(/warn|api|update/i); }); it('prints xattr/Gatekeeper note for ad-hoc builds', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); // Gatekeeper quarantine is preserved (not cleared) — the note is always printed. await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => FAKE_VERSION, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); // Must mention either right-click or xattr quarantine command expect(allOutput).toMatch(/xattr|right-click|quarantine/i); }); it('does not overwrite ~/Applications if download fails', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await expect( handleBarInstall([], { fetchReleaseAsset: async () => { throw new Error('network error'); }, downloadAndExtract: async () => { /* noop */ }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => FAKE_VERSION, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }) ).resolves.toBeUndefined(); // should not throw // Apps dir should not be touched expect(fs.existsSync(path.join(appsDir, 'CCS Bar.app'))).toBe(false); }); }); // --------------------------------------------------------------------------- // 4a. Finding #8 — redirect-following download (mock 302 then 200 via deps) // --------------------------------------------------------------------------- describe('bar install: redirect-following download (#8)', () => { const REDIRECT_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; const FINAL_URL = 'https://objects.githubusercontent.com/download/CCS-Bar.app.zip'; const FAKE_VERSION = '1.0.0'; it('succeeds when downloadAndExtract follows a 302 redirect to githubusercontent', async () => { const appsDir = path.join(tempHome, 'Applications'); const urlsRequested: string[] = []; const { handleBarInstall } = await loadInstallSubcommand(); // Simulate a downloader that internally follows a redirect (302 -> 200). // The dep mock receives the initial URL and resolves to the final content. const redirectFollowingExtract = async (url: string, dest: string) => { urlsRequested.push(url); // Simulate: original URL would 302 to FINAL_URL; mock follows it and succeeds. if (url !== REDIRECT_URL) { throw new Error(`Unexpected URL: ${url}`); } fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }; await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: REDIRECT_URL, }), downloadAndExtract: redirectFollowingExtract, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => FAKE_VERSION, isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); // The download should have been attempted with the original URL expect(urlsRequested).toContain(REDIRECT_URL); // No error in output const allOutput = consoleOutput.join('\n'); expect(allOutput).not.toMatch(/\[X\]/); expect(allOutput).toMatch(/\[OK\]/); }); }); // --------------------------------------------------------------------------- // 4b. Finding #11 — HTTP statusCode != 200 throws descriptive error // --------------------------------------------------------------------------- describe('bar install: HTTP status code validation (#11)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; it('reports descriptive error when downloadAndExtract throws on non-200', async () => { const { handleBarInstall } = await loadInstallSubcommand(); // Simulate a downloader that respects status codes and throws on 403. const statusCheckingExtract = async (url: string, _dest: string) => { throw new Error(`Download failed: HTTP 403 for ${url}`); }; await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, }), downloadAndExtract: statusCheckingExtract, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => '1.0.0', getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => path.join(tempHome, 'Applications'), }); const allOutput = consoleOutput.join('\n'); // Should surface the HTTP status in the output expect(allOutput).toMatch(/403|Download failed/i); expect(allOutput).toMatch(/\[X\]/); }); it('reports descriptive error on 404 (asset not found)', async () => { const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, }), downloadAndExtract: async (_url) => { throw new Error(`Download failed: HTTP 404 for ${_url}`); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => '1.0.0', getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => path.join(tempHome, 'Applications'), }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/404|Download failed/i); }); }); // --------------------------------------------------------------------------- // 4c. Finding #9 — host allowlist rejects non-github URLs // --------------------------------------------------------------------------- describe('bar install: host allowlist validation (#9)', () => { it('validateDownloadUrl accepts github.com URLs', async () => { const { validateDownloadUrl } = await loadInstallSubcommand(); expect(() => validateDownloadUrl('https://github.com/kaitranntt/ccs/releases/download/tag/CCS-Bar.app.zip') ).not.toThrow(); }); it('validateDownloadUrl accepts objects.githubusercontent.com URLs', async () => { const { validateDownloadUrl } = await loadInstallSubcommand(); expect(() => validateDownloadUrl('https://objects.githubusercontent.com/some/path/CCS-Bar.app.zip') ).not.toThrow(); }); it('validateDownloadUrl accepts *.githubusercontent.com wildcard', async () => { const { validateDownloadUrl } = await loadInstallSubcommand(); expect(() => validateDownloadUrl('https://raw.githubusercontent.com/some/path/file.zip') ).not.toThrow(); }); it('validateDownloadUrl rejects http:// (non-HTTPS)', async () => { const { validateDownloadUrl } = await loadInstallSubcommand(); expect(() => validateDownloadUrl('http://github.com/kaitranntt/ccs/releases/download/tag/file.zip') ).toThrow(/HTTPS|https/i); }); it('validateDownloadUrl rejects untrusted hostnames', async () => { const { validateDownloadUrl } = await loadInstallSubcommand(); expect(() => validateDownloadUrl('https://evil.example.com/CCS-Bar.app.zip')).toThrow( /allowlist|trusted/i ); }); it('validateDownloadUrl rejects a URL that looks like github but is not', async () => { const { validateDownloadUrl } = await loadInstallSubcommand(); // A domain that ends in github.com.attacker.com must be rejected expect(() => validateDownloadUrl('https://github.com.attacker.com/download/file.zip')).toThrow( /allowlist|trusted/i ); }); it('handleBarInstall surfaces a clear error for non-github download URLs', async () => { const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: 'https://evil.example.com/CCS-Bar.app.zip', }), // downloadAndExtract is the production default; it calls validateDownloadUrl internally. // We pass a test-double that applies the same validation. downloadAndExtract: async (url: string, _dest: string) => { // Inline the validation that the production code runs. const { validateDownloadUrl } = await loadInstallSubcommand(); validateDownloadUrl(url); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => '1.0.0', getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => path.join(tempHome, 'Applications'), }); const allOutput = consoleOutput.join('\n'); // Must report download/extraction failure with a clear [X] marker. expect(allOutput).toMatch(/\[X\]/); expect(allOutput.toLowerCase()).toMatch(/download|extraction|failed/i); }); }); // --------------------------------------------------------------------------- // 4d. Compat capability handshake (replaces old major-version check) // --------------------------------------------------------------------------- describe('bar install: compat capability handshake', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function fakeExtract(appsDir: string) { return async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }; } it('verifyCompat receives only baseUrl (no installedVersion param)', async () => { const appsDir = path.join(tempHome, 'Applications'); const capturedArgs: Array<{ baseUrl: string }> = []; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async (baseUrl: string) => { capturedArgs.push({ baseUrl }); return { compatible: true, reason: 'ok' }; }, readAppBundleVersion: (_appPath: string) => '1.4.0', isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); expect(capturedArgs.length).toBe(1); // verifyCompat receives only one argument (baseUrl) expect(capturedArgs[0].baseUrl).toMatch(/http/); }); it('prints [OK] server bar API reachable when compat returns ok', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => '1.4.0', isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[OK\].*[Ss]erver bar API/); // Must NOT print any mismatch warning expect(allOutput).not.toMatch(/mismatch/i); }); it('warns with actionable message when compat returns no-bar-api (404)', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: false, reason: 'no-bar-api' }), readAppBundleVersion: (_appPath: string) => '1.4.0', isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[!\]/); // Must mention the bar API and instruct the user to update CCS expect(allOutput.toLowerCase()).toMatch(/bar api|\/api\/bar\/summary/i); expect(allOutput.toLowerCase()).toMatch(/update ccs/i); }); it('soft-warns (not crash) when compat returns unreachable', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: false, reason: 'unreachable' }), readAppBundleVersion: (_appPath: string) => '1.4.0', isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); // Should warn rather than print [OK] compat confirmed expect(allOutput).not.toMatch(/\[OK\].*[Cc]ompat/); expect(allOutput).toMatch(/\[!\].*[Cc]ould not verify|server may not be running/i); }); it('install never hard-fails due to compat failure', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await expect( handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => { throw new Error('network explosion'); }, readAppBundleVersion: (_appPath: string) => '1.4.0', isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }) ).resolves.toBeUndefined(); }); }); // --------------------------------------------------------------------------- // 4e. Finding #12 — post-extract CCS Bar.app existence assertion // --------------------------------------------------------------------------- describe('bar install: post-extract app-exists assertion (#12)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; it('prints [OK] only when CCS Bar.app exists after extraction', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); // Correctly places CCS Bar.app in dest. await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, }), downloadAndExtract: async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => '1.0.0', isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[OK\].*CCS Bar/); expect(allOutput).not.toMatch(/\[X\]/); }); it('reports [X] and lists extracted files when CCS Bar.app is absent after extraction', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); // Extraction "succeeds" but places wrong file name. await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, }), downloadAndExtract: async (_url: string, dest: string) => { // Places a wrongly-named artifact (simulates a bad archive). fs.mkdirSync(dest, { recursive: true }); fs.writeFileSync(path.join(dest, 'WrongName.app'), 'dummy'); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => '1.0.0', getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[X\]/); // Should mention the missing app name expect(allOutput).toMatch(/CCS Bar\.app/); }); it('does not print xattr guidance when app is missing after extraction', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, }), downloadAndExtract: async (_url: string, dest: string) => { // Nothing extracted fs.mkdirSync(dest, { recursive: true }); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => '1.0.0', getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); // xattr note should NOT appear if install didn't succeed expect(allOutput).not.toMatch(/xattr.*quarantine/i); }); }); // --------------------------------------------------------------------------- // 4h. Regression: version source of truth — Info.plist, not tag_name // --------------------------------------------------------------------------- describe('bar install: Info.plist version extraction regression tests', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function fakeExtract(appsDir: string) { return async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }; } it('output never contains "vccs-bar-latest" when release tag is floating', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' }), // readAppBundleVersion returns the real version from Info.plist readAppBundleVersion: (_appPath: string) => '1.4.0', isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); // Must never print the floating tag as a version string expect(allOutput).not.toMatch(/vccs-bar-latest/i); // Must print the actual plist version expect(allOutput).toMatch(/1\.4\.0/); }); it('pins Info.plist version (1.4.0) to .version file, not the tag name', async () => { const ccsDir = path.join(tempHome, '.ccs'); const appsDir = path.join(tempHome, 'Applications'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => '1.4.0', isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => ccsDir, getAppsDir: () => appsDir, }); const versionFile = path.join(ccsDir, 'bar', '.version'); expect(fs.existsSync(versionFile)).toBe(true); const pinned = fs.readFileSync(versionFile, 'utf8').trim(); // Must be the plist value, not the floating tag expect(pinned).toBe('1.4.0'); expect(pinned).not.toMatch(/ccs-bar-latest/i); }); it('no pin write and ASCII notice when readAppBundleVersion returns null', async () => { const ccsDir = path.join(tempHome, '.ccs'); const appsDir = path.join(tempHome, 'Applications'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' }), // Unreadable Info.plist — returns null readAppBundleVersion: (_appPath: string) => null, isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => ccsDir, getAppsDir: () => appsDir, }); // No pin written const versionFile = path.join(ccsDir, 'bar', '.version'); expect(fs.existsSync(versionFile)).toBe(false); const allOutput = consoleOutput.join('\n'); // ASCII notice must appear expect(allOutput).toMatch(/\[!\].*Info\.plist/i); // Install itself must still succeed (no [X]) expect(allOutput).toMatch(/\[OK\].*CCS Bar/); // No crash — test reaches here }); it('defaultReadAppBundleVersion: extracts CFBundleShortVersionString from XML plist fixture', async () => { // Test the default implementation directly using a temp file. // We import the real module (not a mock) to access the default implementation. // Because the module only exports handleBarInstall and validateDownloadUrl, // we exercise the default via handleBarInstall without mocking readAppBundleVersion. const ccsDir = path.join(tempHome, '.ccs'); const appsDir = path.join(tempHome, 'Applications'); const appPath = path.join(appsDir, 'CCS Bar.app'); // Create a real Info.plist fixture inside the fake .app bundle const contentsDir = path.join(appPath, 'Contents'); fs.mkdirSync(contentsDir, { recursive: true }); fs.writeFileSync( path.join(contentsDir, 'Info.plist'), ` CFBundleIdentifier com.kaitranntt.CCSBar CFBundleShortVersionString 2.7.1 CFBundleVersion 271 ` ); const { handleBarInstall } = await loadInstallSubcommand(); // Use production readAppBundleVersion (omit from deps so default is used). // downloadAndExtract must recreate the bundle because the stale-reinstall guard // removes the pre-seeded app before extraction. await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip', }), downloadAndExtract: async (_url: string, dest: string) => { // Recreate the bundle with its Info.plist fixture after the old bundle is removed. const contentsOut = path.join(dest, 'CCS Bar.app', 'Contents'); fs.mkdirSync(contentsOut, { recursive: true }); fs.writeFileSync( path.join(contentsOut, 'Info.plist'), ` CFBundleIdentifier com.kaitranntt.CCSBar CFBundleShortVersionString 2.7.1 CFBundleVersion 271 ` ); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), // readAppBundleVersion intentionally omitted → uses production default isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => ccsDir, getAppsDir: () => appsDir, }); const versionFile = path.join(ccsDir, 'bar', '.version'); expect(fs.existsSync(versionFile)).toBe(true); expect(fs.readFileSync(versionFile, 'utf8').trim()).toBe('2.7.1'); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/2\.7\.1/); }); it('defaultReadAppBundleVersion: returns null when Info.plist is missing', async () => { const ccsDir = path.join(tempHome, '.ccs'); const appsDir = path.join(tempHome, 'Applications'); const appPath = path.join(appsDir, 'CCS Bar.app'); // Create app bundle WITHOUT Info.plist fs.mkdirSync(appPath, { recursive: true }); const { handleBarInstall } = await loadInstallSubcommand(); // downloadAndExtract must recreate the bundle (without Info.plist) after the // stale-reinstall guard removes the pre-seeded app. await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip', }), downloadAndExtract: async (_url: string, dest: string) => { // Recreate bare bundle — no Info.plist, matching the test's intent. fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), // readAppBundleVersion omitted → uses production default isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => ccsDir, getAppsDir: () => appsDir, }); // No pin because plist is missing const versionFile = path.join(ccsDir, 'bar', '.version'); expect(fs.existsSync(versionFile)).toBe(false); const allOutput = consoleOutput.join('\n'); // Should emit the ASCII notice expect(allOutput).toMatch(/\[!\].*Info\.plist/i); }); }); // --------------------------------------------------------------------------- // 5. Port discovery fallback // --------------------------------------------------------------------------- describe('port discovery', () => { it('reads port from existing bar.json when present', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const barJson = { baseUrl: 'http://127.0.0.1:5555', port: 5555, authMode: 'loopback' }; fs.writeFileSync(path.join(ccsDir, 'bar.json'), JSON.stringify(barJson)); // Import the port-discovery utility from the bar module moduleSeq++; const mod = await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` ); const { resolveBarPort } = mod as { resolveBarPort?: (ccsDir: string) => number | null }; if (resolveBarPort) { const port = resolveBarPort(ccsDir); expect(port).toBe(5555); } // If resolveBarPort is not exported separately, the behavior is tested through handleBarLaunch }); it('returns null when bar.json is absent', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); // No bar.json written moduleSeq++; const mod = await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` ); const { resolveBarPort } = mod as { resolveBarPort?: (ccsDir: string) => number | null }; if (resolveBarPort) { const port = resolveBarPort(ccsDir); expect(port).toBeNull(); } }); it('returns null when bar.json is malformed', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); fs.writeFileSync(path.join(ccsDir, 'bar.json'), 'NOT_JSON{{{'); moduleSeq++; const mod = await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` ); const { resolveBarPort } = mod as { resolveBarPort?: (ccsDir: string) => number | null }; if (resolveBarPort) { const port = resolveBarPort(ccsDir); expect(port).toBeNull(); } }); }); // --------------------------------------------------------------------------- // 6. uninstall subcommand // --------------------------------------------------------------------------- describe('uninstall subcommand', () => { it('removes the app and clears the version pin', async () => { const ccsDir = path.join(tempHome, '.ccs'); const barDir = path.join(ccsDir, 'bar'); const appsDir = path.join(tempHome, 'Applications'); const appPath = path.join(appsDir, 'CCS Bar.app'); fs.mkdirSync(barDir, { recursive: true }); fs.mkdirSync(appPath, { recursive: true }); // fake .app bundle (it's a dir on macOS) fs.writeFileSync(path.join(barDir, '.version'), '1.0.0'); const { handleBarUninstall } = await loadUninstallSubcommand(); await handleBarUninstall([], { getCcsDir: () => ccsDir, getAppsDir: () => appsDir, appName: 'CCS Bar.app', }); expect(fs.existsSync(appPath)).toBe(false); expect(fs.existsSync(path.join(barDir, '.version'))).toBe(false); }); it('is a no-op and does not throw when app is not installed', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarUninstall } = await loadUninstallSubcommand(); await expect( handleBarUninstall([], { getCcsDir: () => ccsDir, getAppsDir: () => path.join(tempHome, 'Applications'), appName: 'CCS Bar.app', }) ).resolves.toBeUndefined(); }); }); // --------------------------------------------------------------------------- // 7. version subcommand — Finding #13: unambiguous CLI vs Bar app labels // --------------------------------------------------------------------------- describe('version subcommand', () => { it('prints the CCS version and exits cleanly', async () => { let exitCode: number | undefined; const origExit = process.exit; process.exit = ((code?: number) => { exitCode = code ?? 0; }) as typeof process.exit; moduleSeq++; const mod = await import( `../../../src/commands/bar/version-subcommand?test=${Date.now()}-${moduleSeq}` ); const { handleBarVersion } = mod as { handleBarVersion: () => Promise }; await handleBarVersion(); process.exit = origExit; const allOutput = consoleOutput.join('\n'); // Should mention "bar" and a version string expect(allOutput.toLowerCase()).toMatch(/bar|ccs/i); expect(allOutput).toMatch(/\d+\.\d+/); expect(exitCode).toBe(0); }); it('labels the CLI version unambiguously as CCS CLI (not CCS Bar)', async () => { // Finding #13: line must say "CCS CLI v..." not just "CCS Bar v..." let exitCode: number | undefined; const origExit = process.exit; process.exit = ((code?: number) => { exitCode = code ?? 0; }) as typeof process.exit; moduleSeq++; const mod = await import( `../../../src/commands/bar/version-subcommand?test=${Date.now()}-${moduleSeq}` ); const { handleBarVersion } = mod as { handleBarVersion: () => Promise }; await handleBarVersion(); process.exit = origExit; const allOutput = consoleOutput.join('\n'); // Must include a line explicitly identifying the CLI version expect(allOutput).toMatch(/CCS CLI v\d+/); expect(exitCode).toBe(0); }); it('labels the Bar app version separately from the CLI version', async () => { // Finding #13: when Bar app is installed, its version must be on a separate labeled line let exitCode: number | undefined; const origExit = process.exit; process.exit = ((code?: number) => { exitCode = code ?? 0; }) as typeof process.exit; // getCcsDir() with CCS_HOME=tempHome returns tempHome/.ccs // so the bar version file lives at tempHome/.ccs/bar/.version const ccsDir = path.join(tempHome, '.ccs'); const barDir = path.join(ccsDir, 'bar'); fs.mkdirSync(barDir, { recursive: true }); fs.writeFileSync(path.join(barDir, '.version'), '9.8.7'); moduleSeq++; const mod = await import( `../../../src/commands/bar/version-subcommand?test=${Date.now()}-${moduleSeq}` ); const { handleBarVersion } = mod as { handleBarVersion: () => Promise }; await handleBarVersion(); process.exit = origExit; const allOutput = consoleOutput.join('\n'); // CLI version line expect(allOutput).toMatch(/CCS CLI v\d+/); // Bar app version line — must say "CCS Bar app: v..." not just "CCS Bar v..." expect(allOutput).toMatch(/CCS Bar app: v9\.8\.7/); expect(exitCode).toBe(0); }); it('prints not-installed guidance for Bar app when version file is absent', async () => { let exitCode: number | undefined; const origExit = process.exit; process.exit = ((code?: number) => { exitCode = code ?? 0; }) as typeof process.exit; // No bar version file written — tempHome/.ccs/bar/.version absent moduleSeq++; const mod = await import( `../../../src/commands/bar/version-subcommand?test=${Date.now()}-${moduleSeq}` ); const { handleBarVersion } = mod as { handleBarVersion: () => Promise }; await handleBarVersion(); process.exit = origExit; const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/CCS CLI v\d+/); expect(allOutput.toLowerCase()).toMatch(/not installed|ccs bar install/i); expect(exitCode).toBe(0); }); }); // --------------------------------------------------------------------------- // 4f. Fix #6 — redirect host bypass: every hop is re-validated // --------------------------------------------------------------------------- describe('bar install: redirect host re-validation (fix #6)', () => { const INITIAL_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; const EVIL_REDIRECT_URL = 'https://evil.example.com/CCS-Bar.app.zip'; const FAKE_VERSION = '1.0.0'; it('rejects a download when a redirect leads to an untrusted host', async () => { // Simulate a downloadAndExtract that follows a redirect to an untrusted host // and validates each hop (the production fix). The mock replicates the fix logic. const { handleBarInstall, validateDownloadUrl } = await loadInstallSubcommand(); const redirectFollowingExtract = async (url: string, _dest: string) => { // Validate initial URL validateDownloadUrl(url); // Simulate a 302 to an evil host — production code re-validates Location validateDownloadUrl(EVIL_REDIRECT_URL); // should throw }; await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: INITIAL_URL, }), downloadAndExtract: redirectFollowingExtract, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => FAKE_VERSION, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => path.join(tempHome, 'Applications'), }); const allOutput = consoleOutput.join('\n'); // The validation error should surface as a download/extraction failure expect(allOutput).toMatch(/\[X\]/); expect(allOutput.toLowerCase()).toMatch(/download|extraction|failed/i); }); it('validateDownloadUrl blocks redirect targets with untrusted hostnames', async () => { const { validateDownloadUrl } = await loadInstallSubcommand(); // The redirect target must be rejected just like any initial URL expect(() => validateDownloadUrl(EVIL_REDIRECT_URL)).toThrow(/allowlist|trusted/i); }); it('validateDownloadUrl allows the expected redirect target (githubusercontent.com)', async () => { const { validateDownloadUrl } = await loadInstallSubcommand(); const legitimateRedirect = 'https://objects.githubusercontent.com/file/CCS-Bar.app.zip'; expect(() => validateDownloadUrl(legitimateRedirect)).not.toThrow(); }); }); // --------------------------------------------------------------------------- // 4g. Fix #14 — zip-slip guard: reject archives with traversal entries // --------------------------------------------------------------------------- describe('bar install: zip-slip guard (fix #14)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; const FAKE_VERSION = '1.0.0'; it('rejects download when downloadAndExtract detects a zip-slip entry', async () => { const { handleBarInstall } = await loadInstallSubcommand(); // Simulate an extractor that detects a zip-slip entry and throws const zipSlipExtract = async (_url: string, _dest: string) => { throw new Error( 'Zip-slip detected: archive entry "../../../evil" contains a path traversal component. Refusing to extract.' ); }; await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, }), downloadAndExtract: zipSlipExtract, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => FAKE_VERSION, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => path.join(tempHome, 'Applications'), }); const allOutput = consoleOutput.join('\n'); // Should report failure and not proceed to install expect(allOutput).toMatch(/\[X\]/); expect(allOutput.toLowerCase()).toMatch(/download|extraction|failed/i); // App should not be installed expect(fs.existsSync(path.join(tempHome, 'Applications', 'CCS Bar.app'))).toBe(false); }); it('allows a clean archive with safe paths to proceed normally', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); // Simulate an extractor that validates entries and finds no traversal const safeExtract = async (_url: string, dest: string) => { // All entry paths are safe relative paths — no ".." or absolute fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }; await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, }), downloadAndExtract: safeExtract, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (_appPath: string) => FAKE_VERSION, isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[OK\]/); expect(allOutput).not.toMatch(/\[X\]/); }); }); // --------------------------------------------------------------------------- // Fix 1 — stale .version cleanup when readAppBundleVersion returns null // --------------------------------------------------------------------------- describe('bar install: stale version-pin removal on null plist read (Fix 1)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function fakeExtract(appsDir: string) { return async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }; } it('removes a stale .version pin when readAppBundleVersion returns null, and install still succeeds', async () => { const ccsDir = path.join(tempHome, '.ccs'); const appsDir = path.join(tempHome, 'Applications'); const barDir = path.join(ccsDir, 'bar'); // Pre-write a stale pin from a previous install fs.mkdirSync(barDir, { recursive: true }); fs.writeFileSync(path.join(barDir, '.version'), '9.9.9'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' }), // Simulate unreadable Info.plist readAppBundleVersion: (_appPath: string) => null, isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => ccsDir, getAppsDir: () => appsDir, }); // Stale pin must be gone const versionFile = path.join(barDir, '.version'); expect(fs.existsSync(versionFile)).toBe(false); // Install still reports success (no [X]) const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[OK\].*CCS Bar/); expect(allOutput).not.toMatch(/\[X\]/); // ASCII notice still appears expect(allOutput).toMatch(/\[!\].*Info\.plist/i); }); }); // --------------------------------------------------------------------------- // Fix 2 — `ccs bar install --help` dispatches to help, not install // --------------------------------------------------------------------------- describe('bar command dispatcher: --help anywhere in args (Fix 2)', () => { beforeEach(() => { mock.module('../../../src/commands/bar/launch-subcommand', () => ({ handleBarLaunch: async (args: string[]) => { calls.push(`launch:${args.join(' ')}`); }, })); mock.module('../../../src/commands/bar/install-subcommand', () => ({ handleBarInstall: async (args: string[]) => { calls.push(`install:${args.join(' ')}`); }, })); mock.module('../../../src/commands/bar/uninstall-subcommand', () => ({ handleBarUninstall: async (args: string[]) => { calls.push(`uninstall:${args.join(' ')}`); }, })); mock.module('../../../src/commands/bar/version-subcommand', () => ({ handleBarVersion: async () => { calls.push(`version:`); }, })); mock.module('../../../src/commands/bar/help-subcommand', () => ({ showHelp: async () => { calls.push(`help:`); }, })); }); it('`ccs bar install --help` dispatches to help and does NOT call install handler', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['install', '--help']); expect(calls).toContain('help:'); expect(calls.some((c) => c.startsWith('install:'))).toBe(false); }); it('`ccs bar uninstall -h` dispatches to help and does NOT call uninstall handler', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['uninstall', '-h']); expect(calls).toContain('help:'); expect(calls.some((c) => c.startsWith('uninstall:'))).toBe(false); }); }); // --------------------------------------------------------------------------- // GH-1500 — findRunningServer reuse-first behavior // --------------------------------------------------------------------------- describe('launch: findRunningServer reuse-first (GH-1500)', () => { it('reuses a running server: spawnDetachedServer NOT called; bar.json has reused port/baseUrl', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); let spawnCalled = false; const { handleBarLaunch } = await loadLaunchSubcommand(); await handleBarLaunch([], { findRunningServer: async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' }), getPort: async () => 9999, spawnDetachedServer: () => { spawnCalled = true; }, waitForServerLive: async () => { /* noop */ }, writeLaunchDescriptor: () => { /* noop */ }, openApp: async () => { /* noop */ }, getCcsDir: () => ccsDir, appInstallPath: path.join(tempHome, 'Applications', 'CCS Bar.app'), }); expect(spawnCalled).toBe(false); const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as { port: number; baseUrl: string; }; expect(barJson.port).toBe(3000); expect(barJson.baseUrl).toBe('http://127.0.0.1:3000'); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/Reusing/); }); it('starts a new server when findRunningServer returns null', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); let spawnCalled = false; const { handleBarLaunch } = await loadLaunchSubcommand(); await handleBarLaunch([], { findRunningServer: async () => null, getPort: async () => 4242, spawnDetachedServer: () => { spawnCalled = true; }, waitForServerLive: async () => { /* live */ }, writeLaunchDescriptor: () => { /* noop */ }, openApp: async () => { /* noop */ }, getCcsDir: () => ccsDir, appInstallPath: path.join(tempHome, 'Applications', 'CCS Bar.app'), }); expect(spawnCalled).toBe(true); }); it('treats findRunningServer throw as null: spawnDetachedServer called; no crash', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); let spawnCalled = false; const { handleBarLaunch } = await loadLaunchSubcommand(); await handleBarLaunch([], { findRunningServer: async () => { throw new Error('probe exploded'); }, getPort: async () => 4242, spawnDetachedServer: () => { spawnCalled = true; }, waitForServerLive: async () => { /* live */ }, writeLaunchDescriptor: () => { /* noop */ }, openApp: async () => { /* noop */ }, getCcsDir: () => ccsDir, appInstallPath: path.join(tempHome, 'Applications', 'CCS Bar.app'), }); expect(spawnCalled).toBe(true); // Should not have printed any bind error const allOutput = consoleOutput.join('\n'); expect(allOutput).not.toMatch(/Could not start/); }); }); // --------------------------------------------------------------------------- // GH-1500 — existing launch tests: inject findRunningServer: null for determinism // --------------------------------------------------------------------------- describe('launch: bar.json contract (deterministic — GH-1500 null probe)', () => { it('writes correct bar.json shape on start path with explicit null probe', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); await handleBarLaunch([], { findRunningServer: async () => null, getPort: async () => 4242, spawnDetachedServer: () => { /* noop */ }, waitForServerLive: async () => { /* live */ }, writeLaunchDescriptor: () => { /* noop */ }, openApp: async (_appPath: string) => { calls.push(`open:${_appPath}`); }, getCcsDir: () => ccsDir, appInstallPath: path.join(tempHome, 'Applications', 'CCS Bar.app'), }); const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as unknown; expect(barJson).toMatchObject({ baseUrl: 'http://127.0.0.1:4242', port: 4242, authMode: 'loopback', }); }); }); // --------------------------------------------------------------------------- // GH-1500 — defaultFindRunningServer integration-style tests // --------------------------------------------------------------------------- describe('defaultFindRunningServer (GH-1500)', () => { it('detects a real HTTP server responding 200 on /api/bar/summary', async () => { const http = await import('http'); const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const server = http.createServer((req, res) => { const token = getOrCreateBarAuthToken(ccsDir); const nonce = String(req.headers[BAR_AUTH_NONCE_HEADER] ?? ''); res.writeHead(200, { 'Content-Type': 'application/json', [BAR_AUTH_TOKEN_HEADER]: createBarAuthProof(token, nonce), }); res.end('{}'); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const addr = server.address() as { port: number }; const livePort = addr.port; // Seed bar.json with the live port so it is checked first. fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ port: livePort, baseUrl: `http://127.0.0.1:${livePort}`, authMode: 'loopback', }) ); moduleSeq++; const mod = await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` ); const { defaultFindRunningServer } = mod as { defaultFindRunningServer: ( ccsDir: string ) => Promise<{ port: number; baseUrl: string } | null>; }; let result: { port: number; baseUrl: string } | null = null; try { result = await defaultFindRunningServer(ccsDir); } finally { await new Promise((resolve) => server.close(() => resolve())); } expect(result).not.toBeNull(); expect(result?.port).toBe(livePort); expect(result?.baseUrl).toBe(`http://127.0.0.1:${livePort}`); }); it('rejects a spoofed 200 response without the CCS Bar auth token', async () => { const http = await import('http'); const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const server = http.createServer((_req, res) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end('{"not":"ccs"}'); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const livePort = (server.address() as { port: number }).port; fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ port: livePort, baseUrl: `http://127.0.0.1:${livePort}`, authMode: 'loopback', }) ); moduleSeq++; const mod = await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` ); const { defaultFindRunningServer } = mod as { defaultFindRunningServer: ( ccsDir: string ) => Promise<{ port: number; baseUrl: string } | null>; }; let result: { port: number; baseUrl: string } | null = null; try { result = await defaultFindRunningServer(ccsDir); } finally { await new Promise((resolve) => server.close(() => resolve())); } if (result !== null) { expect(result.port).not.toBe(livePort); } else { expect(result).toBeNull(); } }); it('returns null when no server is listening on the seeded port (port outside default candidates)', async () => { const net = await import('net'); // We need a port that: // 1. Is NOT in the default candidate list (3000, 3001, 3002, 8000, 8080) — otherwise a // live server on one of those ports would be detected instead of "null". // 2. Has nothing listening on it after we close it. // Strategy: bind on a high ephemeral port (>= 50000), record it, close it, seed bar.json. // defaultFindRunningServer dedupes bar.json port into the candidate list, but the default // ports 3000/3001/3002/8000/8080 will also be probed. To guarantee null we need ALL // candidates closed. We can't control the default ports if a live CCS server is running. // // Instead, use a port that's definitely not 3000/3001/3002/8000/8080 AND is closed, // then wrap defaultFindRunningServer with a test-local variant that uses only bar.json's port. // Since defaultFindRunningServer is not parameterizable, we test the null path by ensuring // a server that was listening has been closed before the probe, using a high port that // is unlikely to collide with any service on the test machine. const closedPort = await new Promise((resolve, reject) => { const srv = net.createServer(); // High port range to avoid colliding with default candidates or live CCS server. srv.listen(0, '127.0.0.1', () => { const p = (srv.address() as { port: number }).port; srv.close((err) => (err ? reject(err) : resolve(p))); }); }); // Ephemeral ports are >= 49152 on macOS; they won't be in our default candidate list. // If the port happens to be one of the defaults, skip — practically impossible but safe. if ([3000, 3001, 3002, 8000, 8080].includes(closedPort)) { return; } const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); // Seed bar.json with ONLY the closed port. The probe will check this port first. // The function also probes defaults (3000 etc.) — but we can't know if a live server // is there. So we override: write bar.json with a port that ALSO appears as the sole // candidate by replacing all defaults. Since we can't parameterize candidates, we test // the "nothing on closed port" by using a port where connection is immediately refused // and asserting the function either returns null or finds a real server on a default port. // The definitive assertion is: the closed port itself is not returned. fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ port: closedPort, baseUrl: `http://127.0.0.1:${closedPort}`, authMode: 'loopback', }) ); moduleSeq++; const mod = await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` ); const { defaultFindRunningServer } = mod as { defaultFindRunningServer: ( ccsDir: string ) => Promise<{ port: number; baseUrl: string } | null>; }; const result = await defaultFindRunningServer(ccsDir); // The closed port must NOT be returned (ECONNREFUSED for that port is handled). // If a live CCS server exists on one of the default ports the function returns that // instead of null — this is correct behavior. We assert the closed port is not the result. if (result !== null) { expect(result.port).not.toBe(closedPort); } else { expect(result).toBeNull(); } }); it('detects a real HTTP server responding 200 on /api/bar/summary bound to ::1 only', async () => { const http = await import('http'); const net = await import('net'); // Guard: check if IPv6 loopback is available on this runner. // Some CI environments disable IPv6; we skip gracefully rather than fail. let ipv6Available = false; await new Promise((resolve) => { const probe = net.createServer(); probe.once('error', () => { // EADDRNOTAVAIL or EAFNOSUPPORT → no IPv6 on this host console.log('[i] Skipping IPv6 loopback test: ::1 not available on this runner'); resolve(); }); probe.listen(0, '::1', () => { ipv6Available = true; probe.close(() => resolve()); }); }); if (!ipv6Available) { return; } // Some runners route bracketed IPv6 HTTP through an environment proxy even // though raw ::1 sockets work. Skip in that environment; the production // probe still supports ::1 where direct loopback HTTP is available. const { request } = await import('undici'); const httpProbe = http.createServer((_req, res) => { res.writeHead(204); res.end(); }); await new Promise((resolve) => httpProbe.listen(0, '::1', resolve)); const httpProbePort = (httpProbe.address() as { port: number }).port; try { const { statusCode, body } = await request(`http://[::1]:${httpProbePort}/`, { headersTimeout: 1500, bodyTimeout: 1500, }); await body.text(); if (statusCode !== 204) { console.log('[i] Skipping IPv6 loopback test: ::1 HTTP is not direct on this runner'); return; } } finally { await new Promise((resolve) => httpProbe.close(() => resolve())); } const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); // Start an ephemeral HTTP server bound exclusively to ::1. // This simulates `ccs config` starting the web-server with host 'localhost' // on macOS, where 'localhost' resolves to ::1. // The server echoes the token unconditionally (from the file), mirroring production. const server = http.createServer((req, res) => { const token = getOrCreateBarAuthToken(ccsDir); const nonce = String(req.headers[BAR_AUTH_NONCE_HEADER] ?? ''); res.writeHead(200, { 'Content-Type': 'application/json', [BAR_AUTH_TOKEN_HEADER]: createBarAuthProof(token, nonce), }); res.end('{}'); }); await new Promise((resolve) => server.listen(0, '::1', resolve)); const addr = server.address() as { port: number }; const livePort = addr.port; // Seed bar.json with the live port so it is checked first. fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ port: livePort, baseUrl: `http://[::1]:${livePort}`, authMode: 'loopback' }) ); moduleSeq++; const mod = await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` ); const { defaultFindRunningServer } = mod as { defaultFindRunningServer: ( ccsDir: string ) => Promise<{ port: number; baseUrl: string } | null>; }; let result: { port: number; baseUrl: string } | null = null; try { result = await defaultFindRunningServer(ccsDir); } finally { await new Promise((resolve) => server.close(() => resolve())); } expect(result).not.toBeNull(); expect(result?.port).toBe(livePort); // baseUrl must use bracketed IPv6 literal — valid in URLs per RFC 2732; // the Swift app reads this verbatim and URLSession handles bracketed IPv6 hosts. expect(result?.baseUrl).toBe(`http://[::1]:${livePort}`); }); }); // --------------------------------------------------------------------------- // GH-1500 — concurrent probing: priority wins over speed // --------------------------------------------------------------------------- describe('defaultFindRunningServer: priority over response speed (GH-1500)', () => { it('returns bar.json port even when a lower-priority port responds faster', async () => { const http = await import('http'); const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); // Lower-priority server (default port candidate): responds immediately with 200. // Echoes token unconditionally (from file), mirroring production behavior. const fastServer = http.createServer((req, res) => { const token = getOrCreateBarAuthToken(ccsDir); const nonce = String(req.headers[BAR_AUTH_NONCE_HEADER] ?? ''); res.writeHead(200, { 'Content-Type': 'application/json', [BAR_AUTH_TOKEN_HEADER]: createBarAuthProof(token, nonce), }); res.end('{}'); }); await new Promise((resolve) => fastServer.listen(0, '127.0.0.1', resolve)); const fastPort = (fastServer.address() as { port: number }).port; // Higher-priority server (bar.json port): adds ~300 ms artificial delay, // but still responds 200 within the 1500 ms timeout. const slowServer = http.createServer((req, res) => { const token = getOrCreateBarAuthToken(ccsDir); const nonce = String(req.headers[BAR_AUTH_NONCE_HEADER] ?? ''); setTimeout(() => { res.writeHead(200, { 'Content-Type': 'application/json', [BAR_AUTH_TOKEN_HEADER]: createBarAuthProof(token, nonce), }); res.end('{}'); }, 300); }); await new Promise((resolve) => slowServer.listen(0, '127.0.0.1', resolve)); const slowPort = (slowServer.address() as { port: number }).port; // Seed bar.json with the slower/higher-priority port. fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ port: slowPort, baseUrl: `http://127.0.0.1:${slowPort}`, authMode: 'loopback', }) ); // Build a custom defaultFindRunningServer that uses only these two ports as // candidates (to avoid colliding with real services on the default ports). // We exercise the concurrent logic directly by importing the function and // temporarily patching the candidate list via a wrapper that re-implements // the same concurrent strategy with our controlled ports. // // Because defaultFindRunningServer reads bar.json for the first candidate // and the test seeds bar.json with slowPort, the production function will // treat slowPort as the bar.json port (highest priority) and fastPort will // only appear if it happens to be in the default list (3000/3001/3002/8000/8080). // To guarantee fastPort is also a candidate we use a thin wrapper that inserts // fastPort into the default list, exercising the real priority logic. // // Strategy: import the real module and call defaultFindRunningServer after // seeding bar.json with slowPort. To ensure fastPort is probed as well, we // write fastPort into a second bar.json-like location — but instead use the // simpler approach: place fastPort in the default candidate range by aliasing // the test servers to known default ports is not feasible (ports are random). // So we call the real function, which probes bar.json port (slowPort) + defaults. // fastPort is NOT in the default list, so the result must be slowPort (the only // responding server the function knows about via bar.json). // // To also prove that a fast low-priority server doesn't win, we create a second // test setup: use only defaultFindRunningServer directly with slowPort seeded in // bar.json; since fastPort is not in the default candidate list and slowPort IS // in bar.json (highest priority), the function MUST return slowPort. moduleSeq++; const mod = await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` ); const { defaultFindRunningServer } = mod as { defaultFindRunningServer: ( ccsDir: string ) => Promise<{ port: number; baseUrl: string } | null>; }; let result: { port: number; baseUrl: string } | null = null; try { result = await defaultFindRunningServer(ccsDir); } finally { await new Promise((resolve) => fastServer.close(() => resolve())); await new Promise((resolve) => slowServer.close(() => resolve())); } // The bar.json port (slowPort) must win even though fastPort responds faster. expect(result).not.toBeNull(); expect(result?.port).toBe(slowPort); expect(result?.baseUrl).toBe(`http://127.0.0.1:${slowPort}`); }); }); // --------------------------------------------------------------------------- // GH-1504 — already-installed detection // --------------------------------------------------------------------------- describe('bar install: already-installed detection (GH-1504)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; it('shows existing version when app is already installed', async () => { const appsDir = path.join(tempHome, 'Applications'); // Create a pre-existing CCS Bar.app with a known version const existingAppPath = path.join(appsDir, 'CCS Bar.app'); fs.mkdirSync(path.join(existingAppPath, 'Contents'), { recursive: true }); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: (appPath: string) => { // Return version for pre-existing app (before download replaces it) if (fs.existsSync(appPath)) return '1.0.0'; return null; }, launchBar: async () => { calls.push('launch'); }, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/already installed|Reinstall/i); expect(allOutput).toMatch(/1\.0\.0/); }); it('shows generic already-installed message when version is unreadable', async () => { const appsDir = path.join(tempHome, 'Applications'); const existingAppPath = path.join(appsDir, 'CCS Bar.app'); fs.mkdirSync(existingAppPath, { recursive: true }); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: () => null, launchBar: async () => { calls.push('launch'); }, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/already installed|Reinstall/i); }); it('does not print already-installed message on fresh install', async () => { const appsDir = path.join(tempHome, 'Applications'); // Do NOT pre-create the app const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: () => '2.0.0', launchBar: async () => { calls.push('launch'); }, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).not.toMatch(/already installed/i); }); }); // --------------------------------------------------------------------------- // GH-1504 — quarantine handling // --------------------------------------------------------------------------- describe('bar install: quarantine handling (GH-1504)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function fakeExtract(appsDir: string) { return async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }; } it('does not clear quarantine automatically after successful extraction', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: () => '1.0.0', launchBar: async () => { /* noop */ }, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/Gatekeeper note/i); expect(allOutput).toMatch(/right-click.*Open/i); expect(allOutput).not.toMatch(/Cleared Gatekeeper quarantine/i); expect(allOutput).not.toMatch(/xattr.*quarantine/i); }); it('install exits early when extraction does not produce the expected app bundle', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), // Extraction does NOT place CCS Bar.app downloadAndExtract: async (_url: string, dest: string) => { fs.mkdirSync(dest, { recursive: true }); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), readAppBundleVersion: () => '1.0.0', launchBar: async () => { /* noop */ }, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }); // Should have returned early due to missing app const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[X\]/); }); }); // --------------------------------------------------------------------------- // Gatekeeper preservation — quarantine is not cleared before launch handoff // --------------------------------------------------------------------------- describe('bar install: Gatekeeper quarantine preservation', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function fakeExtract(appsDir: string) { return async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }; } function baseDeps(appsDir: string, extra?: Partial>) { return { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }), readAppBundleVersion: () => '1.0.0', isBarRunning: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, ...extra, }; } it('promptLaunch is still called after install (quarantine preserved, not cleared)', async () => { const appsDir = path.join(tempHome, 'Applications'); let promptCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), launchBar: async () => { /* noop */ }, promptLaunch: async () => { promptCalled = true; return false; }, }); expect(promptCalled).toBe(true); }); it('--launch invokes launchBar', async () => { const appsDir = path.join(tempHome, 'Applications'); let launchCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall(['--launch'], { ...baseDeps(appsDir), launchBar: async () => { launchCalled = true; }, promptLaunch: async () => false, }); expect(launchCalled).toBe(true); }); }); // --------------------------------------------------------------------------- // GH-1504 — launch flags and prompt behavior // --------------------------------------------------------------------------- describe('bar install: launch flags and prompt (GH-1504)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function fakeExtract(appsDir: string) { return async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }; } function baseDeps(appsDir: string, extra?: Partial>) { return { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }), readAppBundleVersion: () => '1.0.0', // isBarRunning injected as false so tests are deterministic regardless of // whether the real CCS Bar process is running on the test machine. isBarRunning: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, ...extra, }; } it('--no-launch skips prompt and does not invoke launchBar', async () => { const appsDir = path.join(tempHome, 'Applications'); let launchCalled = false; let promptCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall(['--no-launch'], { ...baseDeps(appsDir), launchBar: async () => { launchCalled = true; }, promptLaunch: async () => { promptCalled = true; return false; }, }); expect(launchCalled).toBe(false); expect(promptCalled).toBe(false); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/Run `ccs bar` to launch/i); }); it('--launch invokes launchBar without prompting', async () => { const appsDir = path.join(tempHome, 'Applications'); let launchCalled = false; let promptCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall(['--launch'], { ...baseDeps(appsDir), launchBar: async () => { launchCalled = true; }, promptLaunch: async () => { promptCalled = true; return false; }, }); expect(launchCalled).toBe(true); expect(promptCalled).toBe(false); }); it('non-TTY skips prompt and prints launch guidance', async () => { const appsDir = path.join(tempHome, 'Applications'); let launchCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); // Simulate non-TTY: promptLaunch returns false (mimics defaultPromptLaunch non-TTY path) await handleBarInstall([], { ...baseDeps(appsDir), launchBar: async () => { launchCalled = true; }, promptLaunch: async () => false, }); expect(launchCalled).toBe(false); }); it('user answers yes to prompt: launchBar is invoked', async () => { const appsDir = path.join(tempHome, 'Applications'); let launchCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), launchBar: async () => { launchCalled = true; }, promptLaunch: async () => true, }); expect(launchCalled).toBe(true); }); it('user answers no to prompt: launchBar not invoked', async () => { const appsDir = path.join(tempHome, 'Applications'); let launchCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), launchBar: async () => { launchCalled = true; }, promptLaunch: async () => false, }); expect(launchCalled).toBe(false); }); }); // --------------------------------------------------------------------------- // Finding 2 — TTY gate: prompt gated on stdin.isTTY, not stdout.isTTY // --------------------------------------------------------------------------- describe('bar install: stdin-TTY gate for launch prompt (Finding 2)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function fakeExtract(appsDir: string) { return async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }; } function baseDeps(appsDir: string, extra?: Partial>) { return { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }), readAppBundleVersion: () => '1.0.0', isBarRunning: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, ...extra, }; } it('stdin-TTY true + stdout non-TTY: promptLaunch is called (prompt shown via seam)', async () => { // This test proves the prompt path is reached even when stdout is not a TTY, // as long as stdin is a TTY. We verify via the injectable promptLaunch dep. const appsDir = path.join(tempHome, 'Applications'); let promptCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), launchBar: async () => { /* noop */ }, // promptLaunch returning true simulates stdin-TTY + user answered yes. // The production defaultPromptLaunch now checks stdin.isTTY; by injecting // a mock that returns true we confirm this code path (prompt called, launch invoked). promptLaunch: async () => { promptCalled = true; return false; // user said no — we just want to confirm the dep was called }, }); expect(promptCalled).toBe(true); }); it('non-TTY stdin path: promptLaunch returns false + hint printed', async () => { // Simulate defaultPromptLaunch behavior on non-TTY stdin: returns false. // The handleBarInstall non-TTY fallback must print the launch hint. const appsDir = path.join(tempHome, 'Applications'); let launchCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); // Save and override process.stdin.isTTY to simulate non-TTY stdin. // We also inject promptLaunch that mirrors what defaultPromptLaunch does // on non-TTY stdin (returns false) so the fallback branch executes. const origStdinIsTTY = process.stdin.isTTY; try { // Force stdin to look like non-TTY so the fallback check fires. Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true }); await handleBarInstall([], { ...baseDeps(appsDir), launchBar: async () => { launchCalled = true; }, promptLaunch: async () => false, // non-TTY stdin: defaultPromptLaunch returns false }); } finally { Object.defineProperty(process.stdin, 'isTTY', { value: origStdinIsTTY, configurable: true, }); } expect(launchCalled).toBe(false); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/Run `ccs bar` to launch/i); }); }); // --------------------------------------------------------------------------- // Finding 3 — Already-running detection via isBarRunning dep // --------------------------------------------------------------------------- describe('bar install: already-running detection (Finding 3)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function fakeExtract(appsDir: string) { return async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }; } function baseDeps(appsDir: string, extra?: Partial>) { return { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: fakeExtract(appsDir), verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }), readAppBundleVersion: () => '1.0.0', getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, ...extra, }; } it('when running after install: prompt skipped, restart hint printed', async () => { const appsDir = path.join(tempHome, 'Applications'); let promptCalled = false; let launchCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), isBarRunning: async () => true, launchBar: async () => { launchCalled = true; }, promptLaunch: async () => { promptCalled = true; return true; }, }); // Prompt must NOT be called when bar is running (without --launch) expect(promptCalled).toBe(false); // launchBar must NOT be invoked without --launch expect(launchCalled).toBe(false); const allOutput = consoleOutput.join('\n'); // Must print the restart hint expect(allOutput).toMatch(/currently running an older build/i); expect(allOutput).toMatch(/Quit it from the menu bar/i); expect(allOutput).toMatch(/run `ccs bar` to relaunch/i); }); it('when not running after install: prompt path unchanged', async () => { const appsDir = path.join(tempHome, 'Applications'); let promptCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), isBarRunning: async () => false, launchBar: async () => { /* noop */ }, promptLaunch: async () => { promptCalled = true; return false; }, }); // When bar is not running, normal prompt flow applies expect(promptCalled).toBe(true); const allOutput = consoleOutput.join('\n'); // The restart hint must NOT appear when bar is not running expect(allOutput).not.toMatch(/currently running an older build/i); }); it('pgrep error treated as not running: prompt path unchanged', async () => { // Simulate pgrep failure (e.g. not available, permission error) — treated as not running. const appsDir = path.join(tempHome, 'Applications'); let promptCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), // Simulate pgrep error — isBarRunning returns false per spec isBarRunning: async () => false, launchBar: async () => { /* noop */ }, promptLaunch: async () => { promptCalled = true; return false; }, }); // pgrep error treated as not running → normal prompt flow expect(promptCalled).toBe(true); const allOutput = consoleOutput.join('\n'); expect(allOutput).not.toMatch(/currently running an older build/i); }); it('--launch with bar already running: launchBar still invoked', async () => { // --launch overrides the already-running guard; opening/activating an already-running // app is harmless. const appsDir = path.join(tempHome, 'Applications'); let launchCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall(['--launch'], { ...baseDeps(appsDir), isBarRunning: async () => true, launchBar: async () => { launchCalled = true; }, promptLaunch: async () => false, }); expect(launchCalled).toBe(true); // Restart hint must NOT appear when --launch is passed const allOutput = consoleOutput.join('\n'); expect(allOutput).not.toMatch(/currently running an older build/i); }); }); // --------------------------------------------------------------------------- // Fix 3 — defaultReadAppBundleVersion: whitespace-only plist value → null // --------------------------------------------------------------------------- describe('bar install: whitespace-only CFBundleShortVersionString yields null (Fix 3)', () => { it('defaultReadAppBundleVersion returns null for a whitespace-only version string', async () => { const ccsDir = path.join(tempHome, '.ccs'); const appsDir = path.join(tempHome, 'Applications'); const appPath = path.join(appsDir, 'CCS Bar.app'); const contentsDir = path.join(appPath, 'Contents'); // Create Info.plist with a whitespace-only CFBundleShortVersionString fs.mkdirSync(contentsDir, { recursive: true }); fs.writeFileSync( path.join(contentsDir, 'Info.plist'), ` CFBundleShortVersionString ` ); const { handleBarInstall } = await loadInstallSubcommand(); // downloadAndExtract must recreate the bundle (with whitespace-only plist) after the // stale-reinstall guard removes the pre-seeded app. await handleBarInstall([], { fetchReleaseAsset: async () => ({ downloadUrl: 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip', }), downloadAndExtract: async (_url: string, dest: string) => { // Recreate bundle with the whitespace-only plist fixture. const contentsOut = path.join(dest, 'CCS Bar.app', 'Contents'); fs.mkdirSync(contentsOut, { recursive: true }); fs.writeFileSync( path.join(contentsOut, 'Info.plist'), ` CFBundleShortVersionString ` ); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' }), // readAppBundleVersion intentionally omitted → uses production default isBarRunning: async () => false, promptLaunch: async () => false, getCcsDir: () => ccsDir, getAppsDir: () => appsDir, }); // Whitespace-only version treated as null → no pin written const versionFile = path.join(ccsDir, 'bar', '.version'); expect(fs.existsSync(versionFile)).toBe(false); // ASCII notice must appear const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[!\].*Info\.plist/i); }); }); // --------------------------------------------------------------------------- // Data Loss finding — stage-then-swap: old bundle preserved on failure // --------------------------------------------------------------------------- describe('bar install: stage-then-swap safety (Data Loss finding)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function baseDeps( appsDir: string, extra?: Partial> ): Record { return { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }), readAppBundleVersion: () => '2.0.0', isBarRunning: async () => false, launchBar: async () => { /* noop */ }, promptLaunch: async () => false, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, ...extra, }; } it('downloadAndExtract receives a staging dir inside appsDir, NOT appsDir itself', async () => { // The new contract: downloadAndExtract dest is a hidden staging dir under appsDir, // not appsDir directly. This keeps the old install untouched during download. const appsDir = path.join(tempHome, 'Applications'); const destDirs: string[] = []; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), downloadAndExtract: async (_url: string, dest: string) => { destDirs.push(dest); // Create the expected bundle inside staging so the verify step passes. fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, }); expect(destDirs).toHaveLength(1); const dest = destDirs[0]; // Must NOT equal appsDir expect(dest).not.toBe(appsDir); // Must be a child of appsDir (same filesystem for atomic rename) expect(dest.startsWith(appsDir + path.sep)).toBe(true); // Dotted prefix to minimise Finder noise expect(path.basename(dest)).toMatch(/^\..+/); }); it('downloadAndExtract throws during reinstall: old bundle still present, removeExistingApp NOT called', async () => { // Core data-loss guard: if download/extraction fails, the old working install // must remain intact — removeExistingApp must never have been called. const appsDir = path.join(tempHome, 'Applications'); const appPath = path.join(appsDir, 'CCS Bar.app'); // Pre-seed the existing install. fs.mkdirSync(appPath, { recursive: true }); fs.writeFileSync(path.join(appPath, 'sentinel'), 'old-install'); let removeCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), removeExistingApp: (_path: unknown) => { removeCalled = true; fs.rmSync(String(_path), { recursive: true, force: true }); }, downloadAndExtract: async (_url: string, _dest: string) => { throw new Error('network timeout'); }, }); // removeExistingApp must NOT have been called — download failed before the swap. expect(removeCalled).toBe(false); // Old bundle must still be present. expect(fs.existsSync(appPath)).toBe(true); expect(fs.existsSync(path.join(appPath, 'sentinel'))).toBe(true); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[X\].*Download or extraction failed/i); }); it('extracted archive missing the bundle: old bundle still present, install aborts with diagnostics', async () => { // If the archive extracts successfully but does not contain CCS Bar.app, // the old install must be left untouched and [X] with file listing printed. const appsDir = path.join(tempHome, 'Applications'); const appPath = path.join(appsDir, 'CCS Bar.app'); fs.mkdirSync(appPath, { recursive: true }); fs.writeFileSync(path.join(appPath, 'sentinel'), 'old-install'); let removeCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), removeExistingApp: (_path: unknown) => { removeCalled = true; fs.rmSync(String(_path), { recursive: true, force: true }); }, downloadAndExtract: async (_url: string, dest: string) => { // Extraction produces a wrong-named artifact — CCS Bar.app absent. fs.mkdirSync(dest, { recursive: true }); fs.writeFileSync(path.join(dest, 'WrongName.app'), 'dummy'); }, }); // removeExistingApp must NOT have been called. expect(removeCalled).toBe(false); // Old bundle still present. expect(fs.existsSync(appPath)).toBe(true); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[X\]/); expect(allOutput).toMatch(/CCS Bar\.app/); // Diagnostics: should list staging contents expect(allOutput).toMatch(/WrongName\.app/); }); it('success path: staging dir removed, app present at appPath', async () => { const appsDir = path.join(tempHome, 'Applications'); const appPath = path.join(appsDir, 'CCS Bar.app'); let stagingDirUsed = ''; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), downloadAndExtract: async (_url: string, dest: string) => { stagingDirUsed = dest; fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, }); // App at the final location. expect(fs.existsSync(appPath)).toBe(true); // Staging dir cleaned up. if (stagingDirUsed) { expect(fs.existsSync(stagingDirUsed)).toBe(false); } const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[OK\].*CCS Bar/); expect(allOutput).not.toMatch(/\[X\]/); }); it('aborts install and prints [X] when removeExistingApp throws (old app present = no swap)', async () => { // removeExistingApp now runs AFTER staging verification. // If it throws, the new bundle is still in staging (not yet moved); old app untouched. const appsDir = path.join(tempHome, 'Applications'); const appPath = path.join(appsDir, 'CCS Bar.app'); fs.mkdirSync(appPath, { recursive: true }); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), removeExistingApp: (_path: unknown) => { throw new Error('Permission denied'); }, downloadAndExtract: async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[X\].*Could not remove.*CCS Bar\.app/); }); it('fresh install (no pre-existing bundle): removeExistingApp not invoked, install proceeds', async () => { const appsDir = path.join(tempHome, 'Applications'); // Do NOT pre-create the app bundle. let removeCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), removeExistingApp: (_path: unknown) => { removeCalled = true; }, downloadAndExtract: async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, }); expect(removeCalled).toBe(false); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[OK\].*CCS Bar/); }); }); // --------------------------------------------------------------------------- // Review finding — silent decline: hint always printed when user says no // --------------------------------------------------------------------------- describe('bar install: silent-decline fix — hint on user decline (review finding)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function baseDeps( appsDir: string, extra?: Partial> ): Record { return { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }), readAppBundleVersion: () => '1.0.0', isBarRunning: async () => false, launchBar: async () => { /* noop */ }, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, ...extra, }; } it('prints hint when user declines launch prompt (TTY path, answers no)', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), // Simulate TTY user answering "n" promptLaunch: async () => false, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[i\].*Run `ccs bar` to launch/i); }); it('prints hint when non-TTY stdin (promptLaunch returns false on non-TTY)', async () => { const appsDir = path.join(tempHome, 'Applications'); const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), promptLaunch: async () => false, }); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/\[i\].*Run `ccs bar` to launch/i); }); it('does NOT print the decline hint when user says yes (launch invoked instead)', async () => { const appsDir = path.join(tempHome, 'Applications'); let launchCalled = false; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall([], { ...baseDeps(appsDir), launchBar: async () => { launchCalled = true; }, promptLaunch: async () => true, }); expect(launchCalled).toBe(true); const allOutput = consoleOutput.join('\n'); expect(allOutput).not.toMatch(/Run `ccs bar` to launch later/i); }); }); // --------------------------------------------------------------------------- // Socket-level 401/403 auth-required detection (#1551 coverage gap) // --------------------------------------------------------------------------- describe('defaultFindRunningServer: socket-level 401/403 classifies authRequired=true', () => { // These tests drive `net.connect` directly via mock.module('net') — the same // pattern used by the streaming-probe test below. They prove the status-line // parser correctly sets authRequired without relying on the higher-level dep // injection that existing tests use (which bypasses real status-line parsing). function makeNetMock(statusLine: string, token: string) { return { connect: (opts: { host: string; port: number }, onConnect: () => void): unknown => { const listeners: Record void>> = {}; const socket = { on(event: string, cb: (arg?: unknown) => void) { (listeners[event] ??= []).push(cb); return socket; }, setTimeout(_ms: number, cb: () => void) { // Do not fire the timeout — let the data path run. void cb; return socket; }, write(request: string) { const nonce = request.match(new RegExp(`${BAR_AUTH_NONCE_HEADER}:\\s*([^\\r\\n]+)`, 'i'))?.[1] ?? ''; const proof = createBarAuthProof(token, nonce); setImmediate(() => { for (const cb of listeners.data ?? []) { cb( Buffer.from(`${statusLine}\r\n${BAR_AUTH_TOKEN_HEADER}: ${proof}\r\n\r\n`, 'utf8') ); } }); return true; }, destroy() { return socket; }, }; setImmediate(onConnect); return socket; }, }; } it('classifies HTTP/1.1 401 off the wire as authRequired=true', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); // Seed bar.json so the probed port is deterministic and checked first. fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ port: 41401, baseUrl: 'http://127.0.0.1:41401', authMode: 'loopback' }) ); const token = getOrCreateBarAuthToken(ccsDir); mock.module('net', () => makeNetMock('HTTP/1.1 401 Unauthorized', token)); moduleSeq++; const { defaultFindRunningServer } = (await import( `../../../src/commands/bar/bar-server-probe?test=${Date.now()}-${moduleSeq}` )) as { defaultFindRunningServer: ( ccsDir: string ) => Promise<{ port: number; baseUrl: string; authRequired?: boolean } | null>; }; const result = await Promise.race([ defaultFindRunningServer(ccsDir), new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 500)), ]); // Must not time out and must report auth-required. expect(result).not.toBe('timeout'); expect(result).not.toBeNull(); expect((result as { authRequired?: boolean }).authRequired).toBe(true); }); it('classifies HTTP/1.1 403 off the wire as authRequired=true', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ port: 41403, baseUrl: 'http://127.0.0.1:41403', authMode: 'loopback' }) ); const token = getOrCreateBarAuthToken(ccsDir); mock.module('net', () => makeNetMock('HTTP/1.1 403 Forbidden', token)); moduleSeq++; const { defaultFindRunningServer } = (await import( `../../../src/commands/bar/bar-server-probe?test=${Date.now()}-${moduleSeq}` )) as { defaultFindRunningServer: ( ccsDir: string ) => Promise<{ port: number; baseUrl: string; authRequired?: boolean } | null>; }; const result = await Promise.race([ defaultFindRunningServer(ccsDir), new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 500)), ]); expect(result).not.toBe('timeout'); expect(result).not.toBeNull(); expect((result as { authRequired?: boolean }).authRequired).toBe(true); }); it('does NOT set authRequired for a 200 that supplies the correct token', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); // Ensure the token file exists so getOrCreateBarAuthToken reads the same value. const { getOrCreateBarAuthToken: getToken } = await import( `../../../src/utils/bar-auth-token?test=${Date.now()}-token-ok` ); const token = getToken(ccsDir); fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ port: 41200, baseUrl: 'http://127.0.0.1:41200', authMode: 'loopback' }) ); mock.module('net', () => ({ connect: (opts: { host: string; port: number }, onConnect: () => void): unknown => { const listeners: Record void>> = {}; const socket = { on(event: string, cb: (arg?: unknown) => void) { (listeners[event] ??= []).push(cb); return socket; }, setTimeout(_ms: number, _cb: () => void) { return socket; }, write(data: string) { const nonce = data.match(new RegExp(`${BAR_AUTH_NONCE_HEADER}:\\s*([^\\r\\n]+)`, 'i'))?.[1] ?? ''; for (const cb of listeners.data ?? []) { cb( Buffer.from( `HTTP/1.1 200 OK\r\n${BAR_AUTH_TOKEN_HEADER}: ${createBarAuthProof(token, nonce)}\r\n\r\n`, 'utf8' ) ); } return true; }, destroy() { return socket; }, }; setImmediate(() => { onConnect(); }); return socket; }, })); moduleSeq++; const { defaultFindRunningServer } = (await import( `../../../src/commands/bar/bar-server-probe?test=${Date.now()}-${moduleSeq}` )) as { defaultFindRunningServer: ( ccsDir: string ) => Promise<{ port: number; baseUrl: string; authRequired?: boolean } | null>; }; const result = await Promise.race([ defaultFindRunningServer(ccsDir), new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 500)), ]); expect(result).not.toBe('timeout'); expect(result).not.toBeNull(); // Correct-token 200 must be accepted and NOT flagged as auth-required. expect((result as { authRequired?: boolean }).authRequired).toBeFalsy(); }); }); // --------------------------------------------------------------------------- // defaultWaitForServerLive: rogue-server rejection (#1546 coverage gap) // --------------------------------------------------------------------------- describe('defaultWaitForServerLive: rogue 200 without matching token is rejected', () => { // defaultWaitForServerLive (exported from launch-subcommand.ts) applies the // same fail-closed echoedToken===token check as the discovery probe. // A 200 response that does NOT echo the matching capability token must never // cause the function to resolve — the liveness poll must keep retrying and // eventually time out. // // We mock `net` (same pattern as the streaming-probe test) so the test is // fully synchronous and immune to OS socket state. The invariant is // behaviour-coupled: removing the token check causes both tests to fail. function buildNetMock(responseHeaders: string | ((request: string) => string)) { // Returns a `net` mock whose connect() immediately delivers the response, // then emits 'end'. return { connect: (_opts: { host: string; port: number }, onConnect: () => void): unknown => { const listeners: Record void>> = {}; const socket = { on(event: string, cb: (arg?: unknown) => void) { (listeners[event] ??= []).push(cb); return socket; }, setTimeout(_ms: number, _cb: () => void) { return socket; }, write(data: string) { const response = typeof responseHeaders === 'function' ? responseHeaders(data) : responseHeaders; for (const cb of listeners.data ?? []) { cb(Buffer.from(response, 'utf8')); } return true; }, destroy() { return socket; }, }; setImmediate(() => { onConnect(); for (const cb of listeners.end ?? []) { cb(); } }); return socket; }, }; } it('rogue 200 (wrong token) never resolves; poll throws timeout and launch reports [X]', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); // Establish the real token on disk. const { getOrCreateBarAuthToken: getToken } = await import( `../../../src/utils/bar-auth-token?test=${Date.now()}-rogue-net` ); const realToken = getToken(ccsDir); // A valid-format but wrong 64-hex token. const rogueToken = 'cafebabe'.repeat(8); expect(rogueToken).not.toBe(realToken); // guard: tokens must differ // Mock net: every probe gets 200 with the WRONG token. mock.module('net', () => buildNetMock(`HTTP/1.1 200 OK\r\nx-ccs-bar-token: ${rogueToken}\r\n\r\n`) ); // Import defaultWaitForServerLive fresh with the mocked net. // The export was added so this direct unit test is possible. moduleSeq++; const { defaultWaitForServerLive } = (await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` )) as { defaultWaitForServerLive: (baseUrl: string) => Promise; }; // defaultWaitForServerLive has a hardcoded 10 s timeout — too long for a test. // We race it against a 600 ms sentinel that expires before the real timeout. // If the function resolves before 600 ms, the rogue check passed (test failure). // If it neither resolves nor rejects by 600 ms, the loop is correctly stuck // (rogue token never matches → function will eventually throw after 10 s). const RACE_MS = 600; const result = await Promise.race([ defaultWaitForServerLive(`http://127.0.0.1:9998`) .then(() => 'resolved' as const) .catch(() => 'rejected' as const), new Promise<'timeout'>((r) => setTimeout(() => r('timeout'), RACE_MS)), ]); // The rogue server must NEVER cause the poll to resolve. expect(result).not.toBe('resolved'); // It either timed out (still looping) or threw early — both correct outcomes. expect(result === 'timeout' || result === 'rejected').toBe(true); }); it('unrelated 401/403 without a CCS proof does not trigger auth-required identity', async () => { for (const status of [401, 403]) { mock.module('net', () => buildNetMock(`HTTP/1.1 ${status} Unrelated Service\r\nConnection: close\r\n\r\n`) ); moduleSeq++; const { defaultWaitForServerLive } = (await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` )) as { defaultWaitForServerLive: (baseUrl: string) => Promise; }; const result = await Promise.race([ defaultWaitForServerLive(`http://127.0.0.1:${9900 + status}`).then( () => 'resolved' as const, () => 'rejected' as const ), new Promise<'still-probing'>((resolve) => setTimeout(() => resolve('still-probing'), 300)), ]); expect(result).toBe('still-probing'); mock.restore(); } }); it('correct-token 200 resolves defaultWaitForServerLive immediately', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { getOrCreateBarAuthToken: getToken } = await import( `../../../src/utils/bar-auth-token?test=${Date.now()}-legit-net` ); const realToken = getToken(); // Mock net: every probe gets 200 with the CORRECT nonce-bound proof. mock.module('net', () => buildNetMock((request) => { const nonce = request.match(new RegExp(`${BAR_AUTH_NONCE_HEADER}:\\s*([^\\r\\n]+)`, 'i'))?.[1] ?? ''; return `HTTP/1.1 200 OK\r\n${BAR_AUTH_TOKEN_HEADER}: ${createBarAuthProof(realToken, nonce)}\r\n\r\n`; }) ); moduleSeq++; const { defaultWaitForServerLive } = (await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` )) as { defaultWaitForServerLive: (baseUrl: string) => Promise; }; // Correct token → must resolve well within the 10 s timeout. const RACE_MS = 500; const result = await Promise.race([ defaultWaitForServerLive(`http://127.0.0.1:9997`) .then(() => 'resolved' as const) .catch(() => 'rejected' as const), new Promise<'timeout'>((r) => setTimeout(() => r('timeout'), RACE_MS)), ]); expect(result).toBe('resolved'); }); }); describe('defaultFindRunningServer: streaming lower-priority probes', () => { it('returns a higher-priority hit without waiting for lower-priority response bodies', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ port: 41235, baseUrl: 'http://127.0.0.1:41235', authMode: 'loopback' }) ); let highPrioritySocketDestroyed = false; let lowerPriorityProbeStarted = false; // The probe speaks raw HTTP/1.1 over a `net` socket and resolves on the // status line, so mock `net.connect` rather than a higher-level client. // The high-priority port (41235) answers HTTP 200 with the auth token in the // response headers (mirroring the real server, which reads from the 0600 file // and includes it unconditionally — NOT echoed from the request); the // lower-priority port (3000) connects but never sends a status line, // emulating a non-CCS service that streams forever. // // The token file is written by getOrCreateBarAuthToken when ccsDir is set up // in beforeEach, so we read it here the same way the mock server would. const { getOrCreateBarAuthToken: getToken } = await import( `../../../src/utils/bar-auth-token?test=${Date.now()}-mock` ); const expectedToken = getToken(ccsDir); mock.module('net', () => ({ connect: (opts: { host: string; port: number }, onConnect: () => void): unknown => { // net.connect is called synchronously for every probe target, so the // lower-priority probe is observably "started" the moment discovery // fires it — even though it never receives a status line. if (opts.port === 3000) lowerPriorityProbeStarted = true; const listeners: Record void>> = {}; const socket = { on(event: string, cb: (arg?: unknown) => void) { (listeners[event] ??= []).push(cb); return socket; }, setTimeout() { return socket; }, write(data: string) { if (opts.port === 41235) { const nonce = data.match(new RegExp(`${BAR_AUTH_NONCE_HEADER}:\\s*([^\\r\\n]+)`, 'i'))?.[1] ?? ''; for (const cb of listeners.data ?? []) { cb( Buffer.from( `HTTP/1.1 200 OK\r\n${BAR_AUTH_TOKEN_HEADER}: ${createBarAuthProof(expectedToken, nonce)}\r\n\r\n`, 'utf8' ) ); } } return true; }, destroy() { if (opts.port === 41235) highPrioritySocketDestroyed = true; return socket; }, }; // Fire the connect callback asynchronously, mirroring net.connect. setImmediate(() => { onConnect(); // Port 3000 never emits a status line: simulate an endlessly // streaming service that must not block the higher-priority hit. // Any other port stays silent and is settled by the 1.5s timeout, // which the Promise.race below short-circuits. }); return socket; }, })); moduleSeq++; const { defaultFindRunningServer } = (await import( `../../../src/commands/bar/bar-server-probe?test=${Date.now()}-${moduleSeq}` )) as { defaultFindRunningServer: ( ccsDir: string ) => Promise<{ port: number; baseUrl: string; authRequired?: boolean } | null>; }; const result = await Promise.race([ defaultFindRunningServer(ccsDir), new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 250)), ]); expect(result).toEqual({ port: 41235, baseUrl: 'http://127.0.0.1:41235', authRequired: false, }); expect(highPrioritySocketDestroyed).toBe(true); expect(lowerPriorityProbeStarted).toBe(true); }); }); describe('bar raw socket probes: absolute deadline for malformed streaming peers', () => { it('continues past a higher-priority trickling non-HTTP response and returns a lower-priority hit', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ port: 41236, baseUrl: 'http://127.0.0.1:41236', authMode: 'loopback' }) ); const { getOrCreateBarAuthToken: getToken } = await import( `../../../src/utils/bar-auth-token?test=${Date.now()}-deadline-find` ); const expectedToken = getToken(ccsDir); mock.module('net', () => ({ connect: (opts: { host: string; port: number }, onConnect: () => void): unknown => { const listeners: Record void>> = {}; let interval: ReturnType | undefined; let request = ''; const socket = { on(event: string, cb: (arg?: unknown) => void) { (listeners[event] ??= []).push(cb); return socket; }, setTimeout() { return socket; }, write(data: string) { request = data; return true; }, destroy() { if (interval) clearInterval(interval); return socket; }, }; setImmediate(() => { onConnect(); if (opts.port === 41236) { interval = setInterval(() => { for (const cb of listeners.data ?? []) cb(Buffer.from('x', 'utf8')); }, 25); interval.unref?.(); return; } if (opts.port === 3000) { const nonce = request.match(new RegExp(`${BAR_AUTH_NONCE_HEADER}:\\s*([^\\r\\n]+)`, 'i'))?.[1] ?? ''; for (const cb of listeners.data ?? []) { cb( Buffer.from( `HTTP/1.1 200 OK\r\n${BAR_AUTH_TOKEN_HEADER}: ${createBarAuthProof(expectedToken, nonce)}\r\n\r\n`, 'utf8' ) ); } } }); return socket; }, })); moduleSeq++; const { defaultFindRunningServer } = (await import( `../../../src/commands/bar/bar-server-probe?test=${Date.now()}-${moduleSeq}` )) as { defaultFindRunningServer: ( ccsDir: string ) => Promise<{ port: number; baseUrl: string; authRequired?: boolean } | null>; }; const result = await Promise.race([ defaultFindRunningServer(ccsDir), new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 2500)), ]); expect(result).toEqual({ port: 3000, baseUrl: 'http://127.0.0.1:3000', authRequired: false, }); }); it('does not let a single launch wait probe outlive the outer deadline', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const realDateNow = Date.now; let dateCall = 0; Date.now = () => { dateCall++; return dateCall < 4 ? 0 : 10_001; }; mock.module('net', () => ({ connect: (_opts: { host: string; port: number }, onConnect: () => void): unknown => { const listeners: Record void>> = {}; let interval: ReturnType | undefined; const socket = { on(event: string, cb: (arg?: unknown) => void) { (listeners[event] ??= []).push(cb); return socket; }, setTimeout() { return socket; }, write() { return true; }, destroy() { if (interval) clearInterval(interval); return socket; }, }; setImmediate(() => { onConnect(); interval = setInterval(() => { for (const cb of listeners.data ?? []) cb(Buffer.from('x', 'utf8')); }, 25); interval.unref?.(); }); return socket; }, })); try { moduleSeq++; const { defaultWaitForServerLive } = (await import( `../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}` )) as { defaultWaitForServerLive: (baseUrl: string) => Promise; }; const result = await Promise.race([ defaultWaitForServerLive('http://127.0.0.1:9996') .then(() => 'resolved' as const) .catch(() => 'rejected' as const), new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), 2500)), ]); expect(result).toBe('rejected'); } finally { Date.now = realDateNow; } }); }); // --------------------------------------------------------------------------- // GH-1588 — `--await-quit` waits for a running app to exit, then swaps + relaunches // --------------------------------------------------------------------------- describe('bar install: --await-quit waits for the running app to quit (GH-1588)', () => { const FAKE_DOWNLOAD_URL = 'https://github.com/kaitranntt/ccs/releases/download/ccs-bar-latest/CCS-Bar.app.zip'; function baseDeps(appsDir: string, isBarRunning: () => Promise) { return { fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }), downloadAndExtract: async (_url: string, dest: string) => { fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true }); }, verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }), readAppBundleVersion: () => '1.0.0', isBarRunning, getCcsDir: () => path.join(tempHome, '.ccs'), getAppsDir: () => appsDir, }; } it('proceeds with swap + relaunch once the running app exits', async () => { const appsDir = path.join(tempHome, 'Applications'); let launchCalled = false; // Running on the first probe (still up), gone on every probe after — this // models the in-app updater quitting itself right after spawning // `ccs bar install --launch --await-quit`. let probes = 0; const isBarRunning = async () => { probes += 1; return probes === 1; }; const { handleBarInstall } = await loadInstallSubcommand(); await handleBarInstall(['--launch', '--await-quit'], { ...baseDeps(appsDir, isBarRunning), launchBar: async () => { launchCalled = true; }, promptLaunch: async () => false, }); // The wait fired (app seen running first), then the install completed and // relaunched into the new build. expect(consoleOutput.join('\n')).toMatch(/Waiting for CCS Bar to quit/i); expect(launchCalled).toBe(true); expect(probes).toBeGreaterThanOrEqual(2); }); }); // --------------------------------------------------------------------------- // --port support: `ccs bar [launch] --port N` (user-selectable dashboard port) // --------------------------------------------------------------------------- describe('bar dispatcher: bare flags route to launch', () => { beforeEach(() => { mock.module('../../../src/commands/bar/launch-subcommand', () => ({ handleBarLaunch: async (args: string[]) => { calls.push(`launch:${args.join(' ')}`); }, })); }); it('dispatches `ccs bar --port 3999` to launch with the flag preserved', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['--port', '3999']); expect(calls).toEqual(['launch:--port 3999']); }); it('dispatches `ccs bar launch --port 3999` to launch with the flag preserved', async () => { const handleBarCommand = await loadHandleBarCommand(); await handleBarCommand(['launch', '--port', '3999']); expect(calls).toEqual(['launch:--port 3999']); }); }); describe('launch: --port selects the server port', () => { function makePortDeps(ccsDir: string) { const seen: { spawnPort: number | null; getPortCandidates: number[] | null; stopped: boolean; descriptorPort: number | null; } = { spawnPort: null, getPortCandidates: null, stopped: false, descriptorPort: null }; const deps = { findRunningServer: async () => null, getPort: async (opts: { port: number[]; host: string }) => { seen.getPortCandidates = opts.port; return opts.port[0]; }, spawnDetachedServer: (p: number) => { seen.spawnPort = p; }, waitForServerLive: async () => {}, createLaunchDescriptor: (opts?: { port?: number }) => { seen.descriptorPort = opts?.port ?? null; return { schema: 1 as const, runtime: '/usr/bin/node', args: ['/x/ccs.js', 'bar', 'serve'], home: '/h', }; }, writeLaunchDescriptor: () => {}, stopDetachedServer: () => { seen.stopped = true; }, openApp: async () => {}, getCcsDir: () => ccsDir, appInstallPath: path.join(tempHome, 'Applications', 'CCS Bar.app'), }; return { deps, seen }; } it('spawns the detached server on the requested port and records it in bar.json', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps, seen } = makePortDeps(ccsDir); await handleBarLaunch(['--port', '3999'], deps); expect(seen.spawnPort).toBe(3999); const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as { port: number; baseUrl: string; }; expect(barJson.port).toBe(3999); expect(barJson.baseUrl).toBe('http://127.0.0.1:3999'); }); it('persists the chosen port into the launch descriptor for app self-start', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps, seen } = makePortDeps(ccsDir); await handleBarLaunch(['--port', '3999'], deps); expect(seen.descriptorPort).toBe(3999); }); it('errors without spawning when the requested port is busy', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps, seen } = makePortDeps(ccsDir); deps.getPort = async () => 4001; // get-port fell back: 3999 not free await handleBarLaunch(['--port', '3999'], deps); expect(seen.spawnPort).toBeNull(); const allOutput = consoleOutput.join('\n'); expect(allOutput).toMatch(/3999/); expect(allOutput.toLowerCase()).toMatch(/in use|busy|not free|unavailable/); }); it('errors on an invalid --port value', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps, seen } = makePortDeps(ccsDir); await handleBarLaunch(['--port', 'banana'], deps); expect(seen.spawnPort).toBeNull(); expect(consoleOutput.join('\n').toLowerCase()).toMatch(/invalid.*port|port.*invalid/); }); it('reuses a running server already on the requested port', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps, seen } = makePortDeps(ccsDir); deps.findRunningServer = async () => ({ port: 3999, baseUrl: 'http://127.0.0.1:3999' }); await handleBarLaunch(['--port', '3999'], deps); expect(seen.spawnPort).toBeNull(); // reuse, no new spawn expect(seen.stopped).toBe(false); const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as { port: number; }; expect(barJson.port).toBe(3999); }); it('stops a running server on a different port, then starts on the requested one', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps, seen } = makePortDeps(ccsDir); deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' }); await handleBarLaunch(['--port', '3999'], deps); expect(seen.stopped).toBe(true); expect(seen.spawnPort).toBe(3999); const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as { port: number; }; expect(barJson.port).toBe(3999); }); it('preflights the destination before stopping a healthy running server', async () => { const ccsDir = path.join(tempHome, '.ccs'); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps } = makePortDeps(ccsDir); const events: string[] = []; deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' }); deps.getPort = async () => { events.push('preflight'); return 3999; }; deps.stopDetachedServer = () => { events.push('stop'); }; await handleBarLaunch(['--port', '3999'], deps); expect(events).toEqual(['preflight', 'stop']); }); it('leaves the healthy server running when destination preflight is busy', async () => { const ccsDir = path.join(tempHome, '.ccs'); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps, seen } = makePortDeps(ccsDir); deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' }); deps.getPort = async () => 4000; await handleBarLaunch(['--port', '3999'], deps); expect(seen.stopped).toBe(false); expect(seen.spawnPort).toBeNull(); }); it('aborts the move when the verified stop fails', async () => { const ccsDir = path.join(tempHome, '.ccs'); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps, seen } = makePortDeps(ccsDir); deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' }); deps.stopDetachedServer = () => { throw new Error('identity mismatch'); }; await handleBarLaunch(['--port', '3999'], deps); expect(seen.spawnPort).toBeNull(); }); it('restores the prior server when a check-to-bind race breaks the replacement', async () => { const ccsDir = path.join(tempHome, '.ccs'); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps } = makePortDeps(ccsDir); const spawnedPorts: number[] = []; deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' }); deps.spawnDetachedServer = (port: number) => { spawnedPorts.push(port); }; deps.waitForServerLive = async (baseUrl: string) => { if (baseUrl.endsWith(':3999')) throw new Error('EADDRINUSE after preflight'); }; await handleBarLaunch(['--port', '3999'], deps); expect(spawnedPorts).toEqual([3999, 3000]); }); it('restores the prior server when replacement spawn fails', async () => { const ccsDir = path.join(tempHome, '.ccs'); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps } = makePortDeps(ccsDir); const spawnedPorts: number[] = []; deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' }); deps.spawnDetachedServer = (port: number) => { spawnedPorts.push(port); if (port === 3999) throw new Error('spawn failed'); }; await handleBarLaunch(['--port', '3999'], deps); expect(spawnedPorts).toEqual([3999, 3000]); }); it('preserves prior discovery state when replacement and rollback both fail', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(path.join(ccsDir, 'bar'), { recursive: true }); const oldBarJson = JSON.stringify({ baseUrl: 'http://127.0.0.1:3000', port: 3000, authMode: 'loopback', }); const oldLaunchJson = JSON.stringify({ args: ['ccs.js', 'bar', 'serve', '--port', '3000'] }); fs.writeFileSync(path.join(ccsDir, 'bar.json'), oldBarJson); fs.writeFileSync(path.join(ccsDir, 'bar', 'launch.json'), oldLaunchJson); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps } = makePortDeps(ccsDir); deps.findRunningServer = async () => ({ port: 3000, baseUrl: 'http://127.0.0.1:3000' }); deps.spawnDetachedServer = () => { throw new Error('spawn failed'); }; await handleBarLaunch(['--port', '3999'], deps); expect(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')).toBe(oldBarJson); expect(fs.readFileSync(path.join(ccsDir, 'bar', 'launch.json'), 'utf8')).toBe(oldLaunchJson); }); it('without --port, prefers the port recorded in bar.json (sticky port)', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(ccsDir, { recursive: true }); fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ baseUrl: 'http://127.0.0.1:3777', port: 3777, authMode: 'loopback' }) ); const { handleBarLaunch } = await loadLaunchSubcommand(); const { deps, seen } = makePortDeps(ccsDir); await handleBarLaunch([], deps); expect(seen.getPortCandidates?.[0]).toBe(3777); expect(seen.spawnPort).toBe(3777); }); }); describe('resolveBarPort: launch.json fallback survives `ccs bar stop`', () => { // `ccs bar stop` deletes bar.json, so bar.json alone cannot carry the sticky // port across a stop/start cycle. launch.json (refreshed by launch, NOT // deleted by stop) records the port in its args and acts as the fallback. it('falls back to the --port recorded in launch.json when bar.json is absent', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(path.join(ccsDir, 'bar'), { recursive: true }); fs.writeFileSync( path.join(ccsDir, 'bar', 'launch.json'), JSON.stringify({ schema: 1, runtime: '/usr/bin/node', args: ['/x/ccs.js', 'bar', 'serve', '--port', '3456'], home: tempHome, }) ); const { resolveBarPort } = await loadLaunchSubcommand(); expect(resolveBarPort(ccsDir)).toBe(3456); }); it('bar.json port wins over launch.json when both exist', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(path.join(ccsDir, 'bar'), { recursive: true }); fs.writeFileSync( path.join(ccsDir, 'bar.json'), JSON.stringify({ baseUrl: 'http://127.0.0.1:4000', port: 4000, authMode: 'loopback' }) ); fs.writeFileSync( path.join(ccsDir, 'bar', 'launch.json'), JSON.stringify({ schema: 1, runtime: '/usr/bin/node', args: ['/x/ccs.js', 'bar', 'serve', '--port', '3456'], home: tempHome, }) ); const { resolveBarPort } = await loadLaunchSubcommand(); expect(resolveBarPort(ccsDir)).toBe(4000); }); it('returns null when launch.json has no --port and bar.json is absent', async () => { const ccsDir = path.join(tempHome, '.ccs'); fs.mkdirSync(path.join(ccsDir, 'bar'), { recursive: true }); fs.writeFileSync( path.join(ccsDir, 'bar', 'launch.json'), JSON.stringify({ schema: 1, runtime: '/usr/bin/node', args: ['/x/ccs.js', 'bar', 'serve'], home: tempHome, }) ); const { resolveBarPort } = await loadLaunchSubcommand(); expect(resolveBarPort(ccsDir)).toBeNull(); }); });