feed-the-machine 1.6.0 → 1.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.
Files changed (269) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +170 -170
  3. package/bin/brain.py +1340 -0
  4. package/bin/convert_claude_skills_to_codex.py +490 -0
  5. package/bin/generate-manifest.mjs +463 -463
  6. package/bin/harden_codex_skills.py +141 -0
  7. package/bin/install.mjs +491 -491
  8. package/bin/migrate-eng-buddy-data.py +875 -0
  9. package/bin/playbook_engine/__init__.py +1 -0
  10. package/bin/playbook_engine/conftest.py +8 -0
  11. package/bin/playbook_engine/extractor.py +33 -0
  12. package/bin/playbook_engine/manager.py +102 -0
  13. package/bin/playbook_engine/models.py +84 -0
  14. package/bin/playbook_engine/registry.py +35 -0
  15. package/bin/playbook_engine/test_extractor.py +72 -0
  16. package/bin/playbook_engine/test_integration.py +129 -0
  17. package/bin/playbook_engine/test_manager.py +85 -0
  18. package/bin/playbook_engine/test_models.py +166 -0
  19. package/bin/playbook_engine/test_registry.py +67 -0
  20. package/bin/playbook_engine/test_tracer.py +86 -0
  21. package/bin/playbook_engine/tracer.py +93 -0
  22. package/bin/tasks_db.py +456 -0
  23. package/docs/HOOKS.md +243 -243
  24. package/docs/INBOX.md +233 -233
  25. package/ftm/SKILL.md +125 -122
  26. package/ftm-audit/SKILL.md +623 -623
  27. package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
  28. package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
  29. package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
  30. package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
  31. package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
  32. package/ftm-audit/scripts/run-knip.sh +23 -23
  33. package/ftm-audit.yml +2 -2
  34. package/ftm-brainstorm/SKILL.md +1003 -498
  35. package/ftm-brainstorm/evals/evals.json +180 -100
  36. package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
  37. package/ftm-brainstorm/references/agent-prompts.md +552 -224
  38. package/ftm-brainstorm/references/plan-template.md +209 -121
  39. package/ftm-brainstorm.yml +2 -2
  40. package/ftm-browse/SKILL.md +454 -454
  41. package/ftm-browse/daemon/browser-manager.ts +206 -206
  42. package/ftm-browse/daemon/bun.lock +30 -30
  43. package/ftm-browse/daemon/cli.ts +347 -347
  44. package/ftm-browse/daemon/commands.ts +410 -410
  45. package/ftm-browse/daemon/main.ts +357 -357
  46. package/ftm-browse/daemon/package.json +17 -17
  47. package/ftm-browse/daemon/server.ts +189 -189
  48. package/ftm-browse/daemon/snapshot.ts +519 -519
  49. package/ftm-browse/daemon/tsconfig.json +22 -22
  50. package/ftm-browse.yml +4 -4
  51. package/ftm-capture/SKILL.md +370 -370
  52. package/ftm-capture.yml +4 -4
  53. package/ftm-codex-gate/SKILL.md +361 -361
  54. package/ftm-codex-gate.yml +2 -2
  55. package/ftm-config/SKILL.md +422 -345
  56. package/ftm-config.default.yml +125 -82
  57. package/ftm-config.yml +44 -2
  58. package/ftm-council/SKILL.md +416 -416
  59. package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
  60. package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
  61. package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
  62. package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
  63. package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
  64. package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
  65. package/ftm-council.yml +2 -2
  66. package/ftm-dashboard/SKILL.md +163 -163
  67. package/ftm-dashboard.yml +4 -4
  68. package/ftm-debug/SKILL.md +1037 -1037
  69. package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
  70. package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
  71. package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
  72. package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
  73. package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
  74. package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
  75. package/ftm-debug.yml +2 -2
  76. package/ftm-diagram/SKILL.md +277 -277
  77. package/ftm-diagram.yml +2 -2
  78. package/ftm-executor/SKILL.md +777 -777
  79. package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
  80. package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
  81. package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
  82. package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
  83. package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
  84. package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
  85. package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
  86. package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
  87. package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
  88. package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
  89. package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
  90. package/ftm-executor/runtime/package.json +8 -8
  91. package/ftm-executor.yml +2 -2
  92. package/ftm-git/SKILL.md +441 -441
  93. package/ftm-git/evals/evals.json +26 -26
  94. package/ftm-git/evals/promptfoo.yaml +75 -75
  95. package/ftm-git/hooks/post-commit-experience.sh +92 -92
  96. package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
  97. package/ftm-git/references/protocols/REMEDIATION.md +139 -139
  98. package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
  99. package/ftm-git.yml +2 -2
  100. package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
  101. package/ftm-inbox/backend/adapters/_retry.py +64 -64
  102. package/ftm-inbox/backend/adapters/base.py +230 -230
  103. package/ftm-inbox/backend/adapters/freshservice.py +104 -104
  104. package/ftm-inbox/backend/adapters/gmail.py +125 -125
  105. package/ftm-inbox/backend/adapters/jira.py +136 -136
  106. package/ftm-inbox/backend/adapters/registry.py +192 -192
  107. package/ftm-inbox/backend/adapters/slack.py +110 -110
  108. package/ftm-inbox/backend/db/connection.py +54 -54
  109. package/ftm-inbox/backend/db/schema.py +78 -78
  110. package/ftm-inbox/backend/executor/__init__.py +7 -7
  111. package/ftm-inbox/backend/executor/engine.py +149 -149
  112. package/ftm-inbox/backend/executor/step_runner.py +98 -98
  113. package/ftm-inbox/backend/main.py +103 -103
  114. package/ftm-inbox/backend/models/__init__.py +1 -1
  115. package/ftm-inbox/backend/models/unified_task.py +36 -36
  116. package/ftm-inbox/backend/planner/__init__.py +6 -6
  117. package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
  118. package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
  119. package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
  120. package/ftm-inbox/backend/planner/generator.py +127 -127
  121. package/ftm-inbox/backend/planner/schema.py +34 -34
  122. package/ftm-inbox/backend/requirements.txt +5 -5
  123. package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
  124. package/ftm-inbox/backend/routes/execute.py +186 -186
  125. package/ftm-inbox/backend/routes/health.py +52 -52
  126. package/ftm-inbox/backend/routes/inbox.py +68 -68
  127. package/ftm-inbox/backend/routes/plan.py +271 -271
  128. package/ftm-inbox/bin/launchagent.mjs +91 -91
  129. package/ftm-inbox/bin/setup.mjs +188 -188
  130. package/ftm-inbox/bin/start.sh +10 -10
  131. package/ftm-inbox/bin/status.sh +17 -17
  132. package/ftm-inbox/bin/stop.sh +8 -8
  133. package/ftm-inbox/config.example.yml +55 -55
  134. package/ftm-inbox/package-lock.json +2898 -2898
  135. package/ftm-inbox/package.json +26 -26
  136. package/ftm-inbox/postcss.config.js +6 -6
  137. package/ftm-inbox/src/app.css +199 -199
  138. package/ftm-inbox/src/app.html +18 -18
  139. package/ftm-inbox/src/lib/api.ts +166 -166
  140. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
  141. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
  142. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
  143. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
  144. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
  145. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
  146. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
  147. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
  148. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
  149. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
  150. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
  151. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
  152. package/ftm-inbox/src/lib/theme.ts +47 -47
  153. package/ftm-inbox/src/routes/+layout.svelte +76 -76
  154. package/ftm-inbox/src/routes/+page.svelte +401 -401
  155. package/ftm-inbox/svelte.config.js +12 -12
  156. package/ftm-inbox/tailwind.config.ts +63 -63
  157. package/ftm-inbox/tsconfig.json +13 -13
  158. package/ftm-inbox/vite.config.ts +6 -6
  159. package/ftm-intent/SKILL.md +241 -241
  160. package/ftm-intent.yml +2 -2
  161. package/ftm-manifest.json +3794 -3794
  162. package/ftm-map/SKILL.md +291 -291
  163. package/ftm-map/scripts/db.py +712 -712
  164. package/ftm-map/scripts/index.py +415 -415
  165. package/ftm-map/scripts/parser.py +224 -224
  166. package/ftm-map/scripts/queries/go-tags.scm +20 -20
  167. package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
  168. package/ftm-map/scripts/queries/python-tags.scm +31 -31
  169. package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
  170. package/ftm-map/scripts/queries/rust-tags.scm +37 -37
  171. package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
  172. package/ftm-map/scripts/query.py +301 -301
  173. package/ftm-map/scripts/ranker.py +377 -377
  174. package/ftm-map/scripts/requirements.txt +5 -5
  175. package/ftm-map/scripts/setup-hooks.sh +27 -27
  176. package/ftm-map/scripts/setup.sh +56 -56
  177. package/ftm-map/scripts/test_db.py +364 -364
  178. package/ftm-map/scripts/test_parser.py +174 -174
  179. package/ftm-map/scripts/test_query.py +183 -183
  180. package/ftm-map/scripts/test_ranker.py +199 -199
  181. package/ftm-map/scripts/views.py +591 -591
  182. package/ftm-map.yml +2 -2
  183. package/ftm-mind/SKILL.md +201 -1943
  184. package/ftm-mind/evals/promptfoo.yaml +142 -142
  185. package/ftm-mind/references/blackboard-protocol.md +110 -0
  186. package/ftm-mind/references/blackboard-schema.md +328 -328
  187. package/ftm-mind/references/complexity-guide.md +110 -110
  188. package/ftm-mind/references/complexity-sizing.md +138 -0
  189. package/ftm-mind/references/decide-act-protocol.md +172 -0
  190. package/ftm-mind/references/direct-execution.md +51 -0
  191. package/ftm-mind/references/environment-discovery.md +77 -0
  192. package/ftm-mind/references/event-registry.md +319 -319
  193. package/ftm-mind/references/mcp-inventory.md +300 -296
  194. package/ftm-mind/references/ops-routing.md +47 -0
  195. package/ftm-mind/references/orient-protocol.md +234 -0
  196. package/ftm-mind/references/personality.md +40 -0
  197. package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
  198. package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
  199. package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
  200. package/ftm-mind/references/reflexion-protocol.md +249 -249
  201. package/ftm-mind/references/routing/SCENARIOS.md +22 -22
  202. package/ftm-mind/references/routing-scenarios.md +35 -35
  203. package/ftm-mind.yml +2 -2
  204. package/ftm-ops.yml +4 -0
  205. package/ftm-pause/SKILL.md +395 -395
  206. package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
  207. package/ftm-pause/references/protocols/VALIDATION.md +80 -80
  208. package/ftm-pause.yml +2 -2
  209. package/ftm-researcher/SKILL.md +275 -275
  210. package/ftm-researcher/evals/agent-diversity.yaml +17 -17
  211. package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
  212. package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
  213. package/ftm-researcher/references/adaptive-search.md +116 -116
  214. package/ftm-researcher/references/agent-prompts.md +193 -193
  215. package/ftm-researcher/references/council-integration.md +193 -193
  216. package/ftm-researcher/references/output-format.md +203 -203
  217. package/ftm-researcher/references/synthesis-pipeline.md +165 -165
  218. package/ftm-researcher/scripts/score_credibility.py +234 -234
  219. package/ftm-researcher/scripts/validate_research.py +92 -92
  220. package/ftm-researcher.yml +2 -2
  221. package/ftm-resume/SKILL.md +518 -518
  222. package/ftm-resume/references/protocols/VALIDATION.md +172 -172
  223. package/ftm-resume.yml +2 -2
  224. package/ftm-retro/SKILL.md +380 -380
  225. package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
  226. package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
  227. package/ftm-retro.yml +2 -2
  228. package/ftm-routine/SKILL.md +170 -170
  229. package/ftm-routine.yml +4 -4
  230. package/ftm-state/blackboard/capabilities.json +5 -5
  231. package/ftm-state/blackboard/capabilities.schema.json +27 -27
  232. package/ftm-state/blackboard/context.json +37 -23
  233. package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
  234. package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
  235. package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
  236. package/ftm-state/blackboard/experiences/index.json +58 -9
  237. package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
  238. package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
  239. package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
  240. package/ftm-state/blackboard/patterns.json +6 -6
  241. package/ftm-state/schemas/context.schema.json +130 -130
  242. package/ftm-state/schemas/experience-index.schema.json +77 -77
  243. package/ftm-state/schemas/experience.schema.json +78 -78
  244. package/ftm-state/schemas/patterns.schema.json +44 -44
  245. package/ftm-upgrade/SKILL.md +194 -194
  246. package/ftm-upgrade/scripts/check-version.sh +76 -76
  247. package/ftm-upgrade/scripts/upgrade.sh +143 -143
  248. package/ftm-upgrade.yml +2 -2
  249. package/ftm-verify.yml +2 -2
  250. package/ftm.yml +2 -2
  251. package/hooks/ftm-auto-log.sh +137 -0
  252. package/hooks/ftm-blackboard-enforcer.sh +93 -93
  253. package/hooks/ftm-discovery-reminder.sh +90 -90
  254. package/hooks/ftm-drafts-gate.sh +61 -61
  255. package/hooks/ftm-event-logger.mjs +107 -107
  256. package/hooks/ftm-install-hooks.sh +240 -0
  257. package/hooks/ftm-learning-capture.sh +117 -0
  258. package/hooks/ftm-map-autodetect.sh +79 -79
  259. package/hooks/ftm-pending-sync-check.sh +22 -22
  260. package/hooks/ftm-plan-gate.sh +92 -92
  261. package/hooks/ftm-post-commit-trigger.sh +57 -57
  262. package/hooks/ftm-post-compaction.sh +138 -0
  263. package/hooks/ftm-pre-compaction.sh +147 -0
  264. package/hooks/ftm-session-end.sh +52 -0
  265. package/hooks/ftm-session-snapshot.sh +213 -0
  266. package/hooks/settings-template.json +81 -81
  267. package/install.sh +363 -363
  268. package/package.json +84 -84
  269. package/uninstall.sh +25 -25
@@ -1,364 +1,364 @@
1
- """Tests for db.py -- 5-table schema with FTS5."""
2
- import os
3
- import sys
4
- import tempfile
5
- import pytest
6
-
7
- sys.path.insert(0, os.path.dirname(__file__))
8
- from db import (
9
- get_connection,
10
- add_file,
11
- get_file_by_path,
12
- remove_file,
13
- add_symbol,
14
- get_symbol_by_id,
15
- get_symbol_by_name,
16
- get_symbols_by_file,
17
- add_reference,
18
- get_references_by_file,
19
- rebuild_file_edges,
20
- rebuild_symbol_edges,
21
- add_edge,
22
- get_transitive_deps,
23
- get_reverse_deps,
24
- fts_search,
25
- get_stats,
26
- hash_content,
27
- remove_symbols_by_file,
28
- )
29
-
30
-
31
- @pytest.fixture
32
- def conn():
33
- with tempfile.TemporaryDirectory() as tmp:
34
- c = get_connection(tmp)
35
- yield c
36
- c.close()
37
-
38
-
39
- @pytest.fixture
40
- def populated_conn(conn):
41
- """Conn with 3 files, symbols, and references for graph tests."""
42
- f1 = add_file(conn, "src/auth.py", "python", 1.0, line_count=50)
43
- f2 = add_file(conn, "src/api.py", "python", 1.0, line_count=100)
44
- f3 = add_file(conn, "src/utils.py", "python", 1.0, line_count=30)
45
-
46
- s1 = add_symbol(conn, f1, "authenticate", "function", 1, 20, signature="def authenticate(req)")
47
- s2 = add_symbol(conn, f1, "verify_token", "function", 25, 40)
48
- s3 = add_symbol(conn, f2, "handle_request", "function", 1, 50)
49
- s4 = add_symbol(conn, f3, "format_date", "function", 1, 10)
50
-
51
- # api.py references authenticate (defined in auth.py) and format_date (defined in utils.py)
52
- add_reference(conn, f2, "authenticate", 10)
53
- add_reference(conn, f2, "format_date", 20)
54
- # auth.py references format_date (defined in utils.py)
55
- add_reference(conn, f1, "format_date", 30)
56
-
57
- conn.commit()
58
- return conn, {"f1": f1, "f2": f2, "f3": f3, "s1": s1, "s2": s2, "s3": s3, "s4": s4}
59
-
60
-
61
- class TestFileCRUD:
62
- def test_add_and_get(self, conn):
63
- fid = add_file(conn, "src/main.py", "python", 1.0, hash="abc123", line_count=100)
64
- assert fid > 0
65
- row = get_file_by_path(conn, "src/main.py")
66
- assert row is not None
67
- assert row["lang"] == "python"
68
- assert row["hash"] == "abc123"
69
- assert row["line_count"] == 100
70
-
71
- def test_unique_path(self, conn):
72
- add_file(conn, "src/main.py", "python", 1.0)
73
- with pytest.raises(Exception):
74
- add_file(conn, "src/main.py", "python", 2.0)
75
-
76
- def test_get_nonexistent(self, conn):
77
- assert get_file_by_path(conn, "nonexistent.py") is None
78
-
79
- def test_remove_file(self, conn):
80
- add_file(conn, "src/main.py", "python", 1.0)
81
- conn.commit()
82
- remove_file(conn, "src/main.py")
83
- conn.commit()
84
- assert get_file_by_path(conn, "src/main.py") is None
85
-
86
- def test_remove_nonexistent_is_noop(self, conn):
87
- # Should not raise
88
- remove_file(conn, "nonexistent.py")
89
-
90
- def test_remove_cascades(self, conn):
91
- fid = add_file(conn, "src/main.py", "python", 1.0)
92
- add_symbol(conn, fid, "foo", "function", 1, 10)
93
- add_reference(conn, fid, "bar", 5)
94
- conn.commit()
95
- remove_file(conn, "src/main.py")
96
- conn.commit()
97
- assert get_file_by_path(conn, "src/main.py") is None
98
- assert len(get_symbols_by_file(conn, fid)) == 0
99
- assert len(get_references_by_file(conn, fid)) == 0
100
-
101
-
102
- class TestSymbolCRUD:
103
- def test_add_and_get(self, conn):
104
- fid = add_file(conn, "src/main.py", "python", 1.0)
105
- sid = add_symbol(conn, fid, "my_func", "function", 10, 30, signature="def my_func()")
106
- sym = get_symbol_by_id(conn, sid)
107
- assert sym["name"] == "my_func"
108
- assert sym["kind"] == "function"
109
- assert sym["signature"] == "def my_func()"
110
-
111
- def test_get_by_name(self, conn):
112
- fid = add_file(conn, "src/main.py", "python", 1.0)
113
- add_symbol(conn, fid, "my_func", "function", 10, 30)
114
- results = get_symbol_by_name(conn, "my_func")
115
- assert len(results) == 1
116
- assert results[0]["name"] == "my_func"
117
-
118
- def test_get_by_name_multiple_matches(self, conn):
119
- f1 = add_file(conn, "src/a.py", "python", 1.0)
120
- f2 = add_file(conn, "src/b.py", "python", 1.0)
121
- add_symbol(conn, f1, "init", "function", 1, 10)
122
- add_symbol(conn, f2, "init", "function", 1, 10)
123
- results = get_symbol_by_name(conn, "init")
124
- assert len(results) == 2
125
-
126
- def test_get_by_file(self, conn):
127
- fid = add_file(conn, "src/main.py", "python", 1.0)
128
- add_symbol(conn, fid, "foo", "function", 1, 10)
129
- add_symbol(conn, fid, "bar", "class", 15, 30)
130
- syms = get_symbols_by_file(conn, fid)
131
- assert len(syms) == 2
132
-
133
- def test_get_by_file_ordered(self, conn):
134
- fid = add_file(conn, "src/main.py", "python", 1.0)
135
- add_symbol(conn, fid, "second", "function", 20, 30)
136
- add_symbol(conn, fid, "first", "function", 1, 10)
137
- syms = get_symbols_by_file(conn, fid)
138
- assert syms[0]["name"] == "first"
139
- assert syms[1]["name"] == "second"
140
-
141
- def test_parent_id(self, conn):
142
- fid = add_file(conn, "src/main.py", "python", 1.0)
143
- parent = add_symbol(conn, fid, "MyClass", "class", 1, 50)
144
- child = add_symbol(conn, fid, "my_method", "method", 5, 20, parent_id=parent)
145
- sym = get_symbol_by_id(conn, child)
146
- assert sym["parent_id"] == parent
147
-
148
- def test_qualified_name(self, conn):
149
- fid = add_file(conn, "src/main.py", "python", 1.0)
150
- sid = add_symbol(conn, fid, "func", "function", 1, 10, qualified_name="main.func")
151
- sym = get_symbol_by_id(conn, sid)
152
- assert sym["qualified_name"] == "main.func"
153
-
154
-
155
- class TestReferenceCRUD:
156
- def test_add_and_get(self, conn):
157
- fid = add_file(conn, "src/main.py", "python", 1.0)
158
- add_reference(conn, fid, "some_func", 42, kind="call")
159
- refs = get_references_by_file(conn, fid)
160
- assert len(refs) == 1
161
- assert refs[0]["symbol_name"] == "some_func"
162
- assert refs[0]["line"] == 42
163
- assert refs[0]["kind"] == "call"
164
-
165
- def test_default_kind(self, conn):
166
- fid = add_file(conn, "src/main.py", "python", 1.0)
167
- add_reference(conn, fid, "func", 10)
168
- refs = get_references_by_file(conn, fid)
169
- assert refs[0]["kind"] == "call"
170
-
171
- def test_multiple_refs_ordered(self, conn):
172
- fid = add_file(conn, "src/main.py", "python", 1.0)
173
- add_reference(conn, fid, "b_func", 20)
174
- add_reference(conn, fid, "a_func", 5)
175
- refs = get_references_by_file(conn, fid)
176
- assert refs[0]["line"] == 5
177
- assert refs[1]["line"] == 20
178
-
179
-
180
- class TestEdgeRebuilding:
181
- def test_rebuild_file_edges(self, populated_conn):
182
- conn, ids = populated_conn
183
- rebuild_file_edges(conn)
184
- conn.commit()
185
- edges = conn.execute("SELECT * FROM file_edges").fetchall()
186
- # api.py -> auth.py (authenticate), api.py -> utils.py (format_date),
187
- # auth.py -> utils.py (format_date)
188
- assert len(edges) >= 2
189
-
190
- def test_rebuild_file_edges_no_self_edges(self, populated_conn):
191
- conn, ids = populated_conn
192
- rebuild_file_edges(conn)
193
- conn.commit()
194
- self_edges = conn.execute(
195
- "SELECT * FROM file_edges WHERE source_file_id = target_file_id"
196
- ).fetchall()
197
- assert len(self_edges) == 0
198
-
199
- def test_rebuild_symbol_edges(self, populated_conn):
200
- conn, ids = populated_conn
201
- rebuild_symbol_edges(conn)
202
- conn.commit()
203
- edges = conn.execute("SELECT * FROM symbol_edges").fetchall()
204
- assert len(edges) >= 1
205
-
206
- def test_rebuild_is_idempotent(self, populated_conn):
207
- conn, ids = populated_conn
208
- rebuild_file_edges(conn)
209
- rebuild_symbol_edges(conn)
210
- conn.commit()
211
- count1 = conn.execute("SELECT COUNT(*) FROM file_edges").fetchone()[0]
212
- count2 = conn.execute("SELECT COUNT(*) FROM symbol_edges").fetchone()[0]
213
-
214
- rebuild_file_edges(conn)
215
- rebuild_symbol_edges(conn)
216
- conn.commit()
217
- assert conn.execute("SELECT COUNT(*) FROM file_edges").fetchone()[0] == count1
218
- assert conn.execute("SELECT COUNT(*) FROM symbol_edges").fetchone()[0] == count2
219
-
220
-
221
- class TestGraphTraversal:
222
- def test_transitive_deps(self, populated_conn):
223
- conn, ids = populated_conn
224
- rebuild_symbol_edges(conn)
225
- conn.commit()
226
- deps = get_transitive_deps(conn, ids["s3"]) # handle_request
227
- dep_names = {d["name"] for d in deps}
228
- # handle_request refs authenticate and format_date
229
- assert "authenticate" in dep_names or "format_date" in dep_names
230
-
231
- def test_reverse_deps(self, populated_conn):
232
- conn, ids = populated_conn
233
- rebuild_symbol_edges(conn)
234
- conn.commit()
235
- rdeps = get_reverse_deps(conn, ids["s1"]) # authenticate
236
- rdep_names = {d["name"] for d in rdeps}
237
- assert "handle_request" in rdep_names
238
-
239
- def test_no_deps_for_leaf(self, populated_conn):
240
- conn, ids = populated_conn
241
- rebuild_symbol_edges(conn)
242
- conn.commit()
243
- deps = get_transitive_deps(conn, ids["s4"])
244
- assert isinstance(deps, list)
245
-
246
- def test_max_depth_limits_results(self, populated_conn):
247
- conn, ids = populated_conn
248
- rebuild_symbol_edges(conn)
249
- conn.commit()
250
- deps_deep = get_transitive_deps(conn, ids["s3"], max_depth=10)
251
- deps_shallow = get_transitive_deps(conn, ids["s3"], max_depth=0)
252
- assert len(deps_shallow) <= len(deps_deep)
253
-
254
-
255
- class TestManualEdge:
256
- def test_add_edge(self, populated_conn):
257
- conn, ids = populated_conn
258
- add_edge(conn, ids["s1"], ids["s3"], "test_kind")
259
- conn.commit()
260
- edge = conn.execute(
261
- "SELECT * FROM symbol_edges WHERE source_symbol_id=? AND target_symbol_id=? AND kind=?",
262
- (ids["s1"], ids["s3"], "test_kind"),
263
- ).fetchone()
264
- assert edge is not None
265
-
266
- def test_duplicate_edge_ignored(self, populated_conn):
267
- conn, ids = populated_conn
268
- add_edge(conn, ids["s1"], ids["s3"], "test_kind")
269
- # Should not raise
270
- add_edge(conn, ids["s1"], ids["s3"], "test_kind")
271
- conn.commit()
272
-
273
-
274
- class TestFTS:
275
- def test_search_by_name(self, populated_conn):
276
- conn, ids = populated_conn
277
- results = fts_search(conn, "authenticate")
278
- assert len(results) >= 1
279
- assert any(r["name"] == "authenticate" for r in results)
280
-
281
- def test_search_by_signature(self, populated_conn):
282
- conn, ids = populated_conn
283
- results = fts_search(conn, "req")
284
- assert len(results) >= 1
285
-
286
- def test_search_no_results(self, populated_conn):
287
- conn, ids = populated_conn
288
- results = fts_search(conn, "zzzznonexistent")
289
- assert len(results) == 0
290
-
291
- def test_search_with_limit(self, populated_conn):
292
- conn, ids = populated_conn
293
- results = fts_search(conn, "authenticate", limit=1)
294
- assert len(results) <= 1
295
-
296
- def test_results_have_rank(self, populated_conn):
297
- conn, ids = populated_conn
298
- results = fts_search(conn, "authenticate")
299
- assert len(results) >= 1
300
- assert "rank" in results[0]
301
-
302
-
303
- class TestStats:
304
- def test_returns_all_fields(self, populated_conn):
305
- conn, _ = populated_conn
306
- s = get_stats(conn)
307
- assert "file_count" in s
308
- assert "symbol_count" in s
309
- assert "reference_count" in s
310
- assert "edge_count" in s
311
- assert "file_edge_count" in s
312
-
313
- def test_correct_counts(self, populated_conn):
314
- conn, _ = populated_conn
315
- s = get_stats(conn)
316
- assert s["file_count"] == 3
317
- assert s["symbol_count"] == 4
318
- assert s["reference_count"] == 3
319
-
320
-
321
- class TestCascadeDeletes:
322
- def test_remove_file_cascades_symbols(self, populated_conn):
323
- conn, ids = populated_conn
324
- initial_stats = get_stats(conn)
325
- remove_file(conn, "src/auth.py")
326
- conn.commit()
327
- after_stats = get_stats(conn)
328
- assert after_stats["file_count"] == initial_stats["file_count"] - 1
329
- assert after_stats["symbol_count"] < initial_stats["symbol_count"]
330
-
331
- def test_remove_file_cascades_edges(self, populated_conn):
332
- conn, ids = populated_conn
333
- rebuild_file_edges(conn)
334
- rebuild_symbol_edges(conn)
335
- conn.commit()
336
- remove_file(conn, "src/auth.py")
337
- conn.commit()
338
- edges_to_auth = conn.execute(
339
- "SELECT COUNT(*) FROM symbol_edges WHERE source_symbol_id IN (?, ?) OR target_symbol_id IN (?, ?)",
340
- (ids["s1"], ids["s2"], ids["s1"], ids["s2"]),
341
- ).fetchone()[0]
342
- assert edges_to_auth == 0
343
-
344
- def test_remove_symbols_by_file(self, populated_conn):
345
- conn, ids = populated_conn
346
- remove_symbols_by_file(conn, "src/auth.py")
347
- conn.commit()
348
- assert get_symbol_by_id(conn, ids["s1"]) is None
349
- assert get_symbol_by_id(conn, ids["s2"]) is None
350
- # File itself should still exist
351
- assert get_file_by_path(conn, "src/auth.py") is not None
352
-
353
-
354
- class TestHashContent:
355
- def test_deterministic(self):
356
- assert hash_content("hello") == hash_content("hello")
357
-
358
- def test_different_content(self):
359
- assert hash_content("hello") != hash_content("world")
360
-
361
- def test_returns_hex_string(self):
362
- h = hash_content("test")
363
- assert len(h) == 64
364
- assert all(c in "0123456789abcdef" for c in h)
1
+ """Tests for db.py -- 5-table schema with FTS5."""
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ import pytest
6
+
7
+ sys.path.insert(0, os.path.dirname(__file__))
8
+ from db import (
9
+ get_connection,
10
+ add_file,
11
+ get_file_by_path,
12
+ remove_file,
13
+ add_symbol,
14
+ get_symbol_by_id,
15
+ get_symbol_by_name,
16
+ get_symbols_by_file,
17
+ add_reference,
18
+ get_references_by_file,
19
+ rebuild_file_edges,
20
+ rebuild_symbol_edges,
21
+ add_edge,
22
+ get_transitive_deps,
23
+ get_reverse_deps,
24
+ fts_search,
25
+ get_stats,
26
+ hash_content,
27
+ remove_symbols_by_file,
28
+ )
29
+
30
+
31
+ @pytest.fixture
32
+ def conn():
33
+ with tempfile.TemporaryDirectory() as tmp:
34
+ c = get_connection(tmp)
35
+ yield c
36
+ c.close()
37
+
38
+
39
+ @pytest.fixture
40
+ def populated_conn(conn):
41
+ """Conn with 3 files, symbols, and references for graph tests."""
42
+ f1 = add_file(conn, "src/auth.py", "python", 1.0, line_count=50)
43
+ f2 = add_file(conn, "src/api.py", "python", 1.0, line_count=100)
44
+ f3 = add_file(conn, "src/utils.py", "python", 1.0, line_count=30)
45
+
46
+ s1 = add_symbol(conn, f1, "authenticate", "function", 1, 20, signature="def authenticate(req)")
47
+ s2 = add_symbol(conn, f1, "verify_token", "function", 25, 40)
48
+ s3 = add_symbol(conn, f2, "handle_request", "function", 1, 50)
49
+ s4 = add_symbol(conn, f3, "format_date", "function", 1, 10)
50
+
51
+ # api.py references authenticate (defined in auth.py) and format_date (defined in utils.py)
52
+ add_reference(conn, f2, "authenticate", 10)
53
+ add_reference(conn, f2, "format_date", 20)
54
+ # auth.py references format_date (defined in utils.py)
55
+ add_reference(conn, f1, "format_date", 30)
56
+
57
+ conn.commit()
58
+ return conn, {"f1": f1, "f2": f2, "f3": f3, "s1": s1, "s2": s2, "s3": s3, "s4": s4}
59
+
60
+
61
+ class TestFileCRUD:
62
+ def test_add_and_get(self, conn):
63
+ fid = add_file(conn, "src/main.py", "python", 1.0, hash="abc123", line_count=100)
64
+ assert fid > 0
65
+ row = get_file_by_path(conn, "src/main.py")
66
+ assert row is not None
67
+ assert row["lang"] == "python"
68
+ assert row["hash"] == "abc123"
69
+ assert row["line_count"] == 100
70
+
71
+ def test_unique_path(self, conn):
72
+ add_file(conn, "src/main.py", "python", 1.0)
73
+ with pytest.raises(Exception):
74
+ add_file(conn, "src/main.py", "python", 2.0)
75
+
76
+ def test_get_nonexistent(self, conn):
77
+ assert get_file_by_path(conn, "nonexistent.py") is None
78
+
79
+ def test_remove_file(self, conn):
80
+ add_file(conn, "src/main.py", "python", 1.0)
81
+ conn.commit()
82
+ remove_file(conn, "src/main.py")
83
+ conn.commit()
84
+ assert get_file_by_path(conn, "src/main.py") is None
85
+
86
+ def test_remove_nonexistent_is_noop(self, conn):
87
+ # Should not raise
88
+ remove_file(conn, "nonexistent.py")
89
+
90
+ def test_remove_cascades(self, conn):
91
+ fid = add_file(conn, "src/main.py", "python", 1.0)
92
+ add_symbol(conn, fid, "foo", "function", 1, 10)
93
+ add_reference(conn, fid, "bar", 5)
94
+ conn.commit()
95
+ remove_file(conn, "src/main.py")
96
+ conn.commit()
97
+ assert get_file_by_path(conn, "src/main.py") is None
98
+ assert len(get_symbols_by_file(conn, fid)) == 0
99
+ assert len(get_references_by_file(conn, fid)) == 0
100
+
101
+
102
+ class TestSymbolCRUD:
103
+ def test_add_and_get(self, conn):
104
+ fid = add_file(conn, "src/main.py", "python", 1.0)
105
+ sid = add_symbol(conn, fid, "my_func", "function", 10, 30, signature="def my_func()")
106
+ sym = get_symbol_by_id(conn, sid)
107
+ assert sym["name"] == "my_func"
108
+ assert sym["kind"] == "function"
109
+ assert sym["signature"] == "def my_func()"
110
+
111
+ def test_get_by_name(self, conn):
112
+ fid = add_file(conn, "src/main.py", "python", 1.0)
113
+ add_symbol(conn, fid, "my_func", "function", 10, 30)
114
+ results = get_symbol_by_name(conn, "my_func")
115
+ assert len(results) == 1
116
+ assert results[0]["name"] == "my_func"
117
+
118
+ def test_get_by_name_multiple_matches(self, conn):
119
+ f1 = add_file(conn, "src/a.py", "python", 1.0)
120
+ f2 = add_file(conn, "src/b.py", "python", 1.0)
121
+ add_symbol(conn, f1, "init", "function", 1, 10)
122
+ add_symbol(conn, f2, "init", "function", 1, 10)
123
+ results = get_symbol_by_name(conn, "init")
124
+ assert len(results) == 2
125
+
126
+ def test_get_by_file(self, conn):
127
+ fid = add_file(conn, "src/main.py", "python", 1.0)
128
+ add_symbol(conn, fid, "foo", "function", 1, 10)
129
+ add_symbol(conn, fid, "bar", "class", 15, 30)
130
+ syms = get_symbols_by_file(conn, fid)
131
+ assert len(syms) == 2
132
+
133
+ def test_get_by_file_ordered(self, conn):
134
+ fid = add_file(conn, "src/main.py", "python", 1.0)
135
+ add_symbol(conn, fid, "second", "function", 20, 30)
136
+ add_symbol(conn, fid, "first", "function", 1, 10)
137
+ syms = get_symbols_by_file(conn, fid)
138
+ assert syms[0]["name"] == "first"
139
+ assert syms[1]["name"] == "second"
140
+
141
+ def test_parent_id(self, conn):
142
+ fid = add_file(conn, "src/main.py", "python", 1.0)
143
+ parent = add_symbol(conn, fid, "MyClass", "class", 1, 50)
144
+ child = add_symbol(conn, fid, "my_method", "method", 5, 20, parent_id=parent)
145
+ sym = get_symbol_by_id(conn, child)
146
+ assert sym["parent_id"] == parent
147
+
148
+ def test_qualified_name(self, conn):
149
+ fid = add_file(conn, "src/main.py", "python", 1.0)
150
+ sid = add_symbol(conn, fid, "func", "function", 1, 10, qualified_name="main.func")
151
+ sym = get_symbol_by_id(conn, sid)
152
+ assert sym["qualified_name"] == "main.func"
153
+
154
+
155
+ class TestReferenceCRUD:
156
+ def test_add_and_get(self, conn):
157
+ fid = add_file(conn, "src/main.py", "python", 1.0)
158
+ add_reference(conn, fid, "some_func", 42, kind="call")
159
+ refs = get_references_by_file(conn, fid)
160
+ assert len(refs) == 1
161
+ assert refs[0]["symbol_name"] == "some_func"
162
+ assert refs[0]["line"] == 42
163
+ assert refs[0]["kind"] == "call"
164
+
165
+ def test_default_kind(self, conn):
166
+ fid = add_file(conn, "src/main.py", "python", 1.0)
167
+ add_reference(conn, fid, "func", 10)
168
+ refs = get_references_by_file(conn, fid)
169
+ assert refs[0]["kind"] == "call"
170
+
171
+ def test_multiple_refs_ordered(self, conn):
172
+ fid = add_file(conn, "src/main.py", "python", 1.0)
173
+ add_reference(conn, fid, "b_func", 20)
174
+ add_reference(conn, fid, "a_func", 5)
175
+ refs = get_references_by_file(conn, fid)
176
+ assert refs[0]["line"] == 5
177
+ assert refs[1]["line"] == 20
178
+
179
+
180
+ class TestEdgeRebuilding:
181
+ def test_rebuild_file_edges(self, populated_conn):
182
+ conn, ids = populated_conn
183
+ rebuild_file_edges(conn)
184
+ conn.commit()
185
+ edges = conn.execute("SELECT * FROM file_edges").fetchall()
186
+ # api.py -> auth.py (authenticate), api.py -> utils.py (format_date),
187
+ # auth.py -> utils.py (format_date)
188
+ assert len(edges) >= 2
189
+
190
+ def test_rebuild_file_edges_no_self_edges(self, populated_conn):
191
+ conn, ids = populated_conn
192
+ rebuild_file_edges(conn)
193
+ conn.commit()
194
+ self_edges = conn.execute(
195
+ "SELECT * FROM file_edges WHERE source_file_id = target_file_id"
196
+ ).fetchall()
197
+ assert len(self_edges) == 0
198
+
199
+ def test_rebuild_symbol_edges(self, populated_conn):
200
+ conn, ids = populated_conn
201
+ rebuild_symbol_edges(conn)
202
+ conn.commit()
203
+ edges = conn.execute("SELECT * FROM symbol_edges").fetchall()
204
+ assert len(edges) >= 1
205
+
206
+ def test_rebuild_is_idempotent(self, populated_conn):
207
+ conn, ids = populated_conn
208
+ rebuild_file_edges(conn)
209
+ rebuild_symbol_edges(conn)
210
+ conn.commit()
211
+ count1 = conn.execute("SELECT COUNT(*) FROM file_edges").fetchone()[0]
212
+ count2 = conn.execute("SELECT COUNT(*) FROM symbol_edges").fetchone()[0]
213
+
214
+ rebuild_file_edges(conn)
215
+ rebuild_symbol_edges(conn)
216
+ conn.commit()
217
+ assert conn.execute("SELECT COUNT(*) FROM file_edges").fetchone()[0] == count1
218
+ assert conn.execute("SELECT COUNT(*) FROM symbol_edges").fetchone()[0] == count2
219
+
220
+
221
+ class TestGraphTraversal:
222
+ def test_transitive_deps(self, populated_conn):
223
+ conn, ids = populated_conn
224
+ rebuild_symbol_edges(conn)
225
+ conn.commit()
226
+ deps = get_transitive_deps(conn, ids["s3"]) # handle_request
227
+ dep_names = {d["name"] for d in deps}
228
+ # handle_request refs authenticate and format_date
229
+ assert "authenticate" in dep_names or "format_date" in dep_names
230
+
231
+ def test_reverse_deps(self, populated_conn):
232
+ conn, ids = populated_conn
233
+ rebuild_symbol_edges(conn)
234
+ conn.commit()
235
+ rdeps = get_reverse_deps(conn, ids["s1"]) # authenticate
236
+ rdep_names = {d["name"] for d in rdeps}
237
+ assert "handle_request" in rdep_names
238
+
239
+ def test_no_deps_for_leaf(self, populated_conn):
240
+ conn, ids = populated_conn
241
+ rebuild_symbol_edges(conn)
242
+ conn.commit()
243
+ deps = get_transitive_deps(conn, ids["s4"])
244
+ assert isinstance(deps, list)
245
+
246
+ def test_max_depth_limits_results(self, populated_conn):
247
+ conn, ids = populated_conn
248
+ rebuild_symbol_edges(conn)
249
+ conn.commit()
250
+ deps_deep = get_transitive_deps(conn, ids["s3"], max_depth=10)
251
+ deps_shallow = get_transitive_deps(conn, ids["s3"], max_depth=0)
252
+ assert len(deps_shallow) <= len(deps_deep)
253
+
254
+
255
+ class TestManualEdge:
256
+ def test_add_edge(self, populated_conn):
257
+ conn, ids = populated_conn
258
+ add_edge(conn, ids["s1"], ids["s3"], "test_kind")
259
+ conn.commit()
260
+ edge = conn.execute(
261
+ "SELECT * FROM symbol_edges WHERE source_symbol_id=? AND target_symbol_id=? AND kind=?",
262
+ (ids["s1"], ids["s3"], "test_kind"),
263
+ ).fetchone()
264
+ assert edge is not None
265
+
266
+ def test_duplicate_edge_ignored(self, populated_conn):
267
+ conn, ids = populated_conn
268
+ add_edge(conn, ids["s1"], ids["s3"], "test_kind")
269
+ # Should not raise
270
+ add_edge(conn, ids["s1"], ids["s3"], "test_kind")
271
+ conn.commit()
272
+
273
+
274
+ class TestFTS:
275
+ def test_search_by_name(self, populated_conn):
276
+ conn, ids = populated_conn
277
+ results = fts_search(conn, "authenticate")
278
+ assert len(results) >= 1
279
+ assert any(r["name"] == "authenticate" for r in results)
280
+
281
+ def test_search_by_signature(self, populated_conn):
282
+ conn, ids = populated_conn
283
+ results = fts_search(conn, "req")
284
+ assert len(results) >= 1
285
+
286
+ def test_search_no_results(self, populated_conn):
287
+ conn, ids = populated_conn
288
+ results = fts_search(conn, "zzzznonexistent")
289
+ assert len(results) == 0
290
+
291
+ def test_search_with_limit(self, populated_conn):
292
+ conn, ids = populated_conn
293
+ results = fts_search(conn, "authenticate", limit=1)
294
+ assert len(results) <= 1
295
+
296
+ def test_results_have_rank(self, populated_conn):
297
+ conn, ids = populated_conn
298
+ results = fts_search(conn, "authenticate")
299
+ assert len(results) >= 1
300
+ assert "rank" in results[0]
301
+
302
+
303
+ class TestStats:
304
+ def test_returns_all_fields(self, populated_conn):
305
+ conn, _ = populated_conn
306
+ s = get_stats(conn)
307
+ assert "file_count" in s
308
+ assert "symbol_count" in s
309
+ assert "reference_count" in s
310
+ assert "edge_count" in s
311
+ assert "file_edge_count" in s
312
+
313
+ def test_correct_counts(self, populated_conn):
314
+ conn, _ = populated_conn
315
+ s = get_stats(conn)
316
+ assert s["file_count"] == 3
317
+ assert s["symbol_count"] == 4
318
+ assert s["reference_count"] == 3
319
+
320
+
321
+ class TestCascadeDeletes:
322
+ def test_remove_file_cascades_symbols(self, populated_conn):
323
+ conn, ids = populated_conn
324
+ initial_stats = get_stats(conn)
325
+ remove_file(conn, "src/auth.py")
326
+ conn.commit()
327
+ after_stats = get_stats(conn)
328
+ assert after_stats["file_count"] == initial_stats["file_count"] - 1
329
+ assert after_stats["symbol_count"] < initial_stats["symbol_count"]
330
+
331
+ def test_remove_file_cascades_edges(self, populated_conn):
332
+ conn, ids = populated_conn
333
+ rebuild_file_edges(conn)
334
+ rebuild_symbol_edges(conn)
335
+ conn.commit()
336
+ remove_file(conn, "src/auth.py")
337
+ conn.commit()
338
+ edges_to_auth = conn.execute(
339
+ "SELECT COUNT(*) FROM symbol_edges WHERE source_symbol_id IN (?, ?) OR target_symbol_id IN (?, ?)",
340
+ (ids["s1"], ids["s2"], ids["s1"], ids["s2"]),
341
+ ).fetchone()[0]
342
+ assert edges_to_auth == 0
343
+
344
+ def test_remove_symbols_by_file(self, populated_conn):
345
+ conn, ids = populated_conn
346
+ remove_symbols_by_file(conn, "src/auth.py")
347
+ conn.commit()
348
+ assert get_symbol_by_id(conn, ids["s1"]) is None
349
+ assert get_symbol_by_id(conn, ids["s2"]) is None
350
+ # File itself should still exist
351
+ assert get_file_by_path(conn, "src/auth.py") is not None
352
+
353
+
354
+ class TestHashContent:
355
+ def test_deterministic(self):
356
+ assert hash_content("hello") == hash_content("hello")
357
+
358
+ def test_different_content(self):
359
+ assert hash_content("hello") != hash_content("world")
360
+
361
+ def test_returns_hex_string(self):
362
+ h = hash_content("test")
363
+ assert len(h) == 64
364
+ assert all(c in "0123456789abcdef" for c in h)