blob: 9907659d6a9717e04323a5e79c6e3d682c19e916 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { test } from 'node:test';
import {
isTemporaryNpxInstallation,
resolveRuntimeHostNpmGlobalInstallation,
RuntimeHostCliInstallationError,
} from '../runtime-host-cli-installation.js';
test('keeps one owner across release changes in the same npm global slot', async (t) => {
const fixture = await installationFixture(t, '1.0.0');
const first = await resolve(fixture);
await writeFile(fixture.manifestUrl, JSON.stringify({ name: 'maka-agent', version: '2.0.0' }));
const upgraded = await resolve(fixture);
assert.equal(first.owner.installationId, upgraded.owner.installationId);
assert.deepEqual(first.observedRelease, {
version: '1.0.0',
packageRoot: fixture.packageRoot,
cliPath: fixture.cliPath,
});
assert.deepEqual(upgraded.observedRelease, {
...first.observedRelease,
version: '2.0.0',
});
assert.equal('deployment' in upgraded, false);
});
test('uses distinct owners for distinct active npm global roots', async (t) => {
const firstFixture = await installationFixture(t, '1.0.0');
const secondFixture = await installationFixture(t, '1.0.0');
const [first, second] = await Promise.all([resolve(firstFixture), resolve(secondFixture)]);
assert.notEqual(first.owner.installationId, second.owner.installationId);
});
test('rejects a package outside the active npm global root', async (t) => {
const fixture = await installationFixture(t, '1.0.0');
const differentRoot = join(fixture.base, 'other', 'node_modules');
await mkdir(differentRoot, { recursive: true });
await assert.rejects(
resolveRuntimeHostNpmGlobalInstallation(
{ manifestUrl: fixture.manifestUrl, homeDir: fixture.homeDir },
{ resolveGlobalNodeModulesRoot: async () => differentRoot },
),
installationError('unsupported_installation'),
);
});
test('rejects development and npx packages before treating them as owners', async (t) => {
const development = await installationFixture(t, '1.0.0');
await writeFile(
development.manifestUrl,
JSON.stringify({ name: 'maka-agent', version: '1.0.0', private: true }),
);
await assert.rejects(resolve(development), installationError('unsupported_installation'));
const cacheRoot = await mkdtemp(join(tmpdir(), 'maka-cli-npx-installation-'));
t.after(() => rm(cacheRoot, { recursive: true, force: true }));
const packageRoot = join(cacheRoot, '_npx', 'hash', 'node_modules', 'maka-agent');
const cliPath = join(packageRoot, 'dist', 'cli.js');
const manifestUrl = pathToFileURL(join(packageRoot, 'package.json'));
await mkdir(join(packageRoot, 'dist'), { recursive: true });
await writeFile(cliPath, '#!/usr/bin/env node\n');
await writeFile(manifestUrl, JSON.stringify({ name: 'maka-agent', version: '1.0.0' }));
let npmRootRequested = false;
await assert.rejects(
resolveRuntimeHostNpmGlobalInstallation(
{
manifestUrl,
environment: { npm_config_cache: cacheRoot },
homeDir: join(cacheRoot, 'home'),
},
{
resolveGlobalNodeModulesRoot: async () => {
npmRootRequested = true;
return dirname(packageRoot);
},
},
),
installationError('unsupported_installation'),
);
assert.equal(npmRootRequested, false);
});
test('recognizes configured and default npx cache roots without prefix collisions', async (t) => {
const base = await mkdtemp(join(tmpdir(), 'maka-cli-npx-provenance-'));
t.after(() => rm(base, { recursive: true, force: true }));
const configured = join(base, 'cache', '_npx', 'one', 'node_modules', 'maka-agent');
const defaultCache = join(base, 'home', '.npm', '_npx', 'two', 'node_modules', 'maka-agent');
const collision = join(base, 'cache', '_npx-other', 'maka-agent');
await Promise.all([
mkdir(configured, { recursive: true }),
mkdir(defaultCache, { recursive: true }),
mkdir(collision, { recursive: true }),
]);
const input = {
environment: { npm_config_cache: join(base, 'cache') },
homeDir: join(base, 'home'),
platform: 'linux' as const,
};
assert.equal(await isTemporaryNpxInstallation(configured, input), true);
assert.equal(await isTemporaryNpxInstallation(defaultCache, input), true);
assert.equal(await isTemporaryNpxInstallation(collision, input), false);
});
test('recognizes Windows npm cache provenance without classifying an unrelated installation', async (t) => {
const base = await mkdtemp(join(tmpdir(), 'maka-cli-npx-windows-'));
t.after(() => rm(base, { recursive: true, force: true }));
const localAppData = join(base, 'local-app-data');
const homeDir = join(base, 'home');
const defaultPackage = join(
localAppData,
'npm-cache',
'_npx',
'hash',
'node_modules',
'maka-agent',
);
const fallbackPackage = join(homeDir, 'npm-cache', '_npx', 'hash', 'node_modules', 'maka-agent');
const configuredPackage = join(
base,
'custom-cache',
'_npx',
'hash',
'node_modules',
'maka-agent',
);
const installedPackage = join(base, 'global', 'node_modules', 'maka-agent');
const input = { platform: 'win32' as const, homeDir };
await Promise.all(
[defaultPackage, fallbackPackage, configuredPackage, installedPackage].map((path) =>
mkdir(path, { recursive: true }),
),
);
assert.equal(
await isTemporaryNpxInstallation(defaultPackage, {
...input,
environment: { LOCALAPPDATA: localAppData },
}),
true,
);
assert.equal(
await isTemporaryNpxInstallation(fallbackPackage, {
...input,
environment: {},
}),
true,
);
assert.equal(
await isTemporaryNpxInstallation(configuredPackage, {
...input,
environment: { NPM_CONFIG_CACHE: join(base, 'custom-cache') },
}),
true,
);
assert.equal(
await isTemporaryNpxInstallation(installedPackage, {
...input,
environment: { LOCALAPPDATA: localAppData },
}),
false,
);
// A missing package is not evidence of npx unless its path is in a known cache.
assert.equal(
await isTemporaryNpxInstallation(join(base, 'missing-installation'), {
...input,
environment: {},
}),
false,
);
});
test('rejects non-UTF-8 package metadata instead of normalizing an owner observation', async (t) => {
const fixture = await installationFixture(t, '1.0.0');
const document = Buffer.from(JSON.stringify({ name: 'maka-agent', version: '1.0.0' }));
document[document.indexOf('1.0.0')] = 0xff;
await writeFile(fixture.manifestUrl, document);
await assert.rejects(resolve(fixture), installationError('invalid_installation'));
});
async function installationFixture(t: test.TestContext, version: string) {
const base = await realpath(await mkdtemp(join(tmpdir(), 'maka-cli-global-installation-')));
t.after(() => rm(base, { recursive: true, force: true }));
const homeDir = join(base, 'home');
const globalRoot = join(base, 'lib', 'node_modules');
const packageRoot = join(globalRoot, 'maka-agent');
const cliPath = join(packageRoot, 'dist', 'cli.js');
const manifestUrl = pathToFileURL(join(packageRoot, 'package.json'));
await mkdir(join(packageRoot, 'dist'), { recursive: true });
await mkdir(homeDir);
await writeFile(cliPath, '#!/usr/bin/env node\n');
await writeFile(manifestUrl, JSON.stringify({ name: 'maka-agent', version }));
return { base, homeDir, globalRoot, packageRoot, cliPath, manifestUrl };
}
function resolve(fixture: Awaited<ReturnType<typeof installationFixture>>) {
return resolveRuntimeHostNpmGlobalInstallation(
{ manifestUrl: fixture.manifestUrl, homeDir: fixture.homeDir },
{ resolveGlobalNodeModulesRoot: async () => fixture.globalRoot },
);
}
function installationError(code: RuntimeHostCliInstallationError['code']) {
return (error: unknown) =>
error instanceof RuntimeHostCliInstallationError && error.code === code;
}