Files
wehub-resource-sync d48cda4081
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:55:55 +08:00

232 lines
10 KiB
JavaScript

/**
* Tests for scripts/lib/install/link-rewrite.js — rewriting relative links in
* namespaced markdown so they resolve after install (issue #2340).
*/
'use strict';
const assert = require('assert');
const path = require('path');
const {
buildInstallIndex,
isNamespacedSource,
rewriteRelativeLinks,
} = require('../../scripts/lib/install/link-rewrite');
const { createManifestInstallPlan } = require('../../scripts/lib/install-executor');
const REPO_ROOT = path.resolve(__dirname, '..', '..');
// A claude-style namespace placement: skills/<id> -> skills/ecc/<id> and
// rules/<x> -> rules/ecc/<x>. Mirrors what the real adapter emits.
function claudeNamespaceMappings() {
return [
{ sourceRel: 'skills/react-patterns/SKILL.md', destRel: 'skills/ecc/react-patterns/SKILL.md' },
{ sourceRel: 'skills/react-patterns/other.md', destRel: 'skills/ecc/react-patterns/other.md' },
{ sourceRel: 'skills/react-patterns/sub/NOTE.md', destRel: 'skills/ecc/react-patterns/sub/NOTE.md' },
{ sourceRel: 'rules/react/hooks.md', destRel: 'rules/ecc/react/hooks.md' },
{ sourceRel: 'rules/react/testing.md', destRel: 'rules/ecc/react/testing.md' },
{ sourceRel: 'rules/react/coding-style.md', destRel: 'rules/ecc/react/coding-style.md' },
];
}
// An identity placement (non-namespacing adapter): source == dest.
function identityMappings() {
return [
{ sourceRel: 'skills/react-patterns/SKILL.md', destRel: 'skills/react-patterns/SKILL.md' },
{ sourceRel: 'rules/react/hooks.md', destRel: 'rules/react/hooks.md' },
];
}
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
return true;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` Error: ${error.message}`);
// Preserve the full stack (source line + assertion diff) for diagnosis.
console.error(error.stack || error);
return false;
}
}
function runTests() {
console.log('Running install link-rewrite tests...\n');
let passed = 0;
let failed = 0;
const index = buildInstallIndex(claudeNamespaceMappings());
// Parametrize the canonical file-link case over all three affected skills so
// a regression in any one is caught (not just the first).
for (const skill of ['react-patterns', 'react-performance', 'react-testing']) {
if (test(`rewrites ../../rules file link for ${skill}`, () => {
const idx = buildInstallIndex([
{ sourceRel: `skills/${skill}/SKILL.md`, destRel: `skills/ecc/${skill}/SKILL.md` },
{ sourceRel: 'rules/react/hooks.md', destRel: 'rules/ecc/react/hooks.md' },
]);
const before = 'See [rules](../../rules/react/hooks.md) for details.';
const after = rewriteRelativeLinks(before, { sourceRel: `skills/${skill}/SKILL.md`, index: idx });
assert.notStrictEqual(after, before, 'rewrite must change the broken link (not vacuous)');
assert.ok(
after.includes('](../../../rules/ecc/react/hooks.md)'),
`expected corrected link, got: ${after}`
);
assert.ok(!after.includes('](../../rules/'), 'broken depth must be gone');
})) passed++; else failed++;
}
if (test('rewrites a directory link and preserves the trailing slash', () => {
const before = '- Rules: [rules/react/](../../rules/react/)';
const after = rewriteRelativeLinks(before, { sourceRel: 'skills/react-patterns/SKILL.md', index });
assert.notStrictEqual(after, before);
assert.ok(after.includes('](../../../rules/ecc/react/)'), `got: ${after}`);
})) passed++; else failed++;
if (test('leaves an intra-skill sibling link unchanged', () => {
const before = 'Look at [other](./other.md) nearby.';
const after = rewriteRelativeLinks(before, { sourceRel: 'skills/react-patterns/SKILL.md', index });
assert.strictEqual(after, before, 'same-prefix relative path must be preserved');
})) passed++; else failed++;
if (test('leaves links to non-installed targets unchanged', () => {
const before = 'See [pkg](../../package.json) at the root.';
const after = rewriteRelativeLinks(before, { sourceRel: 'skills/react-patterns/SKILL.md', index });
assert.strictEqual(after, before, 'never invent a path to a file the plan does not install');
})) passed++; else failed++;
if (test('leaves external urls, absolute paths, and anchors unchanged', () => {
const before = [
'[ext](https://example.com/rules/react/hooks.md)',
'[abs](/rules/react/hooks.md)',
'[mail](mailto:x@example.com)',
'[anchor](#a-section)',
].join('\n');
const after = rewriteRelativeLinks(before, { sourceRel: 'skills/react-patterns/SKILL.md', index });
assert.strictEqual(after, before);
})) passed++; else failed++;
if (test('preserves a #fragment on a rewritten link', () => {
const before = '[hooks](../../rules/react/hooks.md#use-effect)';
const after = rewriteRelativeLinks(before, { sourceRel: 'skills/react-patterns/SKILL.md', index });
assert.ok(after.includes('](../../../rules/ecc/react/hooks.md#use-effect)'), `got: ${after}`);
})) passed++; else failed++;
if (test('does not rewrite links inside fenced code blocks', () => {
const before = [
'```md',
'[code](../../rules/react/hooks.md)',
'```',
'[prose](../../rules/react/hooks.md)',
].join('\n');
const after = rewriteRelativeLinks(before, { sourceRel: 'skills/react-patterns/SKILL.md', index });
assert.ok(after.includes('[code](../../rules/react/hooks.md)'), 'code-fence link must be untouched');
assert.ok(after.includes('[prose](../../../rules/ecc/react/hooks.md)'), 'prose link must be rewritten');
})) passed++; else failed++;
if (test('computes depth from path math for a nested skill file', () => {
// skills/react-patterns/sub/NOTE.md -> skills/ecc/react-patterns/sub/NOTE.md
// Source link is ../../../rules/react/hooks.md (3 up from sub/).
const before = '[r](../../../rules/react/hooks.md)';
const after = rewriteRelativeLinks(before, { sourceRel: 'skills/react-patterns/sub/NOTE.md', index });
assert.notStrictEqual(after, before, 'nested depth must be recomputed, not hardcoded');
assert.ok(after.includes('](../../../../rules/ecc/react/hooks.md)'), `got: ${after}`);
})) passed++; else failed++;
if (test('is a no-op for a non-namespacing (identity) placement', () => {
const idx = buildInstallIndex(identityMappings());
const before = '[r](../../rules/react/hooks.md)';
const after = rewriteRelativeLinks(before, { sourceRel: 'skills/react-patterns/SKILL.md', index: idx });
assert.strictEqual(after, before, 'identity placement must not touch any link');
})) passed++; else failed++;
if (test('is a no-op when the file itself is not in the plan', () => {
const before = '[r](../../rules/react/hooks.md)';
const after = rewriteRelativeLinks(before, { sourceRel: 'skills/not-installed/SKILL.md', index });
assert.strictEqual(after, before);
})) passed++; else failed++;
// Guards the apply-layer gate: only namespaced files leave the byte-copy
// path, so non-namespaced markdown is still copied verbatim.
if (test('isNamespacedSource flags only files whose install path changed', () => {
assert.strictEqual(
isNamespacedSource('skills/react-patterns/SKILL.md', index), true,
'a namespaced skill file must be flagged'
);
const identity = buildInstallIndex(identityMappings());
assert.strictEqual(
isNamespacedSource('skills/react-patterns/SKILL.md', identity), false,
'an identity-mapped file must stay on the byte-copy path'
);
assert.strictEqual(
isNamespacedSource('skills/not-in-plan/SKILL.md', index), false,
'a file the plan does not install is not namespaced'
);
})) passed++; else failed++;
// Integration: real repo content + real claude plan. Every rewritten link in
// the three React skills must resolve to a destination the SAME plan installs.
if (test('real React skills: rewritten rules links resolve to installed targets', () => {
const fs = require('fs');
const plan = createManifestInstallPlan({
sourceRoot: REPO_ROOT,
homeDir: '/tmp/ecc-link-rewrite-it',
target: 'claude',
moduleIds: ['framework-language', 'rules-core'],
});
const mappings = plan.operations
.filter(op => op.kind === 'copy-file' && op.sourceRelativePath)
.map(op => ({
sourceRel: op.sourceRelativePath,
destRel: path.relative(plan.targetRoot, op.destinationPath),
}));
const realIndex = buildInstallIndex(mappings);
const installedDestRels = new Set(mappings.map(m => m.destRel.replace(/\\/g, '/')));
const extractLinks = text => {
const out = [];
const linkPattern = /\]\(([^()\s#]+)/g;
let m;
while ((m = linkPattern.exec(text)) !== null) {
out.push(m[1]);
}
return out;
};
let changedLinks = 0;
for (const skill of ['react-patterns', 'react-performance', 'react-testing']) {
const sourceRel = `skills/${skill}/SKILL.md`;
const content = fs.readFileSync(path.join(REPO_ROOT, sourceRel), 'utf8');
assert.ok(content.includes('](../../rules/'), `${sourceRel} should have a broken link pre-fix`);
const rewritten = rewriteRelativeLinks(content, { sourceRel, index: realIndex });
assert.ok(!rewritten.includes('](../../rules/'), `${sourceRel} still has the broken depth`);
// Only links we actually changed are validated here; cross-skill links to
// skills outside this module subset are legitimately left untouched.
const before = extractLinks(content);
const after = extractLinks(rewritten);
const installedSkillDir = path.posix.dirname(`skills/ecc/${skill}/SKILL.md`);
for (let i = 0; i < after.length; i += 1) {
if (after[i] === before[i]) {
continue;
}
const resolved = path.posix
.normalize(path.posix.join(installedSkillDir, after[i]))
.replace(/\/+$/, '');
const isFile = installedDestRels.has(resolved);
const isDir = [...installedDestRels].some(d => d.startsWith(`${resolved}/`));
assert.ok(isFile || isDir, `rewritten link ${after[i]} -> ${resolved} is not installed`);
changedLinks += 1;
}
}
assert.ok(changedLinks >= 3, `expected to verify rewritten links, changed ${changedLinks}`);
})) passed++; else failed++;
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
}
runTests();