feat(memory): FPLAN-0271 — relocate config to json-home + unify 9 loaders behind one self-healing config_loader

Move memory.config.json to memory_json/custom_config/ and .plans_processed.json
to memory_json/ root; delete the loose config/ dir. Replace 9 disagreeing
per-loader config readers with one DEFAULT_CONFIG + non-mutating deep-merge +
self-heal (missing file -> write defaults; malformed JSON -> fail loud, never
overwrite). Resolves 8 default divergences incl. the silent enforce-off bug and
rollover 600-vs-500. Static _meta documents consumer files; dead intake removed.
949 tests green, seedgo @memory 100%. Design: DPLAN-0206.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIOSAI
2026-06-13 13:50:42 -07:00
co-authored by Claude Opus 4.8
parent 5336d8f61f
commit a8d05e2602
21 changed files with 1012 additions and 388 deletions
+51 -72
View File
@@ -393,28 +393,27 @@ class TestGetMemoryPython:
class TestProcessPlans:
"""Test process_plans main entry point."""
def _setup_config(self, tmp_path, config_data):
"""Write a memory.config.json and return its path."""
config_dir = tmp_path / "config"
config_dir.mkdir(parents=True, exist_ok=True)
config_path = config_dir / "memory.config.json"
config_path.write_text(json.dumps(config_data), encoding="utf-8")
return config_path
def _mock_config(self, monkeypatch, mod, plans_config):
"""Mock config_loader.section on the plans_processor module."""
mock_cl = MagicMock()
mock_cl.section.return_value = plans_config
monkeypatch.setattr(mod, "config_loader", mock_cl)
def test_process_plans_config_load_fails(self, monkeypatch, tmp_path):
def test_process_plans_defaults_no_plans_dir(self, monkeypatch, tmp_path):
"""With default config (self-healed), plans dir absent → success + 0 files."""
mod = _import_plans_processor(monkeypatch)
# Point _MEMORY_ROOT to tmp_path -- no config file exists
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
self._mock_config(monkeypatch, mod, {"enabled": True, "path": ".backup/processed_plans"})
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
result = mod.process_plans()
assert result["success"] is False
assert "Config load failed" in result["error"]
assert result["success"] is True
assert result["files_processed"] == 0
assert "not found" in result.get("reason", "")
def test_process_plans_disabled(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
self._setup_config(tmp_path, {"plans": {"enabled": False}})
self._mock_config(monkeypatch, mod, {"enabled": False})
result = mod.process_plans()
@@ -424,12 +423,7 @@ class TestProcessPlans:
def test_process_plans_dir_not_found(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": "nonexistent/plans"}},
)
# _find_repo_root will return tmp_path
self._mock_config(monkeypatch, mod, {"enabled": True, "path": "nonexistent/plans"})
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
result = mod.process_plans()
@@ -440,12 +434,12 @@ class TestProcessPlans:
def test_process_plans_no_files(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
self._mock_config(
monkeypatch,
mod,
{"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
@@ -456,18 +450,17 @@ class TestProcessPlans:
def test_process_plans_all_already_processed(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
plan_file = plans_dir / "done.md"
plan_file.write_text("Already processed plan content that is long enough.", encoding="utf-8")
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
self._mock_config(
monkeypatch,
mod,
{"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
# Pre-populate the manifest
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path = tmp_path / ".plans_processed.json"
manifest_path.write_text(json.dumps({"done.md": "2026-01-01T00:00:00"}), encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
@@ -479,9 +472,7 @@ class TestProcessPlans:
def test_process_plans_success(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
# Create plans directory with a file
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
plan_file = plans_dir / "new_plan.md"
@@ -491,31 +482,27 @@ class TestProcessPlans:
encoding="utf-8",
)
self._setup_config(
tmp_path,
self._mock_config(
monkeypatch,
mod,
{
"plans": {
"enabled": True,
"path": str(plans_dir),
"supported_extensions": [".md"],
"collection_name": "test_plans",
}
"enabled": True,
"path": str(plans_dir),
"supported_extensions": [".md"],
"collection_name": "test_plans",
},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
# Empty manifest
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path = tmp_path / ".plans_processed.json"
manifest_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
# Mock _embed_texts to return success
monkeypatch.setattr(
mod,
"_embed_texts",
lambda texts: {"success": True, "embeddings": [[0.1, 0.2]] * len(texts)},
)
# Mock _store_vectors to return success
monkeypatch.setattr(
mod,
"_store_vectors",
@@ -532,13 +519,11 @@ class TestProcessPlans:
assert result["total_chunks"] >= 2
mock_jh.log_operation.assert_called_once()
# Manifest should be updated
updated_manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
assert "new_plan.md" in updated_manifest
def test_process_plans_embed_fails(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
@@ -548,17 +533,17 @@ class TestProcessPlans:
encoding="utf-8",
)
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
self._mock_config(
monkeypatch,
mod,
{"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path = tmp_path / ".plans_processed.json"
manifest_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
# Mock _embed_texts to return failure
monkeypatch.setattr(
mod,
"_embed_texts",
@@ -576,7 +561,6 @@ class TestProcessPlans:
def test_process_plans_no_embeddings(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
@@ -586,17 +570,17 @@ class TestProcessPlans:
encoding="utf-8",
)
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
self._mock_config(
monkeypatch,
mod,
{"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path = tmp_path / ".plans_processed.json"
manifest_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
# Embed succeeds but returns empty embeddings
monkeypatch.setattr(
mod,
"_embed_texts",
@@ -613,7 +597,6 @@ class TestProcessPlans:
def test_process_plans_store_fails(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
@@ -623,23 +606,22 @@ class TestProcessPlans:
encoding="utf-8",
)
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
self._mock_config(
monkeypatch,
mod,
{"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path = tmp_path / ".plans_processed.json"
manifest_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
# Embed succeeds
monkeypatch.setattr(
mod,
"_embed_texts",
lambda texts: {"success": True, "embeddings": [[0.1, 0.2]] * len(texts)},
)
# Store fails
monkeypatch.setattr(
mod,
"_store_vectors",
@@ -657,21 +639,20 @@ class TestProcessPlans:
def test_process_plans_empty_chunks(self, monkeypatch, tmp_path):
mod = _import_plans_processor(monkeypatch)
monkeypatch.setattr(mod, "_MEMORY_ROOT", tmp_path)
plans_dir = tmp_path / "plans"
plans_dir.mkdir()
plan_file = plans_dir / "tiny.md"
# Content that will produce zero chunks (under 30 chars, no headers)
plan_file.write_text("Hi.", encoding="utf-8")
self._setup_config(
tmp_path,
{"plans": {"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]}},
self._mock_config(
monkeypatch,
mod,
{"enabled": True, "path": str(plans_dir), "supported_extensions": [".md"]},
)
monkeypatch.setattr(mod, "_find_repo_root", lambda: tmp_path)
manifest_path = tmp_path / "config" / ".plans_processed.json"
manifest_path = tmp_path / ".plans_processed.json"
manifest_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr(mod, "_PROCESSED_MANIFEST", manifest_path)
@@ -680,10 +661,8 @@ class TestProcessPlans:
result = mod.process_plans()
# No chunks produced, but no errors either -- files_without_chunks path
assert result["success"] is True
assert result["files_processed"] == 0
# File should still be marked in manifest (files_without_chunks)
updated_manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
assert "tiny.md" in updated_manifest