Files
2026-07-13 13:39:12 +08:00

301 lines
10 KiB
TypeScript

/**
* Tests for githubCollector.ts — GitHub agent skill discovery, scoring, scanning, and installation.
* Uses node:test (Node.js built-in test runner), matching the project convention.
*/
import test from "node:test";
import assert from "node:assert/strict";
const {
scoreRepo,
scanText,
inferCategory,
resolveInstallPath,
QUERY_STRATEGIES,
INSTALL_TARGETS,
} = await import("../../src/lib/skills/githubCollector.ts");
// ─── scoreRepo ─────────────────────────────────────────────────────────────
void test("scoreRepo: returns 0.98 for gold repos", () => {
const score = scoreRepo({
fullName: "addyosmani/agent-skills",
description: "Collection of agent skills",
stars: 100,
forks: 10,
hasLicense: true,
topics: [],
});
assert.equal(score, 0.98);
});
void test("scoreRepo: returns reasonable score for good agent repos", () => {
const score = scoreRepo({
fullName: "user/awesome-agent-skills",
description: "Curated list of AI agent skills for LLMs",
stars: 5000,
forks: 200,
hasLicense: true,
topics: ["agent", "ai-agent", "mcp"],
});
assert.ok(score > 0.5);
assert.ok(score <= 1.0);
});
void test("scoreRepo: returns low score for unrelated repos", () => {
const score = scoreRepo({
fullName: "user/todo-app",
description: "A simple todo list app built with react",
stars: 5,
forks: 1,
hasLicense: false,
topics: ["javascript", "react"],
});
assert.ok(score < 0.5);
});
void test("scoreRepo: boosts score for skill-file name signals", () => {
const score = scoreRepo({
fullName: "user/agent-skill-pack",
description: "Agent skill pack for codex",
stars: 100,
forks: 30,
hasLicense: true,
topics: ["agent"],
});
assert.ok(score > 0.3);
assert.ok(score < 0.98);
});
void test("scoreRepo: applies stars bonus logarithmically", () => {
const low = scoreRepo({
fullName: "user/skill-repo",
description: "Agent skill",
stars: 50,
forks: 5,
hasLicense: false,
topics: [],
});
const high = scoreRepo({
fullName: "user/skill-repo",
description: "Agent skill",
stars: 10000,
forks: 500,
hasLicense: false,
topics: [],
});
assert.ok(high > low);
assert.ok(high < 0.98);
});
void test("scoreRepo: caps stars for awesome lists", () => {
const score = scoreRepo({
fullName: "user/awesome-list",
description: "A curated list of awesome things",
stars: 50000,
forks: 2000,
hasLicense: true,
topics: [],
});
// awesome list without skill file signals should be capped below gold-repo threshold
assert.ok(score < 0.9);
});
void test("scoreRepo: handles zero stars gracefully", () => {
const score = scoreRepo({
fullName: "user/new-repo",
description: "Brand new agent skill",
stars: 0,
forks: 0,
hasLicense: false,
topics: [],
});
assert.ok(score >= 0);
assert.ok(score < 0.5);
});
// ─── scanText ──────────────────────────────────────────────────────────────
void test("scanText: empty findings for clean content", () => {
const findings = scanText("print('hello world')\nconst x = 1;\n", "test.md");
assert.equal(findings.length, 0);
});
void test("scanText: detects eval(base64) pattern", () => {
const findings = scanText('eval(base64_decode("dGVzdA=="))', "evil.md");
assert.ok(findings.length > 0);
assert.ok(findings[0].pattern.includes("eval(base64)"));
assert.equal(findings[0].file, "evil.md");
});
void test("scanText: detects hardcoded private keys", () => {
const content =
"-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----";
const findings = scanText(content, "leaked.md");
assert.ok(findings.some((f) => f.pattern.includes("Private key")));
});
void test("scanText: detects API key patterns", () => {
const findings = scanText('const api_key = "abc123def456";', "config.js");
assert.ok(findings.some((f) => f.pattern.includes("API key")));
});
void test("scanText: detects multiple patterns in one file", () => {
const content = `
const api_key = "12345";
password = "secret123";
eval(base64_decode("test"));
`;
const findings = scanText(content, "danger.js");
assert.ok(findings.length >= 3);
});
void test("scanText: provides context around the match", () => {
const findings = scanText("some text before api_key = 'abc123' some text after", "test.js");
assert.ok(findings.length > 0);
assert.ok(findings[0].context.length > 15);
});
void test("scanText: ignores safe content with keyword-like patterns", () => {
const findings = scanText("using password hashing for security\napi version 2.0", "safe.txt");
assert.equal(findings.length, 0);
});
void test("scanText: detects OpenAI key pattern (sk- alphanumeric)", () => {
// OpenAI keys: sk- followed by 20+ alphanumeric chars (no hyphens inside the token part)
const findings = scanText('OPENAI_API_KEY="sk-abcdefghijklmnopqrstuvwxyz1234567890"', ".env");
assert.ok(findings.some((f) => f.pattern.includes("OpenAI API key")));
});
void test("scanText: detects GitHub PAT pattern (ghp_)", () => {
const findings = scanText("GITHUB_TOKEN=ghp_abcdefghijklmnopqrstuvwxyz1234567890123456", ".env");
assert.ok(findings.some((f) => f.pattern.includes("GitHub PAT")));
});
// ─── inferCategory ─────────────────────────────────────────────────────────
void test("inferCategory: returns 'imported-github' for generic skills", () => {
const cat = inferCategory("user/some-skill", "A useful skill for agents");
assert.equal(cat, "imported-github");
});
void test("inferCategory: detects security-related skills", () => {
const cat = inferCategory("user/sec-scan", "Security vulnerability scanner for code");
assert.equal(cat, "security");
});
void test("inferCategory: detects data-science skills", () => {
const cat = inferCategory("user/ml-pipeline", "ML model training and data analytics");
assert.equal(cat, "data-science");
});
void test("inferCategory: detects devops skills", () => {
const cat = inferCategory("user/deploy-tool", "Docker deployment pipeline for CI/CD");
assert.equal(cat, "devops");
});
void test("inferCategory: detects creative skills", () => {
const cat = inferCategory("user/art-gen", "AI image design generator");
assert.equal(cat, "creative");
});
void test("inferCategory: detects productivity skills", () => {
const cat = inferCategory("user/email-assistant", "Email scheduling and document tool");
assert.equal(cat, "productivity");
});
void test("inferCategory: detects research skills", () => {
const cat = inferCategory("user/paper-finder", "arXiv academic paper search");
assert.equal(cat, "research");
});
void test("inferCategory: detects software-development skills", () => {
const cat = inferCategory("user/code-reviewer", "Automated code review and debugging");
assert.equal(cat, "software-development");
});
void test("inferCategory: detects media skills (youtube + transcript keywords)", () => {
const cat = inferCategory("user/yt-dl", "YouTube transcript downloader");
assert.equal(cat, "media");
});
// ─── resolveInstallPath ────────────────────────────────────────────────────
void test("resolveInstallPath: resolves to a valid path string", () => {
const origHome = process.env.HOME;
process.env.HOME = "/home/testuser";
try {
const path = resolveInstallPath("hermes", "sec-scan", "A security scanner");
// The path should start with the HOME directory and contain the category
assert.ok(path.startsWith("/home/testuser"));
assert.ok(path.includes("security"));
assert.ok(path.includes("hermes/skills"));
} finally {
process.env.HOME = origHome;
}
});
void test("resolveInstallPath: infers category from description", () => {
const origHome = process.env.HOME;
process.env.HOME = "/home/testuser";
try {
const path = resolveInstallPath("claude", "data-pipeline", "Data science tool");
assert.ok(path.includes("data-science"));
} finally {
process.env.HOME = origHome;
}
});
void test("resolveInstallPath: uses category for unknown target and throws", () => {
assert.throws(() => resolveInstallPath("unknown", "x", ""), Error);
});
void test("resolveInstallPath: different targets produce different base paths", () => {
const origHome = process.env.HOME;
process.env.HOME = "/home/testuser";
try {
const hermesPath = resolveInstallPath("hermes", "my-skill", "generic");
const claudePath = resolveInstallPath("claude", "my-skill", "generic");
assert.notEqual(hermesPath, claudePath);
assert.ok(hermesPath.includes("AppData/Local/hermes"));
assert.ok(claudePath.includes(".claude"));
} finally {
process.env.HOME = origHome;
}
});
// ─── QUERY_STRATEGIES structural sanity ────────────────────────────────────
void test("QUERY_STRATEGIES: has file, name, and description categories", () => {
assert.ok(Array.isArray(QUERY_STRATEGIES.file));
assert.ok(Array.isArray(QUERY_STRATEGIES.name));
assert.ok(Array.isArray(QUERY_STRATEGIES.description));
});
void test("QUERY_STRATEGIES: has at least 10 file-name queries", () => {
assert.ok(QUERY_STRATEGIES.file.length >= 10);
});
void test("QUERY_STRATEGIES: all queries are non-empty strings", () => {
const all = [...QUERY_STRATEGIES.file, ...QUERY_STRATEGIES.name, ...QUERY_STRATEGIES.description];
assert.ok(all.length > 15);
for (const q of all) {
assert.equal(typeof q, "string");
assert.ok(q.length > 5);
}
});
void test("QUERY_STRATEGIES: each file query ends with a star threshold", () => {
for (const q of QUERY_STRATEGIES.file) {
assert.ok(/stars:>=\d+/.test(q));
}
});
// ─── INSTALL_TARGETS ────────────────────────────────────────────────────────
void test("INSTALL_TARGETS: includes all expected agent targets", () => {
assert.ok(INSTALL_TARGETS.includes("hermes"));
assert.ok(INSTALL_TARGETS.includes("claude"));
assert.ok(INSTALL_TARGETS.includes("gemini"));
assert.ok(INSTALL_TARGETS.includes("opencode"));
});