akm-cli 0.6.0 → 0.7.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/dist/{cli.js → src/cli.js} +672 -29
- package/dist/{commands → src/commands}/config-cli.js +5 -4
- package/dist/src/commands/distill.js +283 -0
- package/dist/src/commands/events.js +108 -0
- package/dist/src/commands/history.js +120 -0
- package/dist/{commands → src/commands}/installed-stashes.js +28 -2
- package/dist/src/commands/proposal.js +119 -0
- package/dist/src/commands/propose.js +171 -0
- package/dist/src/commands/reflect.js +193 -0
- package/dist/{commands → src/commands}/registry-search.js +2 -1
- package/dist/{commands → src/commands}/remember.js +12 -0
- package/dist/{commands → src/commands}/search.js +74 -1
- package/dist/{commands → src/commands}/self-update.js +4 -3
- package/dist/{commands → src/commands}/show.js +67 -2
- package/dist/{core → src/core}/asset-ref.js +5 -5
- package/dist/{core → src/core}/asset-spec.js +12 -0
- package/dist/{core → src/core}/common.js +1 -1
- package/dist/{core → src/core}/config.js +175 -121
- package/dist/{core → src/core}/errors.js +4 -0
- package/dist/src/core/events.js +239 -0
- package/dist/src/core/lesson-lint.js +86 -0
- package/dist/src/core/proposals.js +406 -0
- package/dist/src/core/warn.js +72 -0
- package/dist/{core → src/core}/write-source.js +80 -5
- package/dist/{indexer → src/indexer}/db-search.js +119 -27
- package/dist/{indexer → src/indexer}/db.js +76 -23
- package/dist/{indexer → src/indexer}/file-context.js +0 -3
- package/dist/src/indexer/graph-boost.js +179 -0
- package/dist/src/indexer/graph-extraction.js +212 -0
- package/dist/{indexer → src/indexer}/indexer.js +73 -6
- package/dist/src/indexer/memory-inference.js +263 -0
- package/dist/{indexer → src/indexer}/metadata.js +114 -11
- package/dist/src/integrations/agent/config.js +292 -0
- package/dist/src/integrations/agent/detect.js +94 -0
- package/dist/src/integrations/agent/index.js +17 -0
- package/dist/src/integrations/agent/profiles.js +65 -0
- package/dist/src/integrations/agent/prompts.js +167 -0
- package/dist/src/integrations/agent/spawn.js +221 -0
- package/dist/{integrations → src/integrations}/lockfile.js +0 -26
- package/dist/{llm → src/llm}/client.js +33 -2
- package/dist/src/llm/feature-gate.js +108 -0
- package/dist/src/llm/graph-extract.js +107 -0
- package/dist/src/llm/index-passes.js +35 -0
- package/dist/src/llm/memory-infer.js +86 -0
- package/dist/{output → src/output}/renderers.js +60 -1
- package/dist/src/output/shapes.js +516 -0
- package/dist/{output → src/output}/text.js +447 -4
- package/dist/{registry → src/registry}/build-index.js +14 -4
- package/dist/{registry → src/registry}/factory.js +0 -8
- package/dist/{registry → src/registry}/providers/static-index.js +3 -2
- package/dist/{registry → src/registry}/resolve.js +68 -2
- package/dist/{setup → src/setup}/setup.js +43 -5
- package/dist/{sources → src/sources}/providers/git.js +7 -15
- package/dist/{wiki → src/wiki}/wiki.js +9 -11
- package/dist/tests/add-website-source.test.js +119 -0
- package/dist/tests/agent/agent-config-loader.test.js +70 -0
- package/dist/tests/agent/agent-config.test.js +221 -0
- package/dist/tests/agent/agent-detect.test.js +100 -0
- package/dist/tests/agent/agent-spawn.test.js +234 -0
- package/dist/tests/agent-output.test.js +186 -0
- package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
- package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
- package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
- package/dist/tests/asset-ref.test.js +192 -0
- package/dist/tests/asset-registry.test.js +103 -0
- package/dist/tests/asset-spec.test.js +241 -0
- package/dist/tests/bench/attribution.test.js +995 -0
- package/dist/tests/bench/cleanup-sigint.test.js +83 -0
- package/dist/tests/bench/cleanup.js +203 -0
- package/dist/tests/bench/cleanup.test.js +166 -0
- package/dist/tests/bench/cli.js +683 -0
- package/dist/tests/bench/cli.test.js +177 -0
- package/dist/tests/bench/compare.test.js +556 -0
- package/dist/tests/bench/corpus.js +314 -0
- package/dist/tests/bench/corpus.test.js +258 -0
- package/dist/tests/bench/driver.js +346 -0
- package/dist/tests/bench/driver.test.js +443 -0
- package/dist/tests/bench/evolve-metrics.js +179 -0
- package/dist/tests/bench/evolve-metrics.test.js +187 -0
- package/dist/tests/bench/evolve.js +580 -0
- package/dist/tests/bench/evolve.test.js +616 -0
- package/dist/tests/bench/failure-modes.test.js +300 -0
- package/dist/tests/bench/feedback-integrity.test.js +456 -0
- package/dist/tests/bench/leakage.test.js +125 -0
- package/dist/tests/bench/learning-curve.test.js +133 -0
- package/dist/tests/bench/metrics.js +2319 -0
- package/dist/tests/bench/metrics.test.js +1144 -0
- package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
- package/dist/tests/bench/report.js +1821 -0
- package/dist/tests/bench/report.test.js +989 -0
- package/dist/tests/bench/runner.js +536 -0
- package/dist/tests/bench/runner.test.js +958 -0
- package/dist/tests/bench/search-bridge.test.js +331 -0
- package/dist/tests/bench/tmp.js +41 -0
- package/dist/tests/bench/trajectory.js +116 -0
- package/dist/tests/bench/trajectory.test.js +127 -0
- package/dist/tests/bench/verifier.js +109 -0
- package/dist/tests/bench/verifier.test.js +118 -0
- package/dist/tests/bench/workflow-evaluator.js +557 -0
- package/dist/tests/bench/workflow-evaluator.test.js +421 -0
- package/dist/tests/bench/workflow-spec.js +358 -0
- package/dist/tests/bench/workflow-spec.test.js +363 -0
- package/dist/tests/bench/workflow-trace.js +438 -0
- package/dist/tests/bench/workflow-trace.test.js +254 -0
- package/dist/tests/benchmark-search-quality.js +536 -0
- package/dist/tests/benchmark-suite.js +1441 -0
- package/dist/tests/capture-cli.test.js +112 -0
- package/dist/tests/cli-errors.test.js +203 -0
- package/dist/tests/commands/events.test.js +370 -0
- package/dist/tests/commands/history.test.js +223 -0
- package/dist/tests/commands/import.test.js +103 -0
- package/dist/tests/commands/proposal-cli.test.js +209 -0
- package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
- package/dist/tests/commands/remember.test.js +97 -0
- package/dist/tests/commands/scope-flags.test.js +300 -0
- package/dist/tests/commands/search.test.js +537 -0
- package/dist/tests/commands/show-indexer-parity.test.js +117 -0
- package/dist/tests/commands/show.test.js +294 -0
- package/dist/tests/common.test.js +266 -0
- package/dist/tests/completions.test.js +142 -0
- package/dist/tests/config-cli.test.js +193 -0
- package/dist/tests/config-llm-features.test.js +139 -0
- package/dist/tests/config.test.js +544 -0
- package/dist/tests/contracts/migration-baseline.test.js +43 -0
- package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
- package/dist/tests/contracts/spec-helpers.js +46 -0
- package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
- package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
- package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
- package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
- package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
- package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
- package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
- package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
- package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
- package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
- package/dist/tests/core/write-source.test.js +366 -0
- package/dist/tests/curate-command.test.js +87 -0
- package/dist/tests/db-scoring.test.js +201 -0
- package/dist/tests/db.test.js +654 -0
- package/dist/tests/distill-cli-flag.test.js +208 -0
- package/dist/tests/distill.test.js +515 -0
- package/dist/tests/docker-install.test.js +120 -0
- package/dist/tests/e2e.test.js +1398 -0
- package/dist/tests/embedder.test.js +340 -0
- package/dist/tests/embedding-model-config.test.js +379 -0
- package/dist/tests/feedback-command.test.js +172 -0
- package/dist/tests/file-context.test.js +552 -0
- package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
- package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
- package/dist/tests/fixtures/stashes/load.js +166 -0
- package/dist/tests/fixtures/stashes/load.test.js +88 -0
- package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
- package/dist/tests/frontmatter.test.js +190 -0
- package/dist/tests/fts-field-weighting.test.js +254 -0
- package/dist/tests/fuzzy-search.test.js +230 -0
- package/dist/tests/git-provider-clone.test.js +45 -0
- package/dist/tests/github.test.js +161 -0
- package/dist/tests/graph-boost-ranking.test.js +305 -0
- package/dist/tests/graph-extraction.test.js +282 -0
- package/dist/tests/helpers/usage-events.js +8 -0
- package/dist/tests/index-pass-llm.test.js +161 -0
- package/dist/tests/indexer.test.js +559 -0
- package/dist/tests/info-command.test.js +166 -0
- package/dist/tests/init.test.js +69 -0
- package/dist/tests/install-script.test.js +246 -0
- package/dist/tests/integration/agent-real-profile.test.js +94 -0
- package/dist/tests/issue-36-repro.test.js +304 -0
- package/dist/tests/issues-191-194.test.js +160 -0
- package/dist/tests/lesson-lint.test.js +111 -0
- package/dist/tests/llm-client.test.js +115 -0
- package/dist/tests/llm-feature-gate.test.js +151 -0
- package/dist/tests/llm.test.js +139 -0
- package/dist/tests/lockfile.test.js +216 -0
- package/dist/tests/manifest.test.js +205 -0
- package/dist/tests/markdown.test.js +126 -0
- package/dist/tests/matchers-unit.test.js +189 -0
- package/dist/tests/memory-inference.test.js +299 -0
- package/dist/tests/merge-scoring.test.js +136 -0
- package/dist/tests/metadata.test.js +313 -0
- package/dist/tests/migration-help.test.js +89 -0
- package/dist/tests/origin-resolve.test.js +124 -0
- package/dist/tests/output-baseline.test.js +217 -0
- package/dist/tests/output-shapes-unit.test.js +476 -0
- package/dist/tests/parallel-search.test.js +272 -0
- package/dist/tests/parameter-metadata.test.js +365 -0
- package/dist/tests/paths.test.js +177 -0
- package/dist/tests/progressive-disclosure.test.js +280 -0
- package/dist/tests/proposals.test.js +279 -0
- package/dist/tests/proposed-quality.test.js +271 -0
- package/dist/tests/provider-registry.test.js +32 -0
- package/dist/tests/ranking-regression.test.js +548 -0
- package/dist/tests/reflect-propose.test.js +455 -0
- package/dist/tests/registry-build-index.test.js +378 -0
- package/dist/tests/registry-cli.test.js +290 -0
- package/dist/tests/registry-index-v2.test.js +430 -0
- package/dist/tests/registry-install.test.js +728 -0
- package/dist/tests/registry-providers/parity.test.js +189 -0
- package/dist/tests/registry-providers/skills-sh.test.js +309 -0
- package/dist/tests/registry-providers/static-index.test.js +204 -0
- package/dist/tests/registry-resolve.test.js +126 -0
- package/dist/tests/registry-search.test.js +723 -0
- package/dist/tests/remember-frontmatter.test.js +380 -0
- package/dist/tests/remember-unit.test.js +123 -0
- package/dist/tests/ripgrep-install.test.js +251 -0
- package/dist/tests/ripgrep-resolve.test.js +108 -0
- package/dist/tests/ripgrep.test.js +163 -0
- package/dist/tests/save-command.test.js +94 -0
- package/dist/tests/save-trust-qa-fixes.test.js +270 -0
- package/dist/tests/scoring-pipeline.test.js +648 -0
- package/dist/tests/search-include-proposed-cli.test.js +118 -0
- package/dist/tests/self-update.test.js +442 -0
- package/dist/tests/semantic-search-e2e.test.js +512 -0
- package/dist/tests/semantic-status.test.js +471 -0
- package/dist/tests/setup-run.integration.js +877 -0
- package/dist/tests/setup-wizard.test.js +198 -0
- package/dist/tests/setup.test.js +131 -0
- package/dist/tests/source-add.test.js +11 -0
- package/dist/tests/source-clone.test.js +254 -0
- package/dist/tests/source-manage.test.js +366 -0
- package/dist/tests/source-providers/filesystem.test.js +82 -0
- package/dist/tests/source-providers/git.test.js +252 -0
- package/dist/tests/source-providers/website.test.js +128 -0
- package/dist/tests/source-qa-fixes.test.js +268 -0
- package/dist/tests/source-registry.test.js +350 -0
- package/dist/tests/source-resolve.test.js +100 -0
- package/dist/tests/source-source.test.js +221 -0
- package/dist/tests/source.test.js +533 -0
- package/dist/tests/tar-utils-scan.test.js +73 -0
- package/dist/tests/toggle-components.test.js +73 -0
- package/dist/tests/usage-telemetry.test.js +265 -0
- package/dist/tests/utility-scoring.test.js +558 -0
- package/dist/tests/vault-load-error.test.js +78 -0
- package/dist/tests/vault-qa-fixes.test.js +194 -0
- package/dist/tests/vault.test.js +429 -0
- package/dist/tests/vector-search.test.js +608 -0
- package/dist/tests/walker.test.js +252 -0
- package/dist/tests/wave2-cluster-bc.test.js +228 -0
- package/dist/tests/wave2-cluster-d.test.js +180 -0
- package/dist/tests/wave2-cluster-e.test.js +179 -0
- package/dist/tests/wiki-qa-fixes.test.js +270 -0
- package/dist/tests/wiki.test.js +529 -0
- package/dist/tests/workflow-cli.test.js +271 -0
- package/dist/tests/workflow-markdown.test.js +171 -0
- package/dist/tests/workflow-path-escape.test.js +132 -0
- package/dist/tests/workflow-qa-fixes.test.js +377 -0
- package/dist/tests/workflows/indexer-rejection.test.js +213 -0
- package/docs/README.md +8 -0
- package/docs/migration/release-notes/0.7.0.md +244 -0
- package/package.json +2 -2
- package/dist/core/warn.js +0 -27
- package/dist/output/shapes.js +0 -212
- /package/dist/{commands → src/commands}/completions.js +0 -0
- /package/dist/{commands → src/commands}/curate.js +0 -0
- /package/dist/{commands → src/commands}/info.js +0 -0
- /package/dist/{commands → src/commands}/init.js +0 -0
- /package/dist/{commands → src/commands}/install-audit.js +0 -0
- /package/dist/{commands → src/commands}/migration-help.js +0 -0
- /package/dist/{commands → src/commands}/source-add.js +0 -0
- /package/dist/{commands → src/commands}/source-clone.js +0 -0
- /package/dist/{commands → src/commands}/source-manage.js +0 -0
- /package/dist/{commands → src/commands}/vault.js +0 -0
- /package/dist/{core → src/core}/asset-registry.js +0 -0
- /package/dist/{core → src/core}/frontmatter.js +0 -0
- /package/dist/{core → src/core}/markdown.js +0 -0
- /package/dist/{core → src/core}/paths.js +0 -0
- /package/dist/{indexer → src/indexer}/manifest.js +0 -0
- /package/dist/{indexer → src/indexer}/matchers.js +0 -0
- /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
- /package/dist/{indexer → src/indexer}/search-source.js +0 -0
- /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
- /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
- /package/dist/{indexer → src/indexer}/walker.js +0 -0
- /package/dist/{integrations → src/integrations}/github.js +0 -0
- /package/dist/{llm → src/llm}/embedder.js +0 -0
- /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
- /package/dist/{llm → src/llm}/embedders/local.js +0 -0
- /package/dist/{llm → src/llm}/embedders/remote.js +0 -0
- /package/dist/{llm → src/llm}/embedders/types.js +0 -0
- /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
- /package/dist/{output → src/output}/cli-hints.js +0 -0
- /package/dist/{output → src/output}/context.js +0 -0
- /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
- /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
- /package/dist/{registry → src/registry}/providers/index.js +0 -0
- /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
- /package/dist/{registry → src/registry}/providers/types.js +0 -0
- /package/dist/{registry → src/registry}/types.js +0 -0
- /package/dist/{setup → src/setup}/detect.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
- /package/dist/{setup → src/setup}/steps.js +0 -0
- /package/dist/{sources → src/sources}/include.js +0 -0
- /package/dist/{sources → src/sources}/provider-factory.js +0 -0
- /package/dist/{sources → src/sources}/provider.js +0 -0
- /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
- /package/dist/{sources → src/sources}/providers/index.js +0 -0
- /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
- /package/dist/{sources → src/sources}/providers/npm.js +0 -0
- /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
- /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/website.js +0 -0
- /package/dist/{sources → src/sources}/resolve.js +0 -0
- /package/dist/{sources → src/sources}/types.js +0 -0
- /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
- /package/dist/{version.js → src/version.js} +0 -0
- /package/dist/{workflows → src/workflows}/authoring.js +0 -0
- /package/dist/{workflows → src/workflows}/cli.js +0 -0
- /package/dist/{workflows → src/workflows}/db.js +0 -0
- /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
- /package/dist/{workflows → src/workflows}/parser.js +0 -0
- /package/dist/{workflows → src/workflows}/renderer.js +0 -0
- /package/dist/{workflows → src/workflows}/runs.js +0 -0
- /package/dist/{workflows → src/workflows}/schema.js +0 -0
- /package/dist/{workflows → src/workflows}/validator.js +0 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { searchRegistry } from "../src/commands/registry-search";
|
|
6
|
+
// ── Test fixtures ───────────────────────────────────────────────────────────
|
|
7
|
+
// One entry intentionally carries the legacy `curated` boolean to exercise
|
|
8
|
+
// the v1 parse-and-ignore rule (spec §4.2). The cast is necessary because
|
|
9
|
+
// `curated` was removed from `RegistryStashEntry` in v1.
|
|
10
|
+
const FIXTURE_INDEX = {
|
|
11
|
+
version: 3,
|
|
12
|
+
updatedAt: "2026-03-09T00:00:00Z",
|
|
13
|
+
stashes: [
|
|
14
|
+
{
|
|
15
|
+
id: "npm:@itlackey/openkit",
|
|
16
|
+
name: "@itlackey/openkit",
|
|
17
|
+
description: "Starter stash for building OpenCode extensions with Bun.js",
|
|
18
|
+
ref: "@itlackey/openkit",
|
|
19
|
+
source: "npm",
|
|
20
|
+
homepage: "https://github.com/itlackey/openkit-starter",
|
|
21
|
+
tags: ["opencode", "bun", "typescript", "starter"],
|
|
22
|
+
assetTypes: ["skill", "script", "command"],
|
|
23
|
+
author: "itlackey",
|
|
24
|
+
license: "MIT",
|
|
25
|
+
latestVersion: "1.2.0",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "github:itlackey/dimm-city-stash",
|
|
29
|
+
name: "Dimm City TTRPG Stash",
|
|
30
|
+
description: "Agent skills for Dimm City creaturepunk TTRPG content generation",
|
|
31
|
+
ref: "itlackey/dimm-city-stash",
|
|
32
|
+
source: "github",
|
|
33
|
+
tags: ["ttrpg", "dimm-city", "creaturepunk", "print", "markdown"],
|
|
34
|
+
assetTypes: ["skill", "command", "knowledge"],
|
|
35
|
+
author: "itlackey",
|
|
36
|
+
license: "CC-BY-4.0",
|
|
37
|
+
// Legacy v0.6.x field — kept here to verify v1 parse-and-ignore.
|
|
38
|
+
curated: true,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "github:someone/azure-ops-stash",
|
|
42
|
+
name: "Azure Ops Stash",
|
|
43
|
+
description: "CLI skills for managing Azure Container Apps and DevOps",
|
|
44
|
+
ref: "someone/azure-ops-stash",
|
|
45
|
+
source: "github",
|
|
46
|
+
tags: ["azure", "devops", "container-apps", "infrastructure"],
|
|
47
|
+
assetTypes: ["skill", "script"],
|
|
48
|
+
author: "someone",
|
|
49
|
+
license: "MIT",
|
|
50
|
+
latestVersion: "v0.3.1",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "npm:generic-agent-utils",
|
|
54
|
+
name: "generic-agent-utils",
|
|
55
|
+
description: "Utility functions for agent development",
|
|
56
|
+
ref: "generic-agent-utils",
|
|
57
|
+
source: "npm",
|
|
58
|
+
tags: ["utility", "agent"],
|
|
59
|
+
author: "devperson",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
64
|
+
const createdTmpDirs = [];
|
|
65
|
+
function createTmpDir(prefix = "akm-search-") {
|
|
66
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
67
|
+
createdTmpDirs.push(dir);
|
|
68
|
+
return dir;
|
|
69
|
+
}
|
|
70
|
+
/** Start a minimal HTTP server that serves the fixture index. */
|
|
71
|
+
function serveIndex(index) {
|
|
72
|
+
const body = JSON.stringify(index);
|
|
73
|
+
const server = Bun.serve({
|
|
74
|
+
port: 0,
|
|
75
|
+
fetch() {
|
|
76
|
+
return new Response(body, {
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
url: `http://localhost:${server.port}/index.json`,
|
|
83
|
+
close: () => server.stop(true),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Start a server that always returns an error. */
|
|
87
|
+
function serveError(status) {
|
|
88
|
+
const server = Bun.serve({
|
|
89
|
+
port: 0,
|
|
90
|
+
fetch() {
|
|
91
|
+
return new Response("error", { status });
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
url: `http://localhost:${server.port}/index.json`,
|
|
96
|
+
close: () => server.stop(true),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
afterAll(() => {
|
|
100
|
+
for (const dir of createdTmpDirs) {
|
|
101
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const originalXdgCacheHome = process.env.XDG_CACHE_HOME;
|
|
105
|
+
const originalRegistryUrl = process.env.AKM_REGISTRY_URL;
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
// Isolate cache per test
|
|
108
|
+
process.env.XDG_CACHE_HOME = createTmpDir("akm-search-cache-");
|
|
109
|
+
delete process.env.AKM_REGISTRY_URL;
|
|
110
|
+
});
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
if (originalXdgCacheHome === undefined) {
|
|
113
|
+
delete process.env.XDG_CACHE_HOME;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
process.env.XDG_CACHE_HOME = originalXdgCacheHome;
|
|
117
|
+
}
|
|
118
|
+
if (originalRegistryUrl === undefined) {
|
|
119
|
+
delete process.env.AKM_REGISTRY_URL;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
process.env.AKM_REGISTRY_URL = originalRegistryUrl;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// ── Empty / blank queries ───────────────────────────────────────────────────
|
|
126
|
+
describe("searchRegistry", () => {
|
|
127
|
+
test("returns empty for blank query", async () => {
|
|
128
|
+
const result = await searchRegistry("");
|
|
129
|
+
expect(result).toEqual({ query: "", hits: [], warnings: [] });
|
|
130
|
+
});
|
|
131
|
+
test("returns empty for whitespace query", async () => {
|
|
132
|
+
const result = await searchRegistry(" ");
|
|
133
|
+
expect(result).toEqual({ query: "", hits: [], warnings: [] });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
// ── Scoring and ranking ─────────────────────────────────────────────────────
|
|
137
|
+
describe("scoring", () => {
|
|
138
|
+
test("exact name match ranks highest", async () => {
|
|
139
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
140
|
+
try {
|
|
141
|
+
const result = await searchRegistry("Azure Ops Stash", {
|
|
142
|
+
registries: [{ url: srv.url }],
|
|
143
|
+
});
|
|
144
|
+
expect(result.hits.length).toBeGreaterThan(0);
|
|
145
|
+
expect(result.hits[0].id).toBe("github:someone/azure-ops-stash");
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
srv.close();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
test("tag match surfaces relevant stashes", async () => {
|
|
152
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
153
|
+
try {
|
|
154
|
+
const result = await searchRegistry("creaturepunk", {
|
|
155
|
+
registries: [{ url: srv.url }],
|
|
156
|
+
});
|
|
157
|
+
expect(result.hits.length).toBeGreaterThan(0);
|
|
158
|
+
expect(result.hits[0].id).toBe("github:itlackey/dimm-city-stash");
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
srv.close();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
test("description substring matches", async () => {
|
|
165
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
166
|
+
try {
|
|
167
|
+
const result = await searchRegistry("Container Apps", {
|
|
168
|
+
registries: [{ url: srv.url }],
|
|
169
|
+
});
|
|
170
|
+
expect(result.hits.some((h) => h.id === "github:someone/azure-ops-stash")).toBe(true);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
srv.close();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
test("no match returns empty hits without error", async () => {
|
|
177
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
178
|
+
try {
|
|
179
|
+
const result = await searchRegistry("zzz-nonexistent-xxy", {
|
|
180
|
+
registries: [{ url: srv.url }],
|
|
181
|
+
});
|
|
182
|
+
expect(result.hits).toEqual([]);
|
|
183
|
+
expect(result.warnings).toEqual([]);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
srv.close();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
test("multi-token query scores across fields", async () => {
|
|
190
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
191
|
+
try {
|
|
192
|
+
const result = await searchRegistry("bun typescript starter", {
|
|
193
|
+
registries: [{ url: srv.url }],
|
|
194
|
+
});
|
|
195
|
+
expect(result.hits.length).toBeGreaterThan(0);
|
|
196
|
+
// openkit has all three in its tags
|
|
197
|
+
expect(result.hits[0].id).toBe("npm:@itlackey/openkit");
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
srv.close();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
test("author match works", async () => {
|
|
204
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
205
|
+
try {
|
|
206
|
+
const result = await searchRegistry("devperson", {
|
|
207
|
+
registries: [{ url: srv.url }],
|
|
208
|
+
});
|
|
209
|
+
expect(result.hits.length).toBe(1);
|
|
210
|
+
expect(result.hits[0].id).toBe("npm:generic-agent-utils");
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
srv.close();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
// ── Limit enforcement ───────────────────────────────────────────────────────
|
|
218
|
+
describe("limit enforcement", () => {
|
|
219
|
+
test("limit: 1 returns at most 1 hit", async () => {
|
|
220
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
221
|
+
try {
|
|
222
|
+
const result = await searchRegistry("stash", {
|
|
223
|
+
registries: [{ url: srv.url }],
|
|
224
|
+
limit: 1,
|
|
225
|
+
});
|
|
226
|
+
expect(result.hits.length).toBeLessThanOrEqual(1);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
srv.close();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
test("limit: 0 falls back to default", async () => {
|
|
233
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
234
|
+
try {
|
|
235
|
+
const result = await searchRegistry("stash", {
|
|
236
|
+
registries: [{ url: srv.url }],
|
|
237
|
+
limit: 0,
|
|
238
|
+
});
|
|
239
|
+
// Should not crash, uses default of 20
|
|
240
|
+
expect(result.hits.length).toBeLessThanOrEqual(20);
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
srv.close();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
test("limit: NaN falls back to default", async () => {
|
|
247
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
248
|
+
try {
|
|
249
|
+
const result = await searchRegistry("stash", {
|
|
250
|
+
registries: [{ url: srv.url }],
|
|
251
|
+
limit: NaN,
|
|
252
|
+
});
|
|
253
|
+
expect(result.hits.length).toBeLessThanOrEqual(20);
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
srv.close();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
// ── Caching ─────────────────────────────────────────────────────────────────
|
|
261
|
+
describe("caching", () => {
|
|
262
|
+
test("second call uses cached index (no network needed)", async () => {
|
|
263
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
264
|
+
const url = srv.url;
|
|
265
|
+
// First call — fetches from server
|
|
266
|
+
const result1 = await searchRegistry("openkit", { registries: [{ url }] });
|
|
267
|
+
expect(result1.hits.length).toBeGreaterThan(0);
|
|
268
|
+
// Kill the server
|
|
269
|
+
srv.close();
|
|
270
|
+
// Second call — should use cache
|
|
271
|
+
const result2 = await searchRegistry("openkit", { registries: [{ url }] });
|
|
272
|
+
expect(result2.hits.length).toBeGreaterThan(0);
|
|
273
|
+
expect(result2.hits[0].id).toBe(result1.hits[0].id);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
// ── Error handling ──────────────────────────────────────────────────────────
|
|
277
|
+
describe("error handling", () => {
|
|
278
|
+
test("server error produces warning, not exception", async () => {
|
|
279
|
+
const srv = serveError(500);
|
|
280
|
+
try {
|
|
281
|
+
const result = await searchRegistry("test", { registries: [{ url: srv.url }] });
|
|
282
|
+
expect(result.hits).toEqual([]);
|
|
283
|
+
expect(result.warnings.length).toBe(1);
|
|
284
|
+
expect(result.warnings[0]).toContain("HTTP 500");
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
srv.close();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
test("unreachable server produces warning", async () => {
|
|
291
|
+
const result = await searchRegistry("test", {
|
|
292
|
+
registries: [{ url: "http://127.0.0.1:1/nonexistent" }],
|
|
293
|
+
});
|
|
294
|
+
expect(result.hits).toEqual([]);
|
|
295
|
+
expect(result.warnings.length).toBe(1);
|
|
296
|
+
});
|
|
297
|
+
test("invalid JSON produces warning", async () => {
|
|
298
|
+
const server = Bun.serve({
|
|
299
|
+
port: 0,
|
|
300
|
+
fetch() {
|
|
301
|
+
return new Response("not json", {
|
|
302
|
+
headers: { "Content-Type": "application/json" },
|
|
303
|
+
});
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
try {
|
|
307
|
+
const result = await searchRegistry("test", {
|
|
308
|
+
registries: [{ url: `http://localhost:${server.port}/index.json` }],
|
|
309
|
+
});
|
|
310
|
+
expect(result.hits).toEqual([]);
|
|
311
|
+
expect(result.warnings.length).toBe(1);
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
server.stop(true);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
// ── Multiple registries ─────────────────────────────────────────────────────
|
|
319
|
+
describe("multiple registries", () => {
|
|
320
|
+
test("merges stashes from multiple registry URLs", async () => {
|
|
321
|
+
const index1 = {
|
|
322
|
+
version: 3,
|
|
323
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
324
|
+
stashes: [
|
|
325
|
+
{
|
|
326
|
+
id: "npm:stash-a",
|
|
327
|
+
name: "Stash A",
|
|
328
|
+
description: "First stash",
|
|
329
|
+
ref: "stash-a",
|
|
330
|
+
source: "npm",
|
|
331
|
+
tags: ["deploy"],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
};
|
|
335
|
+
const index2 = {
|
|
336
|
+
version: 3,
|
|
337
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
338
|
+
stashes: [
|
|
339
|
+
{
|
|
340
|
+
id: "github:org/stash-b",
|
|
341
|
+
name: "Stash B",
|
|
342
|
+
description: "Second stash for deploy workflows",
|
|
343
|
+
ref: "org/stash-b",
|
|
344
|
+
source: "github",
|
|
345
|
+
tags: ["deploy"],
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
};
|
|
349
|
+
const srv1 = serveIndex(index1);
|
|
350
|
+
const srv2 = serveIndex(index2);
|
|
351
|
+
try {
|
|
352
|
+
const result = await searchRegistry("deploy", {
|
|
353
|
+
registries: [{ url: srv1.url }, { url: srv2.url }],
|
|
354
|
+
});
|
|
355
|
+
expect(result.hits.length).toBe(2);
|
|
356
|
+
const ids = result.hits.map((h) => h.id);
|
|
357
|
+
expect(ids).toContain("npm:stash-a");
|
|
358
|
+
expect(ids).toContain("github:org/stash-b");
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
srv1.close();
|
|
362
|
+
srv2.close();
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
test("one failing registry does not block others", async () => {
|
|
366
|
+
const goodIndex = {
|
|
367
|
+
version: 3,
|
|
368
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
369
|
+
stashes: [
|
|
370
|
+
{
|
|
371
|
+
id: "npm:good-stash",
|
|
372
|
+
name: "Good Stash",
|
|
373
|
+
ref: "good-stash",
|
|
374
|
+
source: "npm",
|
|
375
|
+
tags: ["works"],
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
};
|
|
379
|
+
const good = serveIndex(goodIndex);
|
|
380
|
+
const bad = serveError(500);
|
|
381
|
+
try {
|
|
382
|
+
const result = await searchRegistry("works", {
|
|
383
|
+
registries: [{ url: good.url }, { url: bad.url }],
|
|
384
|
+
});
|
|
385
|
+
expect(result.hits.length).toBe(1);
|
|
386
|
+
expect(result.hits[0].id).toBe("npm:good-stash");
|
|
387
|
+
expect(result.warnings.length).toBe(1);
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
good.close();
|
|
391
|
+
bad.close();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
// ── Hit shape ───────────────────────────────────────────────────────────────
|
|
396
|
+
describe("hit shape", () => {
|
|
397
|
+
test("includes metadata fields from index", async () => {
|
|
398
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
399
|
+
try {
|
|
400
|
+
const result = await searchRegistry("openkit", { registries: [{ url: srv.url }] });
|
|
401
|
+
const hit = result.hits.find((h) => h.id === "npm:@itlackey/openkit");
|
|
402
|
+
expect(hit).toBeDefined();
|
|
403
|
+
expect(hit?.source).toBe("npm");
|
|
404
|
+
expect(hit?.title).toBe("@itlackey/openkit");
|
|
405
|
+
expect(hit?.ref).toBe("@itlackey/openkit");
|
|
406
|
+
expect(hit?.installRef).toBe("npm:@itlackey/openkit");
|
|
407
|
+
expect(hit?.metadata?.version).toBe("1.2.0");
|
|
408
|
+
expect(hit?.metadata?.author).toBe("itlackey");
|
|
409
|
+
expect(hit?.metadata?.license).toBe("MIT");
|
|
410
|
+
expect(hit?.metadata?.assetTypes).toBe("skill, script, command");
|
|
411
|
+
expect(typeof hit?.score).toBe("number");
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
srv.close();
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
test("installRef is prefixed with source type for github stashes", async () => {
|
|
418
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
419
|
+
try {
|
|
420
|
+
const result = await searchRegistry("azure", { registries: [{ url: srv.url }] });
|
|
421
|
+
const hit = result.hits.find((h) => h.id === "github:someone/azure-ops-stash");
|
|
422
|
+
expect(hit).toBeDefined();
|
|
423
|
+
expect(hit?.installRef).toBe("github:someone/azure-ops-stash");
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
srv.close();
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
test("legacy `curated` key in registry JSON parses and is silently ignored", async () => {
|
|
430
|
+
// Spec §4.2: the legacy registry boolean `curated` is removed in v1.
|
|
431
|
+
// Legacy index JSON containing it MUST parse without error and the key
|
|
432
|
+
// MUST NOT appear on emitted hits.
|
|
433
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
434
|
+
try {
|
|
435
|
+
const result = await searchRegistry("itlackey", { registries: [{ url: srv.url }] });
|
|
436
|
+
const legacyCuratedHit = result.hits.find((h) => h.id === "github:itlackey/dimm-city-stash");
|
|
437
|
+
expect(legacyCuratedHit).toBeDefined();
|
|
438
|
+
expect(legacyCuratedHit).not.toHaveProperty("curated");
|
|
439
|
+
const autoHit = result.hits.find((h) => h.id === "npm:@itlackey/openkit");
|
|
440
|
+
expect(autoHit).toBeDefined();
|
|
441
|
+
expect(autoHit).not.toHaveProperty("curated");
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
srv.close();
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
// ── Environment variable override ───────────────────────────────────────────
|
|
449
|
+
describe("AKM_REGISTRY_URL env var", () => {
|
|
450
|
+
test("uses env var when no explicit URLs provided", async () => {
|
|
451
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
452
|
+
process.env.AKM_REGISTRY_URL = srv.url;
|
|
453
|
+
try {
|
|
454
|
+
const result = await searchRegistry("azure");
|
|
455
|
+
expect(result.hits.length).toBeGreaterThan(0);
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
srv.close();
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
test("supports comma-separated URLs in env var", async () => {
|
|
462
|
+
const srv1 = serveIndex(FIXTURE_INDEX);
|
|
463
|
+
const srv2 = serveIndex({
|
|
464
|
+
version: 3,
|
|
465
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
466
|
+
stashes: [
|
|
467
|
+
{
|
|
468
|
+
id: "npm:extra-stash",
|
|
469
|
+
name: "extra-stash",
|
|
470
|
+
ref: "extra-stash",
|
|
471
|
+
source: "npm",
|
|
472
|
+
tags: ["azure"],
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
});
|
|
476
|
+
process.env.AKM_REGISTRY_URL = `${srv1.url},${srv2.url}`;
|
|
477
|
+
try {
|
|
478
|
+
const result = await searchRegistry("azure");
|
|
479
|
+
const ids = result.hits.map((h) => h.id);
|
|
480
|
+
expect(ids).toContain("github:someone/azure-ops-stash");
|
|
481
|
+
expect(ids).toContain("npm:extra-stash");
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
srv1.close();
|
|
485
|
+
srv2.close();
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
// ── Provenance tagging ──────────────────────────────────────────────────────
|
|
490
|
+
describe("provenance tagging", () => {
|
|
491
|
+
test("hits include registryName from entry config", async () => {
|
|
492
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
493
|
+
try {
|
|
494
|
+
const result = await searchRegistry("openkit", {
|
|
495
|
+
registries: [{ url: srv.url, name: "test-registry" }],
|
|
496
|
+
});
|
|
497
|
+
expect(result.hits.length).toBeGreaterThan(0);
|
|
498
|
+
expect(result.hits[0].registryName).toBe("test-registry");
|
|
499
|
+
}
|
|
500
|
+
finally {
|
|
501
|
+
srv.close();
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
test("registryName is undefined when entry has no name", async () => {
|
|
505
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
506
|
+
try {
|
|
507
|
+
const result = await searchRegistry("openkit", {
|
|
508
|
+
registries: [{ url: srv.url }],
|
|
509
|
+
});
|
|
510
|
+
expect(result.hits.length).toBeGreaterThan(0);
|
|
511
|
+
expect(result.hits[0].registryName).toBeUndefined();
|
|
512
|
+
}
|
|
513
|
+
finally {
|
|
514
|
+
srv.close();
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
// ── Provider-based routing ──────────────────────────────────────────────────
|
|
519
|
+
describe("provider routing", () => {
|
|
520
|
+
test("unknown provider type produces warning, not crash", async () => {
|
|
521
|
+
const result = await searchRegistry("test", {
|
|
522
|
+
registries: [{ url: "http://example.com", provider: "nonexistent-type" }],
|
|
523
|
+
});
|
|
524
|
+
expect(result.hits).toEqual([]);
|
|
525
|
+
expect(result.warnings.length).toBe(1);
|
|
526
|
+
expect(result.warnings[0]).toContain("nonexistent-type");
|
|
527
|
+
});
|
|
528
|
+
test("mixed static-index and skills-sh registries return merged results", async () => {
|
|
529
|
+
const staticSrv = serveIndex({
|
|
530
|
+
version: 3,
|
|
531
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
532
|
+
stashes: [
|
|
533
|
+
{
|
|
534
|
+
id: "npm:deploy-stash",
|
|
535
|
+
name: "deploy-stash",
|
|
536
|
+
description: "Deployment tools",
|
|
537
|
+
ref: "deploy-stash",
|
|
538
|
+
source: "npm",
|
|
539
|
+
tags: ["deploy"],
|
|
540
|
+
},
|
|
541
|
+
],
|
|
542
|
+
});
|
|
543
|
+
const skillsSrv = Bun.serve({
|
|
544
|
+
port: 0,
|
|
545
|
+
fetch() {
|
|
546
|
+
return new Response(JSON.stringify({
|
|
547
|
+
skills: [{ id: "org/skills/deploy-vercel", name: "deploy-vercel", installs: 500, source: "org/skills" }],
|
|
548
|
+
}), { headers: { "Content-Type": "application/json" } });
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
try {
|
|
552
|
+
const result = await searchRegistry("deploy", {
|
|
553
|
+
registries: [
|
|
554
|
+
{ url: staticSrv.url, name: "static" },
|
|
555
|
+
{ url: `http://localhost:${skillsSrv.port}`, name: "skills.sh", provider: "skills-sh" },
|
|
556
|
+
],
|
|
557
|
+
});
|
|
558
|
+
const ids = result.hits.map((h) => h.id);
|
|
559
|
+
expect(ids).toContain("npm:deploy-stash");
|
|
560
|
+
expect(ids).toContain("skills-sh:org/skills/deploy-vercel");
|
|
561
|
+
// installRef should be directly usable with `akm add`
|
|
562
|
+
const npmHit = result.hits.find((h) => h.id === "npm:deploy-stash");
|
|
563
|
+
expect(npmHit?.installRef).toBe("npm:deploy-stash");
|
|
564
|
+
const skillsHit = result.hits.find((h) => h.id === "skills-sh:org/skills/deploy-vercel");
|
|
565
|
+
expect(skillsHit?.installRef).toBe("github:org/skills");
|
|
566
|
+
expect(result.warnings).toEqual([]);
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
staticSrv.close();
|
|
570
|
+
skillsSrv.stop(true);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
test("one provider fails, other succeeds — partial results + warning", async () => {
|
|
574
|
+
const goodSrv = serveIndex({
|
|
575
|
+
version: 3,
|
|
576
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
577
|
+
stashes: [
|
|
578
|
+
{
|
|
579
|
+
id: "npm:good-stash",
|
|
580
|
+
name: "good-stash",
|
|
581
|
+
ref: "good-stash",
|
|
582
|
+
source: "npm",
|
|
583
|
+
tags: ["test"],
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
});
|
|
587
|
+
try {
|
|
588
|
+
const result = await searchRegistry("test", {
|
|
589
|
+
registries: [
|
|
590
|
+
{ url: goodSrv.url, name: "good" },
|
|
591
|
+
{ url: "http://127.0.0.1:1", name: "bad", provider: "skills-sh" },
|
|
592
|
+
],
|
|
593
|
+
});
|
|
594
|
+
expect(result.hits.length).toBe(1);
|
|
595
|
+
expect(result.hits[0].id).toBe("npm:good-stash");
|
|
596
|
+
expect(result.warnings.length).toBe(1);
|
|
597
|
+
}
|
|
598
|
+
finally {
|
|
599
|
+
goodSrv.close();
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
test("default provider is static-index when omitted", async () => {
|
|
603
|
+
const srv = serveIndex(FIXTURE_INDEX);
|
|
604
|
+
try {
|
|
605
|
+
// No provider field — should use static-index
|
|
606
|
+
const result = await searchRegistry("openkit", {
|
|
607
|
+
registries: [{ url: srv.url }],
|
|
608
|
+
});
|
|
609
|
+
expect(result.hits.length).toBeGreaterThan(0);
|
|
610
|
+
expect(result.hits[0].id).toBe("npm:@itlackey/openkit");
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
srv.close();
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
// ── Issue #159: incomplete hits must never appear in JSON output ────────────
|
|
618
|
+
describe("incomplete hits filter (#159)", () => {
|
|
619
|
+
test("hits missing required fields are dropped from response", async () => {
|
|
620
|
+
const { registerProvider } = await import("../src/registry/factory");
|
|
621
|
+
const goodHit = {
|
|
622
|
+
source: "github",
|
|
623
|
+
id: "github:owner/good",
|
|
624
|
+
title: "Good Hit",
|
|
625
|
+
ref: "github:owner/good",
|
|
626
|
+
installRef: "github:owner/good",
|
|
627
|
+
};
|
|
628
|
+
registerProvider("incomplete-hits-test", (() => ({
|
|
629
|
+
type: "incomplete-hits-test",
|
|
630
|
+
async search() {
|
|
631
|
+
return {
|
|
632
|
+
// {} = empty placeholder; missing-id = partial; goodHit = valid
|
|
633
|
+
hits: [{}, { source: "github", title: "x" }, goodHit],
|
|
634
|
+
};
|
|
635
|
+
},
|
|
636
|
+
})));
|
|
637
|
+
const result = await searchRegistry("anything", {
|
|
638
|
+
registries: [{ url: "http://unused", provider: "incomplete-hits-test" }],
|
|
639
|
+
});
|
|
640
|
+
expect(result.hits).toEqual([goodHit]);
|
|
641
|
+
expect(result.hits.every((h) => h && typeof h === "object" && Object.keys(h).length > 0)).toBe(true);
|
|
642
|
+
expect(result.warnings.some((w) => /incomplete hit/i.test(w))).toBe(true);
|
|
643
|
+
});
|
|
644
|
+
test("incomplete asset hits are dropped from assetHits", async () => {
|
|
645
|
+
const { registerProvider } = await import("../src/registry/factory");
|
|
646
|
+
registerProvider("incomplete-assets-test", (() => ({
|
|
647
|
+
type: "incomplete-assets-test",
|
|
648
|
+
async search() {
|
|
649
|
+
return {
|
|
650
|
+
hits: [],
|
|
651
|
+
assetHits: [
|
|
652
|
+
{},
|
|
653
|
+
{ type: "registry-asset", assetType: "skill" },
|
|
654
|
+
{
|
|
655
|
+
type: "registry-asset",
|
|
656
|
+
assetType: "skill",
|
|
657
|
+
assetName: "deploy",
|
|
658
|
+
action: "akm show skill:deploy",
|
|
659
|
+
stash: { id: "x", name: "x" },
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
})));
|
|
665
|
+
const result = await searchRegistry("anything", {
|
|
666
|
+
registries: [{ url: "http://unused", provider: "incomplete-assets-test" }],
|
|
667
|
+
});
|
|
668
|
+
expect(result.assetHits).toBeDefined();
|
|
669
|
+
expect(result.assetHits?.length).toBe(1);
|
|
670
|
+
expect(result.assetHits?.[0].assetName).toBe("deploy");
|
|
671
|
+
});
|
|
672
|
+
// PR #168 review #9: asset hits with missing/empty `stash.id` or `stash.name`
|
|
673
|
+
// are also incomplete and must not propagate to JSON output.
|
|
674
|
+
test("asset hits with missing or empty stash fields are dropped", async () => {
|
|
675
|
+
const { registerProvider } = await import("../src/registry/factory");
|
|
676
|
+
registerProvider("incomplete-stash-test", (() => ({
|
|
677
|
+
type: "incomplete-stash-test",
|
|
678
|
+
async search() {
|
|
679
|
+
return {
|
|
680
|
+
hits: [],
|
|
681
|
+
assetHits: [
|
|
682
|
+
// stash entirely missing
|
|
683
|
+
{
|
|
684
|
+
type: "registry-asset",
|
|
685
|
+
assetType: "skill",
|
|
686
|
+
assetName: "no-stash",
|
|
687
|
+
action: "akm show skill:no-stash",
|
|
688
|
+
},
|
|
689
|
+
// stash present but id is empty
|
|
690
|
+
{
|
|
691
|
+
type: "registry-asset",
|
|
692
|
+
assetType: "skill",
|
|
693
|
+
assetName: "empty-id",
|
|
694
|
+
action: "akm show skill:empty-id",
|
|
695
|
+
stash: { id: "", name: "x" },
|
|
696
|
+
},
|
|
697
|
+
// stash present but name is missing
|
|
698
|
+
{
|
|
699
|
+
type: "registry-asset",
|
|
700
|
+
assetType: "skill",
|
|
701
|
+
assetName: "no-name",
|
|
702
|
+
action: "akm show skill:no-name",
|
|
703
|
+
stash: { id: "x" },
|
|
704
|
+
},
|
|
705
|
+
// valid — only this one should survive
|
|
706
|
+
{
|
|
707
|
+
type: "registry-asset",
|
|
708
|
+
assetType: "skill",
|
|
709
|
+
assetName: "good",
|
|
710
|
+
action: "akm show skill:good",
|
|
711
|
+
stash: { id: "x", name: "x" },
|
|
712
|
+
},
|
|
713
|
+
],
|
|
714
|
+
};
|
|
715
|
+
},
|
|
716
|
+
})));
|
|
717
|
+
const result = await searchRegistry("anything", {
|
|
718
|
+
registries: [{ url: "http://unused", provider: "incomplete-stash-test" }],
|
|
719
|
+
});
|
|
720
|
+
expect(result.assetHits?.length).toBe(1);
|
|
721
|
+
expect(result.assetHits?.[0].assetName).toBe("good");
|
|
722
|
+
});
|
|
723
|
+
});
|