akm-cli 0.6.1 → 0.7.0-rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -0
- package/dist/{cli.js → src/cli.js} +620 -26
- package/dist/{commands → src/commands}/config-cli.js +5 -4
- package/dist/src/commands/distill.js +283 -0
- package/dist/src/commands/events.js +108 -0
- package/dist/src/commands/history.js +120 -0
- package/dist/{commands → src/commands}/installed-stashes.js +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 +2 -1
- package/dist/{commands → src/commands}/remember.js +12 -0
- package/dist/{commands → src/commands}/search.js +74 -1
- package/dist/{commands → src/commands}/self-update.js +4 -3
- package/dist/{commands → src/commands}/show.js +44 -0
- package/dist/{core → src/core}/asset-ref.js +5 -5
- package/dist/{core → src/core}/asset-spec.js +12 -0
- package/dist/{core → src/core}/common.js +1 -1
- package/dist/{core → src/core}/config.js +175 -121
- package/dist/{core → src/core}/errors.js +4 -0
- package/dist/src/core/events.js +239 -0
- package/dist/src/core/lesson-lint.js +86 -0
- package/dist/src/core/proposals.js +406 -0
- package/dist/src/core/warn.js +72 -0
- package/dist/{core → src/core}/write-source.js +80 -5
- package/dist/{indexer → src/indexer}/db-search.js +113 -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 +73 -6
- package/dist/src/indexer/memory-inference.js +263 -0
- package/dist/{indexer → src/indexer}/metadata.js +111 -3
- package/dist/src/integrations/agent/config.js +292 -0
- package/dist/src/integrations/agent/detect.js +94 -0
- package/dist/src/integrations/agent/index.js +17 -0
- package/dist/src/integrations/agent/profiles.js +65 -0
- package/dist/src/integrations/agent/prompts.js +167 -0
- package/dist/src/integrations/agent/spawn.js +221 -0
- package/dist/{integrations → src/integrations}/lockfile.js +0 -26
- package/dist/{llm → src/llm}/client.js +33 -2
- package/dist/src/llm/feature-gate.js +108 -0
- package/dist/src/llm/graph-extract.js +107 -0
- package/dist/src/llm/index-passes.js +35 -0
- package/dist/src/llm/memory-infer.js +86 -0
- package/dist/{output → src/output}/renderers.js +60 -1
- package/dist/src/output/shapes.js +516 -0
- package/dist/{output → src/output}/text.js +447 -4
- package/dist/{registry → src/registry}/build-index.js +14 -4
- package/dist/{registry → src/registry}/factory.js +0 -8
- package/dist/{registry → src/registry}/providers/static-index.js +3 -2
- package/dist/{registry → src/registry}/resolve.js +68 -2
- package/dist/{setup → src/setup}/setup.js +43 -5
- package/dist/{sources → src/sources}/providers/git.js +7 -15
- package/dist/tests/add-website-source.test.js +119 -0
- package/dist/tests/agent/agent-config-loader.test.js +70 -0
- package/dist/tests/agent/agent-config.test.js +221 -0
- package/dist/tests/agent/agent-detect.test.js +100 -0
- package/dist/tests/agent/agent-spawn.test.js +234 -0
- package/dist/tests/agent-output.test.js +186 -0
- package/dist/tests/architecture/agent-no-llm-sdk-guard.test.js +103 -0
- package/dist/tests/architecture/agent-spawn-seam.test.js +193 -0
- package/dist/tests/architecture/llm-stateless-seam.test.js +112 -0
- package/dist/tests/asset-ref.test.js +192 -0
- package/dist/tests/asset-registry.test.js +103 -0
- package/dist/tests/asset-spec.test.js +241 -0
- package/dist/tests/bench/attribution.test.js +995 -0
- package/dist/tests/bench/cleanup-sigint.test.js +83 -0
- package/dist/tests/bench/cleanup.js +203 -0
- package/dist/tests/bench/cleanup.test.js +166 -0
- package/dist/tests/bench/cli.js +683 -0
- package/dist/tests/bench/cli.test.js +177 -0
- package/dist/tests/bench/compare.test.js +556 -0
- package/dist/tests/bench/corpus.js +314 -0
- package/dist/tests/bench/corpus.test.js +258 -0
- package/dist/tests/bench/driver.js +346 -0
- package/dist/tests/bench/driver.test.js +443 -0
- package/dist/tests/bench/evolve-metrics.js +179 -0
- package/dist/tests/bench/evolve-metrics.test.js +187 -0
- package/dist/tests/bench/evolve.js +580 -0
- package/dist/tests/bench/evolve.test.js +616 -0
- package/dist/tests/bench/failure-modes.test.js +300 -0
- package/dist/tests/bench/feedback-integrity.test.js +456 -0
- package/dist/tests/bench/leakage.test.js +125 -0
- package/dist/tests/bench/learning-curve.test.js +133 -0
- package/dist/tests/bench/metrics.js +2319 -0
- package/dist/tests/bench/metrics.test.js +1144 -0
- package/dist/tests/bench/no-os-tmpdir-invariant.test.js +43 -0
- package/dist/tests/bench/report.js +1821 -0
- package/dist/tests/bench/report.test.js +989 -0
- package/dist/tests/bench/runner.js +536 -0
- package/dist/tests/bench/runner.test.js +958 -0
- package/dist/tests/bench/search-bridge.test.js +331 -0
- package/dist/tests/bench/tmp.js +41 -0
- package/dist/tests/bench/trajectory.js +116 -0
- package/dist/tests/bench/trajectory.test.js +127 -0
- package/dist/tests/bench/verifier.js +109 -0
- package/dist/tests/bench/verifier.test.js +118 -0
- package/dist/tests/bench/workflow-evaluator.js +557 -0
- package/dist/tests/bench/workflow-evaluator.test.js +421 -0
- package/dist/tests/bench/workflow-spec.js +358 -0
- package/dist/tests/bench/workflow-spec.test.js +363 -0
- package/dist/tests/bench/workflow-trace.js +438 -0
- package/dist/tests/bench/workflow-trace.test.js +254 -0
- package/dist/tests/benchmark-search-quality.js +536 -0
- package/dist/tests/benchmark-suite.js +1441 -0
- package/dist/tests/capture-cli.test.js +112 -0
- package/dist/tests/cli-errors.test.js +203 -0
- package/dist/tests/commands/events.test.js +370 -0
- package/dist/tests/commands/history.test.js +223 -0
- package/dist/tests/commands/import.test.js +103 -0
- package/dist/tests/commands/proposal-cli.test.js +209 -0
- package/dist/tests/commands/reflect-propose-cli.test.js +333 -0
- package/dist/tests/commands/remember.test.js +97 -0
- package/dist/tests/commands/scope-flags.test.js +300 -0
- package/dist/tests/commands/search.test.js +537 -0
- package/dist/tests/commands/show-indexer-parity.test.js +117 -0
- package/dist/tests/commands/show.test.js +294 -0
- package/dist/tests/common.test.js +266 -0
- package/dist/tests/completions.test.js +142 -0
- package/dist/tests/config-cli.test.js +193 -0
- package/dist/tests/config-llm-features.test.js +139 -0
- package/dist/tests/config.test.js +544 -0
- package/dist/tests/contracts/migration-baseline.test.js +43 -0
- package/dist/tests/contracts/reflect-propose-envelope.test.js +139 -0
- package/dist/tests/contracts/spec-helpers.js +46 -0
- package/dist/tests/contracts/v1-spec-section-11-proposal-queue.test.js +228 -0
- package/dist/tests/contracts/v1-spec-section-12-agent-config.test.js +56 -0
- package/dist/tests/contracts/v1-spec-section-13-lesson-type.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-14-llm-features.test.js +94 -0
- package/dist/tests/contracts/v1-spec-section-4-1-asset-types.test.js +39 -0
- package/dist/tests/contracts/v1-spec-section-4-2-quality-rules.test.js +44 -0
- package/dist/tests/contracts/v1-spec-section-5-configuration.test.js +47 -0
- package/dist/tests/contracts/v1-spec-section-6-orchestration.test.js +40 -0
- package/dist/tests/contracts/v1-spec-section-7-module-layout.test.js +58 -0
- package/dist/tests/contracts/v1-spec-section-8-extension-points.test.js +34 -0
- package/dist/tests/contracts/v1-spec-section-9-4-cli-surface.test.js +75 -0
- package/dist/tests/contracts/v1-spec-section-9-7-llm-agent-boundary.test.js +36 -0
- package/dist/tests/core/write-source.test.js +366 -0
- package/dist/tests/curate-command.test.js +87 -0
- package/dist/tests/db-scoring.test.js +201 -0
- package/dist/tests/db.test.js +654 -0
- package/dist/tests/distill-cli-flag.test.js +208 -0
- package/dist/tests/distill.test.js +515 -0
- package/dist/tests/docker-install.test.js +120 -0
- package/dist/tests/e2e.test.js +1398 -0
- package/dist/tests/embedder.test.js +340 -0
- package/dist/tests/embedding-model-config.test.js +379 -0
- package/dist/tests/feedback-command.test.js +172 -0
- package/dist/tests/file-context.test.js +552 -0
- package/dist/tests/fixtures/scripts/git/summarize-diff.js +9 -0
- package/dist/tests/fixtures/scripts/lint/eslint-check.js +7 -0
- package/dist/tests/fixtures/stashes/load.js +166 -0
- package/dist/tests/fixtures/stashes/load.test.js +88 -0
- package/dist/tests/fixtures/stashes/ranking-baseline/scripts/mem0-search.js +12 -0
- package/dist/tests/frontmatter.test.js +190 -0
- package/dist/tests/fts-field-weighting.test.js +254 -0
- package/dist/tests/fuzzy-search.test.js +230 -0
- package/dist/tests/git-provider-clone.test.js +45 -0
- package/dist/tests/github.test.js +161 -0
- package/dist/tests/graph-boost-ranking.test.js +305 -0
- package/dist/tests/graph-extraction.test.js +282 -0
- package/dist/tests/helpers/usage-events.js +8 -0
- package/dist/tests/index-pass-llm.test.js +161 -0
- package/dist/tests/indexer.test.js +559 -0
- package/dist/tests/info-command.test.js +166 -0
- package/dist/tests/init.test.js +69 -0
- package/dist/tests/install-script.test.js +246 -0
- package/dist/tests/integration/agent-real-profile.test.js +94 -0
- package/dist/tests/issue-36-repro.test.js +304 -0
- package/dist/tests/issues-191-194.test.js +160 -0
- package/dist/tests/lesson-lint.test.js +111 -0
- package/dist/tests/llm-client.test.js +115 -0
- package/dist/tests/llm-feature-gate.test.js +151 -0
- package/dist/tests/llm.test.js +139 -0
- package/dist/tests/lockfile.test.js +216 -0
- package/dist/tests/manifest.test.js +205 -0
- package/dist/tests/markdown.test.js +126 -0
- package/dist/tests/matchers-unit.test.js +189 -0
- package/dist/tests/memory-inference.test.js +299 -0
- package/dist/tests/merge-scoring.test.js +136 -0
- package/dist/tests/metadata.test.js +313 -0
- package/dist/tests/migration-help.test.js +89 -0
- package/dist/tests/origin-resolve.test.js +124 -0
- package/dist/tests/output-baseline.test.js +217 -0
- package/dist/tests/output-shapes-unit.test.js +476 -0
- package/dist/tests/parallel-search.test.js +272 -0
- package/dist/tests/parameter-metadata.test.js +365 -0
- package/dist/tests/paths.test.js +177 -0
- package/dist/tests/progressive-disclosure.test.js +280 -0
- package/dist/tests/proposals.test.js +279 -0
- package/dist/tests/proposed-quality.test.js +271 -0
- package/dist/tests/provider-registry.test.js +32 -0
- package/dist/tests/ranking-regression.test.js +548 -0
- package/dist/tests/reflect-propose.test.js +455 -0
- package/dist/tests/registry-build-index.test.js +378 -0
- package/dist/tests/registry-cli.test.js +290 -0
- package/dist/tests/registry-index-v2.test.js +430 -0
- package/dist/tests/registry-install.test.js +728 -0
- package/dist/tests/registry-providers/parity.test.js +189 -0
- package/dist/tests/registry-providers/skills-sh.test.js +309 -0
- package/dist/tests/registry-providers/static-index.test.js +204 -0
- package/dist/tests/registry-resolve.test.js +126 -0
- package/dist/tests/registry-search.test.js +723 -0
- package/dist/tests/remember-frontmatter.test.js +380 -0
- package/dist/tests/remember-unit.test.js +123 -0
- package/dist/tests/ripgrep-install.test.js +251 -0
- package/dist/tests/ripgrep-resolve.test.js +108 -0
- package/dist/tests/ripgrep.test.js +163 -0
- package/dist/tests/save-command.test.js +94 -0
- package/dist/tests/save-trust-qa-fixes.test.js +270 -0
- package/dist/tests/scoring-pipeline.test.js +648 -0
- package/dist/tests/search-include-proposed-cli.test.js +118 -0
- package/dist/tests/self-update.test.js +442 -0
- package/dist/tests/semantic-search-e2e.test.js +512 -0
- package/dist/tests/semantic-status.test.js +471 -0
- package/dist/tests/setup-run.integration.js +877 -0
- package/dist/tests/setup-wizard.test.js +198 -0
- package/dist/tests/setup.test.js +131 -0
- package/dist/tests/source-add.test.js +11 -0
- package/dist/tests/source-clone.test.js +254 -0
- package/dist/tests/source-manage.test.js +366 -0
- package/dist/tests/source-providers/filesystem.test.js +82 -0
- package/dist/tests/source-providers/git.test.js +252 -0
- package/dist/tests/source-providers/website.test.js +128 -0
- package/dist/tests/source-qa-fixes.test.js +268 -0
- package/dist/tests/source-registry.test.js +350 -0
- package/dist/tests/source-resolve.test.js +100 -0
- package/dist/tests/source-source.test.js +221 -0
- package/dist/tests/source.test.js +533 -0
- package/dist/tests/tar-utils-scan.test.js +73 -0
- package/dist/tests/toggle-components.test.js +73 -0
- package/dist/tests/usage-telemetry.test.js +265 -0
- package/dist/tests/utility-scoring.test.js +558 -0
- package/dist/tests/vault-load-error.test.js +78 -0
- package/dist/tests/vault-qa-fixes.test.js +194 -0
- package/dist/tests/vault.test.js +429 -0
- package/dist/tests/vector-search.test.js +608 -0
- package/dist/tests/walker.test.js +252 -0
- package/dist/tests/wave2-cluster-bc.test.js +228 -0
- package/dist/tests/wave2-cluster-d.test.js +180 -0
- package/dist/tests/wave2-cluster-e.test.js +179 -0
- package/dist/tests/wiki-qa-fixes.test.js +270 -0
- package/dist/tests/wiki.test.js +529 -0
- package/dist/tests/workflow-cli.test.js +271 -0
- package/dist/tests/workflow-markdown.test.js +171 -0
- package/dist/tests/workflow-path-escape.test.js +132 -0
- package/dist/tests/workflow-qa-fixes.test.js +377 -0
- package/dist/tests/workflows/indexer-rejection.test.js +213 -0
- package/docs/README.md +8 -0
- package/docs/migration/release-notes/0.7.0.md +244 -0
- package/package.json +2 -2
- package/dist/core/warn.js +0 -27
- package/dist/output/shapes.js +0 -212
- /package/dist/{commands → src/commands}/completions.js +0 -0
- /package/dist/{commands → src/commands}/curate.js +0 -0
- /package/dist/{commands → src/commands}/info.js +0 -0
- /package/dist/{commands → src/commands}/init.js +0 -0
- /package/dist/{commands → src/commands}/install-audit.js +0 -0
- /package/dist/{commands → src/commands}/migration-help.js +0 -0
- /package/dist/{commands → src/commands}/source-add.js +0 -0
- /package/dist/{commands → src/commands}/source-clone.js +0 -0
- /package/dist/{commands → src/commands}/source-manage.js +0 -0
- /package/dist/{commands → src/commands}/vault.js +0 -0
- /package/dist/{core → src/core}/asset-registry.js +0 -0
- /package/dist/{core → src/core}/frontmatter.js +0 -0
- /package/dist/{core → src/core}/markdown.js +0 -0
- /package/dist/{core → src/core}/paths.js +0 -0
- /package/dist/{indexer → src/indexer}/manifest.js +0 -0
- /package/dist/{indexer → src/indexer}/matchers.js +0 -0
- /package/dist/{indexer → src/indexer}/search-fields.js +0 -0
- /package/dist/{indexer → src/indexer}/search-source.js +0 -0
- /package/dist/{indexer → src/indexer}/semantic-status.js +0 -0
- /package/dist/{indexer → src/indexer}/usage-events.js +0 -0
- /package/dist/{indexer → src/indexer}/walker.js +0 -0
- /package/dist/{integrations → src/integrations}/github.js +0 -0
- /package/dist/{llm → src/llm}/embedder.js +0 -0
- /package/dist/{llm → src/llm}/embedders/cache.js +0 -0
- /package/dist/{llm → src/llm}/embedders/local.js +0 -0
- /package/dist/{llm → src/llm}/embedders/remote.js +0 -0
- /package/dist/{llm → src/llm}/embedders/types.js +0 -0
- /package/dist/{llm → src/llm}/metadata-enhance.js +0 -0
- /package/dist/{output → src/output}/cli-hints.js +0 -0
- /package/dist/{output → src/output}/context.js +0 -0
- /package/dist/{registry → src/registry}/create-provider-registry.js +0 -0
- /package/dist/{registry → src/registry}/origin-resolve.js +0 -0
- /package/dist/{registry → src/registry}/providers/index.js +0 -0
- /package/dist/{registry → src/registry}/providers/skills-sh.js +0 -0
- /package/dist/{registry → src/registry}/providers/types.js +0 -0
- /package/dist/{registry → src/registry}/types.js +0 -0
- /package/dist/{setup → src/setup}/detect.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-install.js +0 -0
- /package/dist/{setup → src/setup}/ripgrep-resolve.js +0 -0
- /package/dist/{setup → src/setup}/steps.js +0 -0
- /package/dist/{sources → src/sources}/include.js +0 -0
- /package/dist/{sources → src/sources}/provider-factory.js +0 -0
- /package/dist/{sources → src/sources}/provider.js +0 -0
- /package/dist/{sources → src/sources}/providers/filesystem.js +0 -0
- /package/dist/{sources → src/sources}/providers/index.js +0 -0
- /package/dist/{sources → src/sources}/providers/install-types.js +0 -0
- /package/dist/{sources → src/sources}/providers/npm.js +0 -0
- /package/dist/{sources → src/sources}/providers/provider-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/sync-from-ref.js +0 -0
- /package/dist/{sources → src/sources}/providers/tar-utils.js +0 -0
- /package/dist/{sources → src/sources}/providers/website.js +0 -0
- /package/dist/{sources → src/sources}/resolve.js +0 -0
- /package/dist/{sources → src/sources}/types.js +0 -0
- /package/dist/{templates → src/templates}/wiki-templates.js +0 -0
- /package/dist/{version.js → src/version.js} +0 -0
- /package/dist/{wiki → src/wiki}/wiki.js +0 -0
- /package/dist/{workflows → src/workflows}/authoring.js +0 -0
- /package/dist/{workflows → src/workflows}/cli.js +0 -0
- /package/dist/{workflows → src/workflows}/db.js +0 -0
- /package/dist/{workflows → src/workflows}/document-cache.js +0 -0
- /package/dist/{workflows → src/workflows}/parser.js +0 -0
- /package/dist/{workflows → src/workflows}/renderer.js +0 -0
- /package/dist/{workflows → src/workflows}/runs.js +0 -0
- /package/dist/{workflows → src/workflows}/schema.js +0 -0
- /package/dist/{workflows → src/workflows}/validator.js +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fixture-stash loader for tests/*.test.ts and bench tasks.
|
|
3
|
+
*
|
|
4
|
+
* Each fixture lives at `tests/fixtures/stashes/<name>/` with a `MANIFEST.json`
|
|
5
|
+
* and the standard akm stash layout. `loadFixtureStash(name)` copies the
|
|
6
|
+
* fixture into a fresh tmp dir, sets `AKM_STASH_DIR`, runs `akm index`, and
|
|
7
|
+
* returns the materialised path plus a cleanup function.
|
|
8
|
+
*
|
|
9
|
+
* See docs/technical/benchmark.md §5.5 for the contract.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
const FIXTURES_ROOT = __dirname;
|
|
16
|
+
const REPO_ROOT = path.resolve(FIXTURES_ROOT, "..", "..", "..");
|
|
17
|
+
const CLI_ENTRY = path.join(REPO_ROOT, "src", "cli.ts");
|
|
18
|
+
/**
|
|
19
|
+
* List the fixture names available under `tests/fixtures/stashes/`.
|
|
20
|
+
*
|
|
21
|
+
* A directory is considered a fixture iff it contains a `MANIFEST.json`.
|
|
22
|
+
* Returned names are sorted alphabetically.
|
|
23
|
+
*/
|
|
24
|
+
export function listFixtures() {
|
|
25
|
+
const entries = fs.readdirSync(FIXTURES_ROOT, { withFileTypes: true });
|
|
26
|
+
const names = [];
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (!entry.isDirectory())
|
|
29
|
+
continue;
|
|
30
|
+
const manifest = path.join(FIXTURES_ROOT, entry.name, "MANIFEST.json");
|
|
31
|
+
if (fs.existsSync(manifest))
|
|
32
|
+
names.push(entry.name);
|
|
33
|
+
}
|
|
34
|
+
names.sort();
|
|
35
|
+
return names;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Synchronous, deterministic SHA-256 hex of every file under the named
|
|
39
|
+
* fixture. Hash input is `<relative-path>\0<file-bytes>\0` for each file in
|
|
40
|
+
* sorted-relative-path order. Used by `bench compare` to refuse cross-fixture
|
|
41
|
+
* diffs.
|
|
42
|
+
*
|
|
43
|
+
* Also exported as `computeFixtureContentHash` for callers that prefer the
|
|
44
|
+
* `compute*` naming convention used by sibling helpers in `tests/bench/`
|
|
45
|
+
* (see `computeTaskCorpusHash` in corpus.ts). The two names point at the
|
|
46
|
+
* SAME implementation — there is exactly one fixture-content hash function
|
|
47
|
+
* in this codebase, and `LoadedFixtureStash.contentHash` reuses it.
|
|
48
|
+
*/
|
|
49
|
+
export function fixtureContentHash(name) {
|
|
50
|
+
const root = fixtureSourceDir(name);
|
|
51
|
+
const files = collectFilesSorted(root);
|
|
52
|
+
const hash = createHash("sha256");
|
|
53
|
+
for (const rel of files) {
|
|
54
|
+
hash.update(rel);
|
|
55
|
+
hash.update("\0");
|
|
56
|
+
hash.update(fs.readFileSync(path.join(root, rel)));
|
|
57
|
+
hash.update("\0");
|
|
58
|
+
}
|
|
59
|
+
return hash.digest("hex");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Alias for `fixtureContentHash` matching the `compute*` naming used by
|
|
63
|
+
* sibling helpers in `tests/bench/`. Reuses the SAME implementation —
|
|
64
|
+
* defining a separate hash function for the same content would risk drift
|
|
65
|
+
* between the report-stamping path and `LoadedFixtureStash.contentHash`.
|
|
66
|
+
*/
|
|
67
|
+
export const computeFixtureContentHash = fixtureContentHash;
|
|
68
|
+
/**
|
|
69
|
+
* Copy the named fixture into a fresh tmp dir, set `AKM_STASH_DIR`, and run
|
|
70
|
+
* `akm index` against it. Returns the tmp path plus a cleanup function that
|
|
71
|
+
* restores the prior env value and recursively removes the tmp dir.
|
|
72
|
+
*
|
|
73
|
+
* Pass `{ skipIndex: true }` if the caller will build its own index and the
|
|
74
|
+
* helper's `akm index` spawn would be wasted work.
|
|
75
|
+
*/
|
|
76
|
+
export function loadFixtureStash(name, options = {}) {
|
|
77
|
+
const sourceDir = fixtureSourceDir(name);
|
|
78
|
+
const contentHash = fixtureContentHash(name);
|
|
79
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), `akm-fixture-${name}-`));
|
|
80
|
+
const stashDir = path.join(tmpRoot, "stash");
|
|
81
|
+
const cacheHome = path.join(tmpRoot, "cache");
|
|
82
|
+
const configHome = path.join(tmpRoot, "config");
|
|
83
|
+
copyDirRecursive(sourceDir, stashDir);
|
|
84
|
+
fs.mkdirSync(cacheHome, { recursive: true });
|
|
85
|
+
fs.mkdirSync(configHome, { recursive: true });
|
|
86
|
+
const priorAkmStashDir = process.env.AKM_STASH_DIR;
|
|
87
|
+
process.env.AKM_STASH_DIR = stashDir;
|
|
88
|
+
if (!options.skipIndex) {
|
|
89
|
+
// Use isolated XDG dirs for the index invocation so the helper never
|
|
90
|
+
// touches the operator's real ~/.cache/akm or pulls in their configured
|
|
91
|
+
// registries / sources. The shipped fixture is the only thing indexed.
|
|
92
|
+
const result = Bun.spawnSync({
|
|
93
|
+
cmd: ["bun", "run", CLI_ENTRY, "index"],
|
|
94
|
+
cwd: stashDir,
|
|
95
|
+
env: {
|
|
96
|
+
...process.env,
|
|
97
|
+
AKM_STASH_DIR: stashDir,
|
|
98
|
+
XDG_CACHE_HOME: cacheHome,
|
|
99
|
+
XDG_CONFIG_HOME: configHome,
|
|
100
|
+
},
|
|
101
|
+
stdout: "pipe",
|
|
102
|
+
stderr: "pipe",
|
|
103
|
+
});
|
|
104
|
+
if (result.exitCode !== 0) {
|
|
105
|
+
// Restore env and clean up before throwing so the caller is not left
|
|
106
|
+
// with a leaked tmp dir or mutated process state.
|
|
107
|
+
if (priorAkmStashDir === undefined)
|
|
108
|
+
delete process.env.AKM_STASH_DIR;
|
|
109
|
+
else
|
|
110
|
+
process.env.AKM_STASH_DIR = priorAkmStashDir;
|
|
111
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
112
|
+
const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "";
|
|
113
|
+
throw new Error(`akm index failed for fixture "${name}" (exit ${result.exitCode}): ${stderr}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const cleanup = () => {
|
|
117
|
+
if (priorAkmStashDir === undefined)
|
|
118
|
+
delete process.env.AKM_STASH_DIR;
|
|
119
|
+
else
|
|
120
|
+
process.env.AKM_STASH_DIR = priorAkmStashDir;
|
|
121
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
122
|
+
};
|
|
123
|
+
return { stashDir, cleanup, contentHash };
|
|
124
|
+
}
|
|
125
|
+
// ── Internals ───────────────────────────────────────────────────────────────
|
|
126
|
+
function fixtureSourceDir(name) {
|
|
127
|
+
if (!isSafeName(name)) {
|
|
128
|
+
throw new Error(`invalid fixture name: ${JSON.stringify(name)}`);
|
|
129
|
+
}
|
|
130
|
+
const dir = path.join(FIXTURES_ROOT, name);
|
|
131
|
+
if (!fs.existsSync(path.join(dir, "MANIFEST.json"))) {
|
|
132
|
+
throw new Error(`fixture not found: ${name} (expected ${dir}/MANIFEST.json)`);
|
|
133
|
+
}
|
|
134
|
+
return dir;
|
|
135
|
+
}
|
|
136
|
+
function isSafeName(name) {
|
|
137
|
+
return /^[a-zA-Z0-9._-]+$/.test(name);
|
|
138
|
+
}
|
|
139
|
+
function collectFilesSorted(root) {
|
|
140
|
+
const out = [];
|
|
141
|
+
const walk = (dir) => {
|
|
142
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const abs = path.join(dir, entry.name);
|
|
145
|
+
if (entry.isDirectory())
|
|
146
|
+
walk(abs);
|
|
147
|
+
else if (entry.isFile())
|
|
148
|
+
out.push(path.relative(root, abs));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
walk(root);
|
|
152
|
+
out.sort();
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function copyDirRecursive(src, dest) {
|
|
156
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
157
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const s = path.join(src, entry.name);
|
|
160
|
+
const d = path.join(dest, entry.name);
|
|
161
|
+
if (entry.isDirectory())
|
|
162
|
+
copyDirRecursive(s, d);
|
|
163
|
+
else if (entry.isFile())
|
|
164
|
+
fs.copyFileSync(s, d);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke tests for the shared fixture-stash loader.
|
|
3
|
+
*
|
|
4
|
+
* Validates that loadFixtureStash, fixtureContentHash, and listFixtures
|
|
5
|
+
* behave as advertised in docs/technical/benchmark.md §5.5.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { computeFixtureContentHash, fixtureContentHash, listFixtures, loadFixtureStash } from "./load";
|
|
11
|
+
describe("loadFixtureStash", () => {
|
|
12
|
+
test("materialises the minimal fixture and cleanup removes it", () => {
|
|
13
|
+
const priorAkmStashDir = process.env.AKM_STASH_DIR;
|
|
14
|
+
const sentinel = "/tmp/some-prior-value";
|
|
15
|
+
process.env.AKM_STASH_DIR = sentinel;
|
|
16
|
+
const { stashDir, cleanup, contentHash } = loadFixtureStash("minimal");
|
|
17
|
+
try {
|
|
18
|
+
expect(fs.existsSync(stashDir)).toBe(true);
|
|
19
|
+
expect(fs.statSync(stashDir).isDirectory()).toBe(true);
|
|
20
|
+
// All five core asset directories from the minimal fixture.
|
|
21
|
+
for (const sub of ["skills", "commands", "agents", "knowledge", "scripts"]) {
|
|
22
|
+
expect(fs.existsSync(path.join(stashDir, sub))).toBe(true);
|
|
23
|
+
}
|
|
24
|
+
// Content hash is non-empty hex.
|
|
25
|
+
expect(contentHash).toMatch(/^[0-9a-f]{64}$/);
|
|
26
|
+
// The helper set AKM_STASH_DIR to the materialised path.
|
|
27
|
+
expect(process.env.AKM_STASH_DIR).toBe(stashDir);
|
|
28
|
+
// Default behaviour runs `akm index`, which writes the SQLite DB into
|
|
29
|
+
// the helper's isolated XDG_CACHE_HOME (sibling of stashDir).
|
|
30
|
+
const tmpRoot = path.dirname(stashDir);
|
|
31
|
+
const dbPath = path.join(tmpRoot, "cache", "akm", "index.db");
|
|
32
|
+
expect(fs.existsSync(dbPath)).toBe(true);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
cleanup();
|
|
36
|
+
}
|
|
37
|
+
// After cleanup, the tmp tree is gone and AKM_STASH_DIR is restored.
|
|
38
|
+
expect(fs.existsSync(stashDir)).toBe(false);
|
|
39
|
+
expect(process.env.AKM_STASH_DIR).toBe(sentinel);
|
|
40
|
+
// Restore the test's own prior value rather than the synthetic sentinel.
|
|
41
|
+
if (priorAkmStashDir === undefined)
|
|
42
|
+
delete process.env.AKM_STASH_DIR;
|
|
43
|
+
else
|
|
44
|
+
process.env.AKM_STASH_DIR = priorAkmStashDir;
|
|
45
|
+
});
|
|
46
|
+
test("with { skipIndex: true } does not invoke akm index", () => {
|
|
47
|
+
const priorAkmStashDir = process.env.AKM_STASH_DIR;
|
|
48
|
+
const { stashDir, cleanup } = loadFixtureStash("minimal", { skipIndex: true });
|
|
49
|
+
try {
|
|
50
|
+
// The fixture is still materialised and AKM_STASH_DIR is still set.
|
|
51
|
+
expect(fs.existsSync(stashDir)).toBe(true);
|
|
52
|
+
expect(process.env.AKM_STASH_DIR).toBe(stashDir);
|
|
53
|
+
// But the index DB the helper would otherwise have created in the
|
|
54
|
+
// isolated XDG_CACHE_HOME is absent — proving no `akm index` ran.
|
|
55
|
+
const tmpRoot = path.dirname(stashDir);
|
|
56
|
+
const dbPath = path.join(tmpRoot, "cache", "akm", "index.db");
|
|
57
|
+
expect(fs.existsSync(dbPath)).toBe(false);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
cleanup();
|
|
61
|
+
}
|
|
62
|
+
if (priorAkmStashDir === undefined)
|
|
63
|
+
delete process.env.AKM_STASH_DIR;
|
|
64
|
+
else
|
|
65
|
+
process.env.AKM_STASH_DIR = priorAkmStashDir;
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe("fixtureContentHash", () => {
|
|
69
|
+
test("is deterministic for the same fixture", () => {
|
|
70
|
+
const a = fixtureContentHash("minimal");
|
|
71
|
+
const b = fixtureContentHash("minimal");
|
|
72
|
+
expect(a).toBe(b);
|
|
73
|
+
expect(a).toMatch(/^[0-9a-f]{64}$/);
|
|
74
|
+
});
|
|
75
|
+
test("computeFixtureContentHash is the same implementation (#250)", () => {
|
|
76
|
+
// Critical addendum: there must be exactly one fixture-content hash
|
|
77
|
+
// function. Two diverging hash implementations for the same content
|
|
78
|
+
// would be a bug.
|
|
79
|
+
expect(computeFixtureContentHash).toBe(fixtureContentHash);
|
|
80
|
+
expect(computeFixtureContentHash("minimal")).toBe(fixtureContentHash("minimal"));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe("listFixtures", () => {
|
|
84
|
+
test("returns all six shipped fixtures, sorted", () => {
|
|
85
|
+
const names = listFixtures();
|
|
86
|
+
expect(names).toEqual(["az-cli", "docker-homelab", "minimal", "multi-domain", "noisy", "ranking-baseline"]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Search memories stored in mem0 for relevant context.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} query - Search query to find relevant memories
|
|
7
|
+
* @param {number} limit - Maximum number of results to return
|
|
8
|
+
*/
|
|
9
|
+
const query = process.argv[2] ?? "";
|
|
10
|
+
const limit = parseInt(process.argv[3] ?? "10", 10);
|
|
11
|
+
console.log(`Searching mem0 for: ${query} (limit: ${limit})`);
|
|
12
|
+
// mem0 search implementation would go here
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { parseFrontmatter, parseFrontmatterBlock, parseYamlScalar, toStringOrUndefined } from "../src/core/frontmatter";
|
|
3
|
+
// ── parseFrontmatter ────────────────────────────────────────────────────────
|
|
4
|
+
describe("parseFrontmatter", () => {
|
|
5
|
+
test("parses basic frontmatter with key-value pairs", () => {
|
|
6
|
+
const raw = "---\ntitle: Hello\ndescription: A test\n---\nBody content\n";
|
|
7
|
+
const result = parseFrontmatter(raw);
|
|
8
|
+
expect(result.data.title).toBe("Hello");
|
|
9
|
+
expect(result.data.description).toBe("A test");
|
|
10
|
+
expect(result.content).toBe("Body content\n");
|
|
11
|
+
expect(result.frontmatter).not.toBeNull();
|
|
12
|
+
});
|
|
13
|
+
test("returns empty data and full content when no frontmatter", () => {
|
|
14
|
+
const raw = "Just some text\nNo frontmatter here\n";
|
|
15
|
+
const result = parseFrontmatter(raw);
|
|
16
|
+
expect(result.data).toEqual({});
|
|
17
|
+
expect(result.content).toBe(raw);
|
|
18
|
+
expect(result.frontmatter).toBeNull();
|
|
19
|
+
expect(result.bodyStartLine).toBe(1);
|
|
20
|
+
});
|
|
21
|
+
test("parses boolean values", () => {
|
|
22
|
+
const raw = "---\nenabled: true\ndisabled: false\n---\n";
|
|
23
|
+
const result = parseFrontmatter(raw);
|
|
24
|
+
expect(result.data.enabled).toBe(true);
|
|
25
|
+
expect(result.data.disabled).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
test("parses numeric values", () => {
|
|
28
|
+
const raw = "---\ncount: 42\npi: 3.14\n---\n";
|
|
29
|
+
const result = parseFrontmatter(raw);
|
|
30
|
+
expect(result.data.count).toBe(42);
|
|
31
|
+
expect(result.data.pi).toBe(3.14);
|
|
32
|
+
});
|
|
33
|
+
test("parses quoted string values", () => {
|
|
34
|
+
const raw = "---\ntitle: \"Hello World\"\nsingle: 'test'\n---\n";
|
|
35
|
+
const result = parseFrontmatter(raw);
|
|
36
|
+
expect(result.data.title).toBe("Hello World");
|
|
37
|
+
expect(result.data.single).toBe("test");
|
|
38
|
+
});
|
|
39
|
+
test("parses nested key-value pairs", () => {
|
|
40
|
+
const raw = "---\npolicy:\n allow: Read,Glob\n deny: Write\n---\nBody\n";
|
|
41
|
+
const result = parseFrontmatter(raw);
|
|
42
|
+
expect(result.data.policy).toEqual({ allow: "Read,Glob", deny: "Write" });
|
|
43
|
+
});
|
|
44
|
+
test("handles empty frontmatter block", () => {
|
|
45
|
+
const raw = "---\n\n---\nBody\n";
|
|
46
|
+
const result = parseFrontmatter(raw);
|
|
47
|
+
expect(result.data).toEqual({});
|
|
48
|
+
expect(result.content).toBe("Body\n");
|
|
49
|
+
});
|
|
50
|
+
test("handles keys with hyphens", () => {
|
|
51
|
+
const raw = "---\nmodel-hint: gpt-4\ntool-policy: allow\n---\n";
|
|
52
|
+
const result = parseFrontmatter(raw);
|
|
53
|
+
expect(result.data["model-hint"]).toBe("gpt-4");
|
|
54
|
+
expect(result.data["tool-policy"]).toBe("allow");
|
|
55
|
+
});
|
|
56
|
+
test("handles empty value (starts nested object)", () => {
|
|
57
|
+
const raw = "---\noptions:\n verbose: true\n---\n";
|
|
58
|
+
const result = parseFrontmatter(raw);
|
|
59
|
+
expect(result.data.options).toEqual({ verbose: true });
|
|
60
|
+
});
|
|
61
|
+
test("bodyStartLine is correct", () => {
|
|
62
|
+
const raw = "---\ntitle: X\ndesc: Y\n---\nBody\n";
|
|
63
|
+
const result = parseFrontmatter(raw);
|
|
64
|
+
expect(result.bodyStartLine).toBe(5);
|
|
65
|
+
});
|
|
66
|
+
test("handles CRLF line endings", () => {
|
|
67
|
+
const raw = "---\r\ntitle: Test\r\n---\r\nBody\r\n";
|
|
68
|
+
const result = parseFrontmatter(raw);
|
|
69
|
+
expect(result.data.title).toBe("Test");
|
|
70
|
+
expect(result.content).toContain("Body");
|
|
71
|
+
});
|
|
72
|
+
// ── List / array support ───────────────────────────────────────────────────
|
|
73
|
+
test("parses flow array (inline style)", () => {
|
|
74
|
+
const raw = "---\ntags: [ops, networking, deploy]\n---\nBody\n";
|
|
75
|
+
const result = parseFrontmatter(raw);
|
|
76
|
+
expect(result.data.tags).toEqual(["ops", "networking", "deploy"]);
|
|
77
|
+
});
|
|
78
|
+
test("parses block-sequence (- item style)", () => {
|
|
79
|
+
const raw = "---\ntags:\n- ops\n- networking\n- deploy\n---\nBody\n";
|
|
80
|
+
const result = parseFrontmatter(raw);
|
|
81
|
+
expect(result.data.tags).toEqual(["ops", "networking", "deploy"]);
|
|
82
|
+
});
|
|
83
|
+
test("parses block-sequence with 2-space indent", () => {
|
|
84
|
+
const raw = "---\ntags:\n - ops\n - networking\n---\nBody\n";
|
|
85
|
+
const result = parseFrontmatter(raw);
|
|
86
|
+
expect(result.data.tags).toEqual(["ops", "networking"]);
|
|
87
|
+
});
|
|
88
|
+
test("parses block-sequence with scalar values (bool, number)", () => {
|
|
89
|
+
const raw = "---\nvalues:\n- true\n- 42\n- hello\n---\n";
|
|
90
|
+
const result = parseFrontmatter(raw);
|
|
91
|
+
expect(result.data.values).toEqual([true, 42, "hello"]);
|
|
92
|
+
});
|
|
93
|
+
test("parses empty flow array", () => {
|
|
94
|
+
const raw = "---\ntags: []\n---\n";
|
|
95
|
+
const result = parseFrontmatter(raw);
|
|
96
|
+
expect(result.data.tags).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
test("block sequence followed by another top-level key", () => {
|
|
99
|
+
const raw = "---\ntags:\n- ops\n- networking\ndescription: A test\n---\nBody\n";
|
|
100
|
+
const result = parseFrontmatter(raw);
|
|
101
|
+
expect(result.data.tags).toEqual(["ops", "networking"]);
|
|
102
|
+
expect(result.data.description).toBe("A test");
|
|
103
|
+
});
|
|
104
|
+
test("mixed styles: flow array and block sequence in same document", () => {
|
|
105
|
+
const raw = "---\ntags: [ops, networking]\naliases:\n- op\n- net\ndescription: test\n---\n";
|
|
106
|
+
const result = parseFrontmatter(raw);
|
|
107
|
+
expect(result.data.tags).toEqual(["ops", "networking"]);
|
|
108
|
+
expect(result.data.aliases).toEqual(["op", "net"]);
|
|
109
|
+
expect(result.data.description).toBe("test");
|
|
110
|
+
});
|
|
111
|
+
test("empty value with no continuation becomes empty string (backward compat)", () => {
|
|
112
|
+
const raw = "---\ntitle: Hello\nempty:\ndescription: test\n---\n";
|
|
113
|
+
const result = parseFrontmatter(raw);
|
|
114
|
+
expect(result.data.title).toBe("Hello");
|
|
115
|
+
expect(result.data.empty).toBe("");
|
|
116
|
+
expect(result.data.description).toBe("test");
|
|
117
|
+
});
|
|
118
|
+
test("single-item block sequence", () => {
|
|
119
|
+
const raw = "---\ntags:\n- solo\n---\n";
|
|
120
|
+
const result = parseFrontmatter(raw);
|
|
121
|
+
expect(result.data.tags).toEqual(["solo"]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
// ── parseFrontmatterBlock ───────────────────────────────────────────────────
|
|
125
|
+
describe("parseFrontmatterBlock", () => {
|
|
126
|
+
test("returns null for content without frontmatter delimiters", () => {
|
|
127
|
+
expect(parseFrontmatterBlock("No frontmatter")).toBeNull();
|
|
128
|
+
});
|
|
129
|
+
test("returns null for content that doesn't start with ---", () => {
|
|
130
|
+
expect(parseFrontmatterBlock("text\n---\nfoo\n---\n")).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
test("extracts frontmatter and content correctly", () => {
|
|
133
|
+
const result = parseFrontmatterBlock("---\nkey: val\n---\nbody\n");
|
|
134
|
+
expect(result).not.toBeNull();
|
|
135
|
+
expect(result?.frontmatter).toBe("key: val");
|
|
136
|
+
expect(result?.content).toBe("body\n");
|
|
137
|
+
});
|
|
138
|
+
test("handles frontmatter without trailing content", () => {
|
|
139
|
+
const result = parseFrontmatterBlock("---\nkey: val\n---\n");
|
|
140
|
+
expect(result).not.toBeNull();
|
|
141
|
+
expect(result?.frontmatter).toBe("key: val");
|
|
142
|
+
expect(result?.content).toBe("");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
// ── parseYamlScalar ─────────────────────────────────────────────────────────
|
|
146
|
+
describe("parseYamlScalar", () => {
|
|
147
|
+
test("returns empty string for empty input", () => {
|
|
148
|
+
expect(parseYamlScalar("")).toBe("");
|
|
149
|
+
});
|
|
150
|
+
test("returns boolean for true/false", () => {
|
|
151
|
+
expect(parseYamlScalar("true")).toBe(true);
|
|
152
|
+
expect(parseYamlScalar("false")).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
test("returns number for numeric strings", () => {
|
|
155
|
+
expect(parseYamlScalar("42")).toBe(42);
|
|
156
|
+
expect(parseYamlScalar("3.14")).toBe(3.14);
|
|
157
|
+
expect(parseYamlScalar("0")).toBe(0);
|
|
158
|
+
expect(parseYamlScalar("-1")).toBe(-1);
|
|
159
|
+
});
|
|
160
|
+
test("strips quotes from quoted strings", () => {
|
|
161
|
+
expect(parseYamlScalar('"hello"')).toBe("hello");
|
|
162
|
+
expect(parseYamlScalar("'world'")).toBe("world");
|
|
163
|
+
});
|
|
164
|
+
test("returns plain string for unquoted non-boolean non-numeric", () => {
|
|
165
|
+
expect(parseYamlScalar("hello")).toBe("hello");
|
|
166
|
+
expect(parseYamlScalar("some-value")).toBe("some-value");
|
|
167
|
+
});
|
|
168
|
+
test("does not strip mismatched quotes", () => {
|
|
169
|
+
expect(parseYamlScalar("\"hello'")).toBe("\"hello'");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
// ── toStringOrUndefined ─────────────────────────────────────────────────────
|
|
173
|
+
describe("toStringOrUndefined", () => {
|
|
174
|
+
test("returns string for non-empty string", () => {
|
|
175
|
+
expect(toStringOrUndefined("hello")).toBe("hello");
|
|
176
|
+
});
|
|
177
|
+
test("returns undefined for empty string", () => {
|
|
178
|
+
expect(toStringOrUndefined("")).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
test("returns undefined for whitespace-only string", () => {
|
|
181
|
+
expect(toStringOrUndefined(" ")).toBeUndefined();
|
|
182
|
+
});
|
|
183
|
+
test("returns undefined for non-string values", () => {
|
|
184
|
+
expect(toStringOrUndefined(42)).toBeUndefined();
|
|
185
|
+
expect(toStringOrUndefined(null)).toBeUndefined();
|
|
186
|
+
expect(toStringOrUndefined(undefined)).toBeUndefined();
|
|
187
|
+
expect(toStringOrUndefined(true)).toBeUndefined();
|
|
188
|
+
expect(toStringOrUndefined({})).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
});
|