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,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `akm remember` frontmatter support (issue #169).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - CLI arg round-trip (--tag, --expires, --source)
|
|
6
|
+
* - --auto heuristics (code, subjective, source, observed_at)
|
|
7
|
+
* - --enrich with mocked chatCompletion (success + failure)
|
|
8
|
+
* - Required-field rejection before any file write
|
|
9
|
+
* - --expires duration → ISO date computation
|
|
10
|
+
* - Zero-flag remember still works (no frontmatter written)
|
|
11
|
+
* - memoryMdRenderer.extractMetadata populates StashEntry fields
|
|
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
|
+
import { parseFrontmatter } from "../src/core/frontmatter";
|
|
19
|
+
import { buildFileContext, buildRenderContext } from "../src/indexer/file-context";
|
|
20
|
+
import { memoryMdRenderer } from "../src/output/renderers";
|
|
21
|
+
// ── CLI harness ──────────────────────────────────────────────────────────────
|
|
22
|
+
const CLI = path.join(__dirname, "..", "src", "cli.ts");
|
|
23
|
+
const tempDirs = [];
|
|
24
|
+
function makeTempDir(prefix) {
|
|
25
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
26
|
+
tempDirs.push(dir);
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
function runCli(args, options) {
|
|
30
|
+
const stashDir = options?.stashDir ?? makeTempDir("akm-rmfm-stash-");
|
|
31
|
+
const xdgCache = makeTempDir("akm-rmfm-cache-");
|
|
32
|
+
const xdgConfig = makeTempDir("akm-rmfm-config-");
|
|
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
|
+
XDG_CACHE_HOME: xdgCache,
|
|
41
|
+
XDG_CONFIG_HOME: xdgConfig,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return { stashDir, result };
|
|
45
|
+
}
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
for (const dir of tempDirs.splice(0)) {
|
|
48
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
// ── Zero-flag path (backward compatibility) ──────────────────────────────────
|
|
52
|
+
describe("zero-flag remember", () => {
|
|
53
|
+
test("writes bare memory with no frontmatter", () => {
|
|
54
|
+
const { stashDir, result } = runCli(["remember", "Deployment needs VPN access"]);
|
|
55
|
+
expect(result.status).toBe(0);
|
|
56
|
+
const json = JSON.parse(result.stdout);
|
|
57
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
58
|
+
// No frontmatter delimiter present
|
|
59
|
+
expect(content.startsWith("---")).toBe(false);
|
|
60
|
+
expect(content).toContain("Deployment needs VPN access");
|
|
61
|
+
expect(stashDir).toBeTruthy();
|
|
62
|
+
});
|
|
63
|
+
test("writes bare memory when reading from stdin", () => {
|
|
64
|
+
const { result } = runCli(["remember"], { input: "VPN needed for staging deploys" });
|
|
65
|
+
expect(result.status).toBe(0);
|
|
66
|
+
const json = JSON.parse(result.stdout);
|
|
67
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
68
|
+
expect(content.startsWith("---")).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
test("reads stdin when --format json is present", () => {
|
|
71
|
+
const { result } = runCli(["remember", "--name", "from-stdin", "--format", "json"], { input: "stdin body" });
|
|
72
|
+
expect(result.status).toBe(0);
|
|
73
|
+
const json = JSON.parse(result.stdout);
|
|
74
|
+
expect(fs.readFileSync(json.path, "utf8")).toContain("stdin body");
|
|
75
|
+
expect(fs.readFileSync(json.path, "utf8")).not.toContain("\njson");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
// ── CLI args (Mode 1) ────────────────────────────────────────────────────────
|
|
79
|
+
describe("remember --tag", () => {
|
|
80
|
+
test("single --tag writes frontmatter with tags array", () => {
|
|
81
|
+
const { result } = runCli(["remember", "VPN required for staging", "--tag", "ops"]);
|
|
82
|
+
expect(result.status).toBe(0);
|
|
83
|
+
const json = JSON.parse(result.stdout);
|
|
84
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
85
|
+
const parsed = parseFrontmatter(content);
|
|
86
|
+
expect(parsed.data.tags).toEqual(["ops"]);
|
|
87
|
+
expect(parsed.content).toContain("VPN required for staging");
|
|
88
|
+
});
|
|
89
|
+
test("multiple --tag flags write all tags", () => {
|
|
90
|
+
const { result } = runCli(["remember", "VPN required for staging", "--tag", "ops", "--tag", "networking"]);
|
|
91
|
+
expect(result.status).toBe(0);
|
|
92
|
+
const json = JSON.parse(result.stdout);
|
|
93
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
94
|
+
const parsed = parseFrontmatter(content);
|
|
95
|
+
expect(parsed.data.tags).toEqual(["ops", "networking"]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe("remember --source", () => {
|
|
99
|
+
test("--source stores a URL as-is", () => {
|
|
100
|
+
const { result } = runCli([
|
|
101
|
+
"remember",
|
|
102
|
+
"Read the deployment guide",
|
|
103
|
+
"--tag",
|
|
104
|
+
"docs",
|
|
105
|
+
"--source",
|
|
106
|
+
"https://example.com/deploy",
|
|
107
|
+
]);
|
|
108
|
+
expect(result.status).toBe(0);
|
|
109
|
+
const json = JSON.parse(result.stdout);
|
|
110
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
111
|
+
const parsed = parseFrontmatter(content);
|
|
112
|
+
expect(parsed.data.source).toBe("https://example.com/deploy");
|
|
113
|
+
});
|
|
114
|
+
test("--source stores an asset ref", () => {
|
|
115
|
+
const { result } = runCli(["remember", "Deploy skill requires VPN", "--tag", "ops", "--source", "skill:deploy"]);
|
|
116
|
+
expect(result.status).toBe(0);
|
|
117
|
+
const json = JSON.parse(result.stdout);
|
|
118
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
119
|
+
const parsed = parseFrontmatter(content);
|
|
120
|
+
expect(parsed.data.source).toBe("skill:deploy");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe("remember --expires", () => {
|
|
124
|
+
test("--expires 30d resolves to a future ISO date ~30 days from now", () => {
|
|
125
|
+
const before = new Date();
|
|
126
|
+
const { result } = runCli(["remember", "Temp access token valid 30 days", "--tag", "security", "--expires", "30d"]);
|
|
127
|
+
expect(result.status).toBe(0);
|
|
128
|
+
const json = JSON.parse(result.stdout);
|
|
129
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
130
|
+
const parsed = parseFrontmatter(content);
|
|
131
|
+
const expires = parsed.data.expires;
|
|
132
|
+
expect(expires).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
133
|
+
// Should be approximately 30 days from now (within 1-day margin)
|
|
134
|
+
const expiresDate = new Date(expires);
|
|
135
|
+
const expectedMin = new Date(before.getTime() + 29 * 24 * 60 * 60 * 1000);
|
|
136
|
+
const expectedMax = new Date(before.getTime() + 31 * 24 * 60 * 60 * 1000);
|
|
137
|
+
expect(expiresDate >= expectedMin).toBe(true);
|
|
138
|
+
expect(expiresDate <= expectedMax).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
test("--expires 12h resolves to a future ISO date ~12h from now", () => {
|
|
141
|
+
const { result } = runCli(["remember", "Short-lived credential", "--tag", "security", "--expires", "12h"]);
|
|
142
|
+
expect(result.status).toBe(0);
|
|
143
|
+
const json = JSON.parse(result.stdout);
|
|
144
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
145
|
+
const parsed = parseFrontmatter(content);
|
|
146
|
+
expect(parsed.data.expires).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
147
|
+
});
|
|
148
|
+
test("--expires 6m resolves to a future ISO date ~6 months from now", () => {
|
|
149
|
+
const { result } = runCli(["remember", "Long-term access", "--tag", "access", "--expires", "6m"]);
|
|
150
|
+
expect(result.status).toBe(0);
|
|
151
|
+
const json = JSON.parse(result.stdout);
|
|
152
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
153
|
+
const parsed = parseFrontmatter(content);
|
|
154
|
+
const expires = parsed.data.expires;
|
|
155
|
+
expect(expires).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
156
|
+
const expiresDate = new Date(expires);
|
|
157
|
+
const expectedMin = new Date(Date.now() + 170 * 24 * 60 * 60 * 1000); // ~5.7 months
|
|
158
|
+
const expectedMax = new Date(Date.now() + 185 * 24 * 60 * 60 * 1000); // ~6.2 months
|
|
159
|
+
expect(expiresDate >= expectedMin).toBe(true);
|
|
160
|
+
expect(expiresDate <= expectedMax).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
test("invalid --expires format produces an error", () => {
|
|
163
|
+
const { result } = runCli(["remember", "Some note", "--tag", "misc", "--expires", "invalid"]);
|
|
164
|
+
expect(result.status).toBe(2);
|
|
165
|
+
const json = JSON.parse(result.stderr);
|
|
166
|
+
expect(json.error).toContain("Invalid --expires format");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
// ── Required-field rejection (before file write) ─────────────────────────────
|
|
170
|
+
describe("required-field rejection", () => {
|
|
171
|
+
test("--source without --tag rejects with missing-fields error before writing", () => {
|
|
172
|
+
const { stashDir, result } = runCli(["remember", "Some note", "--source", "https://example.com"]);
|
|
173
|
+
expect(result.status).toBe(2);
|
|
174
|
+
const json = JSON.parse(result.stderr);
|
|
175
|
+
expect(json.error).toContain("tags");
|
|
176
|
+
expect(json.error).toContain("--tag");
|
|
177
|
+
// Confirm no file was written
|
|
178
|
+
const memoriesDir = path.join(stashDir, "memories");
|
|
179
|
+
const written = fs.existsSync(memoriesDir) && fs.readdirSync(memoriesDir).length > 0;
|
|
180
|
+
expect(written).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
test("--expires without --tag rejects with missing-fields error before writing", () => {
|
|
183
|
+
const { stashDir, result } = runCli(["remember", "Some note", "--expires", "30d"]);
|
|
184
|
+
expect(result.status).toBe(2);
|
|
185
|
+
const json = JSON.parse(result.stderr);
|
|
186
|
+
expect(json.error).toContain("tags");
|
|
187
|
+
const memoriesDir = path.join(stashDir, "memories");
|
|
188
|
+
const written = fs.existsSync(memoriesDir) && fs.readdirSync(memoriesDir).length > 0;
|
|
189
|
+
expect(written).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
// ── --auto heuristics (Mode 2) ───────────────────────────────────────────────
|
|
193
|
+
describe("remember --auto", () => {
|
|
194
|
+
test("body with fenced code block gets tag 'code'", () => {
|
|
195
|
+
const body = "Remember this pattern:\n```ts\nconst x = 1;\n```";
|
|
196
|
+
const { result } = runCli(["remember", body, "--auto"]);
|
|
197
|
+
expect(result.status).toBe(0);
|
|
198
|
+
const json = JSON.parse(result.stdout);
|
|
199
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
200
|
+
const parsed = parseFrontmatter(content);
|
|
201
|
+
const tags = parsed.data.tags;
|
|
202
|
+
expect(tags).toContain("code");
|
|
203
|
+
});
|
|
204
|
+
test("body with URL gets source set automatically", () => {
|
|
205
|
+
const body = "Found this resource https://example.com/guide useful for ops";
|
|
206
|
+
const { result } = runCli(["remember", body, "--auto", "--tag", "docs"]);
|
|
207
|
+
expect(result.status).toBe(0);
|
|
208
|
+
const json = JSON.parse(result.stdout);
|
|
209
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
210
|
+
const parsed = parseFrontmatter(content);
|
|
211
|
+
expect(parsed.data.source).toBe("https://example.com/guide");
|
|
212
|
+
});
|
|
213
|
+
test("body with first-person pronoun gets subjective: true", () => {
|
|
214
|
+
// Must supply --tag because heuristics add subjective but not tags for plain text.
|
|
215
|
+
const body = "I noticed that staging requires VPN every time";
|
|
216
|
+
const { result } = runCli(["remember", body, "--auto", "--tag", "ops"]);
|
|
217
|
+
expect(result.status).toBe(0);
|
|
218
|
+
const json = JSON.parse(result.stdout);
|
|
219
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
220
|
+
const parsed = parseFrontmatter(content);
|
|
221
|
+
expect(parsed.data.subjective).toBe(true);
|
|
222
|
+
expect(parsed.data.tags).toContain("ops");
|
|
223
|
+
});
|
|
224
|
+
test("body with ISO date gets observed_at set", () => {
|
|
225
|
+
const body = "The outage happened on 2026-01-15 and we fixed it quickly";
|
|
226
|
+
const { result } = runCli(["remember", body, "--auto"]);
|
|
227
|
+
// Will fail required-field check if no tags derived from the body
|
|
228
|
+
// Force a tag to ensure we get through
|
|
229
|
+
const { result: r2 } = runCli(["remember", body, "--auto", "--tag", "ops"]);
|
|
230
|
+
expect(r2.status).toBe(0);
|
|
231
|
+
const json = JSON.parse(r2.stdout);
|
|
232
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
233
|
+
const parsed = parseFrontmatter(content);
|
|
234
|
+
expect(parsed.data.observed_at).toBe("2026-01-15");
|
|
235
|
+
void result; // suppress unused variable warning
|
|
236
|
+
});
|
|
237
|
+
test("--auto without any tags from heuristics or CLI still writes the memory", () => {
|
|
238
|
+
// Plain text body — no code block, no URL. Heuristics won't derive any tags.
|
|
239
|
+
const { result } = runCli(["remember", "Plain text note without any tags derivable", "--auto"]);
|
|
240
|
+
expect(result.status).toBe(0);
|
|
241
|
+
const json = JSON.parse(result.stdout);
|
|
242
|
+
expect(fs.existsSync(json.path)).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
test("--auto + explicit --tag satisfies required-field check", () => {
|
|
245
|
+
const body = "No special content here";
|
|
246
|
+
const { result } = runCli(["remember", body, "--auto", "--tag", "misc"]);
|
|
247
|
+
expect(result.status).toBe(0);
|
|
248
|
+
const json = JSON.parse(result.stdout);
|
|
249
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
250
|
+
const parsed = parseFrontmatter(content);
|
|
251
|
+
expect(parsed.data.tags).toContain("misc");
|
|
252
|
+
});
|
|
253
|
+
test("--source CLI arg takes priority over auto-detected URL", () => {
|
|
254
|
+
const body = "See https://example.com/docs for reference";
|
|
255
|
+
const { result } = runCli(["remember", body, "--auto", "--tag", "docs", "--source", "explicit:source"]);
|
|
256
|
+
expect(result.status).toBe(0);
|
|
257
|
+
const json = JSON.parse(result.stdout);
|
|
258
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
259
|
+
const parsed = parseFrontmatter(content);
|
|
260
|
+
// CLI --source wins over auto-detected URL
|
|
261
|
+
expect(parsed.data.source).toBe("explicit:source");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
// ── memoryMdRenderer.extractMetadata ─────────────────────────────────────────
|
|
265
|
+
/** A static MatchResult for memory-md (avoids calling runMatchers and null assertions). */
|
|
266
|
+
const MEMORY_MATCH = { type: "memory", specificity: 10, renderer: "memory-md" };
|
|
267
|
+
describe("memoryMdRenderer.extractMetadata", () => {
|
|
268
|
+
const createdTmpDirs = [];
|
|
269
|
+
afterEach(() => {
|
|
270
|
+
for (const dir of createdTmpDirs.splice(0)) {
|
|
271
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
function writeTmpMemory(content) {
|
|
275
|
+
const stashRoot = fs.mkdtempSync(path.join(os.tmpdir(), "akm-mem-renderer-"));
|
|
276
|
+
createdTmpDirs.push(stashRoot);
|
|
277
|
+
const memoriesDir = path.join(stashRoot, "memories");
|
|
278
|
+
fs.mkdirSync(memoriesDir, { recursive: true });
|
|
279
|
+
const filePath = path.join(memoriesDir, "test-memory.md");
|
|
280
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
281
|
+
return { filePath, stashRoot };
|
|
282
|
+
}
|
|
283
|
+
test("populates tags from frontmatter", () => {
|
|
284
|
+
const { filePath, stashRoot } = writeTmpMemory("---\ntags: [ops, networking]\n---\nDeployment needs VPN access\n");
|
|
285
|
+
const ctx = buildFileContext(stashRoot, filePath);
|
|
286
|
+
const entry = { name: "test-memory", type: "memory" };
|
|
287
|
+
const renderCtx = buildRenderContext(ctx, MEMORY_MATCH, [stashRoot]);
|
|
288
|
+
memoryMdRenderer.extractMetadata?.(entry, renderCtx);
|
|
289
|
+
expect(entry.tags).toContain("ops");
|
|
290
|
+
expect(entry.tags).toContain("networking");
|
|
291
|
+
});
|
|
292
|
+
test("populates description from frontmatter", () => {
|
|
293
|
+
const { filePath, stashRoot } = writeTmpMemory("---\ndescription: VPN required for staging deploys\ntags: [ops]\n---\nBody content\n");
|
|
294
|
+
const ctx = buildFileContext(stashRoot, filePath);
|
|
295
|
+
const entry = { name: "test-memory", type: "memory" };
|
|
296
|
+
const renderCtx = buildRenderContext(ctx, MEMORY_MATCH, [stashRoot]);
|
|
297
|
+
memoryMdRenderer.extractMetadata?.(entry, renderCtx);
|
|
298
|
+
expect(entry.description).toBe("VPN required for staging deploys");
|
|
299
|
+
});
|
|
300
|
+
test("populates searchHints with source, observed_at, expires, subjective", () => {
|
|
301
|
+
const { filePath, stashRoot } = writeTmpMemory("---\ntags: [ops]\nsource: skill:deploy\nobserved_at: 2026-01-15\nexpires: 2026-04-15\nsubjective: true\n---\nVPN needed\n");
|
|
302
|
+
const ctx = buildFileContext(stashRoot, filePath);
|
|
303
|
+
const entry = { name: "test-memory", type: "memory" };
|
|
304
|
+
const renderCtx = buildRenderContext(ctx, MEMORY_MATCH, [stashRoot]);
|
|
305
|
+
memoryMdRenderer.extractMetadata?.(entry, renderCtx);
|
|
306
|
+
expect(entry.searchHints).toBeDefined();
|
|
307
|
+
expect(entry.searchHints).toContain("skill:deploy");
|
|
308
|
+
expect(entry.searchHints).toContain("observed_at:2026-01-15");
|
|
309
|
+
expect(entry.searchHints).toContain("expires:2026-04-15");
|
|
310
|
+
expect(entry.searchHints).toContain("subjective");
|
|
311
|
+
});
|
|
312
|
+
test("observed_at falls back to file mtime when not in frontmatter", () => {
|
|
313
|
+
const { filePath, stashRoot } = writeTmpMemory("---\ntags: [ops]\n---\nSome memory without observed_at\n");
|
|
314
|
+
const ctx = buildFileContext(stashRoot, filePath);
|
|
315
|
+
const entry = { name: "test-memory", type: "memory" };
|
|
316
|
+
const renderCtx = buildRenderContext(ctx, MEMORY_MATCH, [stashRoot]);
|
|
317
|
+
memoryMdRenderer.extractMetadata?.(entry, renderCtx);
|
|
318
|
+
// Should have an observed_at hint derived from mtime
|
|
319
|
+
const mtimeHint = (entry.searchHints ?? []).find((h) => h.startsWith("observed_at:"));
|
|
320
|
+
expect(mtimeHint).toBeDefined();
|
|
321
|
+
// The mtime-based date should be a valid ISO date
|
|
322
|
+
const dateStr = mtimeHint?.slice("observed_at:".length);
|
|
323
|
+
expect(dateStr).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
324
|
+
});
|
|
325
|
+
test("works for bare memory with no frontmatter (no crash)", () => {
|
|
326
|
+
const { filePath, stashRoot } = writeTmpMemory("Just a plain memory without any frontmatter.\n");
|
|
327
|
+
const ctx = buildFileContext(stashRoot, filePath);
|
|
328
|
+
const entry = { name: "test-memory", type: "memory" };
|
|
329
|
+
const renderCtx = buildRenderContext(ctx, MEMORY_MATCH, [stashRoot]);
|
|
330
|
+
// Should not throw
|
|
331
|
+
expect(() => memoryMdRenderer.extractMetadata?.(entry, renderCtx)).not.toThrow();
|
|
332
|
+
// mtime fallback should still fire
|
|
333
|
+
const mtimeHint = (entry.searchHints ?? []).find((h) => h.startsWith("observed_at:"));
|
|
334
|
+
expect(mtimeHint).toBeDefined();
|
|
335
|
+
});
|
|
336
|
+
test("block-sequence tags in frontmatter are parsed correctly", () => {
|
|
337
|
+
const { filePath, stashRoot } = writeTmpMemory("---\ntags:\n- ops\n- networking\n- deploy\n---\nVPN required\n");
|
|
338
|
+
const ctx = buildFileContext(stashRoot, filePath);
|
|
339
|
+
const entry = { name: "test-memory", type: "memory" };
|
|
340
|
+
const renderCtx = buildRenderContext(ctx, MEMORY_MATCH, [stashRoot]);
|
|
341
|
+
memoryMdRenderer.extractMetadata?.(entry, renderCtx);
|
|
342
|
+
expect(entry.tags).toContain("ops");
|
|
343
|
+
expect(entry.tags).toContain("networking");
|
|
344
|
+
expect(entry.tags).toContain("deploy");
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
// ── --enrich (Mode 3) — mocked ───────────────────────────────────────────────
|
|
348
|
+
// These tests directly exercise the heuristic and enrichment helpers by calling
|
|
349
|
+
// the CLI with a mock LLM config. Since we cannot easily intercept the dynamic
|
|
350
|
+
// import inside the CLI process, we test the LLM enrichment path via integration
|
|
351
|
+
// against a non-existent endpoint and verify the graceful-degradation behaviour.
|
|
352
|
+
describe("remember --enrich graceful degradation", () => {
|
|
353
|
+
test("when no LLM is configured, --enrich emits warning but still fails if no tags", () => {
|
|
354
|
+
// No LLM configured in the temp config dir — should warn and return empty tags
|
|
355
|
+
const { result } = runCli(["remember", "Some note about ops", "--enrich"]);
|
|
356
|
+
// Will fail because enrichment produces no tags and no CLI tags given.
|
|
357
|
+
// stderr may contain a warning line followed by a multi-line JSON error block.
|
|
358
|
+
if (result.status !== 0) {
|
|
359
|
+
// Extract the JSON portion (from first '{' to end of stderr)
|
|
360
|
+
const jsonStart = result.stderr.indexOf("{");
|
|
361
|
+
expect(jsonStart).toBeGreaterThanOrEqual(0);
|
|
362
|
+
const jsonStr = result.stderr.slice(jsonStart);
|
|
363
|
+
const json = JSON.parse(jsonStr);
|
|
364
|
+
expect(json.error).toContain("tags");
|
|
365
|
+
}
|
|
366
|
+
// Either path is acceptable: rejection (no tags) or success (if enrichment happened to work)
|
|
367
|
+
});
|
|
368
|
+
test("--enrich with --tag satisfies required-field check even if LLM fails", () => {
|
|
369
|
+
// Providing --tag means we don't depend on LLM for the required field
|
|
370
|
+
const { result } = runCli(["remember", "Some note", "--enrich", "--tag", "misc"]);
|
|
371
|
+
expect(result.status).toBe(0);
|
|
372
|
+
const json = JSON.parse(result.stdout);
|
|
373
|
+
const content = fs.readFileSync(json.path, "utf8");
|
|
374
|
+
const parsed = parseFrontmatter(content);
|
|
375
|
+
// At minimum, the --tag value must be present
|
|
376
|
+
expect(parsed.data.tags).toContain("misc");
|
|
377
|
+
});
|
|
378
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { parse as yamlParse } from "yaml";
|
|
3
|
+
import { buildMemoryFrontmatter, parseDuration, runAutoHeuristics } from "../src/commands/remember";
|
|
4
|
+
describe("parseDuration", () => {
|
|
5
|
+
test("parses days", () => {
|
|
6
|
+
expect(parseDuration("30d")).toBe(30 * 24 * 60 * 60 * 1000);
|
|
7
|
+
expect(parseDuration("1d")).toBe(24 * 60 * 60 * 1000);
|
|
8
|
+
});
|
|
9
|
+
test("parses hours", () => {
|
|
10
|
+
expect(parseDuration("12h")).toBe(12 * 60 * 60 * 1000);
|
|
11
|
+
});
|
|
12
|
+
test("parses months as 30-day approximation", () => {
|
|
13
|
+
expect(parseDuration("6m")).toBe(6 * 30 * 24 * 60 * 60 * 1000);
|
|
14
|
+
});
|
|
15
|
+
test("rejects invalid format", () => {
|
|
16
|
+
expect(() => parseDuration("forever")).toThrow(/Invalid --expires/);
|
|
17
|
+
expect(() => parseDuration("30")).toThrow(/Invalid --expires/);
|
|
18
|
+
expect(() => parseDuration("d30")).toThrow(/Invalid --expires/);
|
|
19
|
+
});
|
|
20
|
+
test("trims whitespace and accepts uppercase units", () => {
|
|
21
|
+
expect(parseDuration(" 7D ")).toBe(7 * 24 * 60 * 60 * 1000);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("buildMemoryFrontmatter — YAML injection guard", () => {
|
|
25
|
+
test("emits a parseable, well-formed YAML block for a normal record", () => {
|
|
26
|
+
const out = buildMemoryFrontmatter({
|
|
27
|
+
description: "VPN required for staging deploys",
|
|
28
|
+
tags: ["ops", "networking"],
|
|
29
|
+
source: "skill:deploy",
|
|
30
|
+
observed_at: "2026-04-24",
|
|
31
|
+
expires: "2026-07-23",
|
|
32
|
+
subjective: false,
|
|
33
|
+
});
|
|
34
|
+
expect(out.startsWith("---\n")).toBe(true);
|
|
35
|
+
expect(out.endsWith("\n---")).toBe(true);
|
|
36
|
+
const inner = out.replace(/^---\n/, "").replace(/\n---$/, "");
|
|
37
|
+
const parsed = yamlParse(inner);
|
|
38
|
+
expect(parsed.description).toBe("VPN required for staging deploys");
|
|
39
|
+
expect(parsed.tags).toEqual(["ops", "networking"]);
|
|
40
|
+
expect(parsed.source).toBe("skill:deploy");
|
|
41
|
+
expect(parsed.observed_at).toBe("2026-04-24");
|
|
42
|
+
expect(parsed.expires).toBe("2026-07-23");
|
|
43
|
+
expect(parsed.subjective).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
test("preserves subjective: true when set", () => {
|
|
46
|
+
const out = buildMemoryFrontmatter({ tags: ["x"], subjective: true });
|
|
47
|
+
const parsed = yamlParse(out.replace(/^---\n/, "").replace(/\n---$/, ""));
|
|
48
|
+
expect(parsed.subjective).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
test("description containing newlines + forged tags cannot inject extra keys", () => {
|
|
51
|
+
// Pre-fix this string would have been emitted as:
|
|
52
|
+
// description: nice
|
|
53
|
+
// tags: [pwned]
|
|
54
|
+
// …producing two real frontmatter keys. With yaml.stringify it is
|
|
55
|
+
// safely quoted as a single string value.
|
|
56
|
+
const malicious = "nice\ntags: [pwned]";
|
|
57
|
+
const out = buildMemoryFrontmatter({ description: malicious, tags: ["expected"] });
|
|
58
|
+
const parsed = yamlParse(out.replace(/^---\n/, "").replace(/\n---$/, ""));
|
|
59
|
+
expect(parsed.tags).toEqual(["expected"]);
|
|
60
|
+
expect(parsed.description).toBe(malicious);
|
|
61
|
+
expect(parsed.pwned).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
test("source containing YAML metacharacters round-trips intact", () => {
|
|
64
|
+
const tricky = "https://example.com/path?q=#anchor: { x: y }";
|
|
65
|
+
const out = buildMemoryFrontmatter({ tags: ["ops"], source: tricky });
|
|
66
|
+
const parsed = yamlParse(out.replace(/^---\n/, "").replace(/\n---$/, ""));
|
|
67
|
+
expect(parsed.source).toBe(tricky);
|
|
68
|
+
});
|
|
69
|
+
test("omits empty fields", () => {
|
|
70
|
+
const out = buildMemoryFrontmatter({ tags: [] });
|
|
71
|
+
expect(out).toBe("---\n---");
|
|
72
|
+
});
|
|
73
|
+
test("omits whitespace-only string fields", () => {
|
|
74
|
+
const out = buildMemoryFrontmatter({ description: " ", tags: ["x"] });
|
|
75
|
+
const parsed = yamlParse(out.replace(/^---\n/, "").replace(/\n---$/, ""));
|
|
76
|
+
expect(parsed.description).toBeUndefined();
|
|
77
|
+
expect(parsed.tags).toEqual(["x"]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("runAutoHeuristics", () => {
|
|
81
|
+
test("detects a fenced code block as the `code` tag", () => {
|
|
82
|
+
const result = runAutoHeuristics("Found this:\n```sh\necho hi\n```");
|
|
83
|
+
expect(result.tags).toContain("code");
|
|
84
|
+
});
|
|
85
|
+
test("does not add `code` tag when no fenced block present", () => {
|
|
86
|
+
const result = runAutoHeuristics("plain prose, no code");
|
|
87
|
+
expect(result.tags).not.toContain("code");
|
|
88
|
+
});
|
|
89
|
+
test("flags first-person pronouns as subjective (lowercase + capital I)", () => {
|
|
90
|
+
expect(runAutoHeuristics("I think we should ship").subjective).toBe(true);
|
|
91
|
+
expect(runAutoHeuristics("we shipped my favorite feature").subjective).toBe(true);
|
|
92
|
+
expect(runAutoHeuristics("our team agreed").subjective).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
test("non-first-person prose is not flagged subjective", () => {
|
|
95
|
+
expect(runAutoHeuristics("The cluster restarted at 3am.").subjective).toBeUndefined();
|
|
96
|
+
// Capitalised My/Our at sentence start currently isn't matched —
|
|
97
|
+
// documented as case-sensitive. If we widen this in a future
|
|
98
|
+
// patch, update this test to reflect the new behaviour.
|
|
99
|
+
expect(runAutoHeuristics("My take is...").subjective).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
test("captures the first URL as source", () => {
|
|
102
|
+
const result = runAutoHeuristics("see https://example.com/docs and also https://example.org");
|
|
103
|
+
expect(result.source).toBe("https://example.com/docs");
|
|
104
|
+
});
|
|
105
|
+
test("captures an explicit ISO date as observed_at", () => {
|
|
106
|
+
const result = runAutoHeuristics("Incident on 2026-04-24, resolved.");
|
|
107
|
+
expect(result.observed_at).toBe("2026-04-24");
|
|
108
|
+
});
|
|
109
|
+
test("interprets `today` as observed_at", () => {
|
|
110
|
+
const result = runAutoHeuristics("today the deploy failed");
|
|
111
|
+
expect(result.observed_at).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
112
|
+
// Should be today's date (loose check — no timezone drift assertions)
|
|
113
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
114
|
+
expect(result.observed_at).toBe(today);
|
|
115
|
+
});
|
|
116
|
+
test("handles plain prose without any signals", () => {
|
|
117
|
+
const result = runAutoHeuristics("Just a regular note about something boring.");
|
|
118
|
+
expect(result.tags).toEqual([]);
|
|
119
|
+
expect(result.source).toBeUndefined();
|
|
120
|
+
expect(result.observed_at).toBeUndefined();
|
|
121
|
+
expect(result.subjective).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
});
|