akm-cli 0.7.0-rc1 → 0.7.1
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/dist/{src/cli.js → cli.js} +100 -16
- package/dist/{src/commands → commands}/config-cli.js +42 -0
- package/dist/{src/commands → commands}/history.js +78 -7
- package/dist/{src/commands → commands}/registry-search.js +69 -6
- package/dist/{src/commands → commands}/search.js +30 -3
- package/dist/{src/commands → commands}/show.js +29 -0
- package/dist/{src/commands → commands}/source-add.js +5 -1
- package/dist/{src/commands → commands}/source-manage.js +7 -1
- package/dist/{src/core → core}/config.js +28 -0
- package/dist/{src/indexer → indexer}/db-search.js +1 -0
- package/dist/{src/indexer → indexer}/indexer.js +16 -2
- package/dist/{src/indexer → indexer}/matchers.js +1 -1
- package/dist/{src/indexer → indexer}/search-source.js +4 -2
- package/dist/{src/integrations → integrations}/agent/profiles.js +1 -1
- package/dist/{src/integrations → integrations}/agent/spawn.js +67 -16
- package/dist/{src/integrations → integrations}/github.js +9 -3
- package/dist/{src/llm → llm}/embedders/remote.js +37 -3
- package/dist/{src/output → output}/cli-hints.js +15 -2
- package/dist/{src/output → output}/renderers.js +3 -1
- package/dist/{src/output → output}/shapes.js +8 -1
- package/dist/{src/output → output}/text.js +156 -3
- package/dist/{src/registry → registry}/build-index.js +5 -4
- package/dist/{src/registry → registry}/providers/static-index.js +3 -1
- package/dist/{src/setup → setup}/setup.js +9 -0
- package/dist/{src/wiki → wiki}/wiki.js +54 -6
- package/dist/{src/workflows → workflows}/runs.js +37 -3
- package/package.json +8 -8
- package/dist/tests/add-website-source.test.js +0 -119
- package/dist/tests/agent/agent-config-loader.test.js +0 -70
- package/dist/tests/agent/agent-config.test.js +0 -221
- package/dist/tests/agent/agent-detect.test.js +0 -100
- package/dist/tests/agent/agent-spawn.test.js +0 -234
- package/dist/tests/agent-output.test.js +0 -186
- package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +0 -103
- package/dist/tests/architecture/agent-spawn-seam.test.js +0 -193
- package/dist/tests/architecture/llm-stateless-seam.test.js +0 -112
- package/dist/tests/asset-ref.test.js +0 -192
- package/dist/tests/asset-registry.test.js +0 -103
- package/dist/tests/asset-spec.test.js +0 -241
- package/dist/tests/bench/attribution.test.js +0 -995
- package/dist/tests/bench/cleanup-sigint.test.js +0 -83
- package/dist/tests/bench/cleanup.js +0 -203
- package/dist/tests/bench/cleanup.test.js +0 -166
- package/dist/tests/bench/cli.js +0 -683
- package/dist/tests/bench/cli.test.js +0 -177
- package/dist/tests/bench/compare.test.js +0 -556
- package/dist/tests/bench/corpus.js +0 -314
- package/dist/tests/bench/corpus.test.js +0 -258
- package/dist/tests/bench/driver.js +0 -346
- package/dist/tests/bench/driver.test.js +0 -443
- package/dist/tests/bench/evolve-metrics.js +0 -179
- package/dist/tests/bench/evolve-metrics.test.js +0 -187
- package/dist/tests/bench/evolve.js +0 -580
- package/dist/tests/bench/evolve.test.js +0 -616
- package/dist/tests/bench/failure-modes.test.js +0 -300
- package/dist/tests/bench/feedback-integrity.test.js +0 -456
- package/dist/tests/bench/leakage.test.js +0 -125
- package/dist/tests/bench/learning-curve.test.js +0 -133
- package/dist/tests/bench/metrics.js +0 -2319
- package/dist/tests/bench/metrics.test.js +0 -1144
- package/dist/tests/bench/no-os-tmpdir-invariant.test.js +0 -43
- package/dist/tests/bench/report.js +0 -1821
- package/dist/tests/bench/report.test.js +0 -989
- package/dist/tests/bench/runner.js +0 -536
- package/dist/tests/bench/runner.test.js +0 -958
- package/dist/tests/bench/search-bridge.test.js +0 -331
- package/dist/tests/bench/tmp.js +0 -41
- package/dist/tests/bench/trajectory.js +0 -116
- package/dist/tests/bench/trajectory.test.js +0 -127
- package/dist/tests/bench/verifier.js +0 -109
- package/dist/tests/bench/verifier.test.js +0 -118
- package/dist/tests/bench/workflow-evaluator.js +0 -557
- package/dist/tests/bench/workflow-evaluator.test.js +0 -421
- package/dist/tests/bench/workflow-spec.js +0 -358
- package/dist/tests/bench/workflow-spec.test.js +0 -363
- package/dist/tests/bench/workflow-trace.js +0 -438
- package/dist/tests/bench/workflow-trace.test.js +0 -254
- package/dist/tests/benchmark-search-quality.js +0 -536
- package/dist/tests/benchmark-suite.js +0 -1441
- package/dist/tests/capture-cli.test.js +0 -112
- package/dist/tests/cli-errors.test.js +0 -203
- package/dist/tests/commands/events.test.js +0 -370
- package/dist/tests/commands/history.test.js +0 -223
- package/dist/tests/commands/import.test.js +0 -103
- package/dist/tests/commands/proposal-cli.test.js +0 -209
- package/dist/tests/commands/reflect-propose-cli.test.js +0 -333
- package/dist/tests/commands/remember.test.js +0 -97
- package/dist/tests/commands/scope-flags.test.js +0 -300
- package/dist/tests/commands/search.test.js +0 -537
- package/dist/tests/commands/show-indexer-parity.test.js +0 -117
- package/dist/tests/commands/show.test.js +0 -294
- package/dist/tests/common.test.js +0 -266
- package/dist/tests/completions.test.js +0 -142
- package/dist/tests/config-cli.test.js +0 -193
- package/dist/tests/config-llm-features.test.js +0 -139
- package/dist/tests/config.test.js +0 -544
- package/dist/tests/contracts/migration-baseline.test.js +0 -43
- package/dist/tests/contracts/reflect-propose-envelope.test.js +0 -139
- package/dist/tests/contracts/spec-helpers.js +0 -46
- package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +0 -228
- package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +0 -56
- package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +0 -34
- package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +0 -94
- package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +0 -39
- package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +0 -44
- package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +0 -47
- package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +0 -40
- package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +0 -58
- package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +0 -34
- package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +0 -75
- package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +0 -36
- package/dist/tests/core/write-source.test.js +0 -366
- package/dist/tests/curate-command.test.js +0 -87
- package/dist/tests/db-scoring.test.js +0 -201
- package/dist/tests/db.test.js +0 -654
- package/dist/tests/distill-cli-flag.test.js +0 -208
- package/dist/tests/distill.test.js +0 -515
- package/dist/tests/docker-install.test.js +0 -120
- package/dist/tests/e2e.test.js +0 -1398
- package/dist/tests/embedder.test.js +0 -340
- package/dist/tests/embedding-model-config.test.js +0 -379
- package/dist/tests/feedback-command.test.js +0 -172
- package/dist/tests/file-context.test.js +0 -552
- package/dist/tests/fixtures/scripts/git/summarize-diff.js +0 -9
- package/dist/tests/fixtures/scripts/lint/eslint-check.js +0 -7
- package/dist/tests/fixtures/stashes/load.js +0 -166
- package/dist/tests/fixtures/stashes/load.test.js +0 -88
- package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +0 -12
- package/dist/tests/frontmatter.test.js +0 -190
- package/dist/tests/fts-field-weighting.test.js +0 -254
- package/dist/tests/fuzzy-search.test.js +0 -230
- package/dist/tests/git-provider-clone.test.js +0 -45
- package/dist/tests/github.test.js +0 -161
- package/dist/tests/graph-boost-ranking.test.js +0 -305
- package/dist/tests/graph-extraction.test.js +0 -282
- package/dist/tests/helpers/usage-events.js +0 -8
- package/dist/tests/index-pass-llm.test.js +0 -161
- package/dist/tests/indexer.test.js +0 -559
- package/dist/tests/info-command.test.js +0 -166
- package/dist/tests/init.test.js +0 -69
- package/dist/tests/install-script.test.js +0 -246
- package/dist/tests/integration/agent-real-profile.test.js +0 -94
- package/dist/tests/issue-36-repro.test.js +0 -304
- package/dist/tests/issues-191-194.test.js +0 -160
- package/dist/tests/lesson-lint.test.js +0 -111
- package/dist/tests/llm-client.test.js +0 -115
- package/dist/tests/llm-feature-gate.test.js +0 -151
- package/dist/tests/llm.test.js +0 -139
- package/dist/tests/lockfile.test.js +0 -216
- package/dist/tests/manifest.test.js +0 -205
- package/dist/tests/markdown.test.js +0 -126
- package/dist/tests/matchers-unit.test.js +0 -189
- package/dist/tests/memory-inference.test.js +0 -299
- package/dist/tests/merge-scoring.test.js +0 -136
- package/dist/tests/metadata.test.js +0 -313
- package/dist/tests/migration-help.test.js +0 -89
- package/dist/tests/origin-resolve.test.js +0 -124
- package/dist/tests/output-baseline.test.js +0 -217
- package/dist/tests/output-shapes-unit.test.js +0 -476
- package/dist/tests/parallel-search.test.js +0 -272
- package/dist/tests/parameter-metadata.test.js +0 -365
- package/dist/tests/paths.test.js +0 -177
- package/dist/tests/progressive-disclosure.test.js +0 -280
- package/dist/tests/proposals.test.js +0 -279
- package/dist/tests/proposed-quality.test.js +0 -271
- package/dist/tests/provider-registry.test.js +0 -32
- package/dist/tests/ranking-regression.test.js +0 -548
- package/dist/tests/reflect-propose.test.js +0 -455
- package/dist/tests/registry-build-index.test.js +0 -378
- package/dist/tests/registry-cli.test.js +0 -290
- package/dist/tests/registry-index-v2.test.js +0 -430
- package/dist/tests/registry-install.test.js +0 -728
- package/dist/tests/registry-providers/parity.test.js +0 -189
- package/dist/tests/registry-providers/skills-sh.test.js +0 -309
- package/dist/tests/registry-providers/static-index.test.js +0 -204
- package/dist/tests/registry-resolve.test.js +0 -126
- package/dist/tests/registry-search.test.js +0 -723
- package/dist/tests/remember-frontmatter.test.js +0 -380
- package/dist/tests/remember-unit.test.js +0 -123
- package/dist/tests/ripgrep-install.test.js +0 -251
- package/dist/tests/ripgrep-resolve.test.js +0 -108
- package/dist/tests/ripgrep.test.js +0 -163
- package/dist/tests/save-command.test.js +0 -94
- package/dist/tests/save-trust-qa-fixes.test.js +0 -270
- package/dist/tests/scoring-pipeline.test.js +0 -648
- package/dist/tests/search-include-proposed-cli.test.js +0 -118
- package/dist/tests/self-update.test.js +0 -442
- package/dist/tests/semantic-search-e2e.test.js +0 -512
- package/dist/tests/semantic-status.test.js +0 -471
- package/dist/tests/setup-run.integration.js +0 -877
- package/dist/tests/setup-wizard.test.js +0 -198
- package/dist/tests/setup.test.js +0 -131
- package/dist/tests/source-add.test.js +0 -11
- package/dist/tests/source-clone.test.js +0 -254
- package/dist/tests/source-manage.test.js +0 -366
- package/dist/tests/source-providers/filesystem.test.js +0 -82
- package/dist/tests/source-providers/git.test.js +0 -252
- package/dist/tests/source-providers/website.test.js +0 -128
- package/dist/tests/source-qa-fixes.test.js +0 -268
- package/dist/tests/source-registry.test.js +0 -350
- package/dist/tests/source-resolve.test.js +0 -100
- package/dist/tests/source-source.test.js +0 -221
- package/dist/tests/source.test.js +0 -533
- package/dist/tests/tar-utils-scan.test.js +0 -73
- package/dist/tests/toggle-components.test.js +0 -73
- package/dist/tests/usage-telemetry.test.js +0 -265
- package/dist/tests/utility-scoring.test.js +0 -558
- package/dist/tests/vault-load-error.test.js +0 -78
- package/dist/tests/vault-qa-fixes.test.js +0 -194
- package/dist/tests/vault.test.js +0 -429
- package/dist/tests/vector-search.test.js +0 -608
- package/dist/tests/walker.test.js +0 -252
- package/dist/tests/wave2-cluster-bc.test.js +0 -228
- package/dist/tests/wave2-cluster-d.test.js +0 -180
- package/dist/tests/wave2-cluster-e.test.js +0 -179
- package/dist/tests/wiki-qa-fixes.test.js +0 -270
- package/dist/tests/wiki.test.js +0 -529
- package/dist/tests/workflow-cli.test.js +0 -271
- package/dist/tests/workflow-markdown.test.js +0 -171
- package/dist/tests/workflow-path-escape.test.js +0 -132
- package/dist/tests/workflow-qa-fixes.test.js +0 -377
- package/dist/tests/workflows/indexer-rejection.test.js +0 -213
- /package/dist/{src/commands → commands}/completions.js +0 -0
- /package/dist/{src/commands → commands}/curate.js +0 -0
- /package/dist/{src/commands → commands}/distill.js +0 -0
- /package/dist/{src/commands → commands}/events.js +0 -0
- /package/dist/{src/commands → commands}/info.js +0 -0
- /package/dist/{src/commands → commands}/init.js +0 -0
- /package/dist/{src/commands → commands}/install-audit.js +0 -0
- /package/dist/{src/commands → commands}/installed-stashes.js +0 -0
- /package/dist/{src/commands → commands}/migration-help.js +0 -0
- /package/dist/{src/commands → commands}/proposal.js +0 -0
- /package/dist/{src/commands → commands}/propose.js +0 -0
- /package/dist/{src/commands → commands}/reflect.js +0 -0
- /package/dist/{src/commands → commands}/remember.js +0 -0
- /package/dist/{src/commands → commands}/self-update.js +0 -0
- /package/dist/{src/commands → commands}/source-clone.js +0 -0
- /package/dist/{src/commands → commands}/vault.js +0 -0
- /package/dist/{src/core → core}/asset-ref.js +0 -0
- /package/dist/{src/core → core}/asset-registry.js +0 -0
- /package/dist/{src/core → core}/asset-spec.js +0 -0
- /package/dist/{src/core → core}/common.js +0 -0
- /package/dist/{src/core → core}/errors.js +0 -0
- /package/dist/{src/core → core}/events.js +0 -0
- /package/dist/{src/core → core}/frontmatter.js +0 -0
- /package/dist/{src/core → core}/lesson-lint.js +0 -0
- /package/dist/{src/core → core}/markdown.js +0 -0
- /package/dist/{src/core → core}/paths.js +0 -0
- /package/dist/{src/core → core}/proposals.js +0 -0
- /package/dist/{src/core → core}/warn.js +0 -0
- /package/dist/{src/core → core}/write-source.js +0 -0
- /package/dist/{src/indexer → indexer}/db.js +0 -0
- /package/dist/{src/indexer → indexer}/file-context.js +0 -0
- /package/dist/{src/indexer → indexer}/graph-boost.js +0 -0
- /package/dist/{src/indexer → indexer}/graph-extraction.js +0 -0
- /package/dist/{src/indexer → indexer}/manifest.js +0 -0
- /package/dist/{src/indexer → indexer}/memory-inference.js +0 -0
- /package/dist/{src/indexer → indexer}/metadata.js +0 -0
- /package/dist/{src/indexer → indexer}/search-fields.js +0 -0
- /package/dist/{src/indexer → indexer}/semantic-status.js +0 -0
- /package/dist/{src/indexer → indexer}/usage-events.js +0 -0
- /package/dist/{src/indexer → indexer}/walker.js +0 -0
- /package/dist/{src/integrations → integrations}/agent/config.js +0 -0
- /package/dist/{src/integrations → integrations}/agent/detect.js +0 -0
- /package/dist/{src/integrations → integrations}/agent/index.js +0 -0
- /package/dist/{src/integrations → integrations}/agent/prompts.js +0 -0
- /package/dist/{src/integrations → integrations}/lockfile.js +0 -0
- /package/dist/{src/llm → llm}/client.js +0 -0
- /package/dist/{src/llm → llm}/embedder.js +0 -0
- /package/dist/{src/llm → llm}/embedders/cache.js +0 -0
- /package/dist/{src/llm → llm}/embedders/local.js +0 -0
- /package/dist/{src/llm → llm}/embedders/types.js +0 -0
- /package/dist/{src/llm → llm}/feature-gate.js +0 -0
- /package/dist/{src/llm → llm}/graph-extract.js +0 -0
- /package/dist/{src/llm → llm}/index-passes.js +0 -0
- /package/dist/{src/llm → llm}/memory-infer.js +0 -0
- /package/dist/{src/llm → llm}/metadata-enhance.js +0 -0
- /package/dist/{src/output → output}/context.js +0 -0
- /package/dist/{src/registry → registry}/create-provider-registry.js +0 -0
- /package/dist/{src/registry → registry}/factory.js +0 -0
- /package/dist/{src/registry → registry}/origin-resolve.js +0 -0
- /package/dist/{src/registry → registry}/providers/index.js +0 -0
- /package/dist/{src/registry → registry}/providers/skills-sh.js +0 -0
- /package/dist/{src/registry → registry}/providers/types.js +0 -0
- /package/dist/{src/registry → registry}/resolve.js +0 -0
- /package/dist/{src/registry → registry}/types.js +0 -0
- /package/dist/{src/setup → setup}/detect.js +0 -0
- /package/dist/{src/setup → setup}/ripgrep-install.js +0 -0
- /package/dist/{src/setup → setup}/ripgrep-resolve.js +0 -0
- /package/dist/{src/setup → setup}/steps.js +0 -0
- /package/dist/{src/sources → sources}/include.js +0 -0
- /package/dist/{src/sources → sources}/provider-factory.js +0 -0
- /package/dist/{src/sources → sources}/provider.js +0 -0
- /package/dist/{src/sources → sources}/providers/filesystem.js +0 -0
- /package/dist/{src/sources → sources}/providers/git.js +0 -0
- /package/dist/{src/sources → sources}/providers/index.js +0 -0
- /package/dist/{src/sources → sources}/providers/install-types.js +0 -0
- /package/dist/{src/sources → sources}/providers/npm.js +0 -0
- /package/dist/{src/sources → sources}/providers/provider-utils.js +0 -0
- /package/dist/{src/sources → sources}/providers/sync-from-ref.js +0 -0
- /package/dist/{src/sources → sources}/providers/tar-utils.js +0 -0
- /package/dist/{src/sources → sources}/providers/website.js +0 -0
- /package/dist/{src/sources → sources}/resolve.js +0 -0
- /package/dist/{src/sources → sources}/types.js +0 -0
- /package/dist/{src/templates → templates}/wiki-templates.js +0 -0
- /package/dist/{src/version.js → version.js} +0 -0
- /package/dist/{src/workflows → workflows}/authoring.js +0 -0
- /package/dist/{src/workflows → workflows}/cli.js +0 -0
- /package/dist/{src/workflows → workflows}/db.js +0 -0
- /package/dist/{src/workflows → workflows}/document-cache.js +0 -0
- /package/dist/{src/workflows → workflows}/parser.js +0 -0
- /package/dist/{src/workflows → workflows}/renderer.js +0 -0
- /package/dist/{src/workflows → workflows}/schema.js +0 -0
- /package/dist/{src/workflows → workflows}/validator.js +0 -0
|
@@ -1,186 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Architecture seam test — `runAgent` is the single agent CLI entry point.
|
|
3
|
-
*
|
|
4
|
-
* Locks v1 spec §9.7 (LLM/agent boundary) and §12 (agent CLI integration).
|
|
5
|
-
* Issue #222.
|
|
6
|
-
*
|
|
7
|
-
* The test exercises the documented `runAgent` interface without
|
|
8
|
-
* spawning a real binary. Every agent-CLI integration in akm passes
|
|
9
|
-
* through this seam; if the shape changes, callers break.
|
|
10
|
-
*
|
|
11
|
-
* Specifically locked:
|
|
12
|
-
* • `runAgent` is exported from `src/integrations/agent/spawn.ts`
|
|
13
|
-
* and re-exported from `src/integrations/agent/index.ts`.
|
|
14
|
-
* • `AgentRunResult` carries the documented envelope.
|
|
15
|
-
* • `AgentFailureReason` is the discriminated union
|
|
16
|
-
* `"timeout" | "spawn_failed" | "non_zero_exit" | "parse_error"`.
|
|
17
|
-
* • Captured stdio captures stdout/stderr; interactive stdio inherits
|
|
18
|
-
* the parent's streams (no captured strings).
|
|
19
|
-
* • A per-call `timeoutMs` override forces a `timeout` reason.
|
|
20
|
-
*/
|
|
21
|
-
import { describe, expect, test } from "bun:test";
|
|
22
|
-
import * as agentBarrel from "../../src/integrations/agent";
|
|
23
|
-
import { runAgent } from "../../src/integrations/agent/spawn";
|
|
24
|
-
const KNOWN_FAILURE_REASONS = new Set([
|
|
25
|
-
"timeout",
|
|
26
|
-
"spawn_failed",
|
|
27
|
-
"non_zero_exit",
|
|
28
|
-
"parse_error",
|
|
29
|
-
]);
|
|
30
|
-
function makeProfile(overrides = {}) {
|
|
31
|
-
return {
|
|
32
|
-
name: "seam-test-agent",
|
|
33
|
-
bin: "seam-test-agent",
|
|
34
|
-
args: [],
|
|
35
|
-
stdio: "captured",
|
|
36
|
-
envPassthrough: ["PATH"],
|
|
37
|
-
parseOutput: "text",
|
|
38
|
-
...overrides,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function asReadableStream(text) {
|
|
42
|
-
const bytes = new TextEncoder().encode(text);
|
|
43
|
-
return new ReadableStream({
|
|
44
|
-
start(controller) {
|
|
45
|
-
controller.enqueue(bytes);
|
|
46
|
-
controller.close();
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
function fakeSpawn(stdout, stderr, exitCode) {
|
|
51
|
-
let calls = 0;
|
|
52
|
-
const spawn = () => {
|
|
53
|
-
calls++;
|
|
54
|
-
const proc = {
|
|
55
|
-
exitCode,
|
|
56
|
-
exited: Promise.resolve(exitCode),
|
|
57
|
-
stdout: asReadableStream(stdout),
|
|
58
|
-
stderr: asReadableStream(stderr),
|
|
59
|
-
stdin: null,
|
|
60
|
-
kill: () => undefined,
|
|
61
|
-
};
|
|
62
|
-
return proc;
|
|
63
|
-
};
|
|
64
|
-
return {
|
|
65
|
-
spawn,
|
|
66
|
-
get calls() {
|
|
67
|
-
return calls;
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
describe("`runAgent` seam (v1 spec §9.7, §12.2)", () => {
|
|
72
|
-
test("`runAgent` is exported from both the spawn module and the agent barrel", () => {
|
|
73
|
-
expect(typeof runAgent).toBe("function");
|
|
74
|
-
expect(agentBarrel.runAgent).toBe(runAgent);
|
|
75
|
-
});
|
|
76
|
-
test("captured-stdio success returns `ok: true` with stdout/stderr strings", async () => {
|
|
77
|
-
const fake = fakeSpawn("agent-output", "agent-stderr", 0);
|
|
78
|
-
const result = await runAgent(makeProfile(), "hello", { spawn: fake.spawn });
|
|
79
|
-
expect(result.ok).toBe(true);
|
|
80
|
-
expect(result.exitCode).toBe(0);
|
|
81
|
-
expect(result.stdout).toBe("agent-output");
|
|
82
|
-
expect(result.stderr).toBe("agent-stderr");
|
|
83
|
-
expect(result.reason).toBeUndefined();
|
|
84
|
-
expect(result.error).toBeUndefined();
|
|
85
|
-
expect(typeof result.durationMs).toBe("number");
|
|
86
|
-
expect(fake.calls).toBe(1);
|
|
87
|
-
});
|
|
88
|
-
test("interactive-stdio mode does not capture stdout/stderr into the result", async () => {
|
|
89
|
-
// Build a spawn that records the stdio options it was given. The
|
|
90
|
-
// contract: when stdio is "interactive", stdout/stderr default to
|
|
91
|
-
// "inherit", which the wrapper must not try to read from.
|
|
92
|
-
let observed;
|
|
93
|
-
const spawn = (_cmd, options) => {
|
|
94
|
-
observed = {
|
|
95
|
-
stdin: options.stdin,
|
|
96
|
-
stdout: options.stdout,
|
|
97
|
-
stderr: options.stderr,
|
|
98
|
-
};
|
|
99
|
-
return {
|
|
100
|
-
exitCode: 0,
|
|
101
|
-
exited: Promise.resolve(0),
|
|
102
|
-
stdout: null,
|
|
103
|
-
stderr: null,
|
|
104
|
-
stdin: null,
|
|
105
|
-
kill: () => undefined,
|
|
106
|
-
};
|
|
107
|
-
};
|
|
108
|
-
const result = await runAgent(makeProfile({ stdio: "interactive" }), "hi", { spawn });
|
|
109
|
-
expect(observed?.stdout).toBe("inherit");
|
|
110
|
-
expect(observed?.stderr).toBe("inherit");
|
|
111
|
-
expect(observed?.stdin).toBe("inherit");
|
|
112
|
-
expect(result.ok).toBe(true);
|
|
113
|
-
expect(result.stdout).toBe("");
|
|
114
|
-
expect(result.stderr).toBe("");
|
|
115
|
-
});
|
|
116
|
-
test("failure-reason discriminated union covers exactly the documented vocabulary", async () => {
|
|
117
|
-
// Synchronous spawn failure → `spawn_failed`.
|
|
118
|
-
const spawnFailedSpawn = () => {
|
|
119
|
-
throw new Error("boom");
|
|
120
|
-
};
|
|
121
|
-
const spawnFailed = await runAgent(makeProfile(), undefined, { spawn: spawnFailedSpawn });
|
|
122
|
-
expect(spawnFailed.ok).toBe(false);
|
|
123
|
-
expect(spawnFailed.reason).toBe("spawn_failed");
|
|
124
|
-
expect(KNOWN_FAILURE_REASONS.has(spawnFailed.reason)).toBe(true);
|
|
125
|
-
// Non-zero exit → `non_zero_exit`.
|
|
126
|
-
const nonZero = fakeSpawn("", "oops", 7);
|
|
127
|
-
const nonZeroResult = await runAgent(makeProfile(), undefined, { spawn: nonZero.spawn });
|
|
128
|
-
expect(nonZeroResult.ok).toBe(false);
|
|
129
|
-
expect(nonZeroResult.reason).toBe("non_zero_exit");
|
|
130
|
-
expect(nonZeroResult.exitCode).toBe(7);
|
|
131
|
-
// Malformed JSON when parseOutput is "json" → `parse_error`.
|
|
132
|
-
const badJson = fakeSpawn("not json", "", 0);
|
|
133
|
-
const parseResult = await runAgent(makeProfile({ parseOutput: "json" }), undefined, { spawn: badJson.spawn });
|
|
134
|
-
expect(parseResult.ok).toBe(false);
|
|
135
|
-
expect(parseResult.reason).toBe("parse_error");
|
|
136
|
-
});
|
|
137
|
-
test("timeout override produces `reason: 'timeout'` deterministically", async () => {
|
|
138
|
-
// A spawn that hangs until kill() is called. We drive the timer
|
|
139
|
-
// synchronously so the timeout fires immediately.
|
|
140
|
-
const hangingSpawn = () => {
|
|
141
|
-
const spawn = () => {
|
|
142
|
-
let resolve;
|
|
143
|
-
const exited = new Promise((r) => {
|
|
144
|
-
resolve = r;
|
|
145
|
-
});
|
|
146
|
-
const proc = {
|
|
147
|
-
exitCode: null,
|
|
148
|
-
exited,
|
|
149
|
-
stdout: asReadableStream(""),
|
|
150
|
-
stderr: asReadableStream(""),
|
|
151
|
-
stdin: null,
|
|
152
|
-
kill: () => resolve?.(143),
|
|
153
|
-
};
|
|
154
|
-
return proc;
|
|
155
|
-
};
|
|
156
|
-
return { spawn };
|
|
157
|
-
};
|
|
158
|
-
const fakeTimers = [];
|
|
159
|
-
let nextId = 1;
|
|
160
|
-
const setTimeoutFn = ((cb) => {
|
|
161
|
-
const id = nextId++;
|
|
162
|
-
fakeTimers.push({ id, cb });
|
|
163
|
-
return id;
|
|
164
|
-
});
|
|
165
|
-
const clearTimeoutFn = ((id) => {
|
|
166
|
-
const idx = fakeTimers.findIndex((t) => t.id === id);
|
|
167
|
-
if (idx >= 0)
|
|
168
|
-
fakeTimers.splice(idx, 1);
|
|
169
|
-
});
|
|
170
|
-
const { spawn } = hangingSpawn();
|
|
171
|
-
const promise = runAgent(makeProfile(), undefined, {
|
|
172
|
-
spawn,
|
|
173
|
-
timeoutMs: 10,
|
|
174
|
-
setTimeoutFn,
|
|
175
|
-
clearTimeoutFn,
|
|
176
|
-
});
|
|
177
|
-
// Drive the timer synchronously.
|
|
178
|
-
expect(fakeTimers.length).toBe(1);
|
|
179
|
-
fakeTimers[0]?.cb();
|
|
180
|
-
const result = await promise;
|
|
181
|
-
expect(result.ok).toBe(false);
|
|
182
|
-
expect(result.reason).toBe("timeout");
|
|
183
|
-
expect(typeof result.error).toBe("string");
|
|
184
|
-
});
|
|
185
|
-
test("`AgentFailureReason` union from the barrel matches the documented vocabulary", () => {
|
|
186
|
-
// Compile-time + runtime: assigning each known reason string to the
|
|
187
|
-
// exported type pins the union shape. If the union narrows or
|
|
188
|
-
// widens, this block fails to compile (the runtime arm just
|
|
189
|
-
// mirrors the same set so the test reads end-to-end).
|
|
190
|
-
const reasons = ["timeout", "spawn_failed", "non_zero_exit", "parse_error"];
|
|
191
|
-
expect(new Set(reasons)).toEqual(KNOWN_FAILURE_REASONS);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Architecture seam test — `src/llm/*` is bounded and stateless.
|
|
3
|
-
*
|
|
4
|
-
* Locks v1 spec §9.7 (LLM/agent boundary) and §14.4 (statelessness
|
|
5
|
-
* invariant). Issue #222.
|
|
6
|
-
*
|
|
7
|
-
* The test inspects the **module shape** of each public LLM helper,
|
|
8
|
-
* not the source text. The contract under test is:
|
|
9
|
-
*
|
|
10
|
-
* 1. Each module's runtime exports are functions (or values that
|
|
11
|
-
* describe pure data, e.g. a model-name constant). They are not
|
|
12
|
-
* instances of stateful clients.
|
|
13
|
-
* 2. The only module-level singleton across `src/llm/*` is the local
|
|
14
|
-
* embedder pipeline in `src/llm/embedder.ts`, which is documented
|
|
15
|
-
* as a stateless model handle and exposes `resetLocalEmbedder()`
|
|
16
|
-
* so tests can construct a fresh pipeline.
|
|
17
|
-
* 3. The transport-level helpers (`chatCompletion`, `enhanceMetadata`,
|
|
18
|
-
* `splitMemoryIntoAtomicFacts`, `resolveIndexPassLLM`) take the
|
|
19
|
-
* connection config as a parameter — they do not read it from
|
|
20
|
-
* module state.
|
|
21
|
-
*
|
|
22
|
-
* Together these properties keep every in-tree LLM call to a single
|
|
23
|
-
* bounded request/response cycle. Crossing this seam (introducing a
|
|
24
|
-
* conversation cache, a streaming session, or a hidden module-level
|
|
25
|
-
* config) is a contract violation and should fail this test.
|
|
26
|
-
*/
|
|
27
|
-
import { describe, expect, test } from "bun:test";
|
|
28
|
-
import * as client from "../../src/llm/client";
|
|
29
|
-
import * as embedder from "../../src/llm/embedder";
|
|
30
|
-
import * as indexPasses from "../../src/llm/index-passes";
|
|
31
|
-
import * as memoryInfer from "../../src/llm/memory-infer";
|
|
32
|
-
import * as metadataEnhance from "../../src/llm/metadata-enhance";
|
|
33
|
-
describe("src/llm/* is bounded and stateless (v1 spec §9.7, §14.4)", () => {
|
|
34
|
-
test("`client` exports are pure functions", () => {
|
|
35
|
-
expect(typeof client.chatCompletion).toBe("function");
|
|
36
|
-
expect(typeof client.stripJsonFences).toBe("function");
|
|
37
|
-
expect(typeof client.parseJsonResponse).toBe("function");
|
|
38
|
-
expect(typeof client.isLlmAvailable).toBe("function");
|
|
39
|
-
expect(typeof client.probeLlmCapabilities).toBe("function");
|
|
40
|
-
});
|
|
41
|
-
test("`client.chatCompletion` accepts the connection config as its first arg", () => {
|
|
42
|
-
// Length-on-function reflects declared (non-rest) parameter count.
|
|
43
|
-
// The contract is: the connection config is a parameter, not module
|
|
44
|
-
// state. Two declared params: (config, messages); options is
|
|
45
|
-
// optional and trailing.
|
|
46
|
-
expect(client.chatCompletion.length).toBeGreaterThanOrEqual(2);
|
|
47
|
-
});
|
|
48
|
-
test("`client` does not export any non-function runtime value (no module-level client instance)", () => {
|
|
49
|
-
for (const [name, value] of Object.entries(client)) {
|
|
50
|
-
if (value === undefined)
|
|
51
|
-
continue;
|
|
52
|
-
expect(typeof value).toBe("function");
|
|
53
|
-
// Eslint-style sanity: anything callable that exposes mutable
|
|
54
|
-
// state at module scope would surface here as a non-function
|
|
55
|
-
// export (an instance, a Map, etc).
|
|
56
|
-
void name;
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
test("`metadata-enhance` exports a single pure helper", () => {
|
|
60
|
-
expect(typeof metadataEnhance.enhanceMetadata).toBe("function");
|
|
61
|
-
expect(metadataEnhance.enhanceMetadata.length).toBeGreaterThanOrEqual(2);
|
|
62
|
-
const runtimeExports = Object.entries(metadataEnhance).filter(([, v]) => v !== undefined);
|
|
63
|
-
expect(runtimeExports.length).toBe(1);
|
|
64
|
-
});
|
|
65
|
-
test("`memory-infer` exports a single pure helper", () => {
|
|
66
|
-
expect(typeof memoryInfer.splitMemoryIntoAtomicFacts).toBe("function");
|
|
67
|
-
expect(memoryInfer.splitMemoryIntoAtomicFacts.length).toBeGreaterThanOrEqual(2);
|
|
68
|
-
const runtimeExports = Object.entries(memoryInfer).filter(([, v]) => v !== undefined);
|
|
69
|
-
expect(runtimeExports.length).toBe(1);
|
|
70
|
-
});
|
|
71
|
-
test("`index-passes` exports a single pure resolver", () => {
|
|
72
|
-
expect(typeof indexPasses.resolveIndexPassLLM).toBe("function");
|
|
73
|
-
expect(indexPasses.resolveIndexPassLLM.length).toBeGreaterThanOrEqual(2);
|
|
74
|
-
const runtimeExports = Object.entries(indexPasses).filter(([, v]) => v !== undefined);
|
|
75
|
-
expect(runtimeExports.length).toBe(1);
|
|
76
|
-
});
|
|
77
|
-
test("`embedder` only exports functions and pure data constants", () => {
|
|
78
|
-
// The embedder facade has more surface than the chat helpers because
|
|
79
|
-
// it owns the local-pipeline cache. Every export must still be a
|
|
80
|
-
// function or a pure data value (e.g. the default model name).
|
|
81
|
-
const allowedNonFunctionNames = new Set(["DEFAULT_LOCAL_MODEL"]);
|
|
82
|
-
for (const [name, value] of Object.entries(embedder)) {
|
|
83
|
-
if (value === undefined)
|
|
84
|
-
continue;
|
|
85
|
-
if (allowedNonFunctionNames.has(name)) {
|
|
86
|
-
// Pure-data constant. Must be a primitive (string/number/boolean).
|
|
87
|
-
expect(["string", "number", "boolean"]).toContain(typeof value);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
expect(typeof value).toBe("function");
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
test("`embedder.resetLocalEmbedder` exists so tests can rebuild the cached pipeline", () => {
|
|
94
|
-
// The local embedder is a documented module-level singleton — it is
|
|
95
|
-
// a stateless model handle, not a session. The reset hook is part
|
|
96
|
-
// of the seam: it lets tests assert pipeline-construction logic
|
|
97
|
-
// without relying on module-load order.
|
|
98
|
-
expect(typeof embedder.resetLocalEmbedder).toBe("function");
|
|
99
|
-
// resetLocalEmbedder is a no-arg function.
|
|
100
|
-
expect(embedder.resetLocalEmbedder.length).toBe(0);
|
|
101
|
-
});
|
|
102
|
-
test("`stripJsonFences` and `parseJsonResponse` are referentially transparent", () => {
|
|
103
|
-
// Two calls with the same input produce the same output. These are
|
|
104
|
-
// pure, so this is not a deep test of statelessness — but it does
|
|
105
|
-
// pin the seam: response parsing is not allowed to learn from
|
|
106
|
-
// prior responses.
|
|
107
|
-
const fenced = '```json\n{"a":1}\n```';
|
|
108
|
-
expect(client.stripJsonFences(fenced)).toBe(client.stripJsonFences(fenced));
|
|
109
|
-
expect(client.parseJsonResponse(fenced)).toEqual({ a: 1 });
|
|
110
|
-
expect(client.parseJsonResponse(fenced)).toEqual({ a: 1 });
|
|
111
|
-
});
|
|
112
|
-
});
|