Files
ECC/tests/ci/nasiko-control-plane.test.js
T

504 lines
23 KiB
JavaScript

/**
* Contract and lifecycle tests for the opt-in Nasiko CLI lifecycle bridge.
*/
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawnSync } = require('child_process');
const REPO_ROOT = path.join(__dirname, '..', '..');
function read(relativePath) {
return fs.readFileSync(path.join(REPO_ROOT, relativePath), 'utf8');
}
function readJson(relativePath) {
return JSON.parse(read(relativePath));
}
async function runTest(name, testFunction) {
try {
await testFunction();
console.log(` ✓ ${name}`);
return true;
} catch (error) {
console.log(` ✗ ${name}`);
console.error(` ${error.message}`);
return false;
}
}
function sha256Digest(value) {
const crypto = require('crypto');
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
}
function tarGzipFixture({
name = 'nasiko',
payload = Buffer.from('x'),
sizeField = null,
padding = true,
terminatorBlocks = 2,
trailing = Buffer.alloc(0),
} = {}) {
const zlib = require('zlib');
const header = Buffer.alloc(512);
header.write(name, 0, 100, 'utf8');
header.write(sizeField || `${payload.length.toString(8).padStart(11, '0')}\0`, 124, 12, 'ascii');
header[156] = '0'.charCodeAt(0);
const paddingBytes = padding ? Buffer.alloc((512 - (payload.length % 512)) % 512) : Buffer.alloc(0);
return zlib.gzipSync(Buffer.concat([
header,
payload,
paddingBytes,
Buffer.alloc(terminatorBlocks * 512),
trailing,
]));
}
async function main() {
console.log('\n=== Testing Nasiko CLI lifecycle bridge ===\n');
const tests = [
['qualifies only pinned platform releases and rejects latest', () => {
const {
getQualifiedRelease,
normalizePlatform,
} = require('../../scripts/lib/nasiko-release');
assert.deepStrictEqual(normalizePlatform('darwin', 'arm64'), {
os: 'darwin',
arch: 'arm64',
binaryName: 'nasiko',
});
assert.deepStrictEqual(normalizePlatform('win32', 'x64'), {
os: 'windows',
arch: 'amd64',
binaryName: 'nasiko.exe',
});
assert.match(getQualifiedRelease('v0.1.0', 'linux', 'x64').manifestDigest, /^sha256:[a-f0-9]{64}$/);
assert.match(getQualifiedRelease('v0.1.0', 'linux', 'x64').binaryDigest, /^sha256:[a-f0-9]{64}$/);
assert.strictEqual(getQualifiedRelease('v0.1.0', 'linux', 'x64').license, 'Apache-2.0');
assert.throws(() => getQualifiedRelease('latest', 'darwin', 'arm64'), /pinned version/i);
assert.throws(() => getQualifiedRelease('v1.0.0', 'darwin', 'arm64'), /not qualified/i);
assert.throws(() => normalizePlatform('freebsd', 'x64'), /unsupported platform/i);
assert.throws(() => normalizePlatform('darwin', 'ia32'), /unsupported architecture/i);
}],
['requires explicit consent while dry-run remains offline and read-only', async () => {
const { installNasiko } = require('../../scripts/lib/nasiko-release');
let fetchCount = 0;
const dependencies = {
fetchBytes: async () => {
fetchCount += 1;
throw new Error('dry-run fetched the network');
},
platform: 'darwin',
arch: 'arm64',
};
await assert.rejects(
installNasiko({ version: 'v0.1.0', yes: false }, dependencies),
/explicit --yes/i
);
const plan = await installNasiko({ version: 'v0.1.0', dryRun: true }, dependencies);
assert.strictEqual(plan.dryRun, true);
assert.strictEqual(plan.version, 'v0.1.0');
assert.strictEqual(plan.registryOrigin, 'https://registry.nasiko.dev');
assert.strictEqual(fetchCount, 0);
}],
['cleans an exclusively created lifecycle lock when initialization fails', () => {
const { acquireLifecycleLock } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-lock-'));
const lockPath = path.join(installRoot, '.ecc-nasiko-lifecycle.lock');
try {
const failingFileSystem = {
...fs,
writeFileSync: () => { throw new Error('lock metadata unavailable'); },
};
assert.throws(() => acquireLifecycleLock(installRoot, failingFileSystem), /metadata unavailable/i);
assert.strictEqual(fs.existsSync(lockPath), false);
const releaseLock = acquireLifecycleLock(installRoot);
assert.strictEqual(fs.existsSync(lockPath), true);
releaseLock();
assert.strictEqual(fs.existsSync(lockPath), false);
} finally { fs.rmSync(installRoot, { recursive: true, force: true }); }
}],
['recovers only locks whose recorded owner is confirmed dead', () => {
const { acquireLifecycleLock } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-stale-lock-'));
const lockPath = path.join(installRoot, '.ecc-nasiko-lifecycle.lock');
try {
fs.writeFileSync(lockPath, `${JSON.stringify({
pid: 424242,
startedAt: '2026-08-25T00:00:00.000Z',
token: 'stale-owner',
})}\n`, { mode: 0o600 });
assert.throws(
() => acquireLifecycleLock(installRoot, fs, { isProcessAlive: () => true }),
/already in progress/i
);
const releaseLock = acquireLifecycleLock(installRoot, fs, { isProcessAlive: () => false });
assert.strictEqual(fs.existsSync(lockPath), true);
releaseLock();
assert.strictEqual(fs.existsSync(lockPath), false);
} finally { fs.rmSync(installRoot, { recursive: true, force: true }); }
}],
['refuses to recover malformed lifecycle-lock ownership', () => {
const { acquireLifecycleLock } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-malformed-lock-'));
const lockPath = path.join(installRoot, '.ecc-nasiko-lifecycle.lock');
try {
fs.writeFileSync(lockPath, '{"pid":"unknown"}\n', { mode: 0o600 });
assert.throws(
() => acquireLifecycleLock(installRoot, fs, { isProcessAlive: () => false }),
/already in progress/i
);
} finally { fs.rmSync(installRoot, { recursive: true, force: true }); }
}],
['recovers a lock abandoned by a finished process', () => {
const { acquireLifecycleLock } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-dead-process-lock-'));
const lockPath = path.join(installRoot, '.ecc-nasiko-lifecycle.lock');
const modulePath = path.join(REPO_ROOT, 'scripts', 'lib', 'nasiko-release.js');
try {
const child = spawnSync(process.execPath, ['-e',
`require(${JSON.stringify(modulePath)}).acquireLifecycleLock(${JSON.stringify(installRoot)});`
], { encoding: 'utf8' });
assert.strictEqual(child.status, 0, child.stderr);
assert.strictEqual(fs.existsSync(lockPath), true);
const releaseLock = acquireLifecycleLock(installRoot);
releaseLock();
assert.strictEqual(fs.existsSync(lockPath), false);
} finally { fs.rmSync(installRoot, { recursive: true, force: true }); }
}],
['a prior release callback never removes a replacement lifecycle lock', () => {
const { acquireLifecycleLock } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-replaced-lock-'));
const lockPath = path.join(installRoot, '.ecc-nasiko-lifecycle.lock');
const displacedPath = `${lockPath}.displaced`;
try {
const releaseLock = acquireLifecycleLock(installRoot);
fs.renameSync(lockPath, displacedPath);
fs.writeFileSync(lockPath, '{"pid":1,"startedAt":"2026-08-25T00:00:00.000Z","token":"replacement"}\n');
releaseLock();
assert.strictEqual(fs.existsSync(lockPath), true);
} finally { fs.rmSync(installRoot, { recursive: true, force: true }); }
}],
['uses descriptor identity when Windows path stats disagree', () => {
const { acquireLifecycleLock } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-windows-identity-'));
const lockPath = path.join(installRoot, '.ecc-nasiko-lifecycle.lock');
const windowsLikeFileSystem = {
...fs,
lstatSync: target => {
const stats = fs.lstatSync(target);
return {
...stats,
dev: Number(stats.dev) + 1,
isDirectory: () => stats.isDirectory(),
isFile: () => stats.isFile(),
isSymbolicLink: () => stats.isSymbolicLink(),
};
},
};
try {
const releaseLock = acquireLifecycleLock(installRoot, windowsLikeFileSystem);
releaseLock();
assert.strictEqual(fs.existsSync(lockPath), false);
} finally { fs.rmSync(installRoot, { recursive: true, force: true }); }
}],
['verifies manifest and blob digests before an atomic install', async () => {
const { installNasiko } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-green-'));
const binary = Buffer.from('#!/bin/sh\necho nasiko 0.1.0\n');
const manifest = Buffer.from(JSON.stringify({
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
layers: [{
mediaType: 'application/gzip',
digest: sha256Digest(Buffer.from('verified archive')),
size: 16,
}],
}));
const archive = Buffer.from('verified archive');
try {
const result = await installNasiko({
version: 'v0.1.0',
yes: true,
installDir: installRoot,
}, {
platform: 'darwin',
arch: 'arm64',
releaseOverride: {
manifestDigest: sha256Digest(manifest),
binaryDigest: sha256Digest(binary),
},
fetchBytes: async (url) => url.includes('/manifests/') ? manifest : archive,
extractBinary: () => binary,
});
assert.strictEqual(result.installed, true);
assert.strictEqual(result.version, 'v0.1.0');
assert.strictEqual(fs.existsSync(path.join(installRoot, 'nasiko')), true);
assert.strictEqual(fs.existsSync(path.join(installRoot, '.ecc-nasiko-install.json')), true);
const { getQualifiedRelease, inspectInstalledNasiko, uninstallNasiko } = require('../../scripts/lib/nasiko-release');
const fakeRelease = {
...getQualifiedRelease('v0.1.0', 'darwin', 'arm64'),
manifestDigest: sha256Digest(manifest),
binaryDigest: sha256Digest(binary),
};
const preview = await uninstallNasiko({ installDir: installRoot, dryRun: true }, {
platform: 'darwin', arch: 'arm64',
});
assert.strictEqual(preview.dryRun, true);
assert.strictEqual(preview.version, 'v0.1.0');
assert.strictEqual(preview.destination, path.join(fs.realpathSync(installRoot), 'nasiko'));
let renameCount = 0;
await assert.rejects(async () => uninstallNasiko({ installDir: installRoot, yes: true }, {
platform: 'darwin', arch: 'arm64',
inspectInstalled: destination => inspectInstalledNasiko(destination, () => fakeRelease),
rename: (source, destination) => {
renameCount += 1;
if (renameCount === 2) throw new Error('metadata staging unavailable');
fs.renameSync(source, destination);
},
}), /metadata staging unavailable/i);
assert.strictEqual(fs.existsSync(path.join(installRoot, 'nasiko')), true);
assert.strictEqual(fs.existsSync(path.join(installRoot, '.ecc-nasiko-install.json')), true);
await uninstallNasiko({ installDir: installRoot, yes: true }, {
platform: 'darwin', arch: 'arm64',
inspectInstalled: destination => inspectInstalledNasiko(destination, () => fakeRelease),
});
assert.strictEqual(fs.existsSync(path.join(installRoot, 'nasiko')), false);
assert.strictEqual(fs.existsSync(path.join(installRoot, '.ecc-nasiko-install.json')), false);
} finally {
fs.rmSync(installRoot, { recursive: true, force: true });
}
}],
['rejects digest mismatch and unsafe archive entries without installing', async () => {
const { extractQualifiedTarGzip, installNasiko } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-reject-'));
const manifest = Buffer.from('{"schemaVersion":2,"layers":[]}');
try {
await assert.rejects(
installNasiko({ version: 'v0.1.0', yes: true, installDir: installRoot }, {
platform: 'darwin',
arch: 'arm64',
releaseOverride: { manifestDigest: `sha256:${'0'.repeat(64)}` },
fetchBytes: async () => manifest,
}),
/manifest digest mismatch/i
);
assert.strictEqual(fs.existsSync(path.join(installRoot, 'nasiko')), false);
assert.throws(() => extractQualifiedTarGzip(Buffer.from('not gzip'), 'nasiko'), /invalid|size limit/i);
} finally {
fs.rmSync(installRoot, { recursive: true, force: true });
}
}],
['accepts one complete tar entry and rejects malformed tar boundaries', () => {
const { extractQualifiedTarGzip } = require('../../scripts/lib/nasiko-release');
assert.deepStrictEqual(
extractQualifiedTarGzip(tarGzipFixture(), 'nasiko'),
Buffer.from('x')
);
assert.throws(
() => extractQualifiedTarGzip(tarGzipFixture({ padding: false }), 'nasiko'),
/unsafe|truncated|terminator/i
);
assert.throws(
() => extractQualifiedTarGzip(tarGzipFixture({ trailing: Buffer.from([1]) }), 'nasiko'),
/unsafe|trailing/i
);
assert.throws(
() => extractQualifiedTarGzip(tarGzipFixture({ sizeField: '00000000001x' }), 'nasiko'),
/size|octal|unsafe/i
);
assert.throws(
() => extractQualifiedTarGzip(tarGzipFixture({ terminatorBlocks: 1 }), 'nasiko'),
/terminator|truncated|unsafe/i
);
}],
['read-only status never executes an unqualified explicit executable', () => {
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-status-'));
const executable = path.join(fixtureRoot, 'nasiko');
const marker = path.join(fixtureRoot, 'executed');
fs.writeFileSync(executable, `#!/bin/sh\ntouch ${JSON.stringify(marker)}\nprintf "nasiko 0.1.0\\n"\n`, { mode: 0o755 });
try {
const result = spawnSync(process.execPath, [
path.join(REPO_ROOT, 'scripts', 'ecc.js'),
'nasiko',
'status',
'--json',
], {
encoding: 'utf8',
env: { ...process.env, ECC_NASIKO_CLI_EXECUTABLE: executable },
});
assert.strictEqual(result.status, 0, result.stderr);
const status = JSON.parse(result.stdout);
assert.strictEqual(status.installed, true);
assert.strictEqual(status.qualified, false);
assert.strictEqual(status.version, null);
assert.strictEqual(status.executable, executable);
assert.strictEqual(fs.existsSync(marker), false);
} finally {
fs.rmSync(fixtureRoot, { recursive: true, force: true });
}
}],
['read-only status has a stable absent result shape', () => {
const { readStatus } = require('../../scripts/nasiko');
const { normalizePlatform } = require('../../scripts/lib/nasiko-release');
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-absent-'));
try {
assert.deepStrictEqual(readStatus({ installDir: fixtureRoot }), {
installed: false,
qualified: false,
version: null,
executable: path.join(fs.realpathSync(fixtureRoot), normalizePlatform().binaryName),
});
} finally { fs.rmSync(fixtureRoot, { recursive: true, force: true }); }
}],
['rejects and never executes an unqualified pre-existing binary', async () => {
const { installNasiko } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-existing-'));
const executable = path.join(installRoot, 'nasiko');
const marker = path.join(installRoot, 'executed');
fs.writeFileSync(executable, `#!/bin/sh\ntouch ${JSON.stringify(marker)}\necho nasiko 0.1.0\n`, { mode: 0o755 });
try {
await assert.rejects(
installNasiko({ version: 'v0.1.0', yes: true, installDir: installRoot }, {
platform: 'darwin', arch: 'arm64',
}),
/unqualified|digest|metadata/i
);
assert.strictEqual(fs.existsSync(marker), false);
} finally {
fs.rmSync(installRoot, { recursive: true, force: true });
}
}],
['rolls back a published binary when metadata persistence fails', async () => {
const { installNasiko } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-rollback-'));
const binary = Buffer.from('qualified binary');
const archive = Buffer.from('verified archive');
const manifest = Buffer.from(JSON.stringify({
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
layers: [{ mediaType: 'application/gzip', digest: sha256Digest(archive), size: archive.length }],
}));
try {
await assert.rejects(
installNasiko({ version: 'v0.1.0', yes: true, installDir: installRoot }, {
platform: 'darwin',
arch: 'arm64',
releaseOverride: {
manifestDigest: sha256Digest(manifest),
binaryDigest: sha256Digest(binary),
},
fetchBytes: async url => url.includes('/manifests/') ? manifest : archive,
extractBinary: () => binary,
writeMetadata: () => { throw new Error('metadata unavailable'); },
}),
/metadata unavailable/i
);
assert.strictEqual(fs.existsSync(path.join(installRoot, 'nasiko')), false);
} finally {
fs.rmSync(installRoot, { recursive: true, force: true });
}
}],
['never overwrites or deletes a destination created during publication', async () => {
const { installNasiko } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-race-'));
const binary = Buffer.from('qualified binary');
const intruder = Buffer.from('concurrent owner');
const archive = Buffer.from('verified archive');
const manifest = Buffer.from(JSON.stringify({ schemaVersion: 2, layers: [{ mediaType: 'application/gzip', digest: sha256Digest(archive), size: archive.length }] }));
try {
await assert.rejects(installNasiko({ version: 'v0.1.0', yes: true, installDir: installRoot }, {
platform: 'darwin', arch: 'arm64',
releaseOverride: { manifestDigest: sha256Digest(manifest), binaryDigest: sha256Digest(binary) },
fetchBytes: async url => url.includes('/manifests/') ? manifest : archive,
extractBinary: () => binary,
beforePublish: destination => fs.writeFileSync(destination, intruder, { flag: 'wx' }),
}), /exist/i);
assert.deepStrictEqual(fs.readFileSync(path.join(installRoot, 'nasiko')), intruder);
} finally { fs.rmSync(installRoot, { recursive: true, force: true }); }
}],
['fails uninstall when staged tombstones cannot be removed', () => {
const { uninstallNasiko } = require('../../scripts/lib/nasiko-release');
const installRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-cleanup-failure-'));
const executable = path.join(installRoot, 'nasiko');
const metadataPath = path.join(installRoot, '.ecc-nasiko-install.json');
fs.writeFileSync(executable, 'qualified binary', { mode: 0o700 });
fs.writeFileSync(metadataPath, '{}', { mode: 0o600 });
try {
assert.throws(() => uninstallNasiko({ installDir: installRoot, yes: true }, {
platform: 'darwin',
arch: 'arm64',
inspectInstalled: () => ({ installed: true, qualified: true, version: 'v0.1.0' }),
remove: target => { throw new Error(`retained ${target}`); },
}), /incomplete|retained|cleanup/i);
assert.ok(fs.readdirSync(installRoot).some(name => name.includes('.remove-')));
} finally { fs.rmSync(installRoot, { recursive: true, force: true }); }
}],
['ships a canonical opt-in skill without silently bundling Nasiko', () => {
const skill = read('skills/nasiko-control-plane/SKILL.md');
assert.match(skill, /^name: nasiko-control-plane$/m);
assert.match(skill, /ecc nasiko status/i);
assert.match(skill, /explicit.*consent|explicit.*--yes/i);
assert.match(skill, /pinned.*v0\.1\.0/i);
assert.match(skill, /telemetry.*opt-in/i);
assert.match(skill, /never.*secrets|never.*credentials/i);
assert.match(skill, /install.*does not prove/i);
assert.match(skill, /ecc nasiko uninstall/i);
assert.doesNotMatch(skill, /curl[^\n]*\|[^\n]*bash|irm[^\n]*\|[^\n]*iex/i);
const modules = readJson('manifests/install-modules.json').modules;
const module = modules.find(candidate => candidate.id === 'nasiko-control-plane');
assert.ok(module, 'nasiko-control-plane module is missing');
assert.deepStrictEqual(module.paths, ['skills/nasiko-control-plane']);
assert.deepStrictEqual(module.dependencies, ['platform-configs']);
assert.strictEqual(module.defaultInstall, false);
assert.strictEqual(module.stability, 'experimental');
const components = readJson('manifests/install-components.json').components;
assert.deepStrictEqual(
components.find(candidate => candidate.id === 'capability:nasiko-control-plane'),
{
id: 'capability:nasiko-control-plane',
family: 'capability',
description: 'Experimental Nasiko CLI lifecycle bridge guidance for pinned installation, read-only status, qualified uninstall, and opt-in telemetry boundaries.',
modules: ['nasiko-control-plane'],
}
);
const profiles = readJson('manifests/install-profiles.json').profiles;
assert.ok(profiles.full.modules.includes('nasiko-control-plane'));
for (const [profileId, profile] of Object.entries(profiles)) {
if (profileId !== 'full') assert.ok(!profile.modules.includes('nasiko-control-plane'));
}
const packageJson = readJson('package.json');
assert.ok(packageJson.files.includes('skills/nasiko-control-plane/'));
assert.ok(packageJson.files.includes('scripts/nasiko.js'));
assert.ok(packageJson.files.includes('scripts/lib/'));
assert.ok(!packageJson.dependencies?.nasiko);
assert.ok(!packageJson.optionalDependencies?.nasiko);
}],
];
let passed = 0;
let failed = 0;
for (const [name, testFunction] of tests) {
if (await runTest(name, testFunction)) passed += 1;
else failed += 1;
}
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
main();