/** * 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")); });