mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
feat(providers): integrate audited free-tier gateways (#9210)
* feat(providers): add Zylo UnoRouter and Poolside registries * feat(providers): integrate audited free-tier gateways * feat: add wave2 free-tier provider registries * feat(providers): add Mixlayer Speka and TokenReply registries * feat: add wave 2 free-tier provider registries * fix: align meganova provider slug * feat(providers): integrate wave2 free-tier gateways * feat(providers): add Wave 3-A free-tier registries * feat(providers): add HelyxAI Auriko and Poixe registries * feat(providers): add Naga AI and Chat Oripe registries * feat(providers): integrate wave3 free-tier gateways * feat(providers): add FreeInference registry * feat(providers): add Free.ai registry * feat(providers): integrate wave4 free-tier gateways * docs: synchronize provider and free-tier inventories * refactor(providers): split audited gateway catalog * feat(providers): add audited Void AI and HelixMind gateways * feat(providers): finalize audited free-tier integration * test(providers): update APIKEY split count to 229 after rebase onto release/v3.8.50 The rebase merged the release catalog (201 APIKEY providers) with the PR's 28 free-tier additions, yielding 229 total. Correct the characterization count so the partition assertion reflects the true merged state. --------- Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com> Co-authored-by: backryun <bakryun0718@proton.me>
This commit is contained in:
committed by
GitHub
parent
f6ccd3cf9f
commit
ecc89eef14
@@ -7,7 +7,7 @@ type AgentSkill = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: "api" | "cli";
|
||||
category: "api" | "cli" | "config";
|
||||
area: string;
|
||||
icon: string;
|
||||
endpoints?: string[];
|
||||
@@ -19,13 +19,14 @@ type AgentSkill = {
|
||||
type SkillCoverage = {
|
||||
api: { have: number; total: number };
|
||||
cli: { have: number; total: number };
|
||||
config: { have: number; total: number };
|
||||
totalSkills: number;
|
||||
generatedAt: string;
|
||||
};
|
||||
|
||||
function makeAgentSkills(): AgentSkill[] {
|
||||
const skills: AgentSkill[] = [];
|
||||
for (let i = 0; i < 22; i++) {
|
||||
for (let i = 0; i < 23; i++) {
|
||||
skills.push({
|
||||
id: `omni-skill-${i}`,
|
||||
name: `API Skill ${i}`,
|
||||
@@ -38,7 +39,7 @@ function makeAgentSkills(): AgentSkill[] {
|
||||
githubUrl: `https://github.com/example/OmniRoute/blob/main/skills/omni-skill-${i}/SKILL.md`,
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < 20; i++) {
|
||||
for (let i = 0; i < 21; i++) {
|
||||
skills.push({
|
||||
id: `cli-skill-${i}`,
|
||||
name: `CLI Skill ${i}`,
|
||||
@@ -51,13 +52,25 @@ function makeAgentSkills(): AgentSkill[] {
|
||||
githubUrl: `https://github.com/example/OmniRoute/blob/main/skills/cli-skill-${i}/SKILL.md`,
|
||||
});
|
||||
}
|
||||
skills.push({
|
||||
id: "config-codex-cli",
|
||||
name: "Config: Codex CLI",
|
||||
description: "Configure Codex CLI to use OmniRoute.",
|
||||
category: "config",
|
||||
area: "config-codex-cli",
|
||||
icon: "terminal",
|
||||
rawUrl:
|
||||
"https://raw.githubusercontent.com/example/OmniRoute/main/skills/config-codex-cli/SKILL.md",
|
||||
githubUrl: "https://github.com/example/OmniRoute/blob/main/skills/config-codex-cli/SKILL.md",
|
||||
});
|
||||
return skills;
|
||||
}
|
||||
|
||||
const FULL_COVERAGE: SkillCoverage = {
|
||||
api: { have: 22, total: 22 },
|
||||
cli: { have: 20, total: 20 },
|
||||
totalSkills: 42,
|
||||
api: { have: 23, total: 23 },
|
||||
cli: { have: 21, total: 21 },
|
||||
config: { have: 1, total: 1 },
|
||||
totalSkills: 45,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
@@ -80,9 +93,7 @@ async function fulfillText(route: Route, body: string, status = 200) {
|
||||
test.describe("Agent Skills page", () => {
|
||||
test.setTimeout(600_000);
|
||||
|
||||
test("renders SkillsConceptCard with data-testid skills-concept-card-agent", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("renders SkillsConceptCard with data-testid skills-concept-card-agent", async ({ page }) => {
|
||||
const skills = makeAgentSkills();
|
||||
|
||||
await page.route(/\/api\/agent-skills(?:\?.*)?$/, async (route) => {
|
||||
@@ -101,7 +112,7 @@ test.describe("Agent Skills page", () => {
|
||||
await expect(conceptCard).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test("grid shows 42 skill cards when filter is 'all'", async ({ page }) => {
|
||||
test("grid shows 45 skill cards when filter is 'all'", async ({ page }) => {
|
||||
const skills = makeAgentSkills();
|
||||
|
||||
await page.route(/\/api\/agent-skills(?:\?.*)?$/, async (route) => {
|
||||
@@ -122,12 +133,10 @@ test.describe("Agent Skills page", () => {
|
||||
});
|
||||
|
||||
const cards = page.locator("[data-testid^='skill-card-']");
|
||||
await expect(cards).toHaveCount(42, { timeout: 15_000 });
|
||||
await expect(cards).toHaveCount(45, { timeout: 15_000 });
|
||||
});
|
||||
|
||||
test("clicking omni-skill-0 card renders markdown in preview pane", async ({
|
||||
page,
|
||||
}) => {
|
||||
test("clicking omni-skill-0 card renders markdown in preview pane", async ({ page }) => {
|
||||
const skills = makeAgentSkills();
|
||||
const mockMarkdown = "# API Skill 0\n\nThis skill manages connections.";
|
||||
|
||||
@@ -199,7 +208,7 @@ test.describe("Agent Skills page", () => {
|
||||
expect(
|
||||
finalUrl.includes("/dashboard/omni-skills") ||
|
||||
finalUrl.includes("/login") ||
|
||||
finalUrl.includes("/onboarding"),
|
||||
finalUrl.includes("/onboarding")
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Integration tests for Agent Skills content integrity.
|
||||
*
|
||||
* Verifies:
|
||||
* 1. All 44 skill IDs from catalog have skills/{id}/ folder with SKILL.md.
|
||||
* 1. All 45 skill IDs from catalog have skills/{id}/ folder with SKILL.md.
|
||||
* 2. Zero omniroute-* folders remain (post-prune: old omniroute-* skill dirs were removed).
|
||||
* 3. 12 specific IDs have <!-- skill:custom-start --> ... <!-- skill:custom-end --> blocks:
|
||||
* omni-mcp, omni-compression, cli-providers, cli-eval, omni-agents-a2a,
|
||||
@@ -15,7 +15,8 @@ import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const { API_SKILL_IDS, CLI_SKILL_IDS, CONFIG_SKILL_IDS } = await import("../../src/lib/agentSkills/catalog.ts");
|
||||
const { API_SKILL_IDS, CLI_SKILL_IDS, CONFIG_SKILL_IDS } =
|
||||
await import("../../src/lib/agentSkills/catalog.ts");
|
||||
|
||||
const SKILLS_DIR = path.resolve(process.cwd(), "skills");
|
||||
const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as string[];
|
||||
@@ -37,9 +38,9 @@ const CUSTOM_BLOCK_IDS = [
|
||||
"config-codex-cli",
|
||||
] as const;
|
||||
|
||||
// ── §1: All 42 catalog IDs have skills/{id}/SKILL.md ─────────────────────────
|
||||
// ── §1: All 45 catalog IDs have skills/{id}/SKILL.md ─────────────────────────
|
||||
|
||||
test("all 44 catalog IDs have a skills/{id}/ directory", () => {
|
||||
test("all 45 catalog IDs have a skills/{id}/ directory", () => {
|
||||
const missing: string[] = [];
|
||||
for (const id of ALL_IDS) {
|
||||
const dirPath = path.join(SKILLS_DIR, id);
|
||||
@@ -50,7 +51,7 @@ test("all 44 catalog IDs have a skills/{id}/ directory", () => {
|
||||
assert.deepEqual(missing, [], `Missing skill directories: ${missing.join(", ")}`);
|
||||
});
|
||||
|
||||
test("all 44 catalog IDs have a skills/{id}/SKILL.md file", () => {
|
||||
test("all 45 catalog IDs have a skills/{id}/SKILL.md file", () => {
|
||||
const missing: string[] = [];
|
||||
for (const id of ALL_IDS) {
|
||||
const skillPath = path.join(SKILLS_DIR, id, "SKILL.md");
|
||||
@@ -75,7 +76,7 @@ test("skills/ has zero omniroute-* directories (all pruned)", () => {
|
||||
assert.deepEqual(
|
||||
omniRouteDirs,
|
||||
[],
|
||||
`Found omniroute-* directories that should have been pruned: ${omniRouteDirs.join(", ")}`,
|
||||
`Found omniroute-* directories that should have been pruned: ${omniRouteDirs.join(", ")}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -85,11 +86,7 @@ test("skills/ directory only contains expected catalog IDs plus README", () => {
|
||||
const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
||||
const expectedSet = new Set(ALL_IDS);
|
||||
const unexpected = dirs.filter((d) => !expectedSet.has(d));
|
||||
assert.deepEqual(
|
||||
unexpected,
|
||||
[],
|
||||
`Unexpected directories in skills/: ${unexpected.join(", ")}`,
|
||||
);
|
||||
assert.deepEqual(unexpected, [], `Unexpected directories in skills/: ${unexpected.join(", ")}`);
|
||||
});
|
||||
|
||||
// ── §3: 10 specific IDs have custom blocks ───────────────────────────────────
|
||||
@@ -97,18 +94,15 @@ test("skills/ directory only contains expected catalog IDs plus README", () => {
|
||||
for (const id of CUSTOM_BLOCK_IDS) {
|
||||
test(`skills/${id}/SKILL.md has <!-- skill:custom-start --> block`, () => {
|
||||
const skillPath = path.join(SKILLS_DIR, id, "SKILL.md");
|
||||
assert.ok(
|
||||
fs.existsSync(skillPath),
|
||||
`skills/${id}/SKILL.md does not exist`,
|
||||
);
|
||||
assert.ok(fs.existsSync(skillPath), `skills/${id}/SKILL.md does not exist`);
|
||||
const content = fs.readFileSync(skillPath, "utf-8");
|
||||
assert.ok(
|
||||
content.includes("<!-- skill:custom-start -->"),
|
||||
`skills/${id}/SKILL.md missing <!-- skill:custom-start --> block`,
|
||||
`skills/${id}/SKILL.md missing <!-- skill:custom-start --> block`
|
||||
);
|
||||
assert.ok(
|
||||
content.includes("<!-- skill:custom-end -->"),
|
||||
`skills/${id}/SKILL.md missing <!-- skill:custom-end --> block`,
|
||||
`skills/${id}/SKILL.md missing <!-- skill:custom-end --> block`
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -130,7 +124,7 @@ test("exactly 13 skills have custom blocks", () => {
|
||||
assert.deepEqual(
|
||||
withCustomBlocks.sort(),
|
||||
expectedIds,
|
||||
`Expected exactly these 13 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}`,
|
||||
`Expected exactly these 13 custom-block IDs: ${expectedIds.join(", ")}\nActual: ${withCustomBlocks.join(", ")}`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* Integration tests for Agent Skills discovery.
|
||||
*
|
||||
* Verifies:
|
||||
* 1. Every ID in API_SKILL_IDS + CLI_SKILL_IDS has a skills/<id>/SKILL.md on disk.
|
||||
* 1. Every catalog ID has a skills/<id>/SKILL.md on disk.
|
||||
* 2. Each SKILL.md has valid frontmatter (name + description) and body ≥ 100 chars.
|
||||
* 3. MCP tool omniroute_agent_skills_list handler returns 44 entries.
|
||||
* 4. A2A skill list-capabilities returns 1 artifact with 44 lines.
|
||||
* 3. MCP tool omniroute_agent_skills_list handler returns 45 entries.
|
||||
* 4. A2A skill list-capabilities returns one artifact containing all 45 entries.
|
||||
*
|
||||
* Does NOT spin up a server — tests handlers directly via imports.
|
||||
*/
|
||||
@@ -15,7 +15,8 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
// Dynamic imports for ESM + tsx compatibility
|
||||
const { API_SKILL_IDS, CLI_SKILL_IDS } = await import("../../src/lib/agentSkills/catalog.ts");
|
||||
const { API_SKILL_IDS, CLI_SKILL_IDS, CONFIG_SKILL_IDS } =
|
||||
await import("../../src/lib/agentSkills/catalog.ts");
|
||||
const { agentSkillTools } = await import("../../open-sse/mcp-server/tools/agentSkillTools.ts");
|
||||
const { executeListCapabilities } = await import("../../src/lib/a2a/skills/listCapabilities.ts");
|
||||
import type { A2ATask } from "../../src/lib/a2a/taskManager.ts";
|
||||
@@ -41,7 +42,7 @@ function parseSkillMarkdown(content: string): { name: string; description: strin
|
||||
|
||||
// ── §1: Filesystem — every skill ID has a SKILL.md ───────────────────────────
|
||||
|
||||
const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS] as string[];
|
||||
const ALL_IDS = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as string[];
|
||||
|
||||
test("skills/ directory exists and is readable", () => {
|
||||
assert.ok(fs.existsSync(SKILLS_DIR), `skills/ directory not found at ${SKILLS_DIR}`);
|
||||
@@ -69,8 +70,19 @@ test("every CLI skill ID has skills/<id>/SKILL.md on disk", () => {
|
||||
assert.deepEqual(missing, [], `Missing CLI SKILL.md files: ${missing.join(", ")}`);
|
||||
});
|
||||
|
||||
test("total skill count is exactly 44 (23 API + 21 CLI)", () => {
|
||||
assert.equal(API_SKILL_IDS.length + CLI_SKILL_IDS.length, 44);
|
||||
test("every config skill ID has skills/<id>/SKILL.md on disk", () => {
|
||||
const missing: string[] = [];
|
||||
for (const id of CONFIG_SKILL_IDS) {
|
||||
const skillPath = path.join(SKILLS_DIR, id, "SKILL.md");
|
||||
if (!fs.existsSync(skillPath)) {
|
||||
missing.push(id);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(missing, [], `Missing config SKILL.md files: ${missing.join(", ")}`);
|
||||
});
|
||||
|
||||
test("total skill count is exactly 45 (23 API + 21 CLI + 1 config)", () => {
|
||||
assert.equal(ALL_IDS.length, 45);
|
||||
});
|
||||
|
||||
// ── §2: Frontmatter validation ────────────────────────────────────────────────
|
||||
@@ -120,14 +132,14 @@ test("each SKILL.md body is at least 100 chars", () => {
|
||||
|
||||
// ── §3: MCP tool omniroute_agent_skills_list ─────────────────────────────────
|
||||
|
||||
test("MCP omniroute_agent_skills_list handler returns count 45 (44 + config)", async () => {
|
||||
test("MCP omniroute_agent_skills_list handler returns count 45", async () => {
|
||||
const result = await agentSkillTools.omniroute_agent_skills_list.handler({});
|
||||
assert.equal(result.count, 45, `Expected 45 but got ${result.count}`);
|
||||
assert.ok(Array.isArray(result.skills));
|
||||
assert.equal(result.skills.length, 45);
|
||||
});
|
||||
|
||||
test("MCP omniroute_agent_skills_list result has all 42 IDs", async () => {
|
||||
test("MCP omniroute_agent_skills_list result has all 45 IDs", async () => {
|
||||
const result = await agentSkillTools.omniroute_agent_skills_list.handler({});
|
||||
const returnedIds = new Set(result.skills.map((s: { id: string }) => s.id));
|
||||
for (const id of ALL_IDS) {
|
||||
@@ -145,7 +157,7 @@ test("A2A list-capabilities returns exactly 1 artifact", async () => {
|
||||
assert.equal(result.artifacts[0].type, "text", "Artifact type should be 'text'");
|
||||
});
|
||||
|
||||
test("A2A list-capabilities artifact content contains 42 skill IDs as table rows", async () => {
|
||||
test("A2A list-capabilities artifact content contains 45 skill IDs as table rows", async () => {
|
||||
const result = await executeListCapabilities(stubTask);
|
||||
const content = result.artifacts[0].content;
|
||||
const rows = content
|
||||
@@ -154,15 +166,15 @@ test("A2A list-capabilities artifact content contains 42 skill IDs as table rows
|
||||
(line) => line.startsWith("| ") && !line.startsWith("| ID") && !line.startsWith("| ---")
|
||||
);
|
||||
// Each skill row starts with "| <id> |"
|
||||
assert.ok(rows.length >= 42, `Expected at least 42 data rows but got ${rows.length}`);
|
||||
assert.equal(rows.length, 45, `Expected 45 data rows but got ${rows.length}`);
|
||||
});
|
||||
|
||||
test("A2A list-capabilities metadata.totalSkills === 45 (44 + config)", async () => {
|
||||
test("A2A list-capabilities metadata.totalSkills === 45", async () => {
|
||||
const result = await executeListCapabilities(stubTask);
|
||||
assert.equal(result.metadata.totalSkills, 45);
|
||||
});
|
||||
|
||||
test("A2A list-capabilities artifact contains all 42 skill IDs", async () => {
|
||||
test("A2A list-capabilities artifact contains all 45 skill IDs", async () => {
|
||||
const result = await executeListCapabilities(stubTask);
|
||||
const content = result.artifacts[0].content;
|
||||
const missing: string[] = [];
|
||||
|
||||
@@ -77,13 +77,21 @@ test("omniroute_agent_skills_list({category:'cli'}) returns exactly 21 entries",
|
||||
assert.ok(result.skills.every((s: { category: string }) => s.category === "cli"));
|
||||
});
|
||||
|
||||
test("omniroute_agent_skills_list({category:'config'}) returns exactly 1 entry", async () => {
|
||||
const result = await agentSkillTools.omniroute_agent_skills_list.handler({ category: "config" });
|
||||
assert.equal(result.count, 1, `Expected 1 config skill but got ${result.count}`);
|
||||
assert.ok(result.skills.every((s: { category: string }) => s.category === "config"));
|
||||
});
|
||||
|
||||
test("omniroute_agent_skills_list result includes coverage shape", async () => {
|
||||
const result = await agentSkillTools.omniroute_agent_skills_list.handler({});
|
||||
assert.ok(result.coverage != null, "coverage should be present");
|
||||
assert.ok(typeof result.coverage.api === "object");
|
||||
assert.ok(typeof result.coverage.cli === "object");
|
||||
assert.ok(typeof result.coverage.config === "object");
|
||||
assert.equal(result.coverage.api.total, 23);
|
||||
assert.equal(result.coverage.cli.total, 21);
|
||||
assert.equal(result.coverage.config.total, 1);
|
||||
assert.ok(typeof result.coverage.totalSkills === "number");
|
||||
assert.ok(typeof result.coverage.generatedAt === "string");
|
||||
});
|
||||
@@ -94,7 +102,7 @@ test("omniroute_agent_skills_list skill entries have required fields", async ()
|
||||
assert.ok(typeof first.id === "string" && first.id.length > 0);
|
||||
assert.ok(typeof first.name === "string" && first.name.length > 0);
|
||||
assert.ok(typeof first.description === "string");
|
||||
assert.ok(first.category === "api" || first.category === "cli");
|
||||
assert.ok(first.category === "api" || first.category === "cli" || first.category === "config");
|
||||
assert.ok(typeof first.area === "string");
|
||||
assert.ok(typeof first.rawUrl === "string");
|
||||
assert.ok(typeof first.githubUrl === "string");
|
||||
@@ -105,6 +113,11 @@ test("AgentSkillsListSchema parses valid category filter", () => {
|
||||
assert.equal(parsed.category, "api");
|
||||
});
|
||||
|
||||
test("AgentSkillsListSchema parses config category filter", () => {
|
||||
const parsed = AgentSkillsListSchema.parse({ category: "config" });
|
||||
assert.equal(parsed.category, "config");
|
||||
});
|
||||
|
||||
test("AgentSkillsListSchema rejects invalid category", () => {
|
||||
assert.throws(() => AgentSkillsListSchema.parse({ category: "unknown" }));
|
||||
});
|
||||
@@ -140,6 +153,14 @@ test("omniroute_agent_skills_get({id:'cli-serve'}) resolves correct cli skill me
|
||||
assert.ok(typeof skill!.name === "string" && skill!.name.length > 0);
|
||||
});
|
||||
|
||||
test("omniroute_agent_skills_get({id:'config-codex-cli'}) resolves config skill metadata", async () => {
|
||||
const { getSkillById } = await import("../../src/lib/agentSkills/catalog.ts");
|
||||
const skill = getSkillById("config-codex-cli");
|
||||
assert.ok(skill != null, "config-codex-cli should exist in catalog");
|
||||
assert.equal(skill!.id, "config-codex-cli");
|
||||
assert.equal(skill!.category, "config");
|
||||
});
|
||||
|
||||
test("omniroute_agent_skills_get with invalid id throws Error", async () => {
|
||||
await assert.rejects(
|
||||
() => agentSkillTools.omniroute_agent_skills_get.handler({ id: "non-existent-skill-xyz" }),
|
||||
@@ -167,14 +188,17 @@ test("omniroute_agent_skills_coverage({}) returns coverage shape", async () => {
|
||||
assert.ok(result != null);
|
||||
assert.ok(typeof result.api === "object");
|
||||
assert.ok(typeof result.cli === "object");
|
||||
assert.ok(typeof result.config === "object");
|
||||
assert.equal(result.api.total, 23);
|
||||
assert.equal(result.cli.total, 21);
|
||||
assert.equal(result.config.total, 1);
|
||||
assert.ok(typeof result.api.have === "number");
|
||||
assert.ok(typeof result.cli.have === "number");
|
||||
assert.ok(result.api.have >= 0 && result.api.have <= 23);
|
||||
assert.ok(result.cli.have >= 0 && result.cli.have <= 21);
|
||||
assert.ok(result.config.have >= 0 && result.config.have <= 1);
|
||||
assert.ok(typeof result.totalSkills === "number");
|
||||
assert.equal(result.totalSkills, result.api.have + result.cli.have + (result.config?.have ?? 0));
|
||||
assert.equal(result.totalSkills, result.api.have + result.cli.have + result.config.have);
|
||||
assert.ok(typeof result.generatedAt === "string");
|
||||
// Validate ISO datetime format
|
||||
assert.ok(!isNaN(Date.parse(result.generatedAt)), "generatedAt should be valid ISO datetime");
|
||||
|
||||
@@ -9,6 +9,7 @@ const {
|
||||
computeCoverage,
|
||||
refreshCatalog,
|
||||
API_SKILL_IDS,
|
||||
CONFIG_SKILL_IDS,
|
||||
CLI_SKILL_IDS,
|
||||
} = await import("../../src/lib/agentSkills/catalog.ts");
|
||||
const agentSkillsConstants = await import("../../src/shared/constants/agentSkills.ts");
|
||||
@@ -25,11 +26,11 @@ test("API_SKILL_IDS has exactly 23 entries", () => {
|
||||
assert.equal(API_SKILL_IDS.length, 23);
|
||||
});
|
||||
|
||||
test("CLI_SKILL_IDS has exactly 20 entries", () => {
|
||||
test("CLI_SKILL_IDS has exactly 21 entries", () => {
|
||||
assert.equal(CLI_SKILL_IDS.length, 21);
|
||||
});
|
||||
|
||||
test("getCatalog() contains exactly 22 api skills", () => {
|
||||
test("getCatalog() contains exactly 23 api skills", () => {
|
||||
const apiSkills = getCatalog().filter((s) => s.category === "api");
|
||||
assert.equal(apiSkills.length, 23);
|
||||
});
|
||||
@@ -39,6 +40,15 @@ test("getCatalog() contains exactly 21 cli skills", () => {
|
||||
assert.equal(cliSkills.length, 21);
|
||||
});
|
||||
|
||||
test("CONFIG_SKILL_IDS has exactly 1 entry", () => {
|
||||
assert.equal(CONFIG_SKILL_IDS.length, 1);
|
||||
});
|
||||
|
||||
test("getCatalog() contains exactly 1 config skill", () => {
|
||||
const configSkills = getCatalog().filter((s) => s.category === "config");
|
||||
assert.equal(configSkills.length, 1);
|
||||
});
|
||||
|
||||
// ─── ID format ────────────────────────────────────────────────────────────────
|
||||
|
||||
test("all skill IDs match regex ^[a-z][a-z0-9-]*$", () => {
|
||||
@@ -218,6 +228,11 @@ test("computeCoverage() returns valid SkillCoverage shape", () => {
|
||||
assert.ok(typeof cov.cli.have === "number");
|
||||
assert.ok(cov.cli.have >= 0 && cov.cli.have <= 21);
|
||||
|
||||
assert.ok(typeof cov.config === "object");
|
||||
assert.equal(cov.config.total, 1);
|
||||
assert.ok(typeof cov.config.have === "number");
|
||||
assert.ok(cov.config.have >= 0 && cov.config.have <= 1);
|
||||
|
||||
assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0));
|
||||
|
||||
// generatedAt must be a valid ISO datetime string
|
||||
@@ -227,7 +242,7 @@ test("computeCoverage() returns valid SkillCoverage shape", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("computeCoverage() api.have + cli.have = totalSkills", () => {
|
||||
test("computeCoverage() category totals add up to totalSkills", () => {
|
||||
const cov = computeCoverage();
|
||||
assert.equal(cov.totalSkills, cov.api.have + cov.cli.have + (cov.config?.have ?? 0));
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* Auth tested via requireManagementAuth with live DB in temp directory.
|
||||
*
|
||||
* Coverage goals:
|
||||
* - GET /api/agent-skills — happy path (43 skills), filters, invalid category
|
||||
* - GET /api/agent-skills — happy path (45 skills), filters, invalid category
|
||||
* - GET /api/agent-skills/[id] — found, 404 not found
|
||||
* - GET /api/agent-skills/[id]/raw — found, 404 not found, 502 on GitHub failure
|
||||
* - GET /api/agent-skills/coverage — happy path
|
||||
@@ -55,7 +55,7 @@ function makeRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
body?: unknown,
|
||||
headers: Record<string, string> = {},
|
||||
headers: Record<string, string> = {}
|
||||
): Request {
|
||||
return new Request(url, {
|
||||
method,
|
||||
@@ -120,7 +120,10 @@ test("GET /api/agent-skills?category=api — returns 23 api skills", async () =>
|
||||
assert.equal(res.status, 200);
|
||||
const body = (await res.json()) as { skills: Array<{ category: string }>; count: number };
|
||||
assert.equal(body.count, 23);
|
||||
assert.ok(body.skills.every((s) => s.category === "api"), "All skills should be api category");
|
||||
assert.ok(
|
||||
body.skills.every((s) => s.category === "api"),
|
||||
"All skills should be api category"
|
||||
);
|
||||
});
|
||||
|
||||
test("GET /api/agent-skills?category=cli — returns 21 cli skills", async () => {
|
||||
@@ -130,7 +133,23 @@ test("GET /api/agent-skills?category=cli — returns 21 cli skills", async () =>
|
||||
assert.equal(res.status, 200);
|
||||
const body = (await res.json()) as { skills: Array<{ category: string }>; count: number };
|
||||
assert.equal(body.count, 21);
|
||||
assert.ok(body.skills.every((s) => s.category === "cli"), "All skills should be cli category");
|
||||
assert.ok(
|
||||
body.skills.every((s) => s.category === "cli"),
|
||||
"All skills should be cli category"
|
||||
);
|
||||
});
|
||||
|
||||
test("GET /api/agent-skills?category=config — returns 1 config skill", async () => {
|
||||
const req = makeRequest("GET", "http://localhost/api/agent-skills?category=config");
|
||||
const res = await listRoute.GET(req);
|
||||
|
||||
assert.equal(res.status, 200);
|
||||
const body = (await res.json()) as { skills: Array<{ category: string }>; count: number };
|
||||
assert.equal(body.count, 1);
|
||||
assert.ok(
|
||||
body.skills.every((s) => s.category === "config"),
|
||||
"All skills should be config category"
|
||||
);
|
||||
});
|
||||
|
||||
test("GET /api/agent-skills?area=providers — returns only providers area skills", async () => {
|
||||
@@ -154,7 +173,7 @@ test("GET /api/agent-skills?category=invalid — returns 400 with sanitized erro
|
||||
// Hard Rule #12: no stack trace exposure
|
||||
assert.ok(
|
||||
!body.error.message.match(/\bat \/|\bat file:\/\//),
|
||||
`Error message must not contain stack trace: "${body.error.message}"`,
|
||||
`Error message must not contain stack trace: "${body.error.message}"`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -192,7 +211,7 @@ test("GET /api/agent-skills/[id] — returns 404 with sanitized error for unknow
|
||||
// Hard Rule #12: no stack trace exposure
|
||||
assert.ok(
|
||||
!body.error.message.match(/\bat \/|\bat file:\/\//),
|
||||
`Error message must not contain stack trace: "${body.error.message}"`,
|
||||
`Error message must not contain stack trace: "${body.error.message}"`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -213,7 +232,7 @@ test("GET /api/agent-skills/[id]/raw — returns 404 with sanitized error for un
|
||||
// Hard Rule #12: no stack trace exposure
|
||||
assert.ok(
|
||||
!body.error.message.match(/\bat \/|\bat file:\/\//),
|
||||
`Error message must not contain stack trace: "${body.error.message}"`,
|
||||
`Error message must not contain stack trace: "${body.error.message}"`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -230,14 +249,14 @@ test("GET /api/agent-skills/[id]/raw — returns markdown or 502 for valid id (n
|
||||
// Either 200 (network available) or 502 (no network) is acceptable
|
||||
assert.ok(
|
||||
res.status === 200 || res.status === 502 || res.status === 500,
|
||||
`Expected 200, 502, or 500 but got ${res.status}`,
|
||||
`Expected 200, 502, or 500 but got ${res.status}`
|
||||
);
|
||||
|
||||
if (res.status === 200) {
|
||||
const contentType = res.headers.get("content-type") ?? "";
|
||||
assert.ok(
|
||||
contentType.includes("text/markdown"),
|
||||
`Expected text/markdown content-type, got: ${contentType}`,
|
||||
`Expected text/markdown content-type, got: ${contentType}`
|
||||
);
|
||||
const cacheControl = res.headers.get("cache-control") ?? "";
|
||||
assert.ok(cacheControl.includes("max-age=3600"), "Cache-Control should include max-age=3600");
|
||||
@@ -247,7 +266,7 @@ test("GET /api/agent-skills/[id]/raw — returns markdown or 502 for valid id (n
|
||||
assert.ok(body.error);
|
||||
assert.ok(
|
||||
!body.error.message.match(/\bat \/|\bat file:\/\//),
|
||||
`Error message must not contain stack trace: "${body.error.message}"`,
|
||||
`Error message must not contain stack trace: "${body.error.message}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -291,7 +310,7 @@ test("POST /api/agent-skills/generate — 401 when auth is required and no token
|
||||
// requireManagementAuth returns 401 or 403 when auth is required and no token
|
||||
assert.ok(
|
||||
res.status === 401 || res.status === 403,
|
||||
`Expected 401 or 403 without auth, got ${res.status}`,
|
||||
`Expected 401 or 403 without auth, got ${res.status}`
|
||||
);
|
||||
|
||||
const body = (await res.json()) as { error: { message: string } | string };
|
||||
@@ -300,7 +319,7 @@ test("POST /api/agent-skills/generate — 401 when auth is required and no token
|
||||
typeof body.error === "string" ? body.error : (body.error as { message: string }).message;
|
||||
assert.ok(
|
||||
!errorMsg.match(/\bat \/|\bat file:\/\//),
|
||||
`Error message must not contain stack trace: "${errorMsg}"`,
|
||||
`Error message must not contain stack trace: "${errorMsg}"`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -318,7 +337,7 @@ test("POST /api/agent-skills/generate — 400 when body is invalid (non-boolean
|
||||
// But with invalid body, 400 should come first
|
||||
assert.ok(
|
||||
res.status === 400 || res.status === 503,
|
||||
`Expected 400 (bad body) or 503 (generator unavailable), got ${res.status}`,
|
||||
`Expected 400 (bad body) or 503 (generator unavailable), got ${res.status}`
|
||||
);
|
||||
|
||||
const body = (await res.json()) as { error: { message: string } };
|
||||
@@ -326,7 +345,7 @@ test("POST /api/agent-skills/generate — 400 when body is invalid (non-boolean
|
||||
// Hard Rule #12: no stack trace exposure
|
||||
assert.ok(
|
||||
!body.error.message.match(/\bat \/|\bat file:\/\//),
|
||||
`Error message must not contain stack trace: "${body.error.message}"`,
|
||||
`Error message must not contain stack trace: "${body.error.message}"`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -345,7 +364,7 @@ test("POST /api/agent-skills/generate — 503 when generator module unavailable
|
||||
// Both are valid depending on merge state.
|
||||
assert.ok(
|
||||
res.status === 200 || res.status === 503,
|
||||
`Expected 200 (generator available) or 503 (generator unavailable), got ${res.status}`,
|
||||
`Expected 200 (generator available) or 503 (generator unavailable), got ${res.status}`
|
||||
);
|
||||
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
@@ -355,7 +374,7 @@ test("POST /api/agent-skills/generate — 503 when generator module unavailable
|
||||
// Hard Rule #12: no stack trace exposure
|
||||
assert.ok(
|
||||
!err.message.match(/\bat \/|\bat file:\/\//),
|
||||
`503 error message must not contain stack trace: "${err.message}"`,
|
||||
`503 error message must not contain stack trace: "${err.message}"`
|
||||
);
|
||||
} else {
|
||||
// 200: body should look like a GeneratorReport
|
||||
@@ -380,7 +399,7 @@ test("POST /api/agent-skills/generate — 400 when request body is not JSON", as
|
||||
// Hard Rule #12: no stack trace exposure
|
||||
assert.ok(
|
||||
!body.error.message.match(/\bat \/|\bat file:\/\//),
|
||||
`Error message must not contain stack trace: "${body.error.message}"`,
|
||||
`Error message must not contain stack trace: "${body.error.message}"`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -394,23 +413,20 @@ test("Hard Rule #12: all error responses contain sanitized messages (no 'at /' p
|
||||
// Collect error responses from various bad inputs
|
||||
const errorResponses: Response[] = [
|
||||
// Invalid category query
|
||||
await listRoute.GET(
|
||||
makeRequest("GET", "http://localhost/api/agent-skills?category=bad-val"),
|
||||
),
|
||||
await listRoute.GET(makeRequest("GET", "http://localhost/api/agent-skills?category=bad-val")),
|
||||
// Unknown skill id
|
||||
await idRoute.GET(makeRequest("GET", "http://localhost/api/agent-skills/unknown-id"), {
|
||||
params: Promise.resolve({ id: "unknown-id" }),
|
||||
}),
|
||||
// Unknown raw skill id
|
||||
await rawRoute.GET(
|
||||
makeRequest("GET", "http://localhost/api/agent-skills/unknown-id/raw"),
|
||||
{ params: Promise.resolve({ id: "unknown-id" }) },
|
||||
),
|
||||
await rawRoute.GET(makeRequest("GET", "http://localhost/api/agent-skills/unknown-id/raw"), {
|
||||
params: Promise.resolve({ id: "unknown-id" }),
|
||||
}),
|
||||
// Invalid generate body (non-boolean)
|
||||
await generateRoute.POST(
|
||||
makeRequest("POST", "http://localhost/api/agent-skills/generate", {
|
||||
dryRun: 42,
|
||||
}),
|
||||
})
|
||||
),
|
||||
];
|
||||
|
||||
@@ -419,14 +435,14 @@ test("Hard Rule #12: all error responses contain sanitized messages (no 'at /' p
|
||||
const contentType = res.headers.get("content-type") ?? "";
|
||||
assert.ok(
|
||||
contentType.includes("application/json") || contentType.includes("json"),
|
||||
`Error response must be JSON, got content-type: ${contentType}`,
|
||||
`Error response must be JSON, got content-type: ${contentType}`
|
||||
);
|
||||
|
||||
const body = (await res.json()) as { error?: { message?: string } };
|
||||
const message = body?.error?.message ?? "";
|
||||
assert.ok(
|
||||
!message.match(/\bat \/|\bat file:\/\//),
|
||||
`Stack trace detected in error response (status ${res.status}): "${message}"`,
|
||||
`Stack trace detected in error response (status ${res.status}): "${message}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -136,8 +136,9 @@ test("AgentSkillSchema — .parse throws on invalid input", () => {
|
||||
test("SkillCoverageSchema — valid coverage parses successfully", () => {
|
||||
const input = {
|
||||
api: { have: 23, total: 23 },
|
||||
cli: { have: 20, total: 20 },
|
||||
totalSkills: 42,
|
||||
cli: { have: 21, total: 21 },
|
||||
config: { have: 1, total: 1 },
|
||||
totalSkills: 45,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = SkillCoverageSchema.safeParse(input);
|
||||
@@ -147,8 +148,9 @@ test("SkillCoverageSchema — valid coverage parses successfully", () => {
|
||||
test("SkillCoverageSchema — wrong total literal (api.total=21) fails", () => {
|
||||
const input = {
|
||||
api: { have: 21, total: 21 },
|
||||
cli: { have: 20, total: 20 },
|
||||
totalSkills: 41,
|
||||
cli: { have: 21, total: 21 },
|
||||
config: { have: 1, total: 1 },
|
||||
totalSkills: 44,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = SkillCoverageSchema.safeParse(input);
|
||||
@@ -158,8 +160,9 @@ test("SkillCoverageSchema — wrong total literal (api.total=21) fails", () => {
|
||||
test("SkillCoverageSchema — wrong total literal (cli.total=19) fails", () => {
|
||||
const input = {
|
||||
api: { have: 23, total: 23 },
|
||||
cli: { have: 19, total: 19 },
|
||||
totalSkills: 41,
|
||||
cli: { have: 20, total: 20 },
|
||||
config: { have: 1, total: 1 },
|
||||
totalSkills: 44,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = SkillCoverageSchema.safeParse(input);
|
||||
@@ -169,8 +172,9 @@ test("SkillCoverageSchema — wrong total literal (cli.total=19) fails", () => {
|
||||
test("SkillCoverageSchema — invalid datetime fails", () => {
|
||||
const input = {
|
||||
api: { have: 23, total: 23 },
|
||||
cli: { have: 20, total: 20 },
|
||||
totalSkills: 42,
|
||||
cli: { have: 21, total: 21 },
|
||||
config: { have: 1, total: 1 },
|
||||
totalSkills: 45,
|
||||
generatedAt: "not-a-date",
|
||||
};
|
||||
const result = SkillCoverageSchema.safeParse(input);
|
||||
@@ -180,8 +184,9 @@ test("SkillCoverageSchema — invalid datetime fails", () => {
|
||||
test("SkillCoverageSchema — negative have value fails", () => {
|
||||
const input = {
|
||||
api: { have: -1, total: 23 },
|
||||
cli: { have: 20, total: 20 },
|
||||
totalSkills: 42,
|
||||
cli: { have: 21, total: 21 },
|
||||
config: { have: 1, total: 1 },
|
||||
totalSkills: 45,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
const result = SkillCoverageSchema.safeParse(input);
|
||||
@@ -207,6 +212,14 @@ test("ListQuerySchema — valid category parses successfully", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("ListQuerySchema — config category parses successfully", () => {
|
||||
const result = ListQuerySchema.safeParse({ category: "config" });
|
||||
assert.equal(result.success, true);
|
||||
if (result.success) {
|
||||
assert.equal(result.data.category, "config");
|
||||
}
|
||||
});
|
||||
|
||||
test("ListQuerySchema — invalid category fails", () => {
|
||||
const result = ListQuerySchema.safeParse({ category: "invalid" });
|
||||
assert.equal(result.success, false);
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
tallyDrift,
|
||||
readProviderTotal,
|
||||
countLocales,
|
||||
readMcpFactsFromSource,
|
||||
listLocalizedDocs,
|
||||
makeRequiredCountsValidator,
|
||||
} from "../../scripts/check/check-docs-counts-sync.mjs";
|
||||
|
||||
// Explicit types for the .mjs exports — keep the test at 0 no-explicit-any warnings.
|
||||
@@ -24,6 +27,11 @@ const tally = tallyDrift as (
|
||||
) => { strict: number; soft: number; lines: string[] };
|
||||
const readTotal = readProviderTotal as () => number;
|
||||
const locales = countLocales as () => number;
|
||||
const mcpFacts = readMcpFactsFromSource as () => { tools: number; scopes: number } | null;
|
||||
const localizedDocs = listLocalizedDocs as (relativePath: string) => string[];
|
||||
const requireCounts = makeRequiredCountsValidator as (
|
||||
requirements: { label: string; value: number }[]
|
||||
) => (content: string) => { ok: boolean; detail: string };
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const GATE = path.resolve(here, "../../scripts/check/check-docs-counts-sync.mjs");
|
||||
@@ -85,13 +93,38 @@ test("a missing file (null content) registers drift, not a crash", () => {
|
||||
// --- live source readers (smoke) -----------------------------------------------------
|
||||
|
||||
test("readProviderTotal reads a real, positive total from the catalog", () => {
|
||||
assert.ok(readTotal() > 100, "provider catalog total should be > 100");
|
||||
assert.ok(readTotal() >= 300, "live provider catalog total should be at least 300");
|
||||
});
|
||||
|
||||
test("countLocales reads a real, positive locale count from config/i18n.json", () => {
|
||||
assert.ok(locales() >= 40, "i18n config should define at least 40 locales");
|
||||
});
|
||||
|
||||
test("source-only MCP fallback matches the canonical inventory and scope union", () => {
|
||||
assert.deepEqual(mcpFacts(), { tools: 107, scopes: 32 });
|
||||
});
|
||||
|
||||
test("localized-doc discovery returns every locale root document", () => {
|
||||
const readmes = localizedDocs("README.md");
|
||||
assert.equal(readmes.length, 42);
|
||||
assert.ok(readmes.includes("docs/i18n/pt-BR/README.md"));
|
||||
assert.ok(readmes.includes("docs/i18n/zh-CN/README.md"));
|
||||
});
|
||||
|
||||
test("required-count validator reports precisely which live markers are missing", () => {
|
||||
const validate = requireCounts([
|
||||
{ label: "providers", value: 327 },
|
||||
{ label: "MCP tools", value: 107 },
|
||||
{ label: "MCP scopes", value: 32 },
|
||||
]);
|
||||
assert.equal(validate("327 providers; 107 tools; 32 scopes").ok, true);
|
||||
const stale = validate("290 providers; 104 tools; 31 scopes");
|
||||
assert.equal(stale.ok, false);
|
||||
assert.match(stale.detail, /providers=327/);
|
||||
assert.match(stale.detail, /MCP tools=107/);
|
||||
assert.match(stale.detail, /MCP scopes=32/);
|
||||
});
|
||||
|
||||
// --- live gate smoke -----------------------------------------------------------------
|
||||
|
||||
test("the gate exits 0 against the current (synced) repo state", () => {
|
||||
@@ -105,6 +138,7 @@ test("the gate exits 0 against the current (synced) repo state", () => {
|
||||
// down to 1.37B, because no gate watched that number.
|
||||
import {
|
||||
checkFreeTierHeadline,
|
||||
checkFreeTierInventory,
|
||||
extractHeadlineClaims,
|
||||
} from "../../scripts/check/check-docs-counts-sync.mjs";
|
||||
|
||||
@@ -146,9 +180,27 @@ test("free-tier gate passes when a file carries no headline at all", () => {
|
||||
assert.equal(checkHeadline("no figures here", TOTALS).ok, true);
|
||||
});
|
||||
|
||||
const checkInventory = checkFreeTierInventory as (
|
||||
content: string,
|
||||
totals: { pools: number; models: number }
|
||||
) => { ok: boolean; detail: string };
|
||||
|
||||
test("free-tier inventory gate accepts the live pool/model counts", () => {
|
||||
assert.equal(
|
||||
checkInventory("43 provider pools / 522 model budget entries", { pools: 43, models: 522 }).ok,
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("free-tier inventory gate rejects stale model counts", () => {
|
||||
const result = checkInventory("43 provider pools / 516 models", { pools: 43, models: 522 });
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.detail, /live catalog has 43 pools \/ 522 model budget entries/);
|
||||
});
|
||||
|
||||
// --- Generic numeric-claim gate (engines / MCP tools / scopes / CLI) --------
|
||||
// Extends the same drift guard to the counts that silently drifted in v3.8.49:
|
||||
// 11→12 engines, 94→104 MCP tools, 30→31 scopes, 26→33 CLI tools.
|
||||
// 11→12 engines, 94→107 MCP tools, 30→32 scopes, 26→33 CLI tools.
|
||||
import { makeNumberClaimValidator } from "../../scripts/check/check-docs-counts-sync.mjs";
|
||||
|
||||
const makeValidator = makeNumberClaimValidator as (
|
||||
@@ -157,19 +209,19 @@ const makeValidator = makeNumberClaimValidator as (
|
||||
) => (content: string) => { ok: boolean; detail: string };
|
||||
|
||||
test("MCP-tools gate accepts the aggregate and rejects a stale one", () => {
|
||||
const v = makeValidator(104, {
|
||||
const v = makeValidator(107, {
|
||||
what: "MCP tools",
|
||||
pattern: /(\d+) tools/gi,
|
||||
skipBefore: /(tools?|definitions?)\s*\(\s*$/i,
|
||||
skipAfter: /^\s*\(\d+ CLI/,
|
||||
});
|
||||
assert.equal(v("MCP Server (104 tools)").ok, true);
|
||||
assert.equal(v("with 104 tools total").ok, true);
|
||||
assert.equal(v("MCP Server (107 tools)").ok, true);
|
||||
assert.equal(v("with 107 tools total").ok, true);
|
||||
assert.equal(v("MCP Server (94 tools)").ok, false);
|
||||
});
|
||||
|
||||
test("MCP-tools gate ignores per-module counts and the CLI catalog total", () => {
|
||||
const v = makeValidator(104, {
|
||||
const v = makeValidator(107, {
|
||||
what: "MCP tools",
|
||||
pattern: /(\d+) tools/gi,
|
||||
skipBefore: /(tools?|definitions?)\s*\(\s*$/i,
|
||||
|
||||
101
tests/unit/free-tier-providers-wave5-integration.test.ts
Normal file
101
tests/unit/free-tier-providers-wave5-integration.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { deriveConfigFromRegistryModelsUrl } from "../../src/app/api/providers/[id]/models/discoveryConfig.ts";
|
||||
|
||||
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
|
||||
const { DefaultExecutor, getExecutor, hasSpecializedExecutor } =
|
||||
await import("../../open-sse/executors/index.ts");
|
||||
const { PROVIDER_ENDPOINTS } = await import("../../src/shared/constants/config.ts");
|
||||
const { isValidModel } = await import("../../src/shared/constants/models.ts");
|
||||
const { AGGREGATOR_PROVIDER_IDS } = await import("../../src/shared/constants/providers.ts");
|
||||
const { APIKEY_PROVIDERS } = await import("../../src/shared/constants/providers/apikey/index.ts");
|
||||
|
||||
const providers = [
|
||||
{
|
||||
id: "void-ai",
|
||||
endpoint: "https://api.voidai.app/v1/chat/completions",
|
||||
modelsUrl: "https://api.voidai.app/v1/models",
|
||||
hasFree: true,
|
||||
},
|
||||
{
|
||||
id: "helixmind",
|
||||
endpoint: "https://helixmind.online/v1/chat/completions",
|
||||
modelsUrl: "https://helixmind.online/v1/models",
|
||||
hasFree: false,
|
||||
},
|
||||
] as const;
|
||||
|
||||
for (const { id, endpoint, modelsUrl, hasFree } of providers) {
|
||||
test(`${id} is fully wired through the public provider interfaces`, () => {
|
||||
const registry = REGISTRY[id];
|
||||
const metadata = APIKEY_PROVIDERS[id];
|
||||
|
||||
assert.ok(registry);
|
||||
assert.ok(metadata);
|
||||
assert.equal(registry.id, id);
|
||||
assert.equal(registry.alias, id);
|
||||
assert.equal(registry.format, "openai");
|
||||
assert.equal(registry.executor, "default");
|
||||
assert.equal(registry.authType, "apikey");
|
||||
assert.equal(registry.authHeader, "bearer");
|
||||
assert.equal(registry.baseUrl, endpoint);
|
||||
assert.equal(registry.modelsUrl, modelsUrl);
|
||||
assert.equal(registry.passthroughModels, true);
|
||||
assert.deepEqual(registry.models, []);
|
||||
|
||||
assert.equal(PROVIDER_ENDPOINTS[id], endpoint);
|
||||
assert.equal(metadata.id, id);
|
||||
assert.equal(metadata.alias, id);
|
||||
assert.equal(metadata.hasFree, hasFree);
|
||||
assert.equal(metadata.passthroughModels, true);
|
||||
assert.equal(AGGREGATOR_PROVIDER_IDS.has(id), true);
|
||||
assert.equal(hasSpecializedExecutor(id), false);
|
||||
|
||||
const executor = getExecutor(id);
|
||||
assert.ok(executor instanceof DefaultExecutor);
|
||||
assert.equal(executor.buildUrl("live-model", false), endpoint);
|
||||
assert.equal(isValidModel(id, "future/live-catalog-model"), true);
|
||||
|
||||
const discovery = deriveConfigFromRegistryModelsUrl(id);
|
||||
assert.ok(discovery);
|
||||
assert.equal(discovery.url, modelsUrl);
|
||||
assert.deepEqual(discovery.parseResponse({ object: "list", data: [{ id: "live-model" }] }), [
|
||||
{ id: "live-model" },
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
test("Void AI metadata keeps the free-plan signal conditional", () => {
|
||||
const metadata = APIKEY_PROVIDERS["void-ai"];
|
||||
|
||||
assert.match(metadata.freeNote ?? "", /free plan/i);
|
||||
assert.match(metadata.freeNote ?? "", /conditional/i);
|
||||
assert.match(metadata.freeNote ?? "", /no numeric quota/i);
|
||||
assert.match(metadata.apiHint ?? "", /authentication.*account.*terms/i);
|
||||
});
|
||||
|
||||
test("HelixMind exposes its verified alternate API surfaces without reviving old quota claims", () => {
|
||||
const registry = REGISTRY.helixmind;
|
||||
const metadata = APIKEY_PROVIDERS.helixmind;
|
||||
|
||||
assert.equal(registry.responsesBaseUrl, "https://helixmind.online/v1/responses");
|
||||
assert.deepEqual(registry.alternateFormats, [
|
||||
{
|
||||
format: "claude",
|
||||
baseUrl: "https://helixmind.online/v1/messages",
|
||||
authHeader: "x-api-key",
|
||||
label: "Anthropic-compatible",
|
||||
},
|
||||
{
|
||||
format: "openai-responses",
|
||||
baseUrl: "https://helixmind.online/v1/responses",
|
||||
authHeader: "bearer",
|
||||
label: "OpenAI Responses",
|
||||
},
|
||||
]);
|
||||
assert.match(metadata.freeNote ?? "", /3 RPM\/50 RPD/i);
|
||||
assert.match(metadata.freeNote ?? "", /no-card/i);
|
||||
assert.match(metadata.freeNote ?? "", /not confirmed/i);
|
||||
assert.doesNotMatch(metadata.freeNote ?? "", /free forever|unlimited/i);
|
||||
});
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Verifies:
|
||||
* - Return shape matches §3.7 contract
|
||||
* - Markdown table contains all 44 skill IDs
|
||||
* - Markdown table contains all 45 skill IDs
|
||||
* - Coverage bounds are within declared totals
|
||||
* - metadata.source === "agent-skills-catalog"
|
||||
* - metadata.generatedAt is an ISO datetime string
|
||||
@@ -13,7 +13,11 @@ import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import type { A2ATask } from "../../src/lib/a2a/taskManager.js";
|
||||
import { executeListCapabilities } from "../../src/lib/a2a/skills/listCapabilities.js";
|
||||
import { API_SKILL_IDS, CLI_SKILL_IDS } from "../../src/lib/agentSkills/catalog.js";
|
||||
import {
|
||||
API_SKILL_IDS,
|
||||
CLI_SKILL_IDS,
|
||||
CONFIG_SKILL_IDS,
|
||||
} from "../../src/lib/agentSkills/catalog.js";
|
||||
|
||||
// Minimal stub — executeListCapabilities only receives the task arg but does not use it
|
||||
const stubTask = {} as A2ATask;
|
||||
@@ -31,20 +35,21 @@ test("executeListCapabilities returns shape matching §3.7 contract", async () =
|
||||
const { metadata } = result;
|
||||
assert.ok(metadata, "metadata exists");
|
||||
assert.equal(metadata.source, "agent-skills-catalog", "metadata.source matches");
|
||||
assert.equal(metadata.totalSkills, 45, "metadata.totalSkills === 45 (44 + config)");
|
||||
assert.equal(metadata.totalSkills, 45, "metadata.totalSkills === 45");
|
||||
assert.ok(metadata.coverage, "metadata.coverage exists");
|
||||
assert.ok(metadata.coverage.api, "metadata.coverage.api exists");
|
||||
assert.ok(metadata.coverage.cli, "metadata.coverage.cli exists");
|
||||
assert.equal(metadata.coverage.api.total, 23, "api.total === 23");
|
||||
assert.equal(metadata.coverage.cli.total, 20, "cli.total === 20");
|
||||
assert.equal(metadata.coverage.cli.total, 21, "cli.total === 21");
|
||||
assert.equal(metadata.coverage.config.total, 1, "config.total === 1");
|
||||
});
|
||||
|
||||
test("executeListCapabilities markdown table contains all 44 API+CLI skill IDs", async () => {
|
||||
test("executeListCapabilities markdown table contains all 45 skill IDs", async () => {
|
||||
const result = await executeListCapabilities(stubTask);
|
||||
const content = result.artifacts[0].content;
|
||||
|
||||
const allIds = [...API_SKILL_IDS, ...CLI_SKILL_IDS] as string[];
|
||||
assert.equal(allIds.length, 44, "API+CLI catalog declares 44 skill IDs");
|
||||
const allIds = [...API_SKILL_IDS, ...CLI_SKILL_IDS, ...CONFIG_SKILL_IDS] as string[];
|
||||
assert.equal(allIds.length, 45, "catalog declares 45 skill IDs");
|
||||
|
||||
for (const id of allIds) {
|
||||
assert.ok(content.includes(id), `Markdown table missing skill ID: ${id}`);
|
||||
@@ -65,6 +70,13 @@ test("metadata.coverage.cli.have is within [0, 21]", async () => {
|
||||
assert.ok(cli.have <= 21, "cli.have <= 21");
|
||||
});
|
||||
|
||||
test("metadata.coverage.config.have is within [0, 1]", async () => {
|
||||
const result = await executeListCapabilities(stubTask);
|
||||
const { config } = result.metadata.coverage;
|
||||
assert.ok(config.have >= 0, "config.have >= 0");
|
||||
assert.ok(config.have <= 1, "config.have <= 1");
|
||||
});
|
||||
|
||||
test("metadata.generatedAt is a valid ISO datetime", async () => {
|
||||
const result = await executeListCapabilities(stubTask);
|
||||
const { generatedAt } = result.metadata;
|
||||
|
||||
Reference in New Issue
Block a user