Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/lib/__tests__/wizard-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as http from 'http';
import * as os from 'os';
import * as path from 'path';
import { zipSync } from 'fflate';
import { scan } from '@posthog/warlock';
import { analytics } from '@utils/analytics';
import {
ASK_BATCH_THRESHOLD,
ASK_CANCELLED_NOTE,
Expand Down Expand Up @@ -1392,6 +1394,43 @@ describe('downloadSkill (e2e over HTTP)', () => {
await server.close();
}
});

// The engine is WASM and has failed to instantiate in the field. Reported as
// `extract` it reads as a corrupt archive, which the pure-JS unzip cannot
// produce, and the run is told to check directory permissions instead.
it('reports a scanner engine failure as the scan step, not extract', async () => {
const zip = dummyZip();
const server = await startServer((_req, res) => {
res.writeHead(200, { 'Content-Type': 'application/zip' });
res.end(Buffer.from(zip));
});
const captured = vi.spyOn(analytics, 'wizardCapture').mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-empty-function
() => {},
);
vi.mocked(scan).mockRejectedValueOnce(new Error('WebAssembly.Module()'));

try {
const result = await downloadSkill(
{
id: 'dummy',
name: 'Dummy',
downloadUrl: `${server.baseUrl}/skill.zip`,
},
tmpDir,
{ triage: undefined },
);

expect(result.success).toBe(false);
expect(captured).toHaveBeenCalledWith(
'skill install failed',
expect.objectContaining({ step: 'scan', skill_id: 'dummy' }),
);
} finally {
captured.mockRestore();
await server.close();
}
});
});

describe('fetchSkillMenu', () => {
Expand Down
7 changes: 6 additions & 1 deletion src/lib/wizard-tools/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export async function downloadSkill(
const skillDir = skillsRoot
? path.join(installDir, skillsRoot, skillEntry.id)
: path.join(installDir, '.claude', 'skills', skillEntry.id);
let step: 'download' | 'extract' = 'download';
let step: 'download' | 'extract' | 'scan' = 'download';

try {
fs.mkdirSync(skillDir, { recursive: true });
Expand All @@ -226,6 +226,11 @@ export async function downloadSkill(
// Same scan the Bash-install hook runs — TS-path installs (linear
// pre-install, MCP/pi install_skill, orchestrator cache + reference)
// must not skip it.
//
// The scan is its own step: it runs the YARA-X WASM engine, and an engine
// that fails to load throws from here. Left as `extract` that lands on the
// event as an unzip failure, which the pure-JS unzip cannot produce.
step = 'scan';
const poisonReason = await scanInstalledSkill(skillDir, triage);
if (poisonReason) {
fs.rmSync(skillDir, { recursive: true, force: true });
Expand Down
Loading