feed-the-machine 1.6.1 → 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.
- package/LICENSE +21 -21
- package/README.md +170 -170
- package/bin/brain.py +1340 -0
- package/bin/convert_claude_skills_to_codex.py +490 -0
- package/bin/generate-manifest.mjs +463 -463
- package/bin/harden_codex_skills.py +141 -0
- package/bin/install.mjs +491 -491
- package/bin/migrate-eng-buddy-data.py +875 -0
- package/bin/playbook_engine/__init__.py +1 -0
- package/bin/playbook_engine/conftest.py +8 -0
- package/bin/playbook_engine/extractor.py +33 -0
- package/bin/playbook_engine/manager.py +102 -0
- package/bin/playbook_engine/models.py +84 -0
- package/bin/playbook_engine/registry.py +35 -0
- package/bin/playbook_engine/test_extractor.py +72 -0
- package/bin/playbook_engine/test_integration.py +129 -0
- package/bin/playbook_engine/test_manager.py +85 -0
- package/bin/playbook_engine/test_models.py +166 -0
- package/bin/playbook_engine/test_registry.py +67 -0
- package/bin/playbook_engine/test_tracer.py +86 -0
- package/bin/playbook_engine/tracer.py +93 -0
- package/bin/tasks_db.py +456 -0
- package/docs/HOOKS.md +243 -243
- package/docs/INBOX.md +233 -233
- package/ftm/SKILL.md +125 -122
- package/ftm-audit/SKILL.md +623 -623
- package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
- package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
- package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
- package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
- package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
- package/ftm-audit/scripts/run-knip.sh +23 -23
- package/ftm-audit.yml +2 -2
- package/ftm-brainstorm/SKILL.md +1003 -498
- package/ftm-brainstorm/evals/evals.json +180 -100
- package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
- package/ftm-brainstorm/references/agent-prompts.md +552 -224
- package/ftm-brainstorm/references/plan-template.md +209 -121
- package/ftm-brainstorm.yml +2 -2
- package/ftm-browse/SKILL.md +454 -454
- package/ftm-browse/daemon/browser-manager.ts +206 -206
- package/ftm-browse/daemon/bun.lock +30 -30
- package/ftm-browse/daemon/cli.ts +347 -347
- package/ftm-browse/daemon/commands.ts +410 -410
- package/ftm-browse/daemon/main.ts +357 -357
- package/ftm-browse/daemon/package.json +17 -17
- package/ftm-browse/daemon/server.ts +189 -189
- package/ftm-browse/daemon/snapshot.ts +519 -519
- package/ftm-browse/daemon/tsconfig.json +22 -22
- package/ftm-browse.yml +4 -4
- package/ftm-capture/SKILL.md +370 -370
- package/ftm-capture.yml +4 -4
- package/ftm-codex-gate/SKILL.md +361 -361
- package/ftm-codex-gate.yml +2 -2
- package/ftm-config/SKILL.md +422 -345
- package/ftm-config.default.yml +125 -82
- package/ftm-config.yml +44 -2
- package/ftm-council/SKILL.md +416 -416
- package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
- package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
- package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
- package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
- package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
- package/ftm-council.yml +2 -2
- package/ftm-dashboard/SKILL.md +163 -163
- package/ftm-dashboard.yml +4 -4
- package/ftm-debug/SKILL.md +1037 -1037
- package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
- package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
- package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
- package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
- package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
- package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
- package/ftm-debug.yml +2 -2
- package/ftm-diagram/SKILL.md +277 -277
- package/ftm-diagram.yml +2 -2
- package/ftm-executor/SKILL.md +777 -777
- package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
- package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
- package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
- package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
- package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
- package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
- package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
- package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
- package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
- package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
- package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
- package/ftm-executor/runtime/package.json +8 -8
- package/ftm-executor.yml +2 -2
- package/ftm-git/SKILL.md +441 -441
- package/ftm-git/evals/evals.json +26 -26
- package/ftm-git/evals/promptfoo.yaml +75 -75
- package/ftm-git/hooks/post-commit-experience.sh +92 -92
- package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
- package/ftm-git/references/protocols/REMEDIATION.md +139 -139
- package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
- package/ftm-git.yml +2 -2
- package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/adapters/_retry.py +64 -64
- package/ftm-inbox/backend/adapters/base.py +230 -230
- package/ftm-inbox/backend/adapters/freshservice.py +104 -104
- package/ftm-inbox/backend/adapters/gmail.py +125 -125
- package/ftm-inbox/backend/adapters/jira.py +136 -136
- package/ftm-inbox/backend/adapters/registry.py +192 -192
- package/ftm-inbox/backend/adapters/slack.py +110 -110
- package/ftm-inbox/backend/db/connection.py +54 -54
- package/ftm-inbox/backend/db/schema.py +78 -78
- package/ftm-inbox/backend/executor/__init__.py +7 -7
- package/ftm-inbox/backend/executor/engine.py +149 -149
- package/ftm-inbox/backend/executor/step_runner.py +98 -98
- package/ftm-inbox/backend/main.py +103 -103
- package/ftm-inbox/backend/models/__init__.py +1 -1
- package/ftm-inbox/backend/models/unified_task.py +36 -36
- package/ftm-inbox/backend/planner/__init__.py +6 -6
- package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/generator.py +127 -127
- package/ftm-inbox/backend/planner/schema.py +34 -34
- package/ftm-inbox/backend/requirements.txt +5 -5
- package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/routes/execute.py +186 -186
- package/ftm-inbox/backend/routes/health.py +52 -52
- package/ftm-inbox/backend/routes/inbox.py +68 -68
- package/ftm-inbox/backend/routes/plan.py +271 -271
- package/ftm-inbox/bin/launchagent.mjs +91 -91
- package/ftm-inbox/bin/setup.mjs +188 -188
- package/ftm-inbox/bin/start.sh +10 -10
- package/ftm-inbox/bin/status.sh +17 -17
- package/ftm-inbox/bin/stop.sh +8 -8
- package/ftm-inbox/config.example.yml +55 -55
- package/ftm-inbox/package-lock.json +2898 -2898
- package/ftm-inbox/package.json +26 -26
- package/ftm-inbox/postcss.config.js +6 -6
- package/ftm-inbox/src/app.css +199 -199
- package/ftm-inbox/src/app.html +18 -18
- package/ftm-inbox/src/lib/api.ts +166 -166
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
- package/ftm-inbox/src/lib/theme.ts +47 -47
- package/ftm-inbox/src/routes/+layout.svelte +76 -76
- package/ftm-inbox/src/routes/+page.svelte +401 -401
- package/ftm-inbox/svelte.config.js +12 -12
- package/ftm-inbox/tailwind.config.ts +63 -63
- package/ftm-inbox/tsconfig.json +13 -13
- package/ftm-inbox/vite.config.ts +6 -6
- package/ftm-intent/SKILL.md +241 -241
- package/ftm-intent.yml +2 -2
- package/ftm-manifest.json +3794 -3794
- package/ftm-map/SKILL.md +291 -291
- package/ftm-map/scripts/db.py +712 -712
- package/ftm-map/scripts/index.py +415 -415
- package/ftm-map/scripts/parser.py +224 -224
- package/ftm-map/scripts/queries/go-tags.scm +20 -20
- package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
- package/ftm-map/scripts/queries/python-tags.scm +31 -31
- package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
- package/ftm-map/scripts/queries/rust-tags.scm +37 -37
- package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
- package/ftm-map/scripts/query.py +301 -301
- package/ftm-map/scripts/ranker.py +377 -377
- package/ftm-map/scripts/requirements.txt +5 -5
- package/ftm-map/scripts/setup-hooks.sh +27 -27
- package/ftm-map/scripts/setup.sh +56 -56
- package/ftm-map/scripts/test_db.py +364 -364
- package/ftm-map/scripts/test_parser.py +174 -174
- package/ftm-map/scripts/test_query.py +183 -183
- package/ftm-map/scripts/test_ranker.py +199 -199
- package/ftm-map/scripts/views.py +591 -591
- package/ftm-map.yml +2 -2
- package/ftm-mind/SKILL.md +201 -1943
- package/ftm-mind/evals/promptfoo.yaml +142 -142
- package/ftm-mind/references/blackboard-protocol.md +110 -0
- package/ftm-mind/references/blackboard-schema.md +328 -328
- package/ftm-mind/references/complexity-guide.md +110 -110
- package/ftm-mind/references/complexity-sizing.md +138 -0
- package/ftm-mind/references/decide-act-protocol.md +172 -0
- package/ftm-mind/references/direct-execution.md +51 -0
- package/ftm-mind/references/environment-discovery.md +77 -0
- package/ftm-mind/references/event-registry.md +319 -319
- package/ftm-mind/references/mcp-inventory.md +300 -296
- package/ftm-mind/references/ops-routing.md +47 -0
- package/ftm-mind/references/orient-protocol.md +234 -0
- package/ftm-mind/references/personality.md +40 -0
- package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
- package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
- package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
- package/ftm-mind/references/reflexion-protocol.md +249 -249
- package/ftm-mind/references/routing/SCENARIOS.md +22 -22
- package/ftm-mind/references/routing-scenarios.md +35 -35
- package/ftm-mind.yml +2 -2
- package/ftm-ops.yml +4 -0
- package/ftm-pause/SKILL.md +395 -395
- package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
- package/ftm-pause/references/protocols/VALIDATION.md +80 -80
- package/ftm-pause.yml +2 -2
- package/ftm-researcher/SKILL.md +275 -275
- package/ftm-researcher/evals/agent-diversity.yaml +17 -17
- package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
- package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
- package/ftm-researcher/references/adaptive-search.md +116 -116
- package/ftm-researcher/references/agent-prompts.md +193 -193
- package/ftm-researcher/references/council-integration.md +193 -193
- package/ftm-researcher/references/output-format.md +203 -203
- package/ftm-researcher/references/synthesis-pipeline.md +165 -165
- package/ftm-researcher/scripts/score_credibility.py +234 -234
- package/ftm-researcher/scripts/validate_research.py +92 -92
- package/ftm-researcher.yml +2 -2
- package/ftm-resume/SKILL.md +518 -518
- package/ftm-resume/references/protocols/VALIDATION.md +172 -172
- package/ftm-resume.yml +2 -2
- package/ftm-retro/SKILL.md +380 -380
- package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
- package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
- package/ftm-retro.yml +2 -2
- package/ftm-routine/SKILL.md +170 -170
- package/ftm-routine.yml +4 -4
- package/ftm-state/blackboard/capabilities.json +5 -5
- package/ftm-state/blackboard/capabilities.schema.json +27 -27
- package/ftm-state/blackboard/context.json +37 -23
- package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
- package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
- package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
- package/ftm-state/blackboard/experiences/index.json +58 -9
- package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
- package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
- package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
- package/ftm-state/blackboard/patterns.json +6 -6
- package/ftm-state/schemas/context.schema.json +130 -130
- package/ftm-state/schemas/experience-index.schema.json +77 -77
- package/ftm-state/schemas/experience.schema.json +78 -78
- package/ftm-state/schemas/patterns.schema.json +44 -44
- package/ftm-upgrade/SKILL.md +194 -194
- package/ftm-upgrade/scripts/check-version.sh +76 -76
- package/ftm-upgrade/scripts/upgrade.sh +143 -143
- package/ftm-upgrade.yml +2 -2
- package/ftm-verify.yml +2 -2
- package/ftm.yml +2 -2
- package/hooks/ftm-auto-log.sh +137 -0
- package/hooks/ftm-blackboard-enforcer.sh +93 -93
- package/hooks/ftm-discovery-reminder.sh +90 -90
- package/hooks/ftm-drafts-gate.sh +61 -61
- package/hooks/ftm-event-logger.mjs +107 -107
- package/hooks/ftm-install-hooks.sh +240 -0
- package/hooks/ftm-learning-capture.sh +117 -0
- package/hooks/ftm-map-autodetect.sh +79 -79
- package/hooks/ftm-pending-sync-check.sh +22 -22
- package/hooks/ftm-plan-gate.sh +92 -92
- package/hooks/ftm-post-commit-trigger.sh +57 -57
- package/hooks/ftm-post-compaction.sh +138 -0
- package/hooks/ftm-pre-compaction.sh +147 -0
- package/hooks/ftm-session-end.sh +52 -0
- package/hooks/ftm-session-snapshot.sh +213 -0
- package/hooks/settings-template.json +81 -81
- package/install.sh +363 -363
- package/package.json +84 -84
- 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)
|