mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-10 10:06:00 +00:00
test: skip symlink tests when the OS refuses symlink creation
Adds a trySymlink helper that logs the existing skip notice and returns false on EPERM/EACCES/ENOSYS, and uses it where tests created symlinks unguarded. Non-admin Windows accounts without Developer Mode now skip these cases instead of failing.
This commit is contained in:
1 parent
25278e7d7b
commit
09de2c247a
12 files changed
+52
-21
No files matched your search
@@ -17,6 +17,7 @@ const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { resolvePython } = require('./lib/helpers/windows-test-env');
|
||||
const { trySymlink } = require('./lib/helpers/symlink-support');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const GUARD = path.join(repoRoot, 'hooks', 'hermes', 'config-protection.py');
|
||||
@@ -111,7 +112,7 @@ run('blocks existing case-variant protected names on every filesystem', () => {
|
||||
run('blocks a dangling symlink to a protected name', () => {
|
||||
const dangling = path.join(tmp, 'eslint.config.cjs');
|
||||
try { fs.unlinkSync(dangling); } catch { /* not present yet */ }
|
||||
fs.symlinkSync(path.join(tmp, 'does-not-exist-at-all'), dangling);
|
||||
if (!trySymlink(path.join(tmp, 'does-not-exist-at-all'), dangling)) return;
|
||||
// Sanity: the link itself exists (lstat succeeds) but resolves nowhere —
|
||||
// os.path.exists() is False here, which is exactly the bypass being pinned.
|
||||
assert.ok(fs.lstatSync(dangling).isSymbolicLink(), 'symlink must exist for the case to be real');
|
||||
|
||||
@@ -4,6 +4,7 @@ const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { trySymlink } = require('./helpers/symlink-support');
|
||||
|
||||
const {
|
||||
beginLegacySyncState,
|
||||
@@ -179,7 +180,7 @@ function runTests() {
|
||||
|
||||
fs.writeFileSync(outsidePath, '<!-- BEGIN ECC -->\n# Outside\n<!-- END ECC -->\n');
|
||||
fs.rmSync(agentsPath);
|
||||
fs.symlinkSync(outsidePath, agentsPath);
|
||||
if (!trySymlink(outsidePath, agentsPath)) return;
|
||||
const symlinkResult = uninstallLegacyCodexSync({ codexHome });
|
||||
assert.strictEqual(symlinkResult.status, 'partial');
|
||||
assert.ok(symlinkResult.retainedPaths.includes(agentsPath));
|
||||
@@ -237,7 +238,7 @@ function runTests() {
|
||||
const linkedPath = path.join(codexHome, 'prompts', 'ecc-plan.md');
|
||||
fs.mkdirSync(path.dirname(linkedPath), { recursive: true });
|
||||
fs.writeFileSync(outsidePath, '# Outside\n');
|
||||
fs.symlinkSync(outsidePath, linkedPath);
|
||||
if (!trySymlink(outsidePath, linkedPath)) return;
|
||||
const statePath = beginLegacySyncState({
|
||||
codexHome,
|
||||
backupDir: path.join(codexHome, 'backups', 'ecc-test'),
|
||||
@@ -257,7 +258,7 @@ function runTests() {
|
||||
const outsideDir = path.join(homeDir, 'outside');
|
||||
fs.mkdirSync(codexHome, { recursive: true });
|
||||
fs.mkdirSync(outsideDir, { recursive: true });
|
||||
fs.symlinkSync(outsideDir, path.join(codexHome, 'prompts'));
|
||||
if (!trySymlink(outsideDir, path.join(codexHome, 'prompts'))) return;
|
||||
const statePath = beginLegacySyncState({
|
||||
codexHome,
|
||||
backupDir: path.join(codexHome, 'backups', 'ecc-test'),
|
||||
@@ -289,7 +290,7 @@ function runTests() {
|
||||
fs.writeFileSync(promptPath, '# ECC generated prompt\n');
|
||||
finalizeLegacySyncState({ statePath });
|
||||
fs.rmSync(promptPath);
|
||||
fs.symlinkSync(outsidePath, promptPath);
|
||||
if (!trySymlink(outsidePath, promptPath)) return;
|
||||
|
||||
const result = uninstallLegacyCodexSync({ codexHome });
|
||||
assert.strictEqual(result.status, 'partial');
|
||||
@@ -481,7 +482,7 @@ function runTests() {
|
||||
});
|
||||
recordLegacySyncPath({ statePath, filePath: promptPath });
|
||||
fs.rmSync(promptPath);
|
||||
fs.symlinkSync(outsidePath, promptPath);
|
||||
if (!trySymlink(outsidePath, promptPath)) return;
|
||||
|
||||
const result = rollbackLegacyCodexSync({ statePath });
|
||||
assert.strictEqual(result.status, 'partial');
|
||||
|
||||
@@ -6,6 +6,7 @@ const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { test, tempDir, cleanup, finish } = require('./helpers');
|
||||
const gate = require('../../../scripts/lib/eval-harness/gate');
|
||||
const { trySymlink } = require('../helpers/symlink-support');
|
||||
const library = path.resolve(__dirname, '../../../scripts/lib/eval-harness');
|
||||
const refused = error => error.code === 'gate.isolation_required';
|
||||
const invalidVariant = error => error.code === 'gate.variant_invalid';
|
||||
@@ -109,11 +110,11 @@ test('symlink manifests and symlink trees cannot hide from digest', () => setup(
|
||||
const file = path.join(c.variant, 'variant.json');
|
||||
const outside = path.join(c.root, 'manifest.json');
|
||||
fs.renameSync(file, outside);
|
||||
fs.symlinkSync(outside, file);
|
||||
if (!trySymlink(outside, file)) return;
|
||||
assert.throws(() => gate.loadVariant(c.variant), invalidVariant);
|
||||
fs.unlinkSync(file);
|
||||
fs.renameSync(outside, file);
|
||||
fs.symlinkSync(c.root, path.join(c.variant, 'link'));
|
||||
if (!trySymlink(c.root, path.join(c.variant, 'link'))) return;
|
||||
assert.throws(() => gate.loadVariant(c.variant), invalidVariant);
|
||||
}));
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const UNSUPPORTED_CODES = new Set(['EPERM', 'EACCES', 'ENOSYS']);
|
||||
|
||||
// Creating symlinks needs Developer Mode or admin rights on Windows. Returns
|
||||
// false (after logging a skip notice) when the OS refuses; rethrows anything else.
|
||||
function trySymlink(target, linkPath, type) {
|
||||
try {
|
||||
fs.symlinkSync(target, linkPath, type);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (!UNSUPPORTED_CODES.has(error.code)) throw error;
|
||||
console.log(' (symlink unsupported on this platform; skipping)');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { trySymlink };
|
||||
@@ -5,6 +5,7 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { trySymlink } = require('./helpers/symlink-support');
|
||||
|
||||
const {
|
||||
MAX_DIAGNOSTICS,
|
||||
@@ -276,7 +277,7 @@ test('never follows a pre-existing destination symlink during create-only public
|
||||
fs.mkdirSync(notes, { recursive: true });
|
||||
fs.writeFileSync(outside, 'outside sentinel');
|
||||
const destination = path.join(notes, 'mem_20260726_01kexample.md');
|
||||
fs.symlinkSync(outside, destination);
|
||||
if (!trySymlink(outside, destination)) return;
|
||||
|
||||
assert.throws(
|
||||
() => saveMemory(
|
||||
@@ -355,7 +356,7 @@ test('rejects a vault path that traverses a symlink before creating directories'
|
||||
const fixture = createFixture();
|
||||
const outside = path.join(fixture.root, 'outside');
|
||||
fs.mkdirSync(outside);
|
||||
fs.symlinkSync(outside, path.join(fixture.projectRoot, '.ecc'));
|
||||
if (!trySymlink(outside, path.join(fixture.projectRoot, '.ecc'))) return;
|
||||
try {
|
||||
assert.throws(
|
||||
() => saveMemory(
|
||||
@@ -377,7 +378,7 @@ test('rejects a symlinked ancestor when roots come back from initializeVault', (
|
||||
try {
|
||||
const initialized = initializeVault({ roots: fixture.roots, scopes: ['project'] });
|
||||
fs.rmSync(path.join(fixture.projectRoot, '.ecc'), { recursive: true, force: true });
|
||||
fs.symlinkSync(outside, path.join(fixture.projectRoot, '.ecc'));
|
||||
if (!trySymlink(outside, path.join(fixture.projectRoot, '.ecc'))) return;
|
||||
|
||||
assert.throws(
|
||||
() => saveMemory(
|
||||
@@ -564,7 +565,7 @@ test('opens regular text files without following a stable symlink', () => {
|
||||
const link = path.join(root, 'link.md');
|
||||
try {
|
||||
fs.writeFileSync(target, 'safe');
|
||||
fs.symlinkSync(target, link);
|
||||
if (!trySymlink(target, link)) return;
|
||||
assert.strictEqual(readRegularTextFile(target, { maxBytes: 16 }), 'safe');
|
||||
assert.throws(
|
||||
() => readRegularTextFile(link, { maxBytes: 16 }),
|
||||
|
||||
@@ -16,6 +16,7 @@ const { repairInstalledStates, buildDoctorReport } = load(require.resolve('../..
|
||||
const { createInstallState, readInstallState, writeInstallState } = require('../../scripts/lib/install-state');
|
||||
const { withOpenCodeInstallLocks } = load(require.resolve('../../scripts/lib/install/opencode-install-lock'));
|
||||
const { removeVerifiedLegacyFile } = require('../../scripts/lib/install/opencode-legacy-migration');
|
||||
const { trySymlink } = require('./helpers/symlink-support');
|
||||
|
||||
const SOURCE_RELATIVE_PATH = path.join('skills', 'skill-comply', 'SKILL.md');
|
||||
const SKILL_CONTENT = '---\nname: skill-comply\ndescription: Synthetic migration fixture.\n---\n\n# Inert fixture\n';
|
||||
@@ -342,7 +343,7 @@ function runTests() {
|
||||
if (defect === 'missing-digest') delete state.operations[2].contentSha256;
|
||||
if (defect === 'source-mismatch') fs.writeFileSync(source, 'different trusted source\n');
|
||||
if (defect === 'source-missing') fs.unlinkSync(source);
|
||||
if (defect === 'symlink') { fs.unlinkSync(plugin); fs.symlinkSync(source, plugin); }
|
||||
if (defect === 'symlink') { fs.unlinkSync(plugin); if (!trySymlink(source, plugin)) return; }
|
||||
if (defect === 'unrecorded-alias') fs.copyFileSync(source, path.join(value.legacyRoot, 'plugins/index.js'));
|
||||
if (defect === 'merge-json') state.operations[1].kind = 'merge-json';
|
||||
writeInstallState(value.legacyStatePath, state);
|
||||
|
||||
@@ -7,6 +7,7 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { trySymlink } = require('./helpers/symlink-support');
|
||||
|
||||
const {
|
||||
createStateStore,
|
||||
@@ -399,7 +400,7 @@ async function runTests() {
|
||||
|
||||
try {
|
||||
fs.writeFileSync(targetPath, 'do not overwrite');
|
||||
fs.symlinkSync(targetPath, dbPath);
|
||||
if (!trySymlink(targetPath, dbPath)) return;
|
||||
|
||||
await assert.rejects(
|
||||
() => createStateStore({ dbPath }),
|
||||
@@ -419,7 +420,7 @@ async function runTests() {
|
||||
|
||||
try {
|
||||
fs.mkdirSync(outsideDir);
|
||||
fs.symlinkSync(outsideDir, linkedParent, process.platform === 'win32' ? 'junction' : 'dir');
|
||||
if (!trySymlink(outsideDir, linkedParent, process.platform === 'win32' ? 'junction' : 'dir')) return;
|
||||
|
||||
await assert.rejects(
|
||||
() => createStateStore({ dbPath }),
|
||||
|
||||
@@ -9,6 +9,7 @@ const { pathToFileURL } = require('url');
|
||||
const { createManifestInstallPlan, applyInstallPlan, previewInstallPlan } = require('../../scripts/lib/install-executor');
|
||||
const { withHookConsent } = require('../../scripts/lib/install/hook-consent');
|
||||
const { repairInstalledStates } = require('../../scripts/lib/install-lifecycle');
|
||||
const { trySymlink } = require('../lib/helpers/symlink-support');
|
||||
const repo = path.resolve(__dirname, '../..');
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
@@ -186,7 +187,7 @@ for (const scope of ['package.json', 'lib/package.json']) {
|
||||
test(`symlinked ${scope} refuses before mutation`, f => {
|
||||
const file = path.join(f.scripts, scope); const outside = path.join(f.dir, 'user-package.json');
|
||||
f.write(outside, '{"type":"commonjs"}\n'); fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.symlinkSync(outside, file); const before = snapshot(f.dir);
|
||||
if (!trySymlink(outside, file)) return; const before = snapshot(f.dir);
|
||||
assert.notStrictEqual(f.install().status, 0); assert.deepStrictEqual(snapshot(f.dir), before);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const { normalizeManifest, buildInventory, collectResources, collectTaskFiles, r
|
||||
const now = '2026-09-08T06:30:00.000Z';
|
||||
const task = (id, paths, extra = {}) => ({ id, repoId: 'repo', paths, ...extra });
|
||||
const fixture = () => ({ version: 1, repositories: [{ id: 'repo', sources: { 'src/a.js': "require('../lib/b')", 'lib/b.js': '' } }], tasks: [task('a', ['src/a.js']), task('b', ['lib/b.js'])], leases: [] });
|
||||
const { trySymlink } = require('../lib/helpers/symlink-support');
|
||||
const run = value => buildInventory(value, { now });
|
||||
|
||||
test('direct import warns when exact-path baseline would miss it; deterministic JSON', () => {
|
||||
@@ -87,7 +88,7 @@ test('task file adapter reads structured status, labels mtime, skips symlinks an
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'coordination-test-'));
|
||||
try {
|
||||
fs.mkdirSync(path.join(dir, 'worker')); fs.writeFileSync(path.join(dir, 'worker', 'STATUS.md'), '- State: running\n- Updated: 2026-09-08T06:29:00Z\n');
|
||||
fs.symlinkSync(path.join(dir, 'worker'), path.join(dir, 'linked'));
|
||||
if (!trySymlink(path.join(dir, 'worker'), path.join(dir, 'linked'))) return;
|
||||
const r = collectTaskFiles(dir); assert.equal(r.tasks.length, 1); assert.equal(r.tasks[0].status, 'running');
|
||||
assert.ok(r.tasks[0].statusFileModifiedAt); assert.equal(r.tasks[0].heartbeatAt, '2026-09-08T06:29:00Z');
|
||||
fs.writeFileSync(path.join(dir, 'large.json'), ' '.repeat(1024 * 1024 + 1));
|
||||
|
||||
@@ -7,6 +7,7 @@ const path = require('path');
|
||||
const { spawn, spawnSync } = require('child_process');
|
||||
const { PassThrough } = require('stream');
|
||||
const { pathToFileURL } = require('url');
|
||||
const { trySymlink } = require('../lib/helpers/symlink-support');
|
||||
|
||||
const SERVER = path.join(__dirname, '..', '..', 'scripts', 'memory-mcp.mjs');
|
||||
const {
|
||||
@@ -450,7 +451,7 @@ async function main() {
|
||||
await test('starts when the npm bin invokes the server through a symlink', async () => {
|
||||
const binRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-memory-bin-'));
|
||||
const binPath = path.join(binRoot, 'ecc-memory-mcp');
|
||||
fs.symlinkSync(SERVER, binPath);
|
||||
if (!trySymlink(SERVER, binPath)) return;
|
||||
try {
|
||||
await withClient(async client => {
|
||||
const tools = await client.listTools();
|
||||
|
||||
@@ -11,6 +11,7 @@ const scriptPath = path.join(repoRoot, 'skills/master-agreement-generator/script
|
||||
const templatePath = path.join(repoRoot, 'skills/master-agreement-generator/references/master-template.example.md');
|
||||
const specPath = path.join(repoRoot, 'skills/master-agreement-generator/references/spec.example.json');
|
||||
const builder = require(scriptPath);
|
||||
const { trySymlink } = require('../lib/helpers/symlink-support');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
@@ -215,7 +216,7 @@ for (const extension of ['md', 'docx']) {
|
||||
fs.mkdirSync(fixture.outDir);
|
||||
const target = path.join(fixture.artifacts, 'external');
|
||||
if (!dangling) fs.writeFileSync(target, 'do not overwrite');
|
||||
fs.symlinkSync(target, path.join(fixture.outDir, `Acme MASTER.${extension}`), 'file');
|
||||
if (!trySymlink(target, path.join(fixture.outDir, `Acme MASTER.${extension}`), 'file')) return;
|
||||
const other = extension === 'md' ? 'docx' : 'md';
|
||||
fs.writeFileSync(path.join(fixture.outDir, `Acme MASTER.${other}`), 'existing output');
|
||||
const before = snapshot(fixture.artifacts);
|
||||
|
||||
@@ -8,6 +8,7 @@ const { spawnSync } = require('node:child_process');
|
||||
const { test } = require('node:test');
|
||||
const script = path.resolve(__dirname, '../../skills/frontend-design-direction/scripts/check-evidence.js');
|
||||
const { checkEvidence } = require(script);
|
||||
const { trySymlink } = require('../lib/helpers/symlink-support');
|
||||
|
||||
function report() {
|
||||
const observation = { status: 'pass', evidence: 'Observed in the local test run.' };
|
||||
@@ -118,8 +119,8 @@ test('CLI requires a nonempty local artifact and fails safely on invalid input',
|
||||
assert.equal(run(input).status, 1, 'empty artifact must fail');
|
||||
fs.writeFileSync(path.join(dir, 'render.png'), 'nonempty local artifact');
|
||||
fs.writeFileSync(path.join(external, 'render.png'), 'nonempty external artifact');
|
||||
fs.symlinkSync(path.join(dir, 'render.png'), path.join(dir, 'linked.png'));
|
||||
fs.symlinkSync(path.join(external, 'render.png'), path.join(dir, 'outside'));
|
||||
if (!trySymlink(path.join(dir, 'render.png'), path.join(dir, 'linked.png'))) return;
|
||||
if (!trySymlink(path.join(external, 'render.png'), path.join(dir, 'outside'))) return;
|
||||
fs.symlinkSync(external, path.join(dir, 'external-dir'), 'junction');
|
||||
for (const artifact of ['.', 'https://example.com/render.png', path.join(dir, 'render.png'),
|
||||
'../render.png', 'C:\\render.png', 'linked.png', 'outside', 'external-dir/render.png']) {
|
||||
|
||||
Reference in new issue
Block a user