Files
codegraph/__tests__/npm-sdk.test.ts
T
Colby MchenryandClaude Opus 5.5 2344ce0722 test: stop test runs leaving temp dirs behind (#2622)
Every full test run left about 40 directories in the temp dir, even when it
passed. On the Windows dev machine about 16,000 had piled up by mid-October.
A full run with TEMP pointed at an empty folder showed where each one came
from:

- function-ref (20) and ts-field-classification (3): each case's `finally`
  cleared `tmpDir` right after `cg.destroy()`, so the `afterEach` that removes
  the dir found nothing to remove.
- npm-sdk (3) and liveness-watchdog (3) never removed the dirs they made.
- extraction's #1502 suite (3) made a dir in `beforeEach` and had no
  `afterEach`; pr19-improvements' schema-version case (1) made one inline.
- setup-home-sandbox (6 on Windows): Vitest runs no hooks for a file whose
  every test is skipped, so each platform-gated file left its home sandbox.
  Each sandbox now records itself in a per-run registry, and a global
  teardown removes whatever is still listed and names any dir still in use.

With the fix, the same full run leaves nothing behind but Node's own reused
compile cache. test-home-sandbox gains two tests: a file's home is listed in
the run's registry, and the teardown removes every listed home, then the
registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:00:12 +00:00

117 lines
5.0 KiB
TypeScript

/**
* Programmatic/embedded SDK entry (`scripts/npm-sdk.js`) tests (issue #354).
*
* The published main package is a thin shim: the CLI `bin` (npm-shim.js) execs
* the bundled Node, while `main` (npm-sdk.js) lets embedded consumers
* `require("@colbymchenry/codegraph")` on their OWN Node by re-exporting the
* compiled library that ships inside the per-platform optionalDependency
* (@colbymchenry/codegraph-<target>/lib/dist/index.js).
*
* These tests stand up a temp main-package dir with a fake platform package as a
* resolvable sibling, then require the SDK in a child process — so resolution,
* the self-heal cache fallback, and the missing-bundle error are exercised
* hermetically with no real bundle, network, or registry.
*/
import { describe, it, expect, afterEach } from 'vitest';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
const SDK_SRC = path.join(__dirname, '..', 'scripts', 'npm-sdk.js');
const target = `${process.platform}-${process.arch}`;
const VERSION = '9.9.9-test';
// Every temp dir a case makes, removed after it.
const tmpDirs: string[] = [];
function mkTmp(label: string): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `cg-sdk-${label}-`));
tmpDirs.push(dir);
return dir;
}
// A temp node_modules with the main package (npm-sdk.js + package.json). The
// fake platform package, when present, is written as a resolvable sibling so the
// SDK's `require.resolve('@colbymchenry/codegraph-<target>/...')` walks to it.
function makeConsumer(): { root: string; mainPkg: string } {
const root = mkTmp('consumer');
const mainPkg = path.join(root, 'node_modules', '@colbymchenry', 'codegraph');
fs.mkdirSync(mainPkg, { recursive: true });
fs.copyFileSync(SDK_SRC, path.join(mainPkg, 'npm-sdk.js'));
fs.writeFileSync(
path.join(mainPkg, 'package.json'),
JSON.stringify({ name: '@colbymchenry/codegraph', version: VERSION, main: 'npm-sdk.js' }) + '\n'
);
return { root, mainPkg };
}
// Write a fake compiled library that exports a sentinel, at the given lib/dist
// root (used both for the platform package and the self-heal cache bundle).
function writeFakeLib(libDistDir: string, sentinel: string): void {
fs.mkdirSync(libDistDir, { recursive: true });
fs.writeFileSync(
path.join(libDistDir, 'index.js'),
`module.exports = { SENTINEL: ${JSON.stringify(sentinel)}, CodeGraph: function CodeGraph() {} };\n`
);
}
function installPlatformPackage(root: string, sentinel: string): void {
const pkgRoot = path.join(root, 'node_modules', '@colbymchenry', `codegraph-${target}`);
writeFakeLib(path.join(pkgRoot, 'lib', 'dist'), sentinel);
fs.writeFileSync(
path.join(pkgRoot, 'package.json'),
JSON.stringify({ name: `@colbymchenry/codegraph-${target}`, version: VERSION }) + '\n'
);
}
// require() the SDK in a child process so each case gets a fresh module cache.
function requireSdk(mainPkg: string, env: Record<string, string> = {}) {
const code =
`try { const m = require(${JSON.stringify(path.join(mainPkg, 'npm-sdk.js'))});` +
` process.stdout.write(JSON.stringify({ sentinel: m.SENTINEL, cg: typeof m.CodeGraph })); }` +
` catch (e) { process.stderr.write(String(e && e.message || e)); process.exit(7); }`;
const r = spawnSync(process.execPath, ['-e', code], {
encoding: 'utf8',
env: { ...process.env, ...env },
});
return { status: r.status, stdout: r.stdout, stderr: r.stderr };
}
describe('npm-sdk programmatic entry', () => {
afterEach(() => {
for (const dir of tmpDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
});
it('re-exports the installed platform bundle library', () => {
const { root, mainPkg } = makeConsumer();
installPlatformPackage(root, 'platform-lib');
// Isolate from any real self-healed cache on this machine.
const r = requireSdk(mainPkg, { CODEGRAPH_INSTALL_DIR: path.join(root, '.empty-cache') });
expect(r.status).toBe(0);
expect(JSON.parse(r.stdout)).toEqual({ sentinel: 'platform-lib', cg: 'function' });
});
it('falls back to a self-healed cache bundle when the optional dep is absent', () => {
const { root, mainPkg } = makeConsumer(); // no platform package installed
const cacheDir = path.join(root, 'cache');
writeFakeLib(
path.join(cacheDir, 'bundles', `${target}-${VERSION}`, 'lib', 'dist'),
'cache-lib'
);
const r = requireSdk(mainPkg, { CODEGRAPH_INSTALL_DIR: cacheDir });
expect(r.status).toBe(0);
expect(JSON.parse(r.stdout)).toEqual({ sentinel: 'cache-lib', cg: 'function' });
});
it('throws an actionable error when no bundle is installed or cached', () => {
const { root, mainPkg } = makeConsumer(); // no platform package, empty cache
const r = requireSdk(mainPkg, { CODEGRAPH_INSTALL_DIR: path.join(root, '.empty-cache') });
expect(r.status).toBe(7);
expect(r.stderr).toContain(`@colbymchenry/codegraph-${target}`);
expect(r.stderr).toContain('not installed');
expect(r.stderr).toContain('registry.npmjs.org');
});
});