akm-cli 0.6.1 → 0.7.0
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} +712 -34
- package/dist/{commands → src/commands}/config-cli.js +47 -4
- package/dist/src/commands/distill.js +283 -0
- package/dist/src/commands/events.js +108 -0
- package/dist/src/commands/history.js +191 -0
- package/dist/{commands → src/commands}/installed-stashes.js +1 -1
- 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 +71 -7
- package/dist/{commands → src/commands}/remember.js +12 -0
- package/dist/{commands → src/commands}/search.js +104 -4
- package/dist/{commands → src/commands}/self-update.js +4 -3
- package/dist/{commands → src/commands}/show.js +73 -0
- package/dist/{commands → src/commands}/source-add.js +5 -1
- package/dist/{commands → src/commands}/source-manage.js +7 -1
- 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 +203 -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 +114 -24
- 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 +88 -7
- package/dist/{indexer → src/indexer}/matchers.js +1 -1
- package/dist/src/indexer/memory-inference.js +263 -0
- package/dist/{indexer → src/indexer}/metadata.js +111 -3
- package/dist/{indexer → src/indexer}/search-source.js +4 -2
- 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 +272 -0
- package/dist/{integrations → src/integrations}/github.js +9 -3
- package/dist/{integrations → src/integrations}/lockfile.js +0 -26
- package/dist/{llm → src/llm}/client.js +33 -2
- package/dist/{llm → src/llm}/embedders/remote.js +37 -3
- 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}/cli-hints.js +15 -2
- package/dist/{output → src/output}/renderers.js +63 -2
- package/dist/src/output/shapes.js +523 -0
- package/dist/src/output/text.js +1116 -0
- package/dist/{registry → src/registry}/build-index.js +19 -8
- package/dist/{registry → src/registry}/factory.js +0 -8
- package/dist/{registry → src/registry}/providers/static-index.js +6 -3
- package/dist/{registry → src/registry}/resolve.js +68 -2
- package/dist/{setup → src/setup}/setup.js +52 -5
- package/dist/{sources → src/sources}/providers/git.js +7 -15
- package/dist/{wiki → src/wiki}/wiki.js +54 -6
- package/dist/{workflows → src/workflows}/runs.js +37 -3
- 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 +996 -0
- package/dist/tests/bench/cleanup-sigint.test.js +83 -0
- package/dist/tests/bench/cleanup.js +234 -0
- package/dist/tests/bench/cleanup.test.js +166 -0
- package/dist/tests/bench/cli.js +1018 -0
- package/dist/tests/bench/cli.test.js +445 -0
- package/dist/tests/bench/compare.test.js +556 -0
- package/dist/tests/bench/corpus.js +317 -0
- package/dist/tests/bench/corpus.test.js +258 -0
- package/dist/tests/bench/doctor.js +525 -0
- package/dist/tests/bench/driver.js +401 -0
- package/dist/tests/bench/driver.test.js +584 -0
- package/dist/tests/bench/environment.js +233 -0
- package/dist/tests/bench/environment.test.js +199 -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 +647 -0
- package/dist/tests/bench/evolve.test.js +624 -0
- package/dist/tests/bench/failure-modes.test.js +349 -0
- package/dist/tests/bench/feedback-integrity.test.js +457 -0
- package/dist/tests/bench/leakage.test.js +228 -0
- package/dist/tests/bench/learning-curve.test.js +134 -0
- package/dist/tests/bench/metrics.js +2395 -0
- package/dist/tests/bench/metrics.test.js +1150 -0
- package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
- package/dist/tests/bench/opencode-config.js +194 -0
- package/dist/tests/bench/opencode-config.test.js +370 -0
- package/dist/tests/bench/report.js +1885 -0
- package/dist/tests/bench/report.test.js +1038 -0
- package/dist/tests/bench/run-config.js +355 -0
- package/dist/tests/bench/run-config.test.js +298 -0
- package/dist/tests/bench/run-curate-test.js +32 -0
- package/dist/tests/bench/run-failing-tasks.js +56 -0
- package/dist/tests/bench/run-full-bench.js +51 -0
- package/dist/tests/bench/run-items36-targeted.js +69 -0
- package/dist/tests/bench/run-nano-quick.js +42 -0
- package/dist/tests/bench/run-waveg-targeted.js +62 -0
- package/dist/tests/bench/runner.js +699 -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 +131 -0
- package/dist/tests/bench/trajectory.js +116 -0
- package/dist/tests/bench/trajectory.test.js +127 -0
- package/dist/tests/bench/verifier.js +114 -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 +345 -0
- package/dist/tests/bench/workflow-spec.test.js +363 -0
- package/dist/tests/bench/workflow-trace.js +472 -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 +204 -0
- package/dist/tests/commands/events.test.js +370 -0
- package/dist/tests/commands/history.test.js +418 -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 +569 -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 +1419 -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 +97 -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 +570 -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 +218 -0
- package/dist/tests/output-shapes-unit.test.js +478 -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 +394 -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 +238 -0
- package/dist/tests/registry-resolve.test.js +126 -0
- package/dist/tests/registry-search.test.js +923 -0
- package/dist/tests/remember-frontmatter.test.js +378 -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 +286 -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 +281 -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 +395 -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/output/text.js +0 -520
- /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-clone.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}/search-fields.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/{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/types.js +0 -0
- /package/dist/{llm → src/llm}/metadata-enhance.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}/schema.js +0 -0
- /package/dist/{workflows → src/workflows}/validator.js +0 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { afterEach, beforeEach, 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
|
+
import { akmHistory } from "../../src/commands/history";
|
|
7
|
+
import { saveConfig } from "../../src/core/config";
|
|
8
|
+
import { appendEvent } from "../../src/core/events";
|
|
9
|
+
import { getDbPath } from "../../src/core/paths";
|
|
10
|
+
import { closeDatabase, openDatabase } from "../../src/indexer/db";
|
|
11
|
+
import { akmIndex } from "../../src/indexer/indexer";
|
|
12
|
+
import { ensureUsageEventsSchema, insertUsageEvent } from "../../src/indexer/usage-events";
|
|
13
|
+
const CLI = path.join(__dirname, "..", "..", "src", "cli.ts");
|
|
14
|
+
const tempDirs = [];
|
|
15
|
+
const savedEnv = {
|
|
16
|
+
AKM_STASH_DIR: process.env.AKM_STASH_DIR,
|
|
17
|
+
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME,
|
|
18
|
+
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
|
|
19
|
+
};
|
|
20
|
+
function makeTempDir(prefix) {
|
|
21
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
22
|
+
tempDirs.push(dir);
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
function writeFile(filePath, content) {
|
|
26
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
27
|
+
fs.writeFileSync(filePath, content);
|
|
28
|
+
}
|
|
29
|
+
function runCli(args) {
|
|
30
|
+
const result = spawnSync("bun", [CLI, ...args], {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
timeout: 30_000,
|
|
33
|
+
env: { ...process.env },
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
status: result.status,
|
|
37
|
+
stdout: result.stdout ?? "",
|
|
38
|
+
stderr: result.stderr ?? "",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function parseJsonOutput(result) {
|
|
42
|
+
const payload = result.stdout.trim() || result.stderr.trim();
|
|
43
|
+
return JSON.parse(payload);
|
|
44
|
+
}
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
process.env.XDG_CACHE_HOME = makeTempDir("akm-history-cache-");
|
|
47
|
+
process.env.XDG_CONFIG_HOME = makeTempDir("akm-history-config-");
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
if (savedEnv.AKM_STASH_DIR === undefined)
|
|
51
|
+
delete process.env.AKM_STASH_DIR;
|
|
52
|
+
else
|
|
53
|
+
process.env.AKM_STASH_DIR = savedEnv.AKM_STASH_DIR;
|
|
54
|
+
if (savedEnv.XDG_CACHE_HOME === undefined)
|
|
55
|
+
delete process.env.XDG_CACHE_HOME;
|
|
56
|
+
else
|
|
57
|
+
process.env.XDG_CACHE_HOME = savedEnv.XDG_CACHE_HOME;
|
|
58
|
+
if (savedEnv.XDG_CONFIG_HOME === undefined)
|
|
59
|
+
delete process.env.XDG_CONFIG_HOME;
|
|
60
|
+
else
|
|
61
|
+
process.env.XDG_CONFIG_HOME = savedEnv.XDG_CONFIG_HOME;
|
|
62
|
+
for (const dir of tempDirs.splice(0)) {
|
|
63
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
describe("akmHistory programmatic API", () => {
|
|
67
|
+
test("returns an empty entry list when no events have been recorded", async () => {
|
|
68
|
+
const db = openDatabase(":memory:");
|
|
69
|
+
try {
|
|
70
|
+
ensureUsageEventsSchema(db);
|
|
71
|
+
const result = await akmHistory({ db });
|
|
72
|
+
expect(result.schemaVersion).toBe(1);
|
|
73
|
+
expect(result.totalCount).toBe(0);
|
|
74
|
+
expect(result.entries).toEqual([]);
|
|
75
|
+
expect(result.ref).toBeUndefined();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
closeDatabase(db);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
test("returns stash-wide history in chronological order when no ref is provided", async () => {
|
|
82
|
+
const db = openDatabase(":memory:");
|
|
83
|
+
try {
|
|
84
|
+
ensureUsageEventsSchema(db);
|
|
85
|
+
insertUsageEvent(db, { event_type: "search", query: "deploy" });
|
|
86
|
+
insertUsageEvent(db, { event_type: "show", entry_ref: "memory:alpha", entry_id: 1 });
|
|
87
|
+
insertUsageEvent(db, {
|
|
88
|
+
event_type: "feedback",
|
|
89
|
+
entry_ref: "memory:alpha",
|
|
90
|
+
entry_id: 1,
|
|
91
|
+
signal: "positive",
|
|
92
|
+
});
|
|
93
|
+
const result = await akmHistory({ db });
|
|
94
|
+
expect(result.totalCount).toBe(3);
|
|
95
|
+
expect(result.entries.map((entry) => entry.eventType)).toEqual(["search", "show", "feedback"]);
|
|
96
|
+
// Each entry has the canonical fields the renderer projects.
|
|
97
|
+
expect(result.entries[0]).toMatchObject({ eventType: "search", query: "deploy" });
|
|
98
|
+
expect(result.entries[1]).toMatchObject({ eventType: "show", ref: "memory:alpha" });
|
|
99
|
+
expect(result.entries[2]).toMatchObject({ eventType: "feedback", ref: "memory:alpha", signal: "positive" });
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
closeDatabase(db);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
test("filters per-asset history when --ref is provided", async () => {
|
|
106
|
+
const db = openDatabase(":memory:");
|
|
107
|
+
try {
|
|
108
|
+
ensureUsageEventsSchema(db);
|
|
109
|
+
insertUsageEvent(db, { event_type: "show", entry_ref: "memory:alpha", entry_id: 1 });
|
|
110
|
+
insertUsageEvent(db, { event_type: "show", entry_ref: "memory:beta", entry_id: 2 });
|
|
111
|
+
insertUsageEvent(db, {
|
|
112
|
+
event_type: "feedback",
|
|
113
|
+
entry_ref: "memory:alpha",
|
|
114
|
+
entry_id: 1,
|
|
115
|
+
signal: "negative",
|
|
116
|
+
});
|
|
117
|
+
const result = await akmHistory({ db, ref: "memory:alpha" });
|
|
118
|
+
expect(result.ref).toBe("memory:alpha");
|
|
119
|
+
expect(result.totalCount).toBe(2);
|
|
120
|
+
expect(result.entries.every((entry) => entry.ref === "memory:alpha")).toBe(true);
|
|
121
|
+
expect(result.entries.map((entry) => entry.eventType)).toEqual(["show", "feedback"]);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
closeDatabase(db);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
test("rejects malformed refs with a UsageError", async () => {
|
|
128
|
+
const db = openDatabase(":memory:");
|
|
129
|
+
try {
|
|
130
|
+
ensureUsageEventsSchema(db);
|
|
131
|
+
await expect(akmHistory({ db, ref: "" })).rejects.toThrow();
|
|
132
|
+
await expect(akmHistory({ db, ref: "not-a-ref" })).rejects.toThrow();
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
closeDatabase(db);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
test("filters by --since when provided", async () => {
|
|
139
|
+
const db = openDatabase(":memory:");
|
|
140
|
+
try {
|
|
141
|
+
ensureUsageEventsSchema(db);
|
|
142
|
+
// Manually insert rows with explicit timestamps so the test is
|
|
143
|
+
// deterministic regardless of clock skew.
|
|
144
|
+
db.prepare("INSERT INTO usage_events (event_type, entry_ref, entry_id, created_at) VALUES (?, ?, ?, ?)").run("show", "memory:alpha", 1, "2025-01-01 00:00:00");
|
|
145
|
+
db.prepare("INSERT INTO usage_events (event_type, entry_ref, entry_id, created_at) VALUES (?, ?, ?, ?)").run("show", "memory:alpha", 1, "2026-04-01 00:00:00");
|
|
146
|
+
const result = await akmHistory({ db, since: "2026-01-01T00:00:00Z" });
|
|
147
|
+
expect(result.totalCount).toBe(1);
|
|
148
|
+
expect(result.entries[0]?.createdAt).toBe("2026-04-01 00:00:00");
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
closeDatabase(db);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
test("rejects malformed --since values", async () => {
|
|
155
|
+
const db = openDatabase(":memory:");
|
|
156
|
+
try {
|
|
157
|
+
ensureUsageEventsSchema(db);
|
|
158
|
+
await expect(akmHistory({ db, since: "definitely-not-a-date" })).rejects.toThrow();
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
closeDatabase(db);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe("akm history CLI", () => {
|
|
166
|
+
test("emits a JSON envelope matching the existing CLI conventions", async () => {
|
|
167
|
+
const stashDir = makeTempDir("akm-history-stash-");
|
|
168
|
+
process.env.AKM_STASH_DIR = stashDir;
|
|
169
|
+
saveConfig({ semanticSearchMode: "off" });
|
|
170
|
+
writeFile(path.join(stashDir, "memories", "alpha.md"), "---\ndescription: alpha memory\n---\nAlpha.\n");
|
|
171
|
+
await akmIndex({ stashDir, full: true });
|
|
172
|
+
// Generate a feedback event so history has something to surface.
|
|
173
|
+
const feedback = runCli(["feedback", "memory:alpha", "--positive", "--format=json"]);
|
|
174
|
+
expect(feedback.status).toBe(0);
|
|
175
|
+
// Per-asset history.
|
|
176
|
+
const perAsset = runCli(["history", "--ref", "memory:alpha", "--format=json"]);
|
|
177
|
+
expect(perAsset.status).toBe(0);
|
|
178
|
+
const perAssetJson = parseJsonOutput(perAsset);
|
|
179
|
+
expect(perAssetJson.ref).toBe("memory:alpha");
|
|
180
|
+
expect(typeof perAssetJson.totalCount).toBe("number");
|
|
181
|
+
expect(Array.isArray(perAssetJson.entries)).toBe(true);
|
|
182
|
+
const entries = perAssetJson.entries;
|
|
183
|
+
expect(entries.some((entry) => entry.eventType === "feedback" && entry.ref === "memory:alpha")).toBe(true);
|
|
184
|
+
// Stash-wide history.
|
|
185
|
+
const stashWide = runCli(["history", "--format=json"]);
|
|
186
|
+
expect(stashWide.status).toBe(0);
|
|
187
|
+
const stashWideJson = parseJsonOutput(stashWide);
|
|
188
|
+
expect(stashWideJson.ref).toBeUndefined();
|
|
189
|
+
expect(typeof stashWideJson.totalCount).toBe("number");
|
|
190
|
+
expect(stashWideJson.totalCount >= 1).toBe(true);
|
|
191
|
+
// Confirm the database actually contains the feedback row we created.
|
|
192
|
+
const db = openDatabase(getDbPath());
|
|
193
|
+
try {
|
|
194
|
+
const events = db
|
|
195
|
+
.prepare("SELECT entry_ref, event_type FROM usage_events WHERE event_type = 'feedback'")
|
|
196
|
+
.all();
|
|
197
|
+
expect(events.find((event) => event.entry_ref === "memory:alpha")).toBeDefined();
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
closeDatabase(db);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
test("renders a human-friendly text report when --format=text", async () => {
|
|
204
|
+
const stashDir = makeTempDir("akm-history-text-stash-");
|
|
205
|
+
process.env.AKM_STASH_DIR = stashDir;
|
|
206
|
+
saveConfig({ semanticSearchMode: "off" });
|
|
207
|
+
writeFile(path.join(stashDir, "memories", "alpha.md"), "---\ndescription: alpha memory\n---\nAlpha.\n");
|
|
208
|
+
await akmIndex({ stashDir, full: true });
|
|
209
|
+
const feedback = runCli(["feedback", "memory:alpha", "--positive", "--format=json"]);
|
|
210
|
+
expect(feedback.status).toBe(0);
|
|
211
|
+
const text = runCli(["history", "--ref", "memory:alpha", "--format=text"]);
|
|
212
|
+
expect(text.status).toBe(0);
|
|
213
|
+
expect(text.stdout).toContain("memory:alpha");
|
|
214
|
+
expect(text.stdout).toContain("[feedback]");
|
|
215
|
+
expect(text.stdout).toContain("signal: positive");
|
|
216
|
+
});
|
|
217
|
+
test("rejects an invalid ref via the JSON error envelope", () => {
|
|
218
|
+
const result = runCli(["history", "--ref", "not-a-valid-ref", "--format=json"]);
|
|
219
|
+
expect(result.status).not.toBe(0);
|
|
220
|
+
const parsed = parseJsonOutput(result);
|
|
221
|
+
expect(parsed.ok).toBe(false);
|
|
222
|
+
expect(typeof parsed.error).toBe("string");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe("akmHistory --include-proposals", () => {
|
|
226
|
+
test("sources field is ['usage_events'] by default", async () => {
|
|
227
|
+
const db = openDatabase(":memory:");
|
|
228
|
+
try {
|
|
229
|
+
ensureUsageEventsSchema(db);
|
|
230
|
+
const result = await akmHistory({ db });
|
|
231
|
+
expect(result.sources).toEqual(["usage_events"]);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
closeDatabase(db);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
test("sources field includes events.jsonl when includeProposals is true", async () => {
|
|
238
|
+
const eventsFile = path.join(makeTempDir("akm-history-events-"), "events.jsonl");
|
|
239
|
+
const db = openDatabase(":memory:");
|
|
240
|
+
try {
|
|
241
|
+
ensureUsageEventsSchema(db);
|
|
242
|
+
const result = await akmHistory({
|
|
243
|
+
db,
|
|
244
|
+
includeProposals: true,
|
|
245
|
+
eventsCtx: { filePath: eventsFile },
|
|
246
|
+
});
|
|
247
|
+
expect(result.sources).toEqual(["usage_events", "events.jsonl"]);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
closeDatabase(db);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
test("proposal accept event (promoted) appears in history with --include-proposals", async () => {
|
|
254
|
+
const eventsFile = path.join(makeTempDir("akm-history-proposal-"), "events.jsonl");
|
|
255
|
+
const db = openDatabase(":memory:");
|
|
256
|
+
try {
|
|
257
|
+
ensureUsageEventsSchema(db);
|
|
258
|
+
// Simulate `akm proposal accept` emitting a promoted event.
|
|
259
|
+
appendEvent({
|
|
260
|
+
eventType: "promoted",
|
|
261
|
+
ref: "skill:deploy",
|
|
262
|
+
metadata: { proposalId: "prop-001", source: "reflect", assetPath: "/stash/skills/deploy.md" },
|
|
263
|
+
}, { filePath: eventsFile });
|
|
264
|
+
const result = await akmHistory({
|
|
265
|
+
db,
|
|
266
|
+
includeProposals: true,
|
|
267
|
+
eventsCtx: { filePath: eventsFile },
|
|
268
|
+
});
|
|
269
|
+
expect(result.totalCount).toBe(1);
|
|
270
|
+
const promoted = result.entries.find((e) => e.eventType === "promoted");
|
|
271
|
+
expect(promoted).toBeDefined();
|
|
272
|
+
expect(promoted?.ref).toBe("skill:deploy");
|
|
273
|
+
expect(promoted?.eventType).toBe("promoted");
|
|
274
|
+
// Metadata from the proposal should be accessible.
|
|
275
|
+
expect(promoted?.metadata?.proposalId).toBe("prop-001");
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
closeDatabase(db);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
test("proposal reject event (rejected) appears in history with --include-proposals", async () => {
|
|
282
|
+
const eventsFile = path.join(makeTempDir("akm-history-reject-"), "events.jsonl");
|
|
283
|
+
const db = openDatabase(":memory:");
|
|
284
|
+
try {
|
|
285
|
+
ensureUsageEventsSchema(db);
|
|
286
|
+
appendEvent({
|
|
287
|
+
eventType: "rejected",
|
|
288
|
+
ref: "memory:old-draft",
|
|
289
|
+
metadata: { proposalId: "prop-002", source: "reflect", reason: "outdated" },
|
|
290
|
+
}, { filePath: eventsFile });
|
|
291
|
+
const result = await akmHistory({
|
|
292
|
+
db,
|
|
293
|
+
includeProposals: true,
|
|
294
|
+
eventsCtx: { filePath: eventsFile },
|
|
295
|
+
});
|
|
296
|
+
expect(result.totalCount).toBe(1);
|
|
297
|
+
const rejected = result.entries.find((e) => e.eventType === "rejected");
|
|
298
|
+
expect(rejected).toBeDefined();
|
|
299
|
+
expect(rejected?.ref).toBe("memory:old-draft");
|
|
300
|
+
expect(rejected?.metadata?.reason).toBe("outdated");
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
closeDatabase(db);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
test("non-proposal events in events.jsonl are excluded even with --include-proposals", async () => {
|
|
307
|
+
const eventsFile = path.join(makeTempDir("akm-history-filter-"), "events.jsonl");
|
|
308
|
+
const db = openDatabase(":memory:");
|
|
309
|
+
try {
|
|
310
|
+
ensureUsageEventsSchema(db);
|
|
311
|
+
// These event types should NOT appear in history even with --include-proposals.
|
|
312
|
+
appendEvent({ eventType: "add", ref: "skill:deploy" }, { filePath: eventsFile });
|
|
313
|
+
appendEvent({ eventType: "reflect_invoked", ref: "memory:alpha" }, { filePath: eventsFile });
|
|
314
|
+
// Only this one should appear.
|
|
315
|
+
appendEvent({ eventType: "promoted", ref: "skill:deploy", metadata: { proposalId: "p1", source: "reflect" } }, { filePath: eventsFile });
|
|
316
|
+
const result = await akmHistory({
|
|
317
|
+
db,
|
|
318
|
+
includeProposals: true,
|
|
319
|
+
eventsCtx: { filePath: eventsFile },
|
|
320
|
+
});
|
|
321
|
+
expect(result.entries.every((e) => e.eventType === "promoted" || e.eventType === "rejected")).toBe(true);
|
|
322
|
+
expect(result.entries.length).toBe(1);
|
|
323
|
+
expect(result.entries[0]?.eventType).toBe("promoted");
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
closeDatabase(db);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
test("usage events and proposal events are merged chronologically", async () => {
|
|
330
|
+
const eventsFile = path.join(makeTempDir("akm-history-merge-"), "events.jsonl");
|
|
331
|
+
const db = openDatabase(":memory:");
|
|
332
|
+
try {
|
|
333
|
+
ensureUsageEventsSchema(db);
|
|
334
|
+
// Insert usage events with explicit timestamps (early, then late).
|
|
335
|
+
db.prepare("INSERT INTO usage_events (event_type, entry_ref, entry_id, created_at) VALUES (?, ?, ?, ?)").run("show", "skill:deploy", 1, "2026-01-01 10:00:00");
|
|
336
|
+
db.prepare("INSERT INTO usage_events (event_type, entry_ref, entry_id, created_at) VALUES (?, ?, ?, ?)").run("feedback", "skill:deploy", 1, "2026-01-03 12:00:00");
|
|
337
|
+
// Append a proposal event between the two usage events.
|
|
338
|
+
appendEvent({ eventType: "promoted", ref: "skill:deploy", metadata: { proposalId: "p3", source: "reflect" } }, {
|
|
339
|
+
filePath: eventsFile,
|
|
340
|
+
now: () => new Date("2026-01-02T09:00:00Z").getTime(),
|
|
341
|
+
});
|
|
342
|
+
const result = await akmHistory({
|
|
343
|
+
db,
|
|
344
|
+
includeProposals: true,
|
|
345
|
+
eventsCtx: { filePath: eventsFile },
|
|
346
|
+
});
|
|
347
|
+
expect(result.totalCount).toBe(3);
|
|
348
|
+
const types = result.entries.map((e) => e.eventType);
|
|
349
|
+
// Chronological: show (Jan 1), promoted (Jan 2), feedback (Jan 3)
|
|
350
|
+
expect(types).toEqual(["show", "promoted", "feedback"]);
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
closeDatabase(db);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
test("--include-proposals ref filter shows only matching ref events", async () => {
|
|
357
|
+
const eventsFile = path.join(makeTempDir("akm-history-ref-filter-"), "events.jsonl");
|
|
358
|
+
const db = openDatabase(":memory:");
|
|
359
|
+
try {
|
|
360
|
+
ensureUsageEventsSchema(db);
|
|
361
|
+
// Two proposal events for different refs.
|
|
362
|
+
appendEvent({ eventType: "promoted", ref: "skill:deploy", metadata: { proposalId: "p1", source: "reflect" } }, { filePath: eventsFile });
|
|
363
|
+
appendEvent({ eventType: "rejected", ref: "memory:draft", metadata: { proposalId: "p2", source: "reflect" } }, { filePath: eventsFile });
|
|
364
|
+
const result = await akmHistory({
|
|
365
|
+
db,
|
|
366
|
+
ref: "skill:deploy",
|
|
367
|
+
includeProposals: true,
|
|
368
|
+
eventsCtx: { filePath: eventsFile },
|
|
369
|
+
});
|
|
370
|
+
// Only the promoted event for skill:deploy should appear.
|
|
371
|
+
expect(result.ref).toBe("skill:deploy");
|
|
372
|
+
expect(result.entries.every((e) => e.ref === "skill:deploy")).toBe(true);
|
|
373
|
+
const promoted = result.entries.find((e) => e.eventType === "promoted");
|
|
374
|
+
expect(promoted).toBeDefined();
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
closeDatabase(db);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
test("akm history --include-proposals CLI flag surfaces proposal lifecycle events", async () => {
|
|
381
|
+
const stashDir = makeTempDir("akm-history-cli-proposals-");
|
|
382
|
+
process.env.AKM_STASH_DIR = stashDir;
|
|
383
|
+
const cacheDir = makeTempDir("akm-history-cli-cache-");
|
|
384
|
+
process.env.XDG_CACHE_HOME = cacheDir;
|
|
385
|
+
saveConfig({ semanticSearchMode: "off" });
|
|
386
|
+
writeFile(path.join(stashDir, "memories", "alpha.md"), "---\ndescription: alpha memory\n---\nAlpha.\n");
|
|
387
|
+
await akmIndex({ stashDir, full: true });
|
|
388
|
+
// We can't easily run a full accept without a real proposal, so instead
|
|
389
|
+
// write a promoted event directly to events.jsonl to verify the CLI flag.
|
|
390
|
+
const eventsFile = path.join(cacheDir, "akm", "events.jsonl");
|
|
391
|
+
fs.mkdirSync(path.dirname(eventsFile), { recursive: true });
|
|
392
|
+
const promoted = {
|
|
393
|
+
schemaVersion: 1,
|
|
394
|
+
ts: new Date().toISOString(),
|
|
395
|
+
eventType: "promoted",
|
|
396
|
+
ref: "memory:alpha",
|
|
397
|
+
metadata: { proposalId: "p-cli-test", source: "reflect", assetPath: "memories/alpha.md" },
|
|
398
|
+
};
|
|
399
|
+
fs.appendFileSync(eventsFile, `${JSON.stringify(promoted)}\n`);
|
|
400
|
+
const result = runCli(["history", "--include-proposals", "--ref", "memory:alpha", "--format=json"]);
|
|
401
|
+
expect(result.status).toBe(0);
|
|
402
|
+
const parsed = parseJsonOutput(result);
|
|
403
|
+
const entries = parsed.entries;
|
|
404
|
+
expect(Array.isArray(entries)).toBe(true);
|
|
405
|
+
// The promoted event should appear.
|
|
406
|
+
const promotedEntry = entries.find((e) => e.eventType === "promoted");
|
|
407
|
+
expect(promotedEntry).toBeDefined();
|
|
408
|
+
expect(promotedEntry?.ref).toBe("memory:alpha");
|
|
409
|
+
// Sources should include events.jsonl.
|
|
410
|
+
expect(Array.isArray(parsed.sources)).toBe(true);
|
|
411
|
+
expect(parsed.sources.includes("events.jsonl")).toBe(true);
|
|
412
|
+
// Verify text output also shows the proposal event.
|
|
413
|
+
const text = runCli(["history", "--include-proposals", "--ref", "memory:alpha", "--format=text"]);
|
|
414
|
+
expect(text.status).toBe(0);
|
|
415
|
+
expect(text.stdout).toContain("[promoted]");
|
|
416
|
+
expect(text.stdout).toContain("events.jsonl");
|
|
417
|
+
});
|
|
418
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* import --target tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies the `--target` flag added to `akm import` per v1 implementation
|
|
5
|
+
* plan §6 decision 3. Resolution order is:
|
|
6
|
+
* --target → defaultWriteTarget → working stash → ConfigError
|
|
7
|
+
*
|
|
8
|
+
* These tests exercise the explicit-target path:
|
|
9
|
+
* - resolves to a configured filesystem source by name
|
|
10
|
+
* - errors on unknown target names (UsageError)
|
|
11
|
+
* - errors on non-writable targets (ConfigError)
|
|
12
|
+
*/
|
|
13
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
const CLI = path.join(__dirname, "..", "..", "src", "cli.ts");
|
|
19
|
+
const tempDirs = [];
|
|
20
|
+
function makeTempDir(prefix) {
|
|
21
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
22
|
+
tempDirs.push(dir);
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
function writeConfig(configDir, body) {
|
|
26
|
+
const akmDir = path.join(configDir, "akm");
|
|
27
|
+
fs.mkdirSync(akmDir, { recursive: true });
|
|
28
|
+
fs.writeFileSync(path.join(akmDir, "config.json"), JSON.stringify(body, null, 2), "utf8");
|
|
29
|
+
}
|
|
30
|
+
function runCli(args, options) {
|
|
31
|
+
const stashDir = options.stashDir ?? makeTempDir("akm-import-stash-");
|
|
32
|
+
const xdgCache = makeTempDir("akm-import-cache-");
|
|
33
|
+
const result = spawnSync("bun", [CLI, ...args], {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
timeout: 30_000,
|
|
36
|
+
input: options.input,
|
|
37
|
+
env: {
|
|
38
|
+
...process.env,
|
|
39
|
+
AKM_STASH_DIR: stashDir,
|
|
40
|
+
AKM_CONFIG_DIR: path.join(options.configDir, "akm"),
|
|
41
|
+
XDG_CACHE_HOME: xdgCache,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return { stashDir, result };
|
|
45
|
+
}
|
|
46
|
+
function makeKnowledgeFile(name, body) {
|
|
47
|
+
const dir = makeTempDir("akm-import-source-");
|
|
48
|
+
const filePath = path.join(dir, name);
|
|
49
|
+
fs.writeFileSync(filePath, body, "utf8");
|
|
50
|
+
return filePath;
|
|
51
|
+
}
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
for (const dir of tempDirs.splice(0)) {
|
|
54
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
describe("import --target", () => {
|
|
58
|
+
test("--target resolves to a configured filesystem source", () => {
|
|
59
|
+
const configDir = makeTempDir("akm-import-config-");
|
|
60
|
+
const targetDir = makeTempDir("akm-import-target-");
|
|
61
|
+
writeConfig(configDir, {
|
|
62
|
+
semanticSearchMode: "off",
|
|
63
|
+
sources: [{ type: "filesystem", name: "writable-target", path: targetDir, writable: true }],
|
|
64
|
+
});
|
|
65
|
+
const sourcePath = makeKnowledgeFile("auth-flow.md", "# Auth flow\n\nOAuth2 walk-through.\n");
|
|
66
|
+
const { stashDir, result } = runCli(["import", sourcePath, "--target", "writable-target"], { configDir });
|
|
67
|
+
expect(result.status).toBe(0);
|
|
68
|
+
const json = JSON.parse(result.stdout);
|
|
69
|
+
expect(json.ok).toBe(true);
|
|
70
|
+
expect(json.ref).toBe("knowledge:auth-flow");
|
|
71
|
+
const expectedPath = path.join(targetDir, "knowledge", "auth-flow.md");
|
|
72
|
+
expect(json.path).toBe(expectedPath);
|
|
73
|
+
expect(fs.existsSync(expectedPath)).toBe(true);
|
|
74
|
+
expect(fs.existsSync(path.join(stashDir, "knowledge", "auth-flow.md"))).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
test("--target with an unknown source name throws a usage error", () => {
|
|
77
|
+
const configDir = makeTempDir("akm-import-config-");
|
|
78
|
+
const targetDir = makeTempDir("akm-import-target-");
|
|
79
|
+
writeConfig(configDir, {
|
|
80
|
+
semanticSearchMode: "off",
|
|
81
|
+
sources: [{ type: "filesystem", name: "real-target", path: targetDir, writable: true }],
|
|
82
|
+
});
|
|
83
|
+
const sourcePath = makeKnowledgeFile("notes.md", "# Notes\n\nSomething.\n");
|
|
84
|
+
const { result } = runCli(["import", sourcePath, "--target", "ghost"], { configDir });
|
|
85
|
+
expect(result.status).toBe(2);
|
|
86
|
+
const json = JSON.parse(result.stderr);
|
|
87
|
+
expect(json.error).toContain('No source named "ghost" is configured');
|
|
88
|
+
expect(json.error).toContain("--target must reference a source name");
|
|
89
|
+
});
|
|
90
|
+
test("--target on a non-writable source throws a config error", () => {
|
|
91
|
+
const configDir = makeTempDir("akm-import-config-");
|
|
92
|
+
const targetDir = makeTempDir("akm-import-target-");
|
|
93
|
+
writeConfig(configDir, {
|
|
94
|
+
semanticSearchMode: "off",
|
|
95
|
+
sources: [{ type: "filesystem", name: "read-only", path: targetDir, writable: false }],
|
|
96
|
+
});
|
|
97
|
+
const sourcePath = makeKnowledgeFile("notes.md", "# Notes\n\nSomething.\n");
|
|
98
|
+
const { result } = runCli(["import", sourcePath, "--target", "read-only"], { configDir });
|
|
99
|
+
expect(result.status).not.toBe(0);
|
|
100
|
+
const json = JSON.parse(result.stderr);
|
|
101
|
+
expect(json.error).toContain("source read-only is not writable");
|
|
102
|
+
});
|
|
103
|
+
});
|