santifer/career-ops · warning
symlink traversal test skipped
Error message
symlink traversal test skipped: ${e.message} What it means
The plugin-loader test suite validates that symlink-based path traversal (an entry or skill symlink escaping the plugin directory) is rejected. The traversal checks are wrapped in try/catch that swaps in a captured console.warn; any unexpected exception inside the block (e.g. filesystem without symlink support, an unexpected throw from validateEntry) downgrades the whole block to this warning instead of failing the run.
Solutions
- On Windows, enable Developer Mode or run the terminal as administrator so symlink creation is permitted, or run the suite under WSL/Linux.
- If the validator is throwing, fix it to return null for invalid manifests — the test expects a null verdict, not an exception.
- Run on a filesystem that supports symlinks; skip acceptance is fine on exotic filesystems.
- Inspect e.message in the warning to distinguish an environment limitation (EPERM) from a code bug (unexpected TypeError).
Example fix
// before (fixture setup)
fs.symlinkSync(escapeTarget, linkedPath);
// after
try { fs.symlinkSync(escapeTarget, linkedPath); } catch (e) { if (e.code === 'EPERM') return t.skip('symlinks unavailable'); throw e; } Defensive patterns
Strategy: try-catch
Validate before calling
let symlinkOk = true; try { const p = join(tmpdir(), 'st'); fs.symlinkSync('target', p); fs.unlinkSync(p); } catch (e) { symlinkOk = e.code !== 'EPERM'; } if (!symlinkOk) console.warn('symlinks unavailable; traversal tests will skip'); Type guard
const canSymlink = () => { try { const p = join(tmpdir(), 'sl-probe'); symlinkSync('x', p); unlinkSync(p); return true; } catch { return false; } }; Try / catch
try { assertSymlinkTraversalRejected(); } catch (e) { warn(`symlink traversal test skipped: ${e.message}`); } Prevention
- On Windows, enable Developer Mode or run elevated so symlinks work; or test under WSL
- Validators should return a rejection verdict (null) rather than throwing on invalid input
- Probe symlink capability once and skip dependent tests cleanly
- Run the suite on a filesystem that supports symlinks
When it happens
Trigger: An exception escapes the block that calls the manifest/entry validator with symlink fixtures (skill: 'linked-skill.md', entry under an escaping linked-dir) — e.g. fs.symlinkSync unsupported on the platform (Windows without privileges), or the validator throwing instead of returning null.
Common situations: Windows CI without Developer Mode / symlink privilege (EPERM creating symlinks), filesystems that disallow symlinks (FAT, some containers), or a validator regression that throws rather than returning a rejection verdict.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- archive render skipped — no Playwright browser in env
- Cannot import generate-pdf.mjs
- concurrent reservation test flaked
- ⚠️ Font reference escapes fonts/, keeping original…
- garena: is not a usable URL segment
AI-assisted analysis of santifer/career-ops@e7abd431fc (2026-09-16).
Data as JSON: /api/errors/80ab58abef154520.
Report an issue: GitHub.
Appendix: source
Thrown at test-all.mjs:15172
if (vm({ ...base, requiredEnv: ['X_TOKEN'], allowedHosts: ['api.x.com'] }) !== null) pass('a valid keyed manifest is accepted');
else fail('valid keyed manifest should be accepted');
if (vm({ ...base, entry: '../../scan.mjs' }) === null) pass('entry escaping the plugin directory is rejected (traversal guard)');
else fail('entry traversal should be rejected');
writeFileSync(join(__manifestTmp, 'outside.mjs'), 'export default {};');
writeFileSync(join(__manifestTmp, 'outside.md'), '# outside\n');
mkdirSync(join(__manifestTmp, 'outside-dir'), { recursive: true });
try {
symlinkSync(join(__manifestTmp, 'outside.mjs'), join(__manifestTmp, 'x', 'linked-entry.mjs'));
symlinkSync(join(__manifestTmp, 'outside.md'), join(__manifestTmp, 'x', 'linked-skill.md'));
symlinkSync(join(__manifestTmp, 'outside-dir'), join(__manifestTmp, 'x', 'linked-dir'), 'dir');
if (vm({ ...base, entry: 'linked-entry.mjs' }) === null) pass('entry symlink escaping the plugin directory is rejected');
else fail('entry symlink traversal should be rejected');
if (vm({ ...base, skill: 'linked-skill.md' }) === null) pass('skill symlink escaping the plugin directory is rejected');
else fail('skill symlink traversal should be rejected');
if (vm({ ...base, entry: 'linked-dir/missing-entry.mjs' }) === null) pass('missing entry under an escaping symlink directory is rejected');
else fail('missing entry under symlink traversal should be rejected');
} catch (e) {
warn(`symlink traversal test skipped: ${e.message}`);
}
if (validateManifest({ ...base, id: 'y' }, '/tmp/x', 'x') === null) pass('manifest id must equal the directory name');
else fail('id != dirname should be rejected');
if (vm({ ...base, apiVersion: 2 }) === null) pass('unknown apiVersion is rejected (forward-compat gate)');
else fail('apiVersion 2 should be rejected');
console.warn = __origWarn;
// Build an isolated tmp project root.
__pluginTmp = mkdtempSync(join(tmpdir(), 'co-plugins-'));
mkdirSync(join(__pluginTmp, 'plugins'), { recursive: true });
// (a) BYTE-IDENTICAL no-op when config/plugins.yml is absent — and NO env mutation.
const beforeGemini = process.env.GEMINI_API_KEY;
const map = new Map([['greenhouse', { id: 'greenhouse', fetch() {} }]]);
await mergeProviderPlugins(map, { root: __pluginTmp });
if (map.size === 1 && map.get('greenhouse')) pass('mergeProviderPlugins is a no-op when config/plugins.yml is absent');
else fail(`merge should be a no-op without plugins.yml (size=${map.size})`);
if (process.env.GEMINI_API_KEY === beforeGemini) pass('no .env is read / no env mutation when plugins.yml is absent (byte-identical guarantee)');View on GitHub (pinned to e7abd431fc)