'use strict'; const assert = require('assert'); const crypto = require('crypto'); const fs = require('fs'); const os = require('os'); const path = require('path'); const lifecycle = require('./packed-artifact-lifecycle'); let passed = 0; let failed = 0; function test(name, fn) { try { fn(); console.log(` ✓ ${name}`); passed += 1; } catch (error) { console.log(` ✗ ${name}`); console.log(` Error: ${error.message}`); failed += 1; } } console.log('\n=== Testing packed-artifact lifecycle runner ===\n'); test('resolves package and hash from explicit environment variables', () => { const options = lifecycle.parseEnvironment({ ECC_RELEASE_PACKAGE: 'release-artifacts/ecc-universal-2.2.0.tgz', ECC_RELEASE_SHA256: 'a'.repeat(64), }, '/workspace'); assert.strictEqual( options.packagePath, path.resolve('/workspace', 'release-artifacts/ecc-universal-2.2.0.tgz') ); assert.strictEqual(options.expectedSha256, 'a'.repeat(64)); }); test('rejects missing, malformed, and non-tgz release inputs', () => { assert.throws(() => lifecycle.parseEnvironment({}, '/workspace'), /ECC_RELEASE_PACKAGE/); assert.throws(() => lifecycle.parseEnvironment({ ECC_RELEASE_PACKAGE: 'package.zip', ECC_RELEASE_SHA256: 'a'.repeat(64), }, '/workspace'), /\.tgz/); assert.throws(() => lifecycle.parseEnvironment({ ECC_RELEASE_PACKAGE: 'release-artifacts/ecc-universal-2.2.0.tgz', ECC_RELEASE_SHA256: 'not-a-hash', }, '/workspace'), /SHA-256/); assert.throws(() => lifecycle.parseEnvironment({ ECC_RELEASE_PACKAGE: '../release-artifacts/ecc-universal-2.2.0.tgz', ECC_RELEASE_SHA256: 'a'.repeat(64), }, '/workspace'), /release-artifacts/); assert.throws(() => lifecycle.parseEnvironment({ ECC_RELEASE_PACKAGE: '/tmp/ecc-universal-2.2.0.tgz', ECC_RELEASE_SHA256: 'a'.repeat(64), }, '/workspace'), /release-artifacts/); }); test('hashFile computes a lowercase SHA-256 digest', () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-packed-hash-')); const filePath = path.join(tempDir, 'package.tgz'); try { fs.writeFileSync(filePath, 'exact packed bytes'); const expected = crypto.createHash('sha256').update('exact packed bytes').digest('hex'); assert.strictEqual(lifecycle.hashFile(filePath), expected); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } }); test('assertHash rejects an artifact whose bytes do not match', () => { assert.throws( () => lifecycle.assertHash('a'.repeat(64), 'b'.repeat(64)), /does not match/ ); }); test('lifecycle child processes receive no inherited credentials', () => { const environment = lifecycle.createLifecycleEnvironment({ PATH: '/tools', GITHUB_TOKEN: 'github-secret', NODE_AUTH_TOKEN: 'npm-secret', ACTIONS_RUNTIME_TOKEN: 'actions-secret', AWS_SECRET_ACCESS_KEY: 'cloud-secret', }, '/isolated-home'); assert.strictEqual(environment.PATH, '/tools'); assert.strictEqual(environment.HOME, '/isolated-home'); assert.strictEqual(environment.USERPROFILE, '/isolated-home'); assert.strictEqual(environment.GITHUB_TOKEN, undefined); assert.strictEqual(environment.NODE_AUTH_TOKEN, undefined); assert.strictEqual(environment.ACTIONS_RUNTIME_TOKEN, undefined); assert.strictEqual(environment.AWS_SECRET_ACCESS_KEY, undefined); }); test('public CLI invocations use npm exec instead of internal package paths', () => { const invocation = lifecycle.getNpmExecInvocation( ['ecc-universal', 'setup', '--help'], { ComSpec: 'C:\\Windows\\System32\\cmd.exe' }, 'win32' ); assert.strictEqual(invocation.command, 'C:\\Windows\\System32\\cmd.exe'); assert.deepStrictEqual(invocation.args, [ '/d', '/s', '/c', 'npm exec --offline --yes=false -- ecc-universal setup --help', ]); const unixInvocation = lifecycle.getNpmExecInvocation( ['ecc', 'doctor', '--target', 'cursor', '--json'], {}, 'linux' ); assert.strictEqual(unixInvocation.command, 'npm'); assert.deepStrictEqual( unixInvocation.args.slice(0, 4), ['exec', '--offline', '--yes=false', '--'] ); assert.strictEqual(unixInvocation.args[4], 'ecc'); assert.ok(!unixInvocation.args.some(argument => argument.includes('node_modules'))); }); test('lifecycle cleanup retries Windows file locks without masking results', () => { const source = fs.readFileSync( path.join(__dirname, 'packed-artifact-lifecycle.js'), 'utf8' ); assert.match(source, /maxRetries:\s*10/); assert.match(source, /retryDelay:\s*100/); assert.match(source, /Could not remove lifecycle temp root/); }); console.log(`\nPassed: ${passed}`); console.log(`Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);