-
Notifications
You must be signed in to change notification settings - Fork 231
Expand file tree
/
Copy pathjustfile
More file actions
836 lines (699 loc) · 39.1 KB
/
Copy pathjustfile
File metadata and controls
836 lines (699 loc) · 39.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
# Basic Memory - Modern Command Runner
PYTEST_FLAGS := env_var_or_default("BASIC_MEMORY_PYTEST_FLAGS", "--import-mode=importlib")
TESTMON_SELECT_FLAGS := env_var_or_default("BASIC_MEMORY_TESTMON_SELECT_FLAGS", "--import-mode=importlib --testmon --testmon-forceselect")
TESTMON_REFRESH_FLAGS := env_var_or_default("BASIC_MEMORY_TESTMON_REFRESH_FLAGS", "--import-mode=importlib --testmon-noselect")
# CI shards the Postgres unit suite across parallel jobs via pytest-split
# (e.g. "--splits 3 --group 2"). Empty locally.
PYTEST_SPLIT_FLAGS := env_var_or_default("BASIC_MEMORY_PYTEST_SPLIT_FLAGS", "")
# Install dependencies
install:
uv sync
@echo ""
@echo "💡 Remember to activate the virtual environment by running: source .venv/bin/activate"
# ==============================================================================
# DATABASE BACKEND TESTING
# ==============================================================================
# Basic Memory supports dual database backends (SQLite and Postgres).
# By default, tests run against SQLite (fast, no dependencies).
# Set BASIC_MEMORY_TEST_POSTGRES=1 to run against Postgres (uses testcontainers).
#
# Quick Start:
# just check # Run static checks only (fix, format, typecheck)
# just fast-check # Fast static check: fix, format, typecheck
# just fast-test # Run pytest-testmon impacted tests
# just test # Run all tests against SQLite and Postgres
# just test-sqlite # Run all tests against SQLite
# just test-postgres # Run all tests against Postgres (testcontainers)
# just test-unit-sqlite # Run unit tests against SQLite
# just test-unit-postgres # Run unit tests against Postgres
# just test-int-sqlite # Run integration tests against SQLite
# just test-int-postgres # Run integration tests against Postgres
#
# CI runs both in parallel for faster feedback.
# ==============================================================================
# Run all tests against SQLite and Postgres
test: test-sqlite test-postgres
# Run all tests against SQLite
test-sqlite: test-unit-sqlite test-int-sqlite
# Run all tests against Postgres (uses testcontainers)
test-postgres: test-unit-postgres test-int-postgres
# Run unit tests against SQLite
test-unit-sqlite:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} tests
# Run unit tests against Postgres
# Exit code 5 (no tests collected) is success: a pytest-split shard can be empty.
test-unit-postgres:
#!/usr/bin/env bash
set -euo pipefail
BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} {{PYTEST_SPLIT_FLAGS}} tests || test $? -eq 5
# Run integration tests against SQLite (excludes semantic tests and on-demand benchmarks —
# use just test-semantic / run benchmark files explicitly)
test-int-sqlite:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m "not semantic and not benchmark" test-int
# Run integration tests against Postgres
# Note: Uses timeout due to FastMCP Client + asyncpg cleanup hang (tests pass, process hangs on exit)
# See: https://github.com/jlowin/fastmcp/issues/1311
test-int-postgres:
#!/usr/bin/env bash
set -euo pipefail
# Use gtimeout (macOS/Homebrew) or timeout (Linux)
TIMEOUT_CMD=$(command -v gtimeout || command -v timeout || echo "")
if [[ -n "$TIMEOUT_CMD" ]]; then
$TIMEOUT_CMD --signal=KILL 600 bash -c 'BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m "not semantic and not benchmark" test-int' || test $? -eq 137
else
echo "⚠️ No timeout command found, running without timeout..."
BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m "not semantic and not benchmark" test-int
fi
# Fast test selection for local iteration; run targeted tests explicitly when possible.
fast-test *args: testmon-seed
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{TESTMON_SELECT_FLAGS}} --testmon-env=local {{args}}
# Run tests impacted by recent changes (requires pytest-testmon).
# Backcompat alias for the fast-test recipe.
testmon *args:
just fast-test {{args}}
# Seed pytest-testmon data into this worktree from the shared Git cache.
testmon-seed:
uv run python scripts/testmon_cache.py seed
# Refresh the shared pytest-testmon cache from a full backend test run.
testmon-refresh:
#!/usr/bin/env bash
set -euo pipefail
BASIC_MEMORY_PYTEST_FLAGS="{{TESTMON_REFRESH_FLAGS}}" just test
uv run python scripts/testmon_cache.py refresh
# Show local and shared pytest-testmon cache locations.
testmon-status:
uv run python scripts/testmon_cache.py status
# Run MCP smoke test (fast end-to-end loop)
test-smoke:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m smoke test-int/mcp/test_smoke_integration.py
# Fast static check: auto-fix lint, format, and typecheck, but do not run tests.
fast-check:
just fix
just format
just typecheck
# Fast local loop with live OpenAI-backed checks disabled.
fast-check-no-openai:
OPENAI_API_KEY= just fast-check
# ==============================================================================
# Runtime / Event Indexing Refactor
# ==============================================================================
# Focused portable storage-event contract tests.
storage-event-contract-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/test_runtime_storage_events.py \
tests/index/test_storage_event_operation_processor.py \
tests/index/test_storage_event_orchestration.py
# Focused provider-neutral project-index orchestration surface tests.
project-index-surface-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_project_index_surface.py
# Focused provider-neutral project-index workflow tests.
project-index-workflow-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/indexing/test_project_index_workflow.py
# Focused provider-neutral project-index coordinator tests.
project-index-runner-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/indexing/test_project_index_runner.py
# Focused provider-neutral change-planning tests.
change-planning-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/indexing/test_change_planning.py
# Focused local project-index adapter tests.
local-project-index-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py
# Focused local project-index scan parity tests.
local-project-index-scan-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_scan_parity.py
# Focused local project-index directory delete parity test.
local-project-index-directory-delete-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_directory_delete_removes_notes_and_repairs_survivors
# Focused local project-index hidden-file parity test.
local-project-index-hidden-file-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_skips_hidden_markdown_files
# Focused local project-index null-checksum repair parity test.
local-project-index-null-checksum-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_repairs_null_checksum_entities
# Focused local project-index file timestamp parity tests.
local-project-index-timestamp-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_uses_file_mtime_for_new_markdown_entities \
tests/index/test_local_project_index.py::test_local_project_index_updates_entity_mtime_on_file_modification
# Focused local project-index regular-file parity tests.
local-project-index-regular-file-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_indexes_regular_files \
tests/index/test_local_project_index.py::test_local_project_index_updates_regular_file_checksum \
tests/index/test_local_project_index.py::test_local_project_index_moves_and_deletes_regular_file_entities \
tests/index/test_local_project_index.py::test_local_project_index_resolves_regular_file_relations
# Focused local project-index markdown move conflict parity test.
local-project-index-markdown-move-conflict-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_moves_markdown_over_deleted_path_with_permalink_repair
# Focused local project-index changed-during-index parity test.
local-project-index-race-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_reads_current_file_when_file_changes_after_observation
# Focused local project-index duplicate permalink parity test.
local-project-index-permalink-conflict-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_resolves_duplicate_permalink_update
# Focused local project-index new duplicate permalink parity test.
local-project-index-new-permalink-conflict-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_resolves_new_duplicate_permalink
# Focused local project-index path-derived permalink conflict parity test.
local-project-index-path-conflict-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_assigns_unique_permalinks_for_path_conflicts
# Focused local project-index frontmatter policy parity tests.
local-project-index-frontmatter-policy-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_does_not_add_frontmatter_when_disabled \
tests/index/test_local_project_index.py::test_local_project_index_indexes_thematic_break_content_without_frontmatter \
tests/index/test_local_project_index.py::test_local_project_index_writes_frontmatter_when_enabled_even_if_permalinks_disabled
# Focused local project-index thematic-break frontmatter parity test.
local-project-index-thematic-break-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_indexes_thematic_break_content_without_frontmatter
# Focused local project-index relation resolution parity test.
local-project-index-relation-parity-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_resolves_order_dependent_relations_after_batches \
tests/index/test_local_project_index.py::test_local_project_index_deduplicates_relations_by_type
# Focused local project-index observation category parity test.
local-project-index-observation-category-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_preserves_loose_observation_categories
# Focused local project-index wikilink stability parity test.
local-project-index-wikilink-stability-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_project_index.py::test_local_project_index_keeps_wikilink_source_stable_when_target_appears
# Focused per-file indexing runner/model tests.
file-index-runner-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/indexing/test_index_file_runner.py \
tests/indexing/test_file_indexer.py \
tests/indexing/test_models.py
# Focused file-batch indexing runner/payload tests.
file-index-batch-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/indexing/test_file_batch_runner.py \
tests/indexing/test_job_payloads.py
# Focused batch-index semantic dependency parity test.
file-index-semantic-dependency-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/indexing/test_batch_indexer.py::test_batch_indexer_keeps_file_indexed_when_semantic_dependencies_are_missing
# Focused startup wiring for local project-index fanout.
local-project-index-startup-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/services/test_initialization.py::test_initialize_file_indexing_uses_project_index_runtime_for_initial_sync_by_default
# Focused CLI project-index surface tests.
project-index-cli-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/cli/test_db_reindex.py \
tests/cli/test_status_wait_timeout.py
# Focused project-wide indexing orchestration surface tests.
project-index-contract-test: project-index-surface-test project-index-workflow-test project-index-runner-test change-planning-test local-project-index-test local-project-index-scan-test local-project-index-markdown-move-conflict-test local-project-index-new-permalink-conflict-test local-project-index-path-conflict-test local-project-index-thematic-break-test local-project-index-observation-category-test local-project-index-wikilink-stability-test local-project-index-startup-test project-index-cli-test
# Focused event-based indexing contract tests for the cloud/core extraction loop.
local-event-index-regular-file-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_watch_regular_file_parity.py
# Focused local event-index relation cleanup parity test.
local-event-index-relation-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_watch_regular_file_parity.py::test_local_event_index_deletes_regular_file_relation_target_and_repairs_search
# Focused local event-index atomic-write parity test.
local-event-index-atomic-write-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_watch_stress_parity.py::test_local_event_index_handles_rapid_atomic_writes_to_same_file
# Focused local filesystem event temp/backup filtering parity test.
filesystem-event-temp-file-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_filesystem_events.py::test_editor_swap_and_backup_changes_are_filtered_before_indexing
# Focused local event-index larger watcher batch parity tests.
local-event-index-stress-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/index/test_local_watch_stress_parity.py
# Focused event-based indexing contract tests for the cloud/core extraction loop.
event-index-contract-test: storage-event-contract-test filesystem-event-temp-file-test local-event-index-atomic-write-test local-event-index-stress-test
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/indexing/test_external_file_delete_runner.py \
tests/index/test_filesystem_events.py \
tests/index/test_inline_storage_event_processor.py \
tests/index/test_local_watch_ignore_parity.py \
tests/index/test_local_watch_regular_file_parity.py \
tests/index/test_local_watch_orchestration.py \
tests/index/test_repository_storage_event_project_resolution.py \
tests/services/test_initialization.py::test_initialize_file_indexing_wires_event_index_runtime_by_default
# Focused parity loop for local project scans and shared storage-event routing.
event-index-parity-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/test_runtime.py::TestRuntimeContracts::test_runtime_storage_event_operation_plans_index_delete_and_skip_work \
tests/test_runtime_observed_index_files.py \
tests/index/test_local_project_index.py \
tests/index/test_filesystem_events.py \
tests/index/test_storage_event_operation_processor.py \
tests/index/test_storage_event_orchestration.py
# Focused indexing contract suite for the cloud/core extraction loop.
index-contract-test: file-index-runner-test file-index-batch-test file-index-semantic-dependency-test project-index-contract-test event-index-contract-test
# Focused core contract suite used by the basic-memory-cloud runtime refactor loop.
runtime-core-pytest *args:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov {{args}}
# Focused PR #1002 Codex feedback regressions.
pr-1002-feedback-test:
BASIC_MEMORY_ENV=test BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED=true LOGFIRE_IGNORE_NO_CONFIG=1 uv run pytest -p pytest_mock -q --no-cov \
tests/runtime/test_deleted_note_response.py \
tests/repository/test_accepted_note_search_repository.py \
tests/indexing/test_project_index_workflow.py \
tests/indexing/test_accepted_note_write_runner.py \
tests/indexing/test_directory_delete_runner.py
runtime-core-fast-check-no-openai:
OPENAI_API_KEY= just fast-check
runtime-refactor-contract-test:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -q --no-cov \
tests/indexing/test_accepted_note_write_runner.py \
tests/indexing/test_accepted_note_enqueue_runner.py \
tests/indexing/test_note_content_read_repair_runner.py \
tests/runtime/test_accepted_note_response_planning.py \
tests/runtime/test_deleted_note_response.py \
tests/runtime/test_pending_note_materialization.py \
tests/runtime/test_note_content_read_planning.py
just index-contract-test
# Reset Postgres test database (drops and recreates schema)
# Useful when Alembic migration state gets out of sync during development
# Uses credentials from docker-compose-postgres.yml
postgres-reset:
docker exec basic-memory-postgres psql -U ${POSTGRES_USER:-basic_memory_user} -d ${POSTGRES_TEST_DB:-basic_memory_test} -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
@echo "✅ Postgres test database reset"
# Run Alembic migrations manually against Postgres test database
# Useful for debugging migration issues
# Uses credentials from docker-compose-postgres.yml (can override with env vars)
postgres-migrate:
@cd src/basic_memory/alembic && \
BASIC_MEMORY_DATABASE_BACKEND=postgres \
BASIC_MEMORY_DATABASE_URL=${POSTGRES_TEST_URL:-postgresql+asyncpg://basic_memory_user:dev_password@localhost:5433/basic_memory_test} \
uv run alembic upgrade head
@echo "✅ Migrations applied to Postgres test database"
# Run Windows-specific tests only (only works on Windows platform)
# These tests verify Windows-specific database optimizations (locking mode, NullPool)
# Will be skipped automatically on non-Windows platforms
test-windows:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m windows tests test-int
# Run benchmark tests only (performance testing)
# These are slow tests that measure sync performance with various file counts
# Excluded from default test runs to keep CI fast
test-benchmark:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m benchmark tests test-int
# Run semantic search quality benchmarks (all combos)
test-semantic:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m semantic test-int/semantic/
# Run semantic benchmarks with JSON artifact output, then show report
test-semantic-report:
BASIC_MEMORY_ENV=test BASIC_MEMORY_BENCHMARK_OUTPUT=.benchmarks/semantic-quality.jsonl uv run pytest -p pytest_mock -v -s --no-cov -m semantic test-int/semantic/
uv run python test-int/semantic/report.py .benchmarks/semantic-quality.jsonl
# Run opt-in live LiteLLM provider checks against configured external APIs
test-litellm-live *args:
BASIC_MEMORY_ENV=test BASIC_MEMORY_RUN_LITELLM_INTEGRATION=1 PYTHONPATH=test-int:src uv run python -m semantic.litellm_live_harness {{args}}
# Run semantic benchmarks (Postgres combos only)
test-semantic-postgres:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} -m semantic -k postgres test-int/semantic/
# View semantic benchmark results (rich formatted table)
# Usage: just semantic-report [--filter-combo sqlite] [--filter-suite paraphrase] [--sort-by avg_latency_ms]
semantic-report *args:
uv run python test-int/semantic/report.py .benchmarks/semantic-quality.jsonl {{args}}
# Compare two search benchmark JSONL outputs
# Usage:
# just benchmark-compare .benchmarks/search-baseline.jsonl .benchmarks/search-candidate.jsonl
# just benchmark-compare .benchmarks/search-baseline.jsonl .benchmarks/search-candidate.jsonl --format markdown --show-missing
benchmark-compare baseline candidate *args:
uv run python test-int/compare_search_benchmarks.py "{{baseline}}" "{{candidate}}" --format table {{args}}
# Run all tests including Windows, Postgres, and Benchmarks (for CI/comprehensive testing)
# Use this before releasing to ensure everything works across all backends and platforms
test-all:
BASIC_MEMORY_ENV=test uv run pytest -p pytest_mock -v --no-cov {{PYTEST_FLAGS}} tests test-int
# Generate HTML coverage report
coverage:
#!/usr/bin/env bash
set -euo pipefail
uv run coverage erase
echo "🔎 Coverage (SQLite)..."
BASIC_MEMORY_ENV=test uv run coverage run --source=basic_memory -m pytest -p pytest_mock -v --no-cov tests test-int
echo "🔎 Coverage (Postgres via testcontainers)..."
# Note: Uses timeout due to FastMCP Client + asyncpg cleanup hang (tests pass, process hangs on exit)
# See: https://github.com/jlowin/fastmcp/issues/1311
TIMEOUT_CMD=$(command -v gtimeout || command -v timeout || echo "")
if [[ -n "$TIMEOUT_CMD" ]]; then
$TIMEOUT_CMD --signal=KILL 600 bash -c 'BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run coverage run --source=basic_memory -m pytest -p pytest_mock -v --no-cov -m postgres tests test-int' || test $? -eq 137
else
echo "⚠️ No timeout command found, running without timeout..."
BASIC_MEMORY_ENV=test BASIC_MEMORY_TEST_POSTGRES=1 uv run coverage run --source=basic_memory -m pytest -p pytest_mock -v --no-cov -m postgres tests test-int
fi
echo "🧩 Combining coverage data..."
uv run coverage combine
uv run coverage report -m
uv run coverage html
echo "Coverage report generated in htmlcov/index.html"
# Lint and fix code (calls fix)
lint: fix
# Lint and fix code
fix:
uv run ruff check --fix --unsafe-fixes src tests test-int
# Type check code (ty)
typecheck:
uv run ty check src tests test-int
# Type check code (pyright)
typecheck-pyright:
uv run pyright
# Type check code (ty)
typecheck-ty:
just typecheck
# Clean build artifacts and cache files
clean:
find . -type f -name '*.pyc' -delete
find . -type d -name '__pycache__' -exec rm -r {} +
rm -rf installer/build/ installer/dist/ dist/
rm -f rw.*.dmg .coverage.*
# Format code with ruff
format:
uv run ruff format .
# Run MCP inspector tool
run-inspector:
npx @modelcontextprotocol/inspector
# Run doctor checks in an isolated temp home/config
doctor:
#!/usr/bin/env bash
set -euo pipefail
TMP_HOME=$(mktemp -d)
TMP_CONFIG=$(mktemp -d)
HOME="$TMP_HOME" \
BASIC_MEMORY_ENV=test \
BASIC_MEMORY_HOME="$TMP_HOME/basic-memory" \
BASIC_MEMORY_CONFIG_DIR="$TMP_CONFIG" \
./.venv/bin/python -m basic_memory.cli.main doctor --local
# Run an isolated Logfire smoke workflow for local trace inspection
telemetry-smoke:
#!/usr/bin/env bash
set -euo pipefail
TMP_HOME=$(mktemp -d)
TMP_CONFIG=$(mktemp -d)
TMP_PROJECT=$(mktemp -d)
export HOME="$TMP_HOME"
export BASIC_MEMORY_ENV="${BASIC_MEMORY_ENV:-dev}"
export BASIC_MEMORY_HOME="$TMP_PROJECT/home-root"
export BASIC_MEMORY_CONFIG_DIR="$TMP_CONFIG"
export BASIC_MEMORY_NO_PROMOS=1
export BASIC_MEMORY_LOG_LEVEL="${BASIC_MEMORY_LOG_LEVEL:-INFO}"
export BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED="${BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED:-false}"
export BASIC_MEMORY_LOGFIRE_ENABLED="${BASIC_MEMORY_LOGFIRE_ENABLED:-true}"
export BASIC_MEMORY_LOGFIRE_ENVIRONMENT="${BASIC_MEMORY_LOGFIRE_ENVIRONMENT:-telemetry-smoke}"
if [[ -z "${BASIC_MEMORY_LOGFIRE_SEND_TO_LOGFIRE:-}" ]]; then
if [[ -n "${LOGFIRE_TOKEN:-}" ]]; then
export BASIC_MEMORY_LOGFIRE_SEND_TO_LOGFIRE=true
else
export BASIC_MEMORY_LOGFIRE_SEND_TO_LOGFIRE=false
fi
fi
mkdir -p "$BASIC_MEMORY_HOME"
echo "Telemetry smoke setup:"
echo " logfire_enabled=$BASIC_MEMORY_LOGFIRE_ENABLED"
echo " send_to_logfire=$BASIC_MEMORY_LOGFIRE_SEND_TO_LOGFIRE"
echo " log_level=$BASIC_MEMORY_LOG_LEVEL"
echo " semantic_search_enabled=$BASIC_MEMORY_SEMANTIC_SEARCH_ENABLED"
echo " logfire_environment=$BASIC_MEMORY_LOGFIRE_ENVIRONMENT"
echo " project_path=$TMP_PROJECT"
./.venv/bin/python -m basic_memory.cli.main project add telemetry-smoke "$TMP_PROJECT" --default --local
./.venv/bin/python -m basic_memory.cli.main tool write-note --title "Telemetry Smoke" --folder notes --content "hello from smoke" --project telemetry-smoke --local
./.venv/bin/python -m basic_memory.cli.main tool read-note notes/telemetry-smoke --project telemetry-smoke --local
./.venv/bin/python -m basic_memory.cli.main tool edit-note notes/telemetry-smoke --operation append --content $'\n\nsmoke edit line' --project telemetry-smoke --local
./.venv/bin/python -m basic_memory.cli.main tool build-context notes/telemetry-smoke --project telemetry-smoke --local --page-size 5 --max-related 5
./.venv/bin/python -m basic_memory.cli.main tool search-notes telemetry --project telemetry-smoke --local
./.venv/bin/python -m basic_memory.cli.main doctor --local
echo ""
echo "Telemetry smoke complete."
echo "Search Logfire for:"
echo " service_name: basic-memory-cli"
echo " environment: $BASIC_MEMORY_LOGFIRE_ENVIRONMENT"
echo " span names: mcp.tool.write_note, mcp.tool.read_note, mcp.tool.edit_note, mcp.tool.build_context, mcp.tool.search_notes, sync.project.run"
# Update all dependencies to latest versions
update-deps:
uv sync --upgrade
# Run static code quality checks. Use `just test` for the actual test suites.
check: lint format typecheck
# Run all code quality checks and all test suites, including semantic benchmarks
check-all: lint format typecheck test test-semantic
# Validate every consolidated agent package (Claude Code, Codex, skills, Hermes, OpenClaw)
package-check: package-check-claude-code package-check-codex package-check-skills package-check-hermes package-check-openclaw
# Alias for plugin/package validation during consolidation work
plugins-check: package-check
# Validate the host-native agent harnesses
agent-harness-check: package-check-claude-code package-check-hermes package-check-openclaw
# Claude Code plugin: manifests, bundled skills, bundled agent, and strict plugin validation
package-check-claude-code:
just --justfile plugins/claude-code/justfile --working-directory plugins/claude-code check
# Codex plugin: manifest, bundled skills, hooks, MCP config, and schemas
package-check-codex:
just --justfile plugins/codex/justfile --working-directory plugins/codex check
# Shared top-level SKILL.md source
package-check-skills:
just --justfile skills/justfile --working-directory skills check
# Hermes plugin: native manifest plus hermetic unit test suite
package-check-hermes:
just --justfile integrations/hermes/justfile --working-directory integrations/hermes check
# OpenClaw plugin: install deps, copy skills, typecheck, lint, build, test, and npm pack dry-run
package-check-openclaw:
just --justfile integrations/openclaw/justfile --working-directory integrations/openclaw install
just --justfile integrations/openclaw/justfile --working-directory integrations/openclaw release-check
# Generate Alembic migration with descriptive message
migration message:
cd src/basic_memory/alembic && alembic revision --autogenerate -m "{{message}}"
# Set the Basic Memory version across release manifests (scope: all | core | packages)
set-version version scope="all":
python3 scripts/update_versions.py "{{version}}" --scope "{{scope}}"
# Preview a version update without writing (scope: all | core | packages)
set-version-dry-run version scope="all":
python3 scripts/update_versions.py "{{version}}" --scope "{{scope}}" --dry-run
# Set the version for just the plugin/agent artifacts (plugins, marketplaces, Hermes, OpenClaw)
set-packages-version version:
just set-version "{{version}}" packages
# Preview a plugin/agent-artifact version update without writing
set-packages-version-dry-run version:
just set-version-dry-run "{{version}}" packages
# Preview the consolidated manifest version update without changing files
release-dry-run version:
just set-version-dry-run "{{version}}"
# Create a stable release (e.g., just release v0.13.2)
release version:
#!/usr/bin/env bash
set -euo pipefail
# Validate version format
if [[ ! "{{version}}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Invalid version format. Use: v0.13.2"
exit 1
fi
# Extract version number without 'v' prefix
VERSION_NUM=$(echo "{{version}}" | sed 's/^v//')
echo "🚀 Creating stable release {{version}}"
# Pre-flight checks
echo "📋 Running pre-flight checks..."
if [[ -n $(git status --porcelain) ]]; then
echo "❌ Uncommitted changes found. Please commit or stash them first."
exit 1
fi
if [[ $(git branch --show-current) != "main" ]]; then
echo "❌ Not on main branch. Switch to main first."
exit 1
fi
# Check if tag already exists
if git tag -l "{{version}}" | grep -q "{{version}}"; then
echo "❌ Tag {{version}} already exists"
exit 1
fi
# Changelog must already be on main (land it via a normal PR first)
if ! grep -q "^## {{version}} " CHANGELOG.md; then
echo "❌ CHANGELOG.md has no entry for {{version}}. Land one via PR first."
exit 1
fi
# Run quality checks
echo "🔍 Running lint checks..."
just lint
just typecheck
# Update all package manifests to the one Basic Memory product version.
echo "📝 Updating consolidated package versions..."
just set-version "{{version}}"
# Trigger: main's ruleset rejects direct pushes ("Changes must be made
# through a pull request").
# Why: the version bump must land on main before the tag is cut, so it
# rides a release PR that is rebase-merged (the repo disallows merge
# commits).
# Outcome: the bump commit gets a new SHA on main; the tag is created on
# that rebased commit, found by its commit subject.
COMMIT_SUBJECT="chore: update version to $VERSION_NUM for {{version}} release"
git checkout -b "release/{{version}}"
git add \
src/basic_memory/__init__.py \
server.json \
.claude-plugin/marketplace.json \
plugins/claude-code/.claude-plugin/plugin.json \
plugins/claude-code/.claude-plugin/marketplace.json \
plugins/codex/.codex-plugin/plugin.json \
integrations/hermes/plugin.yaml \
integrations/hermes/__init__.py \
integrations/openclaw/package.json
git commit -s -m "$COMMIT_SUBJECT"
echo "📤 Opening release PR..."
git push -u origin "release/{{version}}"
gh pr create --title "chore(core): release {{version}}" \
--body "Version bump for {{version}}. See CHANGELOG.md for release notes."
# Trigger: the PR may not be mergeable synchronously (merge gates,
# required checks added later, or GitHub still computing mergeability).
# Why: the tag must point at the bump commit on main, so the recipe
# cannot tag until the merge has actually landed.
# Outcome: try a direct rebase-merge, fall back to queueing auto-merge,
# then poll main for the rebased bump commit before tagging.
if ! gh pr merge "release/{{version}}" --rebase --delete-branch; then
echo "⚠️ Direct merge did not complete (merge gates pending?). Queueing auto-merge..."
gh pr merge "release/{{version}}" --rebase --delete-branch --auto
fi
echo "⏳ Waiting for the bump commit to land on main..."
TAG_COMMIT=""
for _ in $(seq 1 60); do
git fetch origin main --quiet
TAG_COMMIT=$(git log FETCH_HEAD --fixed-strings --grep "$COMMIT_SUBJECT" --format='%H' -1)
[[ -n "$TAG_COMMIT" ]] && break
sleep 5
done
if [[ -z "$TAG_COMMIT" ]]; then
echo "❌ Bump commit not on main after 5 minutes (merge still pending?)."
echo " Once the release PR merges, finish the release manually:"
echo " git fetch origin main"
echo " git tag {{version}} \$(git log FETCH_HEAD --fixed-strings --grep \"$COMMIT_SUBJECT\" --format='%H' -1)"
echo " git push origin {{version}}"
exit 1
fi
git checkout main
git pull --ff-only origin main
git branch -D "release/{{version}}" 2>/dev/null || true
echo "🏷️ Creating tag {{version}} at $TAG_COMMIT..."
git tag "{{version}}" "$TAG_COMMIT"
git push origin "{{version}}"
echo "✅ Release {{version}} created successfully!"
echo "📦 GitHub Actions will build and publish to PyPI"
echo "🔗 Monitor at: https://github.com/basicmachines-co/basic-memory/actions"
echo ""
echo "📝 REMINDER: Post-release tasks:"
echo " 1. docs.basicmemory.com - Add a What's New page under content/2.whats-new/"
echo " and bump the badge in content/index.md (see that repo's CLAUDE.md)"
echo " 2. basicmemory.com - No version number in the site UI; for a significant"
echo " release optionally add a post under src/content/blog/. Skip for patches."
echo " 3. MCP Registry - Run: mcp-publisher publish"
echo " See: .claude/commands/release/release.md for detailed instructions"
# Create a beta release (e.g., just beta v0.13.2b1)
beta version:
#!/usr/bin/env bash
set -euo pipefail
# Validate version format (allow beta/rc suffixes)
if [[ ! "{{version}}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(b[0-9]+|rc[0-9]+)$ ]]; then
echo "❌ Invalid beta version format. Use: v0.13.2b1 or v0.13.2rc1"
exit 1
fi
# Extract version number without 'v' prefix
VERSION_NUM=$(echo "{{version}}" | sed 's/^v//')
echo "🧪 Creating beta release {{version}}"
# Pre-flight checks
echo "📋 Running pre-flight checks..."
if [[ -n $(git status --porcelain) ]]; then
echo "❌ Uncommitted changes found. Please commit or stash them first."
exit 1
fi
if [[ $(git branch --show-current) != "main" ]]; then
echo "❌ Not on main branch. Switch to main first."
exit 1
fi
# Check if tag already exists
if git tag -l "{{version}}" | grep -q "{{version}}"; then
echo "❌ Tag {{version}} already exists"
exit 1
fi
# Run quality checks
echo "🔍 Running lint checks..."
just lint
just typecheck
# Update all package manifests to the one Basic Memory product version.
echo "📝 Updating consolidated package versions..."
just set-version "{{version}}"
# Trigger: main's ruleset rejects direct pushes ("Changes must be made
# through a pull request").
# Why: the version bump must land on main before the tag is cut, so it
# rides a release PR that is rebase-merged (the repo disallows merge
# commits).
# Outcome: the bump commit gets a new SHA on main; the tag is created on
# that rebased commit, found by its commit subject.
COMMIT_SUBJECT="chore: update version to $VERSION_NUM for {{version}} beta release"
git checkout -b "release/{{version}}"
git add \
src/basic_memory/__init__.py \
server.json \
.claude-plugin/marketplace.json \
plugins/claude-code/.claude-plugin/plugin.json \
plugins/claude-code/.claude-plugin/marketplace.json \
plugins/codex/.codex-plugin/plugin.json \
integrations/hermes/plugin.yaml \
integrations/hermes/__init__.py \
integrations/openclaw/package.json
git commit -s -m "$COMMIT_SUBJECT"
echo "📤 Opening release PR..."
git push -u origin "release/{{version}}"
gh pr create --title "chore(core): release {{version}}" \
--body "Version bump for {{version}} beta."
# Trigger: the PR may not be mergeable synchronously (merge gates,
# required checks added later, or GitHub still computing mergeability).
# Why: the tag must point at the bump commit on main, so the recipe
# cannot tag until the merge has actually landed.
# Outcome: try a direct rebase-merge, fall back to queueing auto-merge,
# then poll main for the rebased bump commit before tagging.
if ! gh pr merge "release/{{version}}" --rebase --delete-branch; then
echo "⚠️ Direct merge did not complete (merge gates pending?). Queueing auto-merge..."
gh pr merge "release/{{version}}" --rebase --delete-branch --auto
fi
echo "⏳ Waiting for the bump commit to land on main..."
TAG_COMMIT=""
for _ in $(seq 1 60); do
git fetch origin main --quiet
TAG_COMMIT=$(git log FETCH_HEAD --fixed-strings --grep "$COMMIT_SUBJECT" --format='%H' -1)
[[ -n "$TAG_COMMIT" ]] && break
sleep 5
done
if [[ -z "$TAG_COMMIT" ]]; then
echo "❌ Bump commit not on main after 5 minutes (merge still pending?)."
echo " Once the release PR merges, finish the release manually:"
echo " git fetch origin main"
echo " git tag {{version}} \$(git log FETCH_HEAD --fixed-strings --grep \"$COMMIT_SUBJECT\" --format='%H' -1)"
echo " git push origin {{version}}"
exit 1
fi
git checkout main
git pull --ff-only origin main
git branch -D "release/{{version}}" 2>/dev/null || true
echo "🏷️ Creating tag {{version}} at $TAG_COMMIT..."
git tag "{{version}}" "$TAG_COMMIT"
git push origin "{{version}}"
echo "✅ Beta release {{version}} created successfully!"
echo "📦 GitHub Actions will build and publish to PyPI as pre-release"
echo "🔗 Monitor at: https://github.com/basicmachines-co/basic-memory/actions"
echo "📥 Install with: uv tool install basic-memory --pre"
echo ""
echo "📝 REMINDER: For stable releases, update documentation sites:"
echo " 1. docs.basicmemory.com - Add a What's New page under content/2.whats-new/"
echo " and bump the badge in content/index.md (see that repo's CLAUDE.md)"
echo " 2. basicmemory.com - No version number in the site UI; for a significant"
echo " release optionally add a post under src/content/blog/. Skip for patches."
echo " See: .claude/commands/release/release.md for detailed instructions"
# List all available recipes
default:
@just --list