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,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for setup-time agent CLI detection.
|
|
3
|
+
*
|
|
4
|
+
* Acceptance coverage:
|
|
5
|
+
* • Detects every built-in profile bin via the injected `which` probe.
|
|
6
|
+
* • Picks the first available profile as the default.
|
|
7
|
+
* • Honours an existing `agent.default` when that profile is still
|
|
8
|
+
* available (round-trip stability).
|
|
9
|
+
* • Returns `undefined` when nothing is installed.
|
|
10
|
+
* • `stepAgentCliDetection` produces a config-shaped result the wizard
|
|
11
|
+
* can `apply()`.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, test } from "bun:test";
|
|
14
|
+
import { detectAgentCliProfiles, pickDefaultAgentProfile } from "../../src/integrations/agent/detect";
|
|
15
|
+
function whichOnly(installed) {
|
|
16
|
+
const set = new Set(installed);
|
|
17
|
+
return (bin) => (set.has(bin) ? `/usr/local/bin/${bin}` : undefined);
|
|
18
|
+
}
|
|
19
|
+
describe("detectAgentCliProfiles", () => {
|
|
20
|
+
test("reports every built-in profile, available iff bin found", () => {
|
|
21
|
+
const results = detectAgentCliProfiles(undefined, whichOnly(["claude", "codex"]));
|
|
22
|
+
const names = results.map((r) => r.name).sort();
|
|
23
|
+
expect(names).toEqual(["aider", "claude", "codex", "gemini", "opencode"]);
|
|
24
|
+
expect(results.find((r) => r.name === "claude")?.available).toBe(true);
|
|
25
|
+
expect(results.find((r) => r.name === "codex")?.available).toBe(true);
|
|
26
|
+
expect(results.find((r) => r.name === "gemini")?.available).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
test("includes user-defined profiles via the resolver", () => {
|
|
29
|
+
const results = detectAgentCliProfiles({ profiles: { rover: { bin: "rover-cli" } } }, whichOnly(["rover-cli"]));
|
|
30
|
+
const rover = results.find((r) => r.name === "rover");
|
|
31
|
+
expect(rover?.available).toBe(true);
|
|
32
|
+
expect(rover?.resolvedPath).toContain("rover-cli");
|
|
33
|
+
});
|
|
34
|
+
test("returns nothing-installed when the probe always says no", () => {
|
|
35
|
+
const results = detectAgentCliProfiles(undefined, whichOnly([]));
|
|
36
|
+
expect(results.every((r) => !r.available)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("pickDefaultAgentProfile", () => {
|
|
40
|
+
test("picks the first available result when no existing default", () => {
|
|
41
|
+
const picked = pickDefaultAgentProfile([
|
|
42
|
+
{ name: "aider", bin: "aider", available: false },
|
|
43
|
+
{ name: "claude", bin: "claude", available: true },
|
|
44
|
+
{ name: "codex", bin: "codex", available: true },
|
|
45
|
+
]);
|
|
46
|
+
expect(picked).toBe("claude");
|
|
47
|
+
});
|
|
48
|
+
test("keeps an existing available default", () => {
|
|
49
|
+
const picked = pickDefaultAgentProfile([
|
|
50
|
+
{ name: "claude", bin: "claude", available: true },
|
|
51
|
+
{ name: "codex", bin: "codex", available: true },
|
|
52
|
+
], "codex");
|
|
53
|
+
expect(picked).toBe("codex");
|
|
54
|
+
});
|
|
55
|
+
test("falls back when the existing default is no longer available", () => {
|
|
56
|
+
const picked = pickDefaultAgentProfile([
|
|
57
|
+
{ name: "claude", bin: "claude", available: true },
|
|
58
|
+
{ name: "codex", bin: "codex", available: false },
|
|
59
|
+
], "codex");
|
|
60
|
+
expect(picked).toBe("claude");
|
|
61
|
+
});
|
|
62
|
+
test("returns undefined when nothing is available", () => {
|
|
63
|
+
const picked = pickDefaultAgentProfile([
|
|
64
|
+
{ name: "claude", bin: "claude", available: false },
|
|
65
|
+
{ name: "codex", bin: "codex", available: false },
|
|
66
|
+
]);
|
|
67
|
+
expect(picked).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("stepAgentCliDetection (setup wizard)", () => {
|
|
71
|
+
test("persists default + leaves block absent when nothing detected & no prior config", async () => {
|
|
72
|
+
const { stepAgentCliDetection } = await import("../../src/setup/setup");
|
|
73
|
+
const result = stepAgentCliDetection({ semanticSearchMode: "auto" }, () => [
|
|
74
|
+
{ name: "claude", bin: "claude", available: false },
|
|
75
|
+
{ name: "codex", bin: "codex", available: false },
|
|
76
|
+
]);
|
|
77
|
+
expect(result.agent).toBeUndefined();
|
|
78
|
+
expect(result.detections).toHaveLength(2);
|
|
79
|
+
});
|
|
80
|
+
test("writes agent.default to the first detected profile", async () => {
|
|
81
|
+
const { stepAgentCliDetection } = await import("../../src/setup/setup");
|
|
82
|
+
const result = stepAgentCliDetection({ semanticSearchMode: "auto" }, () => [
|
|
83
|
+
{ name: "claude", bin: "claude", available: false },
|
|
84
|
+
{ name: "codex", bin: "codex", available: true },
|
|
85
|
+
]);
|
|
86
|
+
expect(result.agent?.default).toBe("codex");
|
|
87
|
+
});
|
|
88
|
+
test("preserves user-overridden default when still available", async () => {
|
|
89
|
+
const { stepAgentCliDetection } = await import("../../src/setup/setup");
|
|
90
|
+
const result = stepAgentCliDetection({
|
|
91
|
+
semanticSearchMode: "auto",
|
|
92
|
+
agent: { default: "aider", profiles: { aider: { args: ["--no-auto-commits"] } } },
|
|
93
|
+
}, () => [
|
|
94
|
+
{ name: "claude", bin: "claude", available: true },
|
|
95
|
+
{ name: "aider", bin: "aider", available: true },
|
|
96
|
+
]);
|
|
97
|
+
expect(result.agent?.default).toBe("aider");
|
|
98
|
+
expect(result.agent?.profiles?.aider?.args).toEqual(["--no-auto-commits"]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the agent CLI spawn wrapper (`runAgent`).
|
|
3
|
+
*
|
|
4
|
+
* Acceptance coverage:
|
|
5
|
+
* • Captured stdio collects stdout/stderr.
|
|
6
|
+
* • Hard timeout maps to `reason: "timeout"`.
|
|
7
|
+
* • Non-zero exit maps to `reason: "non_zero_exit"`.
|
|
8
|
+
* • Synchronous spawn failure maps to `reason: "spawn_failed"`.
|
|
9
|
+
* • Malformed JSON output (when `parseOutput: "json"`) maps to
|
|
10
|
+
* `reason: "parse_error"`.
|
|
11
|
+
* • Successful run returns `ok: true`, captured `stdout`, parsed JSON.
|
|
12
|
+
*
|
|
13
|
+
* The wrapper takes a `spawn` injection point so we never touch real
|
|
14
|
+
* binaries here. Where we do touch a real subprocess (one fast `bun -e`
|
|
15
|
+
* timeout test) we keep the timeout small and deterministic.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, expect, test } from "bun:test";
|
|
18
|
+
import { runAgent } from "../../src/integrations/agent/spawn";
|
|
19
|
+
function makeProfile(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
name: "test-agent",
|
|
22
|
+
bin: "test-agent",
|
|
23
|
+
args: [],
|
|
24
|
+
stdio: "captured",
|
|
25
|
+
envPassthrough: ["PATH"],
|
|
26
|
+
parseOutput: "text",
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function asReadableStream(text) {
|
|
31
|
+
const bytes = new TextEncoder().encode(text);
|
|
32
|
+
return new ReadableStream({
|
|
33
|
+
start(controller) {
|
|
34
|
+
controller.enqueue(bytes);
|
|
35
|
+
controller.close();
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function fakeSpawnFn(config) {
|
|
40
|
+
const state = { kills: 0 };
|
|
41
|
+
const spawn = () => {
|
|
42
|
+
if (config.throwSync)
|
|
43
|
+
throw config.throwSync;
|
|
44
|
+
let resolveExit = () => { };
|
|
45
|
+
const exited = new Promise((resolve, reject) => {
|
|
46
|
+
resolveExit = resolve;
|
|
47
|
+
if (config.rejectExit) {
|
|
48
|
+
reject(config.rejectExit);
|
|
49
|
+
}
|
|
50
|
+
else if (!config.hangsUntilKilled) {
|
|
51
|
+
resolve(config.exitCode);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const proc = {
|
|
55
|
+
exitCode: config.hangsUntilKilled ? null : config.exitCode,
|
|
56
|
+
exited,
|
|
57
|
+
stdout: asReadableStream(config.stdout ?? ""),
|
|
58
|
+
stderr: asReadableStream(config.stderr ?? ""),
|
|
59
|
+
stdin: null,
|
|
60
|
+
kill() {
|
|
61
|
+
state.kills += 1;
|
|
62
|
+
// Simulate process exit on signal.
|
|
63
|
+
resolveExit(143);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
return proc;
|
|
67
|
+
};
|
|
68
|
+
return { spawn, kills: 0 };
|
|
69
|
+
}
|
|
70
|
+
describe("runAgent — captured stdio", () => {
|
|
71
|
+
test("returns ok:true with stdout/stderr on exit 0", async () => {
|
|
72
|
+
const { spawn } = fakeSpawnFn({ exitCode: 0, stdout: "hello\n", stderr: "" });
|
|
73
|
+
const result = await runAgent(makeProfile(), "go", { spawn });
|
|
74
|
+
expect(result.ok).toBe(true);
|
|
75
|
+
expect(result.exitCode).toBe(0);
|
|
76
|
+
expect(result.stdout).toBe("hello\n");
|
|
77
|
+
expect(result.reason).toBeUndefined();
|
|
78
|
+
expect(typeof result.durationMs).toBe("number");
|
|
79
|
+
});
|
|
80
|
+
test("non-zero exit yields structured `non_zero_exit`", async () => {
|
|
81
|
+
const { spawn } = fakeSpawnFn({ exitCode: 7, stderr: "boom" });
|
|
82
|
+
const result = await runAgent(makeProfile(), "go", { spawn });
|
|
83
|
+
expect(result.ok).toBe(false);
|
|
84
|
+
expect(result.reason).toBe("non_zero_exit");
|
|
85
|
+
expect(result.exitCode).toBe(7);
|
|
86
|
+
expect(result.stderr).toBe("boom");
|
|
87
|
+
expect(result.error).toContain("exited with code 7");
|
|
88
|
+
});
|
|
89
|
+
test("synchronous spawn failure yields `spawn_failed`", async () => {
|
|
90
|
+
const { spawn } = fakeSpawnFn({ exitCode: 0, throwSync: new Error("ENOENT: command not found") });
|
|
91
|
+
const result = await runAgent(makeProfile(), "go", { spawn });
|
|
92
|
+
expect(result.ok).toBe(false);
|
|
93
|
+
expect(result.reason).toBe("spawn_failed");
|
|
94
|
+
expect(result.error).toContain("ENOENT");
|
|
95
|
+
expect(result.exitCode).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
test("rejected proc.exited yields `spawn_failed`", async () => {
|
|
98
|
+
const { spawn } = fakeSpawnFn({ exitCode: 0, rejectExit: new Error("kernel ate it") });
|
|
99
|
+
const result = await runAgent(makeProfile(), "go", { spawn });
|
|
100
|
+
expect(result.ok).toBe(false);
|
|
101
|
+
expect(result.reason).toBe("spawn_failed");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe("runAgent — timeout", () => {
|
|
105
|
+
test("kills the subprocess and reports `timeout`", async () => {
|
|
106
|
+
// Drive the timer manually so the assertion is deterministic.
|
|
107
|
+
let timerCallback;
|
|
108
|
+
const fakeSet = ((cb) => {
|
|
109
|
+
timerCallback = cb;
|
|
110
|
+
return 1;
|
|
111
|
+
});
|
|
112
|
+
const fakeClear = (() => { });
|
|
113
|
+
const { spawn } = fakeSpawnFn({ exitCode: 0, hangsUntilKilled: true });
|
|
114
|
+
const promise = runAgent(makeProfile(), "go", {
|
|
115
|
+
spawn,
|
|
116
|
+
setTimeoutFn: fakeSet,
|
|
117
|
+
clearTimeoutFn: fakeClear,
|
|
118
|
+
timeoutMs: 100,
|
|
119
|
+
});
|
|
120
|
+
// Kick the deadline.
|
|
121
|
+
expect(timerCallback).toBeDefined();
|
|
122
|
+
timerCallback?.();
|
|
123
|
+
const result = await promise;
|
|
124
|
+
expect(result.ok).toBe(false);
|
|
125
|
+
expect(result.reason).toBe("timeout");
|
|
126
|
+
expect(result.error).toContain("100ms");
|
|
127
|
+
});
|
|
128
|
+
test("real timeout against `bun -e` sleeping past the deadline (deterministic & fast)", async () => {
|
|
129
|
+
const profile = makeProfile({ bin: "bun", args: ["-e", "await new Promise(r => setTimeout(r, 5000))"] });
|
|
130
|
+
const start = Date.now();
|
|
131
|
+
const result = await runAgent(profile, undefined, { timeoutMs: 250 });
|
|
132
|
+
const elapsed = Date.now() - start;
|
|
133
|
+
expect(result.ok).toBe(false);
|
|
134
|
+
expect(result.reason).toBe("timeout");
|
|
135
|
+
// Should bail well before the 5-second sleep would complete.
|
|
136
|
+
expect(elapsed).toBeLessThan(2000);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("runAgent — JSON parse mode", () => {
|
|
140
|
+
test("parses JSON stdout and surfaces it via `parsed`", async () => {
|
|
141
|
+
const { spawn } = fakeSpawnFn({ exitCode: 0, stdout: '{"role":"agent"}' });
|
|
142
|
+
const result = await runAgent(makeProfile({ parseOutput: "json" }), "go", { spawn });
|
|
143
|
+
expect(result.ok).toBe(true);
|
|
144
|
+
expect(result.parsed).toEqual({ role: "agent" });
|
|
145
|
+
});
|
|
146
|
+
test("malformed JSON yields `parse_error`", async () => {
|
|
147
|
+
const { spawn } = fakeSpawnFn({ exitCode: 0, stdout: "not json {" });
|
|
148
|
+
const result = await runAgent(makeProfile({ parseOutput: "json" }), "go", { spawn });
|
|
149
|
+
expect(result.ok).toBe(false);
|
|
150
|
+
expect(result.reason).toBe("parse_error");
|
|
151
|
+
expect(result.error).toBeTruthy();
|
|
152
|
+
});
|
|
153
|
+
// ── #284 GAP-HIGH 10: parseOutput=json + non-zero exit + non-JSON stderr ──
|
|
154
|
+
test("parseOutput=json + non-zero exit: non_zero_exit precedence (parse_error suppressed)", async () => {
|
|
155
|
+
// Non-zero exit must surface as `non_zero_exit`, not `parse_error`, even
|
|
156
|
+
// when the stdout/stderr payload is malformed JSON. The exit code is the
|
|
157
|
+
// primary failure signal; parse failures are downstream of a successful run.
|
|
158
|
+
const { spawn } = fakeSpawnFn({
|
|
159
|
+
exitCode: 5,
|
|
160
|
+
stdout: "not json {",
|
|
161
|
+
stderr: "agent panic: kernel ate my JSON",
|
|
162
|
+
});
|
|
163
|
+
const result = await runAgent(makeProfile({ parseOutput: "json" }), "go", { spawn });
|
|
164
|
+
expect(result.ok).toBe(false);
|
|
165
|
+
expect(result.reason).toBe("non_zero_exit");
|
|
166
|
+
expect(result.exitCode).toBe(5);
|
|
167
|
+
expect(result.stderr).toBe("agent panic: kernel ate my JSON");
|
|
168
|
+
expect(result.parsed).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
// ── #284 GAP-HIGH 11: timeoutMs precedence ────────────────────────────────
|
|
172
|
+
describe("runAgent — timeoutMs precedence", () => {
|
|
173
|
+
test("options.timeoutMs overrides profile.timeoutMs", async () => {
|
|
174
|
+
// Both profile and options carry a timeoutMs. Options must win.
|
|
175
|
+
let timerCallback;
|
|
176
|
+
let observedDeadlineMs;
|
|
177
|
+
const fakeSet = ((cb, ms) => {
|
|
178
|
+
timerCallback = cb;
|
|
179
|
+
observedDeadlineMs = ms;
|
|
180
|
+
return 1;
|
|
181
|
+
});
|
|
182
|
+
const fakeClear = (() => { });
|
|
183
|
+
const { spawn } = fakeSpawnFn({ exitCode: 0, hangsUntilKilled: true });
|
|
184
|
+
const profile = makeProfile({ timeoutMs: 999_999 });
|
|
185
|
+
const promise = runAgent(profile, "go", {
|
|
186
|
+
spawn,
|
|
187
|
+
setTimeoutFn: fakeSet,
|
|
188
|
+
clearTimeoutFn: fakeClear,
|
|
189
|
+
timeoutMs: 250,
|
|
190
|
+
});
|
|
191
|
+
expect(observedDeadlineMs).toBe(250); // override won
|
|
192
|
+
timerCallback?.();
|
|
193
|
+
const result = await promise;
|
|
194
|
+
expect(result.reason).toBe("timeout");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
describe("runAgent — argument and env construction", () => {
|
|
198
|
+
test("appends prompt after profile.args and options.args", async () => {
|
|
199
|
+
let capturedCmd;
|
|
200
|
+
const spawn = (cmd) => {
|
|
201
|
+
capturedCmd = cmd;
|
|
202
|
+
return {
|
|
203
|
+
exitCode: 0,
|
|
204
|
+
exited: Promise.resolve(0),
|
|
205
|
+
stdout: asReadableStream(""),
|
|
206
|
+
stderr: asReadableStream(""),
|
|
207
|
+
stdin: null,
|
|
208
|
+
kill() { },
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
await runAgent(makeProfile({ args: ["--profile-arg"] }), "the-prompt", { spawn, args: ["--call-arg"] });
|
|
212
|
+
expect(capturedCmd).toEqual(["test-agent", "--profile-arg", "--call-arg", "the-prompt"]);
|
|
213
|
+
});
|
|
214
|
+
test("env is filtered by envPassthrough plus profile/options env", async () => {
|
|
215
|
+
let capturedEnv;
|
|
216
|
+
const spawn = (_cmd, opts) => {
|
|
217
|
+
capturedEnv = opts.env;
|
|
218
|
+
return {
|
|
219
|
+
exitCode: 0,
|
|
220
|
+
exited: Promise.resolve(0),
|
|
221
|
+
stdout: asReadableStream(""),
|
|
222
|
+
stderr: asReadableStream(""),
|
|
223
|
+
stdin: null,
|
|
224
|
+
kill() { },
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
await runAgent(makeProfile({ envPassthrough: ["KEEP_ME"], env: { PROFILE_VAR: "1" } }), undefined, {
|
|
228
|
+
spawn,
|
|
229
|
+
env: { CALL_VAR: "2" },
|
|
230
|
+
envSource: { KEEP_ME: "yes", DROP_ME: "no" },
|
|
231
|
+
});
|
|
232
|
+
expect(capturedEnv).toEqual({ KEEP_ME: "yes", PROFILE_VAR: "1", CALL_VAR: "2" });
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
const CLI = path.join(__dirname, "..", "src", "cli.ts");
|
|
7
|
+
const tempDirs = [];
|
|
8
|
+
function makeTempDir(prefix) {
|
|
9
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
10
|
+
tempDirs.push(dir);
|
|
11
|
+
return dir;
|
|
12
|
+
}
|
|
13
|
+
function writeFile(filePath, content) {
|
|
14
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
15
|
+
fs.writeFileSync(filePath, content);
|
|
16
|
+
}
|
|
17
|
+
function writeConfig(configDir, config) {
|
|
18
|
+
const configPath = path.join(configDir, "akm", "config.json");
|
|
19
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
20
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
21
|
+
}
|
|
22
|
+
function runCli(stashDir, args, config) {
|
|
23
|
+
const xdgCache = makeTempDir("akm-agent-cache-");
|
|
24
|
+
const xdgConfig = makeTempDir("akm-agent-config-");
|
|
25
|
+
if (config)
|
|
26
|
+
writeConfig(xdgConfig, config);
|
|
27
|
+
const result = spawnSync("bun", [CLI, ...args], {
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
timeout: 30_000,
|
|
30
|
+
env: {
|
|
31
|
+
...process.env,
|
|
32
|
+
AKM_STASH_DIR: stashDir,
|
|
33
|
+
XDG_CACHE_HOME: xdgCache,
|
|
34
|
+
XDG_CONFIG_HOME: xdgConfig,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (result.status !== 0) {
|
|
38
|
+
throw new Error(`CLI exited ${result.status}:\n${result.stderr}`);
|
|
39
|
+
}
|
|
40
|
+
return result.stdout.trim();
|
|
41
|
+
}
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
for (const dir of tempDirs.splice(0)) {
|
|
44
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
describe("--for-agent output mode", () => {
|
|
48
|
+
function makeStash() {
|
|
49
|
+
const stashDir = makeTempDir("akm-agent-stash-");
|
|
50
|
+
writeFile(path.join(stashDir, "agents", "architect.md"), "---\ndescription: System architecture agent\ntags: [arch, design]\n---\nYou are an architect.\n");
|
|
51
|
+
writeFile(path.join(stashDir, "scripts", "deploy.sh"), "#!/usr/bin/env bash\necho deploy\n");
|
|
52
|
+
writeFile(path.join(stashDir, "commands", "release.md"), "---\ndescription: Release process\n---\nRun release {{version}}\n");
|
|
53
|
+
return stashDir;
|
|
54
|
+
}
|
|
55
|
+
test("--for-agent search output has only: name, ref, type, description, action, score", () => {
|
|
56
|
+
const stashDir = makeStash();
|
|
57
|
+
const output = runCli(stashDir, ["search", "architect", "--format=json", "--for-agent"]);
|
|
58
|
+
const json = JSON.parse(output);
|
|
59
|
+
expect(json.hits.length).toBeGreaterThan(0);
|
|
60
|
+
const hit = json.hits[0];
|
|
61
|
+
const keys = Object.keys(hit);
|
|
62
|
+
// Must have these agent-essential fields (when present)
|
|
63
|
+
expect(keys).toContain("name");
|
|
64
|
+
expect(keys).toContain("type");
|
|
65
|
+
expect(keys).toContain("action");
|
|
66
|
+
// Only allowed keys (estimatedTokens is optional — present when fileSize is known)
|
|
67
|
+
const allowedKeys = new Set(["name", "ref", "type", "description", "action", "score", "estimatedTokens"]);
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
expect(allowedKeys.has(key)).toBe(true);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
test("--for-agent search output does NOT have: schemaVersion, stashDir, path, whyMatched, origin, editable", () => {
|
|
73
|
+
const stashDir = makeStash();
|
|
74
|
+
const output = runCli(stashDir, ["search", "architect", "--format=json", "--for-agent"]);
|
|
75
|
+
const json = JSON.parse(output);
|
|
76
|
+
// Top-level envelope must not have these
|
|
77
|
+
expect(json).not.toHaveProperty("schemaVersion");
|
|
78
|
+
expect(json).not.toHaveProperty("stashDir");
|
|
79
|
+
expect(json).not.toHaveProperty("timing");
|
|
80
|
+
// Hits must not have these
|
|
81
|
+
const hits = json.hits;
|
|
82
|
+
for (const hit of hits) {
|
|
83
|
+
expect(hit).not.toHaveProperty("path");
|
|
84
|
+
expect(hit).not.toHaveProperty("whyMatched");
|
|
85
|
+
expect(hit).not.toHaveProperty("origin");
|
|
86
|
+
expect(hit).not.toHaveProperty("editable");
|
|
87
|
+
expect(hit).not.toHaveProperty("editHint");
|
|
88
|
+
expect(hit).not.toHaveProperty("tags");
|
|
89
|
+
expect(hit).not.toHaveProperty("size");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
test("--for-agent show output strips non-essential fields", () => {
|
|
93
|
+
const stashDir = makeStash();
|
|
94
|
+
const output = runCli(stashDir, ["show", "command:release.md", "--format=json", "--for-agent"]);
|
|
95
|
+
const json = JSON.parse(output);
|
|
96
|
+
// Must have essential fields
|
|
97
|
+
expect(json).toHaveProperty("name");
|
|
98
|
+
expect(json).toHaveProperty("type");
|
|
99
|
+
// Must NOT have non-essential fields
|
|
100
|
+
expect(json).not.toHaveProperty("schemaVersion");
|
|
101
|
+
expect(json).not.toHaveProperty("path");
|
|
102
|
+
expect(json).not.toHaveProperty("origin");
|
|
103
|
+
expect(json).not.toHaveProperty("editable");
|
|
104
|
+
expect(json).not.toHaveProperty("editHint");
|
|
105
|
+
});
|
|
106
|
+
test("--for-agent show output keeps content/run/action", () => {
|
|
107
|
+
const stashDir = makeStash();
|
|
108
|
+
// Command has template content
|
|
109
|
+
const cmdOutput = runCli(stashDir, ["show", "command:release.md", "--format=json", "--for-agent"]);
|
|
110
|
+
const cmdJson = JSON.parse(cmdOutput);
|
|
111
|
+
expect(cmdJson).toHaveProperty("template");
|
|
112
|
+
expect(cmdJson).toHaveProperty("action");
|
|
113
|
+
// Script has run field
|
|
114
|
+
const scriptOutput = runCli(stashDir, ["show", "script:deploy.sh", "--format=json", "--for-agent"]);
|
|
115
|
+
const scriptJson = JSON.parse(scriptOutput);
|
|
116
|
+
expect(scriptJson).toHaveProperty("run");
|
|
117
|
+
expect(scriptJson).toHaveProperty("action");
|
|
118
|
+
});
|
|
119
|
+
test("standard output (without --for-agent) is unchanged", () => {
|
|
120
|
+
const stashDir = makeStash();
|
|
121
|
+
// Default brief search still has same shape
|
|
122
|
+
const searchOutput = runCli(stashDir, ["search", "architect", "--format=json"]);
|
|
123
|
+
const searchJson = JSON.parse(searchOutput);
|
|
124
|
+
// hits is always present; warnings may appear when semantic search is pending
|
|
125
|
+
expect(Object.keys(searchJson)).toContain("hits");
|
|
126
|
+
// Standard brief output includes at least name, type, action (may also include estimatedTokens etc.)
|
|
127
|
+
const hit = searchJson.hits[0] ?? {};
|
|
128
|
+
expect(hit).toHaveProperty("name");
|
|
129
|
+
expect(hit).toHaveProperty("type");
|
|
130
|
+
expect(hit).toHaveProperty("action");
|
|
131
|
+
// Default show still has origin
|
|
132
|
+
const showOutput = runCli(stashDir, ["show", "command:release.md", "--format=json"]);
|
|
133
|
+
const showJson = JSON.parse(showOutput);
|
|
134
|
+
expect(showJson).toHaveProperty("origin");
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
describe("--format jsonl", () => {
|
|
138
|
+
function makeStash() {
|
|
139
|
+
const stashDir = makeTempDir("akm-jsonl-stash-");
|
|
140
|
+
writeFile(path.join(stashDir, "agents", "architect.md"), "---\ndescription: System architecture agent\n---\nYou are an architect.\n");
|
|
141
|
+
writeFile(path.join(stashDir, "scripts", "deploy.sh"), "#!/usr/bin/env bash\necho deploy\n");
|
|
142
|
+
return stashDir;
|
|
143
|
+
}
|
|
144
|
+
test("JSONL format outputs one JSON object per line for search hits", () => {
|
|
145
|
+
const stashDir = makeStash();
|
|
146
|
+
// QA #14: empty query now rejects; use a real keyword that matches stash assets.
|
|
147
|
+
// Use "architect" since architect.md has that word in both name and content.
|
|
148
|
+
const output = runCli(stashDir, ["search", "architect", "--format=jsonl"]);
|
|
149
|
+
const lines = output.split("\n").filter((line) => line.trim().length > 0);
|
|
150
|
+
// Should have at least 1 hit
|
|
151
|
+
expect(lines.length).toBeGreaterThanOrEqual(1);
|
|
152
|
+
// Each line must be its own object, not wrapped in an envelope
|
|
153
|
+
for (const line of lines) {
|
|
154
|
+
const parsed = JSON.parse(line);
|
|
155
|
+
expect(typeof parsed).toBe("object");
|
|
156
|
+
expect(parsed).toHaveProperty("name");
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
test("each JSONL line is valid parseable JSON", () => {
|
|
160
|
+
const stashDir = makeStash();
|
|
161
|
+
const output = runCli(stashDir, ["search", "deploy", "--format=jsonl"]);
|
|
162
|
+
const lines = output.split("\n").filter((line) => line.trim().length > 0);
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
expect(() => JSON.parse(line)).not.toThrow();
|
|
165
|
+
const parsed = JSON.parse(line);
|
|
166
|
+
expect(typeof parsed).toBe("object");
|
|
167
|
+
expect(Array.isArray(parsed)).toBe(false);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
test("JSONL combined with --for-agent uses agent shaping", () => {
|
|
171
|
+
const stashDir = makeStash();
|
|
172
|
+
const output = runCli(stashDir, ["search", "deploy", "--format=jsonl", "--for-agent"]);
|
|
173
|
+
const lines = output.split("\n").filter((line) => line.trim().length > 0);
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
const parsed = JSON.parse(line);
|
|
176
|
+
const allowedKeys = new Set(["name", "ref", "type", "description", "action", "score", "estimatedTokens"]);
|
|
177
|
+
for (const key of Object.keys(parsed)) {
|
|
178
|
+
expect(allowedKeys.has(key)).toBe(true);
|
|
179
|
+
}
|
|
180
|
+
// Must not have stripped fields
|
|
181
|
+
expect(parsed).not.toHaveProperty("path");
|
|
182
|
+
expect(parsed).not.toHaveProperty("origin");
|
|
183
|
+
expect(parsed).not.toHaveProperty("whyMatched");
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression guard — `src/integrations/agent/**` does not import LLM SDKs.
|
|
3
|
+
*
|
|
4
|
+
* Locks v1 spec §9.7 (LLM/agent boundary) and §12 (CLI shell-out only).
|
|
5
|
+
* Issue #222.
|
|
6
|
+
*
|
|
7
|
+
* **This test is defence-in-depth, not the primary enforcement.**
|
|
8
|
+
*
|
|
9
|
+
* The primary enforcement of the agent shell-out invariant is:
|
|
10
|
+
* 1. The seam test in `agent-spawn-seam.test.ts`, which locks the
|
|
11
|
+
* `runAgent` interface.
|
|
12
|
+
* 2. The TypeScript module graph — vendor SDKs are not in
|
|
13
|
+
* `package.json`, so an accidental import would fail to resolve at
|
|
14
|
+
* build time.
|
|
15
|
+
* 3. Code review and the architectural boundary documented in
|
|
16
|
+
* `docs/technical/architecture.md`.
|
|
17
|
+
*
|
|
18
|
+
* The guard below scans file contents under `src/integrations/agent/`
|
|
19
|
+
* for known LLM SDK package names. It exists to surface accidental
|
|
20
|
+
* regressions in PRs (e.g. someone copies an example that pulls in a
|
|
21
|
+
* vendor SDK before the type-check would catch it). The list is
|
|
22
|
+
* intentionally narrow — it names specific vendor packages, not broad
|
|
23
|
+
* patterns — so it does not flag legitimate code.
|
|
24
|
+
*
|
|
25
|
+
* Adding a new SDK package to the list (when a new vendor ships) is a
|
|
26
|
+
* one-line change. Removing the test entirely is a contract violation:
|
|
27
|
+
* agents are reachable only via the spawn wrapper.
|
|
28
|
+
*/
|
|
29
|
+
import { describe, expect, test } from "bun:test";
|
|
30
|
+
import fs from "node:fs";
|
|
31
|
+
import path from "node:path";
|
|
32
|
+
const REPO_ROOT = path.resolve(import.meta.dir, "..", "..");
|
|
33
|
+
const AGENT_DIR = path.join(REPO_ROOT, "src", "integrations", "agent");
|
|
34
|
+
/**
|
|
35
|
+
* Specific vendor SDK package names whose presence in the agent
|
|
36
|
+
* integration tree would indicate the shell-out invariant has been
|
|
37
|
+
* crossed. The names are matched as quoted-import strings, not as
|
|
38
|
+
* arbitrary substrings, so unrelated mentions in comments do not
|
|
39
|
+
* trip the guard.
|
|
40
|
+
*/
|
|
41
|
+
const FORBIDDEN_LLM_SDK_PACKAGES = [
|
|
42
|
+
"@anthropic-ai/sdk",
|
|
43
|
+
"@anthropic-ai/bedrock-sdk",
|
|
44
|
+
"@anthropic-ai/vertex-sdk",
|
|
45
|
+
"openai",
|
|
46
|
+
"@google/generative-ai",
|
|
47
|
+
"@google/genai",
|
|
48
|
+
"@google-ai/generativelanguage",
|
|
49
|
+
"cohere-ai",
|
|
50
|
+
"@mistralai/mistralai",
|
|
51
|
+
"@huggingface/inference",
|
|
52
|
+
"groq-sdk",
|
|
53
|
+
"ollama",
|
|
54
|
+
"langchain",
|
|
55
|
+
"@langchain/core",
|
|
56
|
+
"@langchain/openai",
|
|
57
|
+
"@langchain/anthropic",
|
|
58
|
+
"ai",
|
|
59
|
+
"replicate",
|
|
60
|
+
];
|
|
61
|
+
function listAgentSourceFiles() {
|
|
62
|
+
const out = [];
|
|
63
|
+
function walk(dir) {
|
|
64
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
65
|
+
const abs = path.join(dir, entry.name);
|
|
66
|
+
if (entry.isDirectory())
|
|
67
|
+
walk(abs);
|
|
68
|
+
else if (entry.isFile() && entry.name.endsWith(".ts"))
|
|
69
|
+
out.push(abs);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
walk(AGENT_DIR);
|
|
73
|
+
return out.sort();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Match `from "<pkg>"` and `from '<pkg>'` and the equivalent
|
|
77
|
+
* `import("<pkg>")` / `require("<pkg>")` forms. This is deliberately
|
|
78
|
+
* a quoted-import match, not a free-text substring match, so writing
|
|
79
|
+
* about a vendor SDK in a comment ("never imports `openai`") does not
|
|
80
|
+
* trip the guard.
|
|
81
|
+
*/
|
|
82
|
+
function buildImportRegex(pkg) {
|
|
83
|
+
const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
84
|
+
return new RegExp(String.raw `(?:from|import\(|require\()\s*['"]` + escaped + `(?:/[^'"]*)?['"]`);
|
|
85
|
+
}
|
|
86
|
+
describe("regression guard: src/integrations/agent/** never imports LLM SDKs", () => {
|
|
87
|
+
test("the agent integration tree exists", () => {
|
|
88
|
+
expect(fs.existsSync(AGENT_DIR)).toBe(true);
|
|
89
|
+
const files = listAgentSourceFiles();
|
|
90
|
+
expect(files.length).toBeGreaterThan(0);
|
|
91
|
+
});
|
|
92
|
+
test.each([...FORBIDDEN_LLM_SDK_PACKAGES])("no file imports %s", (pkg) => {
|
|
93
|
+
const re = buildImportRegex(pkg);
|
|
94
|
+
const offenders = [];
|
|
95
|
+
for (const file of listAgentSourceFiles()) {
|
|
96
|
+
const text = fs.readFileSync(file, "utf8");
|
|
97
|
+
if (re.test(text)) {
|
|
98
|
+
offenders.push(path.relative(REPO_ROOT, file));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
expect(offenders).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
});
|