mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +00:00
fix(bar): stop install at the manual step when quarantine clearing fails
A failed quarantine clear previously fell through to the launch handoff, so a default-yes prompt (or --launch) opened the still quarantined app straight into the Gatekeeper block. Install now ends after printing the manual xattr guidance, with a hint to run 'ccs bar' once quarantine is cleared; --launch does not override a failed clear.
This commit is contained in:
@@ -553,7 +553,9 @@ export async function handleBarInstall(
|
|||||||
|
|
||||||
// 5. Quarantine handling: run `xattr -dr com.apple.quarantine` automatically.
|
// 5. Quarantine handling: run `xattr -dr com.apple.quarantine` automatically.
|
||||||
// This clears the Gatekeeper quarantine flag that ad-hoc builds receive on download.
|
// This clears the Gatekeeper quarantine flag that ad-hoc builds receive on download.
|
||||||
// On success: print [OK] confirmation. On failure: fall back to printed guidance.
|
// On success: print [OK] confirmation. On failure: fall back to printed guidance and
|
||||||
|
// SKIP the launch handoff entirely — launching a still-quarantined app hits the
|
||||||
|
// Gatekeeper block, so the user must clear quarantine manually first.
|
||||||
const quarantineCleared = await clearQuarantine(appPath);
|
const quarantineCleared = await clearQuarantine(appPath);
|
||||||
if (quarantineCleared) {
|
if (quarantineCleared) {
|
||||||
console.log('[OK] Cleared Gatekeeper quarantine.');
|
console.log('[OK] Cleared Gatekeeper quarantine.');
|
||||||
@@ -562,6 +564,8 @@ export async function handleBarInstall(
|
|||||||
console.log(' If macOS says the app is "damaged" or "unverified", run:');
|
console.log(' If macOS says the app is "damaged" or "unverified", run:');
|
||||||
console.log(` xattr -dr com.apple.quarantine "${appPath}"`);
|
console.log(` xattr -dr com.apple.quarantine "${appPath}"`);
|
||||||
console.log(' Or right-click the app and select Open.');
|
console.log(' Or right-click the app and select Open.');
|
||||||
|
console.log('[i] After clearing quarantine, run `ccs bar` to launch.');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Capability handshake via GET /api/bar/summary.
|
// 6. Capability handshake via GET /api/bar/summary.
|
||||||
|
|||||||
@@ -2034,8 +2034,12 @@ describe('bar install: quarantine handling (GH-1504)', () => {
|
|||||||
expect(allOutput).toMatch(/\[OK\].*[Cc]leared.*[Qq]uarantine/);
|
expect(allOutput).toMatch(/\[OK\].*[Cc]leared.*[Qq]uarantine/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('quarantine failure is non-fatal: falls back to printed xattr hint', async () => {
|
it('quarantine failure is non-fatal: falls back to printed xattr hint; launch handoff skipped', async () => {
|
||||||
|
// Launch Retry finding: when clearQuarantine fails, the entire launch handoff must be
|
||||||
|
// skipped (prompt and launchBar must NOT be called) and the follow-up hint must be printed.
|
||||||
const appsDir = path.join(tempHome, 'Applications');
|
const appsDir = path.join(tempHome, 'Applications');
|
||||||
|
let launchCalled = false;
|
||||||
|
let promptCalled = false;
|
||||||
|
|
||||||
const { handleBarInstall } = await loadInstallSubcommand();
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
@@ -2046,9 +2050,12 @@ describe('bar install: quarantine handling (GH-1504)', () => {
|
|||||||
readAppBundleVersion: () => '1.0.0',
|
readAppBundleVersion: () => '1.0.0',
|
||||||
clearQuarantine: async () => false,
|
clearQuarantine: async () => false,
|
||||||
launchBar: async () => {
|
launchBar: async () => {
|
||||||
/* noop */
|
launchCalled = true;
|
||||||
|
},
|
||||||
|
promptLaunch: async () => {
|
||||||
|
promptCalled = true;
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
promptLaunch: async () => false,
|
|
||||||
getCcsDir: () => path.join(tempHome, '.ccs'),
|
getCcsDir: () => path.join(tempHome, '.ccs'),
|
||||||
getAppsDir: () => appsDir,
|
getAppsDir: () => appsDir,
|
||||||
});
|
});
|
||||||
@@ -2060,6 +2067,13 @@ describe('bar install: quarantine handling (GH-1504)', () => {
|
|||||||
|
|
||||||
// Fallback xattr hint printed
|
// Fallback xattr hint printed
|
||||||
expect(allOutput).toMatch(/xattr.*quarantine/i);
|
expect(allOutput).toMatch(/xattr.*quarantine/i);
|
||||||
|
|
||||||
|
// Launch Retry finding: prompt and launch must both be skipped
|
||||||
|
expect(promptCalled).toBe(false);
|
||||||
|
expect(launchCalled).toBe(false);
|
||||||
|
|
||||||
|
// Follow-up hint must guide the user to run `ccs bar` after manual clear
|
||||||
|
expect(allOutput).toMatch(/After clearing quarantine.*ccs bar/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clearQuarantine is NOT called when extraction fails (app path absent)', async () => {
|
it('clearQuarantine is NOT called when extraction fails (app path absent)', async () => {
|
||||||
@@ -2095,6 +2109,126 @@ describe('bar install: quarantine handling (GH-1504)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Launch Retry finding — quarantine failure blocks entire launch handoff
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('bar install: launch retry finding — quarantine failure skips launch handoff', () => {
|
||||||
|
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<Record<string, unknown>>) {
|
||||||
|
return {
|
||||||
|
fetchReleaseAsset: async () => ({ downloadUrl: FAKE_DOWNLOAD_URL }),
|
||||||
|
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('clearQuarantine false: promptLaunch is NOT called', async () => {
|
||||||
|
const appsDir = path.join(tempHome, 'Applications');
|
||||||
|
let promptCalled = false;
|
||||||
|
|
||||||
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
|
await handleBarInstall([], {
|
||||||
|
...baseDeps(appsDir),
|
||||||
|
clearQuarantine: async () => false,
|
||||||
|
launchBar: async () => { /* noop */ },
|
||||||
|
promptLaunch: async () => {
|
||||||
|
promptCalled = true;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(promptCalled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clearQuarantine false + --launch: launchBar is NOT called (explicit flag cannot override failed clear)', async () => {
|
||||||
|
// --launch does not bypass a failed quarantine clear — Gatekeeper would still block.
|
||||||
|
const appsDir = path.join(tempHome, 'Applications');
|
||||||
|
let launchCalled = false;
|
||||||
|
|
||||||
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
|
await handleBarInstall(['--launch'], {
|
||||||
|
...baseDeps(appsDir),
|
||||||
|
clearQuarantine: async () => false,
|
||||||
|
launchBar: async () => {
|
||||||
|
launchCalled = true;
|
||||||
|
},
|
||||||
|
promptLaunch: async () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(launchCalled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clearQuarantine false: follow-up hint printed directing user to run `ccs bar` after manual clear', async () => {
|
||||||
|
const appsDir = path.join(tempHome, 'Applications');
|
||||||
|
|
||||||
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
|
await handleBarInstall([], {
|
||||||
|
...baseDeps(appsDir),
|
||||||
|
clearQuarantine: async () => false,
|
||||||
|
launchBar: async () => { /* noop */ },
|
||||||
|
promptLaunch: async () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allOutput = consoleOutput.join('\n');
|
||||||
|
expect(allOutput).toMatch(/After clearing quarantine.*ccs bar/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clearQuarantine true: launch handoff proceeds normally (prompt called)', async () => {
|
||||||
|
// Successful clear must not change existing behavior — prompt is still called.
|
||||||
|
const appsDir = path.join(tempHome, 'Applications');
|
||||||
|
let promptCalled = false;
|
||||||
|
|
||||||
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
|
await handleBarInstall([], {
|
||||||
|
...baseDeps(appsDir),
|
||||||
|
clearQuarantine: async () => true,
|
||||||
|
launchBar: async () => { /* noop */ },
|
||||||
|
promptLaunch: async () => {
|
||||||
|
promptCalled = true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(promptCalled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clearQuarantine true + --launch: launchBar invoked as before', async () => {
|
||||||
|
const appsDir = path.join(tempHome, 'Applications');
|
||||||
|
let launchCalled = false;
|
||||||
|
|
||||||
|
const { handleBarInstall } = await loadInstallSubcommand();
|
||||||
|
|
||||||
|
await handleBarInstall(['--launch'], {
|
||||||
|
...baseDeps(appsDir),
|
||||||
|
clearQuarantine: async () => true,
|
||||||
|
launchBar: async () => {
|
||||||
|
launchCalled = true;
|
||||||
|
},
|
||||||
|
promptLaunch: async () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(launchCalled).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// GH-1504 — launch flags and prompt behavior
|
// GH-1504 — launch flags and prompt behavior
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user