Bumps package.json, .claude-plugin/plugin.json and the README badge to 7.8.2; adds the release entry to CHANGELOG.md, docs/version-history.md, the README recent-versions table and the CLAUDE.md highlights block. Also fixes test pollution introduced with the ide-extension regression suite: its temp roots used an `llmsec-jb-plugin-` prefix, and jetbrains-parser.test.mjs asserts globally that no `llmsec-jb-*` directory survives anywhere in tmpdir. The shared prefix made that assertion fail depending on test order — it passed on the first full run and failed on the next. Prefix is now `llmsec-nullmanifest-`. npm test: 1901/1901 green, three consecutive runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
95 lines
4 KiB
JavaScript
95 lines
4 KiB
JavaScript
// ide-extension-null-manifest.test.mjs — Regression tests for unparseable
|
||
// JetBrains plugins.
|
||
//
|
||
// parseIntelliJPlugin signals "could not parse" as { manifest: null, warnings }
|
||
// — a TRUTHY object — while parseVSCodeExtension signals it as a bare null.
|
||
// scanOneExtension only guarded the bare-null form, so a JetBrains plugin with
|
||
// no lib/ directory, an unreadable lib/, no jars, or no extractable jar reached
|
||
// `manifest.hasSignature` and threw a TypeError. mapConcurrent had no per-item
|
||
// isolation, so that one plugin aborted the entire ide-scan.
|
||
|
||
import { describe, it, after } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { join } from 'node:path';
|
||
import { tmpdir } from 'node:os';
|
||
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
|
||
import { __testing } from '../../scanners/ide-extension-scanner.mjs';
|
||
|
||
const { scanOneExtension } = __testing;
|
||
const created = [];
|
||
|
||
after(async () => {
|
||
for (const dir of created) await rm(dir, { recursive: true, force: true });
|
||
});
|
||
|
||
async function makePluginRoot(name) {
|
||
// Prefix must NOT start with `llmsec-jb-`: jetbrains-parser.test.mjs asserts
|
||
// that no `llmsec-jb-*` directory survives anywhere in tmpdir, and that
|
||
// assertion is global, so a shared prefix collides depending on test order.
|
||
const root = await mkdtemp(join(tmpdir(), 'llmsec-nullmanifest-'));
|
||
created.push(root);
|
||
const dir = join(root, name);
|
||
await mkdir(dir, { recursive: true });
|
||
return dir;
|
||
}
|
||
|
||
function jbExt(location, id) {
|
||
return {
|
||
id,
|
||
version: '1.0.0',
|
||
type: 'jetbrains',
|
||
location,
|
||
publisher: 'unknown',
|
||
source: 'test',
|
||
isBuiltin: false,
|
||
signed: false,
|
||
};
|
||
}
|
||
|
||
describe('ide-extension-scanner — unparseable JetBrains plugin', () => {
|
||
it('does not throw when lib/ is missing entirely', async () => {
|
||
const dir = await makePluginRoot('NoLibPlugin');
|
||
const result = await scanOneExtension(jbExt(dir, 'NoLibPlugin'), { targetBase: dir });
|
||
assert.ok(result, 'expected a result object, not a throw');
|
||
assert.equal(result.id, 'NoLibPlugin');
|
||
assert.ok(
|
||
result.warnings.some(w => /lib/i.test(w)),
|
||
`expected a parse warning, got: ${JSON.stringify(result.warnings)}`
|
||
);
|
||
assert.equal(result.aggregate.verdict, 'ALLOW');
|
||
});
|
||
|
||
it('does not throw when lib/ exists but holds no jars', async () => {
|
||
const dir = await makePluginRoot('EmptyLibPlugin');
|
||
await mkdir(join(dir, 'lib'), { recursive: true });
|
||
await writeFile(join(dir, 'lib', 'notes.txt'), 'not a jar', 'utf8');
|
||
const result = await scanOneExtension(jbExt(dir, 'EmptyLibPlugin'), { targetBase: dir });
|
||
assert.ok(result, 'expected a result object, not a throw');
|
||
assert.ok(result.warnings.length >= 1);
|
||
assert.equal(result.aggregate.verdict, 'ALLOW');
|
||
});
|
||
|
||
it('does not throw when lib/ is a file rather than a directory', async () => {
|
||
const dir = await makePluginRoot('LibIsFilePlugin');
|
||
await writeFile(join(dir, 'lib'), 'this is not a directory', 'utf8');
|
||
const result = await scanOneExtension(jbExt(dir, 'LibIsFilePlugin'), { targetBase: dir });
|
||
assert.ok(result, 'expected a result object, not a throw');
|
||
assert.ok(result.warnings.length >= 1);
|
||
});
|
||
|
||
it('does not throw when the only jar is corrupt (unextractable)', async () => {
|
||
const dir = await makePluginRoot('CorruptJarPlugin');
|
||
await mkdir(join(dir, 'lib'), { recursive: true });
|
||
await writeFile(join(dir, 'lib', 'broken.jar'), 'PK truncated garbage', 'utf8');
|
||
const result = await scanOneExtension(jbExt(dir, 'CorruptJarPlugin'), { targetBase: dir });
|
||
assert.ok(result, 'expected a result object, not a throw');
|
||
assert.ok(result.warnings.length >= 1);
|
||
});
|
||
|
||
it('reports a parse failure without inventing findings', async () => {
|
||
const dir = await makePluginRoot('QuietPlugin');
|
||
const result = await scanOneExtension(jbExt(dir, 'QuietPlugin'), { targetBase: dir });
|
||
const counts = result.aggregate.counts;
|
||
assert.equal(counts.critical + counts.high + counts.medium, 0);
|
||
});
|
||
});
|