mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
feat(docker): add hardened CLI test harness (#2625)
* feat(install): add hardened Docker test harness * feat(docker): complete isolated CLI session lifecycle * fix(docker): exercise packed public CLI offline * fix(docker): close hardened harness review gaps * test(docker): bound harness subprocesses
This commit is contained in:
@@ -0,0 +1,420 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const { spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.join(__dirname, '..', '..');
|
||||
const harnessRoot = path.join(repoRoot, 'docker', 'plugin-setup');
|
||||
const SUBPROCESS_TIMEOUT_MS = 30_000;
|
||||
const files = {
|
||||
ci: path.join(repoRoot, '.github', 'workflows', 'ci.yml'),
|
||||
compose: path.join(harnessRoot, 'compose.yaml'),
|
||||
dockerfile: path.join(harnessRoot, 'Dockerfile'),
|
||||
fixtureProject: path.join(
|
||||
repoRoot,
|
||||
'tests',
|
||||
'fixtures',
|
||||
'docker-plugin-project',
|
||||
'package.json'
|
||||
),
|
||||
fixtureRunner: path.join(harnessRoot, 'run-fixture-tests.sh'),
|
||||
interactivePlan: path.join(harnessRoot, 'interactive-plan.js'),
|
||||
packageJson: path.join(repoRoot, 'package.json'),
|
||||
packedCliPreparer: path.join(harnessRoot, 'prepare-packed-cli.js'),
|
||||
platformRunner: path.join(harnessRoot, 'run-platform-tests.js'),
|
||||
planValidator: path.join(harnessRoot, 'verify-install-plan.js'),
|
||||
projectDirResolver: path.join(harnessRoot, 'resolve-project-dir.js'),
|
||||
realRunner: path.join(harnessRoot, 'run-real-cli.sh'),
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function read(filePath) {
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
}
|
||||
|
||||
function runNode(argv, options = {}) {
|
||||
const result = spawnSync(process.execPath, argv, {
|
||||
...options,
|
||||
shell: false,
|
||||
timeout: SUBPROCESS_TIMEOUT_MS,
|
||||
});
|
||||
assert.ifError(result.error);
|
||||
return result;
|
||||
}
|
||||
|
||||
console.log('\n=== Docker plugin setup harness tests ===\n');
|
||||
|
||||
test('ships the focused Docker harness and default fixture project', () => {
|
||||
for (const filePath of Object.values(files)) {
|
||||
assert.ok(
|
||||
fs.existsSync(filePath),
|
||||
`Missing ${path.relative(repoRoot, filePath)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('builds pinned Debian and Ubuntu images as a non-root user', () => {
|
||||
const dockerfile = read(files.dockerfile);
|
||||
const compose = read(files.compose);
|
||||
assert.match(dockerfile, /node:22-bookworm-slim@sha256:[a-f0-9]{64}/);
|
||||
assert.match(dockerfile, /ARG OS_IMAGE=/);
|
||||
assert.match(dockerfile, /FROM \$\{NODE_IMAGE\} AS node-runtime/);
|
||||
assert.match(dockerfile, /FROM \$\{OS_IMAGE\}/);
|
||||
assert.match(dockerfile, /COPY --from=node-runtime \/usr\/local\/ \/usr\/local\//);
|
||||
assert.match(dockerfile, /ARG CLAUDE_CODE_VERSION=\d+\.\d+\.\d+/);
|
||||
assert.match(dockerfile, /@anthropic-ai\/claude-code@\$\{CLAUDE_CODE_VERSION\}/);
|
||||
assert.match(dockerfile, /@iarna\/toml@2\.2\.5/);
|
||||
assert.match(dockerfile, /ajv@8\.20\.0/);
|
||||
assert.match(dockerfile, /sql\.js@1\.14\.1/);
|
||||
assert.match(dockerfile, /--ignore-scripts/);
|
||||
assert.match(
|
||||
dockerfile,
|
||||
/@anthropic-ai\/claude-code\/install\.cjs/
|
||||
);
|
||||
assert.match(dockerfile, /ENV DISABLE_AUTOUPDATER=1/);
|
||||
assert.match(dockerfile, /ENV HOME=\/tmp\/ecc-home/);
|
||||
assert.match(dockerfile, /ENV NODE_PATH=\/usr\/local\/lib\/node_modules/);
|
||||
assert.match(dockerfile, /chown 1000:1000 \/workspace/);
|
||||
assert.match(dockerfile, /USER 1000:1000/);
|
||||
assert.doesNotMatch(dockerfile, /:latest/);
|
||||
assert.match(compose, /image:\s*ecc-plugin-setup:debian/);
|
||||
assert.match(compose, /image:\s*ecc-plugin-setup:ubuntu/);
|
||||
assert.match(compose, /ubuntu:24\.04@sha256:[a-f0-9]{64}/);
|
||||
assert.match(compose, /real-cli-ubuntu:/);
|
||||
assert.match(
|
||||
compose,
|
||||
/fixture-tests:[\s\S]*?user:\s*["']1000:1000["']/
|
||||
);
|
||||
assert.strictEqual(
|
||||
(compose.match(/node:22-bookworm-slim@sha256:[a-f0-9]{64}/g) || []).length,
|
||||
1,
|
||||
'The pinned Node image must have one source of truth in Compose'
|
||||
);
|
||||
assert.match(compose, /x-node-image:\s*&node-image/);
|
||||
assert.match(compose, /image:\s*\*node-image/);
|
||||
assert.match(compose, /NODE_IMAGE:\s*\*node-image/);
|
||||
assert.match(compose, /OS_IMAGE:\s*\*node-image/);
|
||||
});
|
||||
|
||||
test('keeps checkout and source project read-only with hardened defaults', () => {
|
||||
const compose = read(files.compose);
|
||||
assert.match(compose, /network_mode:\s*none/);
|
||||
assert.match(compose, /x-real-cli:[\s\S]*?network_mode:\s*none[\s\S]*?services:/);
|
||||
assert.match(
|
||||
compose,
|
||||
/real-cli-networked:[\s\S]*?profiles:[\s\S]*?-\s*networked[\s\S]*?network_mode:\s*default/
|
||||
);
|
||||
assert.match(compose, /read_only:\s*true/);
|
||||
assert.match(compose, /no-new-privileges:true/);
|
||||
assert.match(compose, /cap_drop:\s*\n\s*-\s*ALL/);
|
||||
assert.match(compose, /pids_limit:\s*256/);
|
||||
assert.match(compose, /target:\s*\/ecc\s*\n\s*read_only:\s*true/);
|
||||
assert.match(compose, /target:\s*\/source-project\s*\n\s*read_only:\s*true/);
|
||||
assert.match(compose, /CLAUDE_CONFIG_DIR:\s*\/tmp\/ecc-claude-config/);
|
||||
assert.match(
|
||||
compose,
|
||||
/\/tmp:rw,nosuid,nodev,exec,size=\$\{ECC_TMPFS_SIZE:-2g\},uid=1000,gid=1000,mode=0700/
|
||||
);
|
||||
assert.match(
|
||||
compose,
|
||||
/\/workspace:rw,nosuid,nodev,noexec,size=\$\{ECC_WORKSPACE_SIZE:-1g\},uid=1000,gid=1000,mode=0700/
|
||||
);
|
||||
assert.match(compose, /NPM_CONFIG_CACHE:\s*\/tmp\/npm-cache/);
|
||||
assert.doesNotMatch(
|
||||
compose,
|
||||
/ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN|env_file:/
|
||||
);
|
||||
});
|
||||
|
||||
test('real runner copies into tmpfs and exposes only explicit safe modes', () => {
|
||||
const runner = read(files.realRunner);
|
||||
assert.match(runner, /ECC_PROJECT_DIR:-\/workspace\/project/);
|
||||
assert.match(runner, /mkdir -p "\$HOME" "\$CLAUDE_CONFIG_DIR" "\$NPM_CONFIG_CACHE"/);
|
||||
assert.match(runner, /dry-run\|install\|plugin\|shell/);
|
||||
assert.match(runner, /--target claude-project/);
|
||||
assert.match(runner, /--dry-run/);
|
||||
assert.match(runner, /verify-install-plan\.js.*--dry-run/);
|
||||
assert.match(runner, /resolve-project-dir\.js/);
|
||||
assert.match(
|
||||
runner,
|
||||
/project_dir="\$\([\s\S]*?resolve-project-dir\.js[\s\S]*?\)"\s*\nreadonly project_dir/
|
||||
);
|
||||
assert.doesNotMatch(runner, /readonly project_dir="\$\(/);
|
||||
assert.match(runner, /prepare-packed-cli\.js/);
|
||||
assert.match(runner, /run_ecc install/);
|
||||
assert.match(runner, /run_ecc list-installed --json/);
|
||||
assert.match(runner, /run_ecc doctor --target claude-project/);
|
||||
assert.match(runner, /\[\[ -e "\$project_dir\/\.claude" \]\]/);
|
||||
assert.doesNotMatch(
|
||||
runner,
|
||||
/scripts\/ecc\.js" setup|--move-scope|\bmigrate\b/
|
||||
);
|
||||
assert.doesNotMatch(runner, /scripts\/ecc\.js" install/);
|
||||
assert.doesNotMatch(runner, /\beval\b|rm\s+-rf/);
|
||||
});
|
||||
|
||||
test('prepares a local npm artifact through the confined public bin contract', () => {
|
||||
const preparer = read(files.packedCliPreparer);
|
||||
assert.match(preparer, /spawnSync\(executable, argv/);
|
||||
assert.match(preparer, /run\(['"]npm['"]/);
|
||||
assert.match(preparer, /['"]pack['"]/);
|
||||
assert.match(preparer, /['"]--ignore-scripts['"]/);
|
||||
assert.match(preparer, /npm_config_offline:\s*['"]true['"]/);
|
||||
assert.match(preparer, /run\(['"]tar['"]/);
|
||||
assert.match(preparer, /shell:\s*false/g);
|
||||
assert.match(
|
||||
preparer,
|
||||
/const CHILD_PROCESS_TIMEOUT_MS\s*=\s*5 \* 60 \* 1000;/
|
||||
);
|
||||
assert.match(preparer, /timeout:\s*CHILD_PROCESS_TIMEOUT_MS/);
|
||||
assert.doesNotMatch(preparer, /execSync\(|\beval\b/);
|
||||
|
||||
const { validatePackedPackage } = require(files.packedCliPreparer);
|
||||
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-packed-cli-'));
|
||||
|
||||
function createFixture(name, options = {}) {
|
||||
const packageRoot = path.join(fixtureRoot, name);
|
||||
fs.mkdirSync(path.join(packageRoot, 'scripts'), { recursive: true });
|
||||
fs.mkdirSync(path.join(packageRoot, 'manifests'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(packageRoot, 'package.json'),
|
||||
JSON.stringify({
|
||||
name: options.packageName || 'ecc-universal',
|
||||
version: '2.1.0',
|
||||
bin: options.bin === undefined ? { ecc: 'scripts/ecc.js' } : options.bin,
|
||||
})
|
||||
);
|
||||
fs.writeFileSync(path.join(packageRoot, 'scripts', 'ecc.js'), '#!/usr/bin/env node\n');
|
||||
fs.chmodSync(path.join(packageRoot, 'scripts', 'ecc.js'), 0o755);
|
||||
for (const manifest of [
|
||||
'install-components.json',
|
||||
'install-modules.json',
|
||||
'install-profiles.json',
|
||||
]) {
|
||||
if (manifest !== options.omitManifest) {
|
||||
fs.writeFileSync(path.join(packageRoot, 'manifests', manifest), '{}\n');
|
||||
}
|
||||
}
|
||||
return packageRoot;
|
||||
}
|
||||
|
||||
try {
|
||||
const validRoot = createFixture('valid');
|
||||
assert.strictEqual(
|
||||
validatePackedPackage(validRoot),
|
||||
path.join(validRoot, 'scripts', 'ecc.js')
|
||||
);
|
||||
|
||||
for (const [name, options, pattern] of [
|
||||
['wrong-name', { packageName: 'not-ecc' }, /package name/i],
|
||||
['missing-bin', { bin: {} }, /bin\.ecc/i],
|
||||
['escaping-bin', { bin: { ecc: '../escape.js' } }, /bin\.ecc/i],
|
||||
['missing-manifest', { omitManifest: 'install-profiles.json' }, /missing/i],
|
||||
]) {
|
||||
assert.throws(() => validatePackedPackage(createFixture(name, options)), pattern);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('normalizes the isolated project path before enforcing workspace containment', () => {
|
||||
const valid = runNode([
|
||||
files.projectDirResolver,
|
||||
'/workspace/nested/../project',
|
||||
], { encoding: 'utf8' });
|
||||
assert.strictEqual(valid.status, 0, valid.stderr);
|
||||
assert.strictEqual(valid.stdout.trim(), '/workspace/project');
|
||||
|
||||
for (const candidate of [
|
||||
'/workspace',
|
||||
'/workspace/../tmp/project',
|
||||
'/tmp/project',
|
||||
'workspace/project',
|
||||
]) {
|
||||
const invalid = runNode([
|
||||
files.projectDirResolver,
|
||||
candidate,
|
||||
], { encoding: 'utf8' });
|
||||
assert.strictEqual(invalid.status, 2, `${candidate}: ${invalid.stderr}`);
|
||||
assert.match(invalid.stderr, /within \/workspace/i);
|
||||
}
|
||||
});
|
||||
|
||||
test('fixture runner delegates to the cross-platform test entry point', () => {
|
||||
const runner = read(files.fixtureRunner);
|
||||
assert.match(runner, /id -u/);
|
||||
assert.match(runner, /id -g/);
|
||||
assert.match(runner, /must run as uid\/gid 1000:1000/i);
|
||||
assert.match(
|
||||
runner,
|
||||
/exec node docker\/plugin-setup\/run-platform-tests\.js/
|
||||
);
|
||||
});
|
||||
|
||||
test('uses one shell-free focused runner across Linux, macOS, and Windows', () => {
|
||||
const ci = read(files.ci);
|
||||
const packageJson = read(files.packageJson);
|
||||
const platformRunner = read(files.platformRunner);
|
||||
|
||||
assert.match(
|
||||
ci,
|
||||
/os:\s*\[ubuntu-latest,\s*windows-latest,\s*macos-latest\]/
|
||||
);
|
||||
assert.match(
|
||||
packageJson,
|
||||
/"test:plugin-setup-platform":\s*"node docker\/plugin-setup\/run-platform-tests\.js"/
|
||||
);
|
||||
assert.match(platformRunner, /spawnSync\(/);
|
||||
assert.match(platformRunner, /shell:\s*false/);
|
||||
assert.match(
|
||||
platformRunner,
|
||||
/const CHILD_PROCESS_TIMEOUT_MS\s*=\s*5 \* 60 \* 1000;/
|
||||
);
|
||||
assert.match(platformRunner, /timeout:\s*CHILD_PROCESS_TIMEOUT_MS/);
|
||||
assert.match(platformRunner, /Object\.fromEntries\(/);
|
||||
assert.match(platformRunner, /Object\.entries\(process\.env\)\.filter/);
|
||||
assert.doesNotMatch(platformRunner, /delete childEnv\[/);
|
||||
assert.match(platformRunner, /tests\/lib\/install-manifests\.test\.js/);
|
||||
assert.match(platformRunner, /tests\/lib\/install-targets\.test\.js/);
|
||||
assert.match(platformRunner, /tests\/lib\/install-executor\.test\.js/);
|
||||
assert.doesNotMatch(platformRunner, /\beval\b|execSync\(/);
|
||||
});
|
||||
|
||||
test('emits docker exec as an executable plus argv integration contract', () => {
|
||||
const result = runNode([
|
||||
files.interactivePlan,
|
||||
'--container', 'ecc-plugin-shell',
|
||||
'--workdir', '/workspace/project',
|
||||
'--json',
|
||||
'--',
|
||||
'node',
|
||||
'-p',
|
||||
'process.stdin.isTTY',
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.deepStrictEqual(JSON.parse(result.stdout), {
|
||||
contractVersion: 1,
|
||||
executable: 'docker',
|
||||
argv: [
|
||||
'exec',
|
||||
'-it',
|
||||
'-w',
|
||||
'/workspace/project',
|
||||
'ecc-plugin-shell',
|
||||
'node',
|
||||
'-p',
|
||||
'process.stdin.isTTY',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps Docker session values as argv entries and validates boundaries', () => {
|
||||
const literalArgument = '$(touch should-not-run)';
|
||||
const result = runNode([
|
||||
files.interactivePlan,
|
||||
'--container', 'ecc.plugin-shell_1',
|
||||
'--workdir', '/workspace/project with spaces',
|
||||
'--json',
|
||||
'--',
|
||||
'printf',
|
||||
'%s',
|
||||
literalArgument,
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.deepStrictEqual(JSON.parse(result.stdout).argv.slice(-3), [
|
||||
'printf',
|
||||
'%s',
|
||||
literalArgument,
|
||||
]);
|
||||
|
||||
for (const args of [
|
||||
['--container', '../escape', '--json'],
|
||||
['--container', 'valid-name', '--workdir', 'relative/path', '--json'],
|
||||
['--container', 'valid-name', '--workdir', '/workspace/../tmp', '--json'],
|
||||
]) {
|
||||
const invalid = runNode([files.interactivePlan, ...args], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.strictEqual(invalid.status, 2);
|
||||
assert.match(invalid.stderr, /invalid/i);
|
||||
}
|
||||
});
|
||||
|
||||
test('validates dry-run target confinement and nonempty operations', () => {
|
||||
const projectDir = path.join(repoRoot, 'workspace-project');
|
||||
const installRoot = path.join(projectDir, '.claude');
|
||||
const safePlan = {
|
||||
dryRun: true,
|
||||
plan: {
|
||||
target: 'claude-project',
|
||||
installRoot,
|
||||
operations: [
|
||||
{ destinationPath: path.join(installRoot, 'rules', 'ecc', 'base.md') },
|
||||
],
|
||||
},
|
||||
};
|
||||
const safe = runNode(
|
||||
[files.planValidator, projectDir, '--dry-run'],
|
||||
{ encoding: 'utf8', input: JSON.stringify(safePlan) }
|
||||
);
|
||||
assert.strictEqual(safe.status, 0, safe.stderr);
|
||||
|
||||
const unsafePlan = {
|
||||
...safePlan,
|
||||
plan: {
|
||||
...safePlan.plan,
|
||||
operations: [{ destinationPath: '/tmp/escape.md' }],
|
||||
},
|
||||
};
|
||||
const unsafe = runNode(
|
||||
[files.planValidator, projectDir, '--dry-run'],
|
||||
{ encoding: 'utf8', input: JSON.stringify(unsafePlan) }
|
||||
);
|
||||
assert.strictEqual(unsafe.status, 1);
|
||||
assert.match(unsafe.stderr, /outside/i);
|
||||
|
||||
for (const installRootValue of [undefined, 42, { path: installRoot }]) {
|
||||
const invalidRootPlan = {
|
||||
...safePlan,
|
||||
plan: {
|
||||
...safePlan.plan,
|
||||
installRoot: installRootValue,
|
||||
},
|
||||
};
|
||||
const invalidRoot = runNode(
|
||||
[files.planValidator, projectDir, '--dry-run'],
|
||||
{ encoding: 'utf8', input: JSON.stringify(invalidRootPlan) }
|
||||
);
|
||||
assert.strictEqual(invalidRoot.status, 1);
|
||||
assert.match(invalidRoot.stderr, /install root is not confined/i);
|
||||
assert.doesNotMatch(invalidRoot.stderr, /ERR_INVALID_ARG_TYPE|TypeError/);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "ecc-docker-plugin-test-project",
|
||||
"version": "0.0.0",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..', '..');
|
||||
const skillPath = path.join(repoRoot, 'skills', 'docker-patterns', 'SKILL.md');
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const skill = fs.readFileSync(skillPath, 'utf8');
|
||||
|
||||
console.log('\n=== Docker patterns skill tests ===\n');
|
||||
|
||||
test('triggers for hardened installer and cross-platform harness work', () => {
|
||||
const frontmatter = skill.match(/^---\n([\s\S]*?)\n---/);
|
||||
assert.ok(frontmatter, 'SKILL.md frontmatter is missing');
|
||||
assert.match(frontmatter[1], /description:.*installer/i);
|
||||
assert.match(frontmatter[1], /description:.*macOS.*Windows/i);
|
||||
});
|
||||
|
||||
test('documents the ECC plugin setup harness and safe operating modes', () => {
|
||||
assert.match(skill, /docker\/plugin-setup\/compose\.yaml/);
|
||||
assert.match(skill, /\breal-cli\b/);
|
||||
assert.match(skill, /\breal-cli-ubuntu\b/);
|
||||
assert.match(skill, /\bfixture-tests\b/);
|
||||
assert.match(skill, /dry-run.*install.*plugin.*shell/is);
|
||||
assert.doesNotMatch(skill, /explicit modes such as.*migrate/i);
|
||||
});
|
||||
|
||||
test('requires hardened ephemeral installer execution', () => {
|
||||
for (const pattern of [
|
||||
/read[_ -]only/i,
|
||||
/tmpfs/i,
|
||||
/no-new-privileges/i,
|
||||
/cap_drop/i,
|
||||
/pids_limit/i,
|
||||
/non-root/i,
|
||||
/digest/i,
|
||||
/credential/i,
|
||||
]) {
|
||||
assert.match(skill, pattern);
|
||||
}
|
||||
});
|
||||
|
||||
test('states the honest macOS and Windows validation boundary', () => {
|
||||
assert.match(skill, /macOS cannot run as a Docker container/i);
|
||||
assert.match(skill, /Windows containers require a Windows Docker engine/i);
|
||||
assert.match(skill, /native.*ubuntu.*macOS.*Windows.*CI/is);
|
||||
assert.doesNotMatch(skill, /macOS container image|simulate Windows/i);
|
||||
});
|
||||
|
||||
test('provides a repeatable build, run, inspect, and cleanup sequence', () => {
|
||||
assert.match(skill, /docker compose.*build.*real-cli.*real-cli-ubuntu/is);
|
||||
assert.match(skill, /docker compose.*run.*real-cli.*dry-run/is);
|
||||
assert.match(skill, /docker image inspect/is);
|
||||
assert.match(skill, /down --remove-orphans/);
|
||||
});
|
||||
|
||||
test('documents the private named-container lifecycle and terminal boundary', () => {
|
||||
assert.match(skill, /ECC_TMPFS_SIZE/);
|
||||
assert.match(skill, /\/workspace.*mode=0700/is);
|
||||
assert.match(skill, /NPM_CONFIG_CACHE.*\/tmp\/npm-cache/is);
|
||||
assert.match(skill, /docker compose.*run.*--detach.*--name/is);
|
||||
assert.match(skill, /interactive-plan\.js/);
|
||||
assert.match(skill, /executable.*argv/is);
|
||||
assert.match(skill, /docker exec -it/);
|
||||
assert.match(skill, /reconnect/i);
|
||||
assert.match(skill, /docker rm.*ecc-plugin-shell/is);
|
||||
assert.match(skill, /host credentials.*opt-in/is);
|
||||
assert.doesNotMatch(skill, /skills\/docker-patterns\/scripts\/open-interactive\.js/);
|
||||
});
|
||||
|
||||
test('requires the offline smoke to execute the locally packed public bin', () => {
|
||||
assert.match(skill, /npm pack.*--ignore-scripts/is);
|
||||
assert.match(skill, /package\.json.*bin\.ecc/is);
|
||||
assert.match(skill, /locally packed/i);
|
||||
assert.match(skill, /network_mode:\s*none/);
|
||||
assert.match(skill, /does not\s+rely on.*host `node_modules`/is);
|
||||
});
|
||||
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user