mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
fix(bar): verify CCS Bar release archive digest (#1532)
Verifies the GitHub release asset sha256 digest before extracting the CCS Bar archive.
This commit is contained in:
1 parent
db44845857
commit
d2848d3931
2 files changed
+148
-62
No files matched your search
@@ -36,11 +36,8 @@ const BAR_GITHUB_REPO = 'kaitranntt/ccs';
|
|||||||
/**
|
/**
|
||||||
* Allowlist of hostnames from which we will accept asset downloads.
|
* Allowlist of hostnames from which we will accept asset downloads.
|
||||||
* GitHub releases redirect from github.com to objects.githubusercontent.com.
|
* GitHub releases redirect from github.com to objects.githubusercontent.com.
|
||||||
*
|
* Artifact authenticity is enforced separately with the GitHub release asset
|
||||||
* TODO(checksum-v2): once release assets ship a checksums.txt/.sha256 file,
|
* SHA-256 digest before extraction.
|
||||||
* wire SHA-256 verification here. The download URL is already validated for
|
|
||||||
* host+HTTPS as a v1 minimum guard. The verifier hook below is the intended
|
|
||||||
* extension point.
|
|
||||||
*/
|
*/
|
||||||
const DOWNLOAD_HOST_ALLOWLIST: ReadonlyArray<string> = [
|
const DOWNLOAD_HOST_ALLOWLIST: ReadonlyArray<string> = [
|
||||||
'github.com',
|
'github.com',
|
||||||
@@ -53,6 +50,7 @@ const DOWNLOAD_HOST_ALLOWLIST: ReadonlyArray<string> = [
|
|||||||
|
|
||||||
export interface ReleaseAssetResult {
|
export interface ReleaseAssetResult {
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
|
sha256: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompatResult {
|
export interface CompatResult {
|
||||||
@@ -68,10 +66,10 @@ export interface InstallDeps {
|
|||||||
*/
|
*/
|
||||||
fetchReleaseAsset: (tag: string, asset: string) => Promise<ReleaseAssetResult>;
|
fetchReleaseAsset: (tag: string, asset: string) => Promise<ReleaseAssetResult>;
|
||||||
/**
|
/**
|
||||||
* Download the zip archive and extract the .app bundle into dest/.
|
* Download the zip archive, verify its SHA-256 digest, and extract the .app bundle into dest/.
|
||||||
* Production: uses undici to stream + extract (with redirect + status check).
|
* Production: uses undici to stream + verify + extract (with redirect + status check).
|
||||||
*/
|
*/
|
||||||
downloadAndExtract: (url: string, dest: string) => Promise<void>;
|
downloadAndExtract: (url: string, dest: string, expectedSha256: string) => Promise<void>;
|
||||||
/**
|
/**
|
||||||
* GET {baseUrl}/api/bar/summary — capability handshake.
|
* GET {baseUrl}/api/bar/summary — capability handshake.
|
||||||
* 200 → compatible; 404 → no-bar-api; else/unreachable → unreachable.
|
* 200 → compatible; 404 → no-bar-api; else/unreachable → unreachable.
|
||||||
@@ -190,7 +188,16 @@ async function defaultFetchReleaseAsset(tag: string, asset: string): Promise<Rel
|
|||||||
throw new Error(`Asset "${asset}" not found in release ${tag}`);
|
throw new Error(`Asset "${asset}" not found in release ${tag}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { downloadUrl: found.browser_download_url as string };
|
const digest = typeof found.digest === 'string' ? found.digest : '';
|
||||||
|
const match = /^sha256:([a-fA-F0-9]{64})$/.exec(digest.trim());
|
||||||
|
if (!match || !match[1]) {
|
||||||
|
throw new Error(
|
||||||
|
`Asset "${asset}" in release ${tag} does not include a valid sha256 digest. ` +
|
||||||
|
'Refusing to install an unverifiable CCS Bar archive.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { downloadUrl: found.browser_download_url as string, sha256: match[1].toLowerCase() };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -203,20 +210,30 @@ async function defaultFetchReleaseAsset(tag: string, asset: string): Promise<Rel
|
|||||||
* The previous maxRedirections:5 let undici follow redirects to ANY host unchecked.
|
* The previous maxRedirections:5 let undici follow redirects to ANY host unchecked.
|
||||||
* - Fix #14: list zip entries with `unzip -l` before extracting; reject the archive
|
* - Fix #14: list zip entries with `unzip -l` before extracting; reject the archive
|
||||||
* if any entry path contains ".." or starts with "/" (zip-slip guard).
|
* if any entry path contains ".." or starts with "/" (zip-slip guard).
|
||||||
|
* - Security: verify the downloaded archive against the release asset SHA-256
|
||||||
|
* digest before any extraction or installation.
|
||||||
* - Finding #11: check `statusCode` and throw a descriptive error before streaming.
|
* - Finding #11: check `statusCode` and throw a descriptive error before streaming.
|
||||||
* - Finding #9: validate host+HTTPS before making the first request.
|
* - Finding #9: validate host+HTTPS before making the first request.
|
||||||
*/
|
*/
|
||||||
async function defaultDownloadAndExtract(url: string, dest: string): Promise<void> {
|
async function defaultDownloadAndExtract(
|
||||||
|
url: string,
|
||||||
|
dest: string,
|
||||||
|
expectedSha256: string
|
||||||
|
): Promise<void> {
|
||||||
const { request } = await import('undici');
|
const { request } = await import('undici');
|
||||||
const { createWriteStream, mkdirSync } = fs;
|
const { createHash } = await import('crypto');
|
||||||
|
const { createReadStream, createWriteStream, mkdirSync } = fs;
|
||||||
const { promisify } = await import('util');
|
const { promisify } = await import('util');
|
||||||
const { pipeline } = await import('stream');
|
const { pipeline } = await import('stream');
|
||||||
const streamPipeline = promisify(pipeline);
|
const streamPipeline = promisify(pipeline);
|
||||||
const { execFile } = await import('child_process');
|
const { execFile } = await import('child_process');
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
// Validate initial URL (Finding #9)
|
// Validate initial URL (Finding #9) and require an integrity pin.
|
||||||
validateDownloadUrl(url);
|
validateDownloadUrl(url);
|
||||||
|
if (!/^[a-fA-F0-9]{64}$/.test(expectedSha256)) {
|
||||||
|
throw new Error('Missing or invalid SHA-256 digest for CCS Bar archive. Refusing to install.');
|
||||||
|
}
|
||||||
|
|
||||||
mkdirSync(dest, { recursive: true });
|
mkdirSync(dest, { recursive: true });
|
||||||
|
|
||||||
@@ -263,10 +280,25 @@ async function defaultDownloadAndExtract(url: string, dest: string): Promise<voi
|
|||||||
|
|
||||||
const tmpZip = path.join(os.tmpdir(), `ccs-bar-${Date.now()}.zip`);
|
const tmpZip = path.join(os.tmpdir(), `ccs-bar-${Date.now()}.zip`);
|
||||||
|
|
||||||
// Stream body to tmpZip
|
// Stream body to tmpZip, then verify the archive before inspecting/extracting it.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
await streamPipeline(body as any, createWriteStream(tmpZip));
|
await streamPipeline(body as any, createWriteStream(tmpZip));
|
||||||
|
|
||||||
|
const hash = createHash('sha256');
|
||||||
|
await streamPipeline(createReadStream(tmpZip), hash);
|
||||||
|
const actualSha256 = hash.digest('hex');
|
||||||
|
if (actualSha256.toLowerCase() !== expectedSha256.toLowerCase()) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tmpZip);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
'Downloaded CCS Bar archive failed SHA-256 verification. ' +
|
||||||
|
`Expected ${expectedSha256.toLowerCase()}, got ${actualSha256.toLowerCase()}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Fix #14: zip-slip guard — inspect entries before extraction.
|
// Fix #14: zip-slip guard — inspect entries before extraction.
|
||||||
// `unzip -l` lists entries in a machine-readable format; we scan for ".." or
|
// `unzip -l` lists entries in a machine-readable format; we scan for ".." or
|
||||||
// absolute paths that would escape the destination directory.
|
// absolute paths that would escape the destination directory.
|
||||||
@@ -525,7 +557,7 @@ export async function handleBarInstall(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { downloadUrl } = releaseInfo;
|
const { downloadUrl, sha256 } = releaseInfo;
|
||||||
console.log(`[i] Installing CCS Bar from ${BAR_RELEASE_TAG}...`);
|
console.log(`[i] Installing CCS Bar from ${BAR_RELEASE_TAG}...`);
|
||||||
|
|
||||||
// 1b. Stage-then-swap: extract into a hidden staging dir on the same filesystem
|
// 1b. Stage-then-swap: extract into a hidden staging dir on the same filesystem
|
||||||
@@ -553,7 +585,7 @@ export async function handleBarInstall(
|
|||||||
|
|
||||||
// 2. Download and extract into staging, NOT appsDir.
|
// 2. Download and extract into staging, NOT appsDir.
|
||||||
try {
|
try {
|
||||||
await downloadAndExtract(downloadUrl, stagingDir);
|
await downloadAndExtract(downloadUrl, stagingDir, sha256);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
console.error(`[X] Download or extraction failed: ${msg}`);
|
console.error(`[X] Download or extraction failed: ${msg}`);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ let tempHome: string;
|
|||||||
let originalCcsHome: string | undefined;
|
let originalCcsHome: string | undefined;
|
||||||
let originalConsoleLog: typeof console.log;
|
let originalConsoleLog: typeof console.log;
|
||||||
let originalConsoleError: typeof console.error;
|
let originalConsoleError: typeof console.error;
|
||||||
|
const FAKE_SHA256 = 'a'.repeat(64);
|
||||||
|
|
||||||
function captureConsole(): void {
|
function captureConsole(): void {
|
||||||
originalConsoleLog = console.log;
|
originalConsoleLog = console.log;
|
||||||
@@ -405,7 +406,7 @@ describe('bar install subcommand', () => {
|
|||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async (tag: string, _asset: string) => {
|
fetchReleaseAsset: async (tag: string, _asset: string) => {
|
||||||
fetchedUrls.push(tag);
|
fetchedUrls.push(tag);
|
||||||
return { downloadUrl: FAKE_DOWNLOAD_URL };
|
return { downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 };
|
||||||
},
|
},
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async (_baseUrl: string) => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async (_baseUrl: string) => ({ compatible: true, reason: 'ok' }),
|
||||||
@@ -422,6 +423,30 @@ describe('bar install subcommand', () => {
|
|||||||
expect(fetchedUrls).not.toContain(expect.stringMatching(/^\d+\.\d+\.\d+$/));
|
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,
|
||||||
|
clearQuarantine: async () => true,
|
||||||
|
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 () => {
|
it('pins the Info.plist version (not tag name) to ~/.ccs/bar/.version', async () => {
|
||||||
const ccsDir = path.join(tempHome, '.ccs');
|
const ccsDir = path.join(tempHome, '.ccs');
|
||||||
const appsDir = path.join(tempHome, 'Applications');
|
const appsDir = path.join(tempHome, 'Applications');
|
||||||
@@ -430,7 +455,7 @@ describe('bar install subcommand', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
||||||
readAppBundleVersion: (_appPath: string) => FAKE_VERSION,
|
readAppBundleVersion: (_appPath: string) => FAKE_VERSION,
|
||||||
@@ -453,7 +478,7 @@ describe('bar install subcommand', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async (baseUrl: string) => {
|
verifyCompat: async (baseUrl: string) => {
|
||||||
compatCalls.push(baseUrl);
|
compatCalls.push(baseUrl);
|
||||||
@@ -476,7 +501,7 @@ describe('bar install subcommand', () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
handleBarInstall([], {
|
handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: false, reason: 'no-bar-api' }),
|
verifyCompat: async () => ({ compatible: false, reason: 'no-bar-api' }),
|
||||||
readAppBundleVersion: (_appPath: string) => FAKE_VERSION,
|
readAppBundleVersion: (_appPath: string) => FAKE_VERSION,
|
||||||
@@ -499,7 +524,7 @@ describe('bar install subcommand', () => {
|
|||||||
// Inject clearQuarantine returning false so the fallback xattr guidance is
|
// Inject clearQuarantine returning false so the fallback xattr guidance is
|
||||||
// always printed regardless of host platform (/usr/bin/xattr availability).
|
// always printed regardless of host platform (/usr/bin/xattr availability).
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
||||||
readAppBundleVersion: (_appPath: string) => FAKE_VERSION,
|
readAppBundleVersion: (_appPath: string) => FAKE_VERSION,
|
||||||
@@ -737,7 +762,7 @@ describe('bar install: compat capability handshake', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async (baseUrl: string) => {
|
verifyCompat: async (baseUrl: string) => {
|
||||||
capturedArgs.push({ baseUrl });
|
capturedArgs.push({ baseUrl });
|
||||||
@@ -761,7 +786,7 @@ describe('bar install: compat capability handshake', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
||||||
readAppBundleVersion: (_appPath: string) => '1.4.0',
|
readAppBundleVersion: (_appPath: string) => '1.4.0',
|
||||||
@@ -783,7 +808,7 @@ describe('bar install: compat capability handshake', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: false, reason: 'no-bar-api' }),
|
verifyCompat: async () => ({ compatible: false, reason: 'no-bar-api' }),
|
||||||
readAppBundleVersion: (_appPath: string) => '1.4.0',
|
readAppBundleVersion: (_appPath: string) => '1.4.0',
|
||||||
@@ -806,7 +831,7 @@ describe('bar install: compat capability handshake', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: false, reason: 'unreachable' }),
|
verifyCompat: async () => ({ compatible: false, reason: 'unreachable' }),
|
||||||
readAppBundleVersion: (_appPath: string) => '1.4.0',
|
readAppBundleVersion: (_appPath: string) => '1.4.0',
|
||||||
@@ -829,7 +854,7 @@ describe('bar install: compat capability handshake', () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
handleBarInstall([], {
|
handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => {
|
verifyCompat: async () => {
|
||||||
throw new Error('network explosion');
|
throw new Error('network explosion');
|
||||||
@@ -948,7 +973,7 @@ describe('bar install: Info.plist version extraction regression tests', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
||||||
// readAppBundleVersion returns the real version from Info.plist
|
// readAppBundleVersion returns the real version from Info.plist
|
||||||
@@ -974,7 +999,7 @@ describe('bar install: Info.plist version extraction regression tests', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
||||||
readAppBundleVersion: (_appPath: string) => '1.4.0',
|
readAppBundleVersion: (_appPath: string) => '1.4.0',
|
||||||
@@ -1000,7 +1025,7 @@ describe('bar install: Info.plist version extraction regression tests', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
||||||
// Unreadable Info.plist — returns null
|
// Unreadable Info.plist — returns null
|
||||||
@@ -1501,7 +1526,7 @@ describe('bar install: stale version-pin removal on null plist read (Fix 1)', ()
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
||||||
// Simulate unreadable Info.plist
|
// Simulate unreadable Info.plist
|
||||||
@@ -1603,9 +1628,10 @@ describe('launch: findRunningServer reuse-first (GH-1500)', () => {
|
|||||||
|
|
||||||
expect(spawnCalled).toBe(false);
|
expect(spawnCalled).toBe(false);
|
||||||
|
|
||||||
const barJson = JSON.parse(
|
const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as {
|
||||||
fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')
|
port: number;
|
||||||
) as { port: number; baseUrl: string };
|
baseUrl: string;
|
||||||
|
};
|
||||||
expect(barJson.port).toBe(3000);
|
expect(barJson.port).toBe(3000);
|
||||||
expect(barJson.baseUrl).toBe('http://127.0.0.1:3000');
|
expect(barJson.baseUrl).toBe('http://127.0.0.1:3000');
|
||||||
|
|
||||||
@@ -1685,9 +1711,7 @@ describe('launch: bar.json contract (deterministic — GH-1500 null probe)', ()
|
|||||||
appInstallPath: path.join(tempHome, 'Applications', 'CCS Bar.app'),
|
appInstallPath: path.join(tempHome, 'Applications', 'CCS Bar.app'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const barJson = JSON.parse(
|
const barJson = JSON.parse(fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')) as unknown;
|
||||||
fs.readFileSync(path.join(ccsDir, 'bar.json'), 'utf8')
|
|
||||||
) as unknown;
|
|
||||||
expect(barJson).toMatchObject({
|
expect(barJson).toMatchObject({
|
||||||
baseUrl: 'http://127.0.0.1:4242',
|
baseUrl: 'http://127.0.0.1:4242',
|
||||||
port: 4242,
|
port: 4242,
|
||||||
@@ -1718,7 +1742,11 @@ describe('defaultFindRunningServer (GH-1500)', () => {
|
|||||||
fs.mkdirSync(ccsDir, { recursive: true });
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(ccsDir, 'bar.json'),
|
path.join(ccsDir, 'bar.json'),
|
||||||
JSON.stringify({ port: livePort, baseUrl: `http://127.0.0.1:${livePort}`, authMode: 'loopback' })
|
JSON.stringify({
|
||||||
|
port: livePort,
|
||||||
|
baseUrl: `http://127.0.0.1:${livePort}`,
|
||||||
|
authMode: 'loopback',
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
moduleSeq++;
|
moduleSeq++;
|
||||||
@@ -1726,7 +1754,9 @@ describe('defaultFindRunningServer (GH-1500)', () => {
|
|||||||
`../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}`
|
`../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}`
|
||||||
);
|
);
|
||||||
const { defaultFindRunningServer } = mod as {
|
const { defaultFindRunningServer } = mod as {
|
||||||
defaultFindRunningServer: (ccsDir: string) => Promise<{ port: number; baseUrl: string } | null>;
|
defaultFindRunningServer: (
|
||||||
|
ccsDir: string
|
||||||
|
) => Promise<{ port: number; baseUrl: string } | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let result: { port: number; baseUrl: string } | null = null;
|
let result: { port: number; baseUrl: string } | null = null;
|
||||||
@@ -1796,7 +1826,9 @@ describe('defaultFindRunningServer (GH-1500)', () => {
|
|||||||
`../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}`
|
`../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}`
|
||||||
);
|
);
|
||||||
const { defaultFindRunningServer } = mod as {
|
const { defaultFindRunningServer } = mod as {
|
||||||
defaultFindRunningServer: (ccsDir: string) => Promise<{ port: number; baseUrl: string } | null>;
|
defaultFindRunningServer: (
|
||||||
|
ccsDir: string
|
||||||
|
) => Promise<{ port: number; baseUrl: string } | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await defaultFindRunningServer(ccsDir);
|
const result = await defaultFindRunningServer(ccsDir);
|
||||||
@@ -1858,7 +1890,9 @@ describe('defaultFindRunningServer (GH-1500)', () => {
|
|||||||
`../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}`
|
`../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}`
|
||||||
);
|
);
|
||||||
const { defaultFindRunningServer } = mod as {
|
const { defaultFindRunningServer } = mod as {
|
||||||
defaultFindRunningServer: (ccsDir: string) => Promise<{ port: number; baseUrl: string } | null>;
|
defaultFindRunningServer: (
|
||||||
|
ccsDir: string
|
||||||
|
) => Promise<{ port: number; baseUrl: string } | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let result: { port: number; baseUrl: string } | null = null;
|
let result: { port: number; baseUrl: string } | null = null;
|
||||||
@@ -1947,7 +1981,9 @@ describe('defaultFindRunningServer: priority over response speed (GH-1500)', ()
|
|||||||
`../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}`
|
`../../../src/commands/bar/launch-subcommand?test=${Date.now()}-${moduleSeq}`
|
||||||
);
|
);
|
||||||
const { defaultFindRunningServer } = mod as {
|
const { defaultFindRunningServer } = mod as {
|
||||||
defaultFindRunningServer: (ccsDir: string) => Promise<{ port: number; baseUrl: string } | null>;
|
defaultFindRunningServer: (
|
||||||
|
ccsDir: string
|
||||||
|
) => Promise<{ port: number; baseUrl: string } | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let result: { port: number; baseUrl: string } | null = null;
|
let result: { port: number; baseUrl: string } | null = null;
|
||||||
@@ -1982,7 +2018,7 @@ describe('bar install: already-installed detection (GH-1504)', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: async (_url: string, dest: string) => {
|
downloadAndExtract: async (_url: string, dest: string) => {
|
||||||
fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true });
|
fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true });
|
||||||
},
|
},
|
||||||
@@ -2014,7 +2050,7 @@ describe('bar install: already-installed detection (GH-1504)', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: async (_url: string, dest: string) => {
|
downloadAndExtract: async (_url: string, dest: string) => {
|
||||||
fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true });
|
fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true });
|
||||||
},
|
},
|
||||||
@@ -2040,7 +2076,7 @@ describe('bar install: already-installed detection (GH-1504)', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: async (_url: string, dest: string) => {
|
downloadAndExtract: async (_url: string, dest: string) => {
|
||||||
fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true });
|
fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true });
|
||||||
},
|
},
|
||||||
@@ -2081,7 +2117,7 @@ describe('bar install: quarantine handling (GH-1504)', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
||||||
readAppBundleVersion: () => '1.0.0',
|
readAppBundleVersion: () => '1.0.0',
|
||||||
@@ -2115,7 +2151,7 @@ describe('bar install: quarantine handling (GH-1504)', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
||||||
readAppBundleVersion: () => '1.0.0',
|
readAppBundleVersion: () => '1.0.0',
|
||||||
@@ -2154,7 +2190,7 @@ describe('bar install: quarantine handling (GH-1504)', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
// Extraction does NOT place CCS Bar.app
|
// Extraction does NOT place CCS Bar.app
|
||||||
downloadAndExtract: async (_url: string, dest: string) => {
|
downloadAndExtract: async (_url: string, dest: string) => {
|
||||||
fs.mkdirSync(dest, { recursive: true });
|
fs.mkdirSync(dest, { recursive: true });
|
||||||
@@ -2196,7 +2232,7 @@ describe('bar install: launch retry finding — quarantine failure skips launch
|
|||||||
|
|
||||||
function baseDeps(appsDir: string, extra?: Partial<Record<string, unknown>>) {
|
function baseDeps(appsDir: string, extra?: Partial<Record<string, unknown>>) {
|
||||||
return {
|
return {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }),
|
||||||
readAppBundleVersion: () => '1.0.0',
|
readAppBundleVersion: () => '1.0.0',
|
||||||
@@ -2216,7 +2252,9 @@ describe('bar install: launch retry finding — quarantine failure skips launch
|
|||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
...baseDeps(appsDir),
|
...baseDeps(appsDir),
|
||||||
clearQuarantine: async () => false,
|
clearQuarantine: async () => false,
|
||||||
launchBar: async () => { /* noop */ },
|
launchBar: async () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
promptLaunch: async () => {
|
promptLaunch: async () => {
|
||||||
promptCalled = true;
|
promptCalled = true;
|
||||||
return true;
|
return true;
|
||||||
@@ -2253,7 +2291,9 @@ describe('bar install: launch retry finding — quarantine failure skips launch
|
|||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
...baseDeps(appsDir),
|
...baseDeps(appsDir),
|
||||||
clearQuarantine: async () => false,
|
clearQuarantine: async () => false,
|
||||||
launchBar: async () => { /* noop */ },
|
launchBar: async () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
promptLaunch: async () => false,
|
promptLaunch: async () => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2271,7 +2311,9 @@ describe('bar install: launch retry finding — quarantine failure skips launch
|
|||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
...baseDeps(appsDir),
|
...baseDeps(appsDir),
|
||||||
clearQuarantine: async () => true,
|
clearQuarantine: async () => true,
|
||||||
launchBar: async () => { /* noop */ },
|
launchBar: async () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
promptLaunch: async () => {
|
promptLaunch: async () => {
|
||||||
promptCalled = true;
|
promptCalled = true;
|
||||||
return false;
|
return false;
|
||||||
@@ -2316,7 +2358,7 @@ describe('bar install: launch flags and prompt (GH-1504)', () => {
|
|||||||
|
|
||||||
function baseDeps(appsDir: string, extra?: Partial<Record<string, unknown>>) {
|
function baseDeps(appsDir: string, extra?: Partial<Record<string, unknown>>) {
|
||||||
return {
|
return {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }),
|
||||||
readAppBundleVersion: () => '1.0.0',
|
readAppBundleVersion: () => '1.0.0',
|
||||||
@@ -2453,7 +2495,7 @@ describe('bar install: xattr absolute path contract (Finding 1)', () => {
|
|||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' }),
|
||||||
readAppBundleVersion: () => '1.0.0',
|
readAppBundleVersion: () => '1.0.0',
|
||||||
@@ -2461,7 +2503,9 @@ describe('bar install: xattr absolute path contract (Finding 1)', () => {
|
|||||||
quarantineArgs.push(appPath);
|
quarantineArgs.push(appPath);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
launchBar: async () => { /* noop */ },
|
launchBar: async () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
promptLaunch: async () => false,
|
promptLaunch: async () => false,
|
||||||
isBarRunning: async () => false,
|
isBarRunning: async () => false,
|
||||||
getCcsDir: () => path.join(tempHome, '.ccs'),
|
getCcsDir: () => path.join(tempHome, '.ccs'),
|
||||||
@@ -2490,7 +2534,7 @@ describe('bar install: stdin-TTY gate for launch prompt (Finding 2)', () => {
|
|||||||
|
|
||||||
function baseDeps(appsDir: string, extra?: Partial<Record<string, unknown>>) {
|
function baseDeps(appsDir: string, extra?: Partial<Record<string, unknown>>) {
|
||||||
return {
|
return {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }),
|
||||||
readAppBundleVersion: () => '1.0.0',
|
readAppBundleVersion: () => '1.0.0',
|
||||||
@@ -2512,7 +2556,9 @@ describe('bar install: stdin-TTY gate for launch prompt (Finding 2)', () => {
|
|||||||
|
|
||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
...baseDeps(appsDir),
|
...baseDeps(appsDir),
|
||||||
launchBar: async () => { /* noop */ },
|
launchBar: async () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
// promptLaunch returning true simulates stdin-TTY + user answered yes.
|
// promptLaunch returning true simulates stdin-TTY + user answered yes.
|
||||||
// The production defaultPromptLaunch now checks stdin.isTTY; by injecting
|
// The production defaultPromptLaunch now checks stdin.isTTY; by injecting
|
||||||
// a mock that returns true we confirm this code path (prompt called, launch invoked).
|
// a mock that returns true we confirm this code path (prompt called, launch invoked).
|
||||||
@@ -2577,7 +2623,7 @@ describe('bar install: already-running detection (Finding 3)', () => {
|
|||||||
|
|
||||||
function baseDeps(appsDir: string, extra?: Partial<Record<string, unknown>>) {
|
function baseDeps(appsDir: string, extra?: Partial<Record<string, unknown>>) {
|
||||||
return {
|
return {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: fakeExtract(appsDir),
|
downloadAndExtract: fakeExtract(appsDir),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }),
|
||||||
readAppBundleVersion: () => '1.0.0',
|
readAppBundleVersion: () => '1.0.0',
|
||||||
@@ -2628,7 +2674,9 @@ describe('bar install: already-running detection (Finding 3)', () => {
|
|||||||
await handleBarInstall([], {
|
await handleBarInstall([], {
|
||||||
...baseDeps(appsDir),
|
...baseDeps(appsDir),
|
||||||
isBarRunning: async () => false,
|
isBarRunning: async () => false,
|
||||||
launchBar: async () => { /* noop */ },
|
launchBar: async () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
promptLaunch: async () => {
|
promptLaunch: async () => {
|
||||||
promptCalled = true;
|
promptCalled = true;
|
||||||
return false;
|
return false;
|
||||||
@@ -2653,7 +2701,9 @@ describe('bar install: already-running detection (Finding 3)', () => {
|
|||||||
...baseDeps(appsDir),
|
...baseDeps(appsDir),
|
||||||
// Simulate pgrep error — isBarRunning returns false per spec
|
// Simulate pgrep error — isBarRunning returns false per spec
|
||||||
isBarRunning: async () => false,
|
isBarRunning: async () => false,
|
||||||
launchBar: async () => { /* noop */ },
|
launchBar: async () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
promptLaunch: async () => {
|
promptLaunch: async () => {
|
||||||
promptCalled = true;
|
promptCalled = true;
|
||||||
return false;
|
return false;
|
||||||
@@ -2772,12 +2822,14 @@ describe('bar install: stage-then-swap safety (Data Loss finding)', () => {
|
|||||||
extra?: Partial<Record<string, unknown>>
|
extra?: Partial<Record<string, unknown>>
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }),
|
verifyCompat: async () => ({ compatible: true, reason: 'ok' as const }),
|
||||||
readAppBundleVersion: () => '2.0.0',
|
readAppBundleVersion: () => '2.0.0',
|
||||||
clearQuarantine: async () => true,
|
clearQuarantine: async () => true,
|
||||||
isBarRunning: async () => false,
|
isBarRunning: async () => false,
|
||||||
launchBar: async () => { /* noop */ },
|
launchBar: async () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
promptLaunch: async () => false,
|
promptLaunch: async () => false,
|
||||||
getCcsDir: () => path.join(tempHome, '.ccs'),
|
getCcsDir: () => path.join(tempHome, '.ccs'),
|
||||||
getAppsDir: () => appsDir,
|
getAppsDir: () => appsDir,
|
||||||
@@ -2966,7 +3018,7 @@ describe('bar install: silent-decline fix — hint on user decline (review findi
|
|||||||
extra?: Partial<Record<string, unknown>>
|
extra?: Partial<Record<string, unknown>>
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL, sha256: FAKE_SHA256 }),
|
||||||
downloadAndExtract: async (_url: string, dest: string) => {
|
downloadAndExtract: async (_url: string, dest: string) => {
|
||||||
fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true });
|
fs.mkdirSync(path.join(dest, 'CCS Bar.app'), { recursive: true });
|
||||||
},
|
},
|
||||||
@@ -2974,7 +3026,9 @@ describe('bar install: silent-decline fix — hint on user decline (review findi
|
|||||||
readAppBundleVersion: () => '1.0.0',
|
readAppBundleVersion: () => '1.0.0',
|
||||||
clearQuarantine: async () => true,
|
clearQuarantine: async () => true,
|
||||||
isBarRunning: async () => false,
|
isBarRunning: async () => false,
|
||||||
launchBar: async () => { /* noop */ },
|
launchBar: async () => {
|
||||||
|
/* noop */
|
||||||
|
},
|
||||||
getCcsDir: () => path.join(tempHome, '.ccs'),
|
getCcsDir: () => path.join(tempHome, '.ccs'),
|
||||||
getAppsDir: () => appsDir,
|
getAppsDir: () => appsDir,
|
||||||
...extra,
|
...extra,
|
||||||
|
|||||||
Reference in new issue
Block a user