witan-code 0.3.0__tar.gz → 0.4.0__tar.gz

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 (85) hide show
  1. {witan_code-0.3.0 → witan_code-0.4.0}/CHANGELOG.md +16 -0
  2. {witan_code-0.3.0 → witan_code-0.4.0}/PKG-INFO +25 -6
  3. {witan_code-0.3.0 → witan_code-0.4.0}/README.md +22 -5
  4. {witan_code-0.3.0 → witan_code-0.4.0}/docs/CLI_REFERENCE.md +7 -1
  5. {witan_code-0.3.0 → witan_code-0.4.0}/pyproject.toml +5 -2
  6. witan_code-0.4.0/tests/test_cli_output_format.py +91 -0
  7. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_elicit.py +3 -54
  8. witan_code-0.4.0/tests/test_graph.py +23 -0
  9. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_repo.py +3 -12
  10. witan_code-0.4.0/tests/test_setup.py +103 -0
  11. {witan_code-0.3.0 → witan_code-0.4.0}/uv.lock +148 -62
  12. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/bridge.py +5 -8
  13. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/cli.py +130 -89
  14. witan_code-0.4.0/witan_code/elicit.py +54 -0
  15. witan_code-0.4.0/witan_code/graph.py +98 -0
  16. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/hooks.py +1 -1
  17. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/indexer.py +3 -9
  18. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/maintenance.py +26 -53
  19. witan_code-0.4.0/witan_code/output.py +42 -0
  20. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/repo.py +6 -37
  21. witan_code-0.4.0/witan_code/setup.py +106 -0
  22. witan_code-0.3.0/queries/code_read.gq +0 -152
  23. witan_code-0.3.0/tests/test_detach.py +0 -39
  24. witan_code-0.3.0/tests/test_graph.py +0 -39
  25. witan_code-0.3.0/tests/test_setup.py +0 -266
  26. witan_code-0.3.0/witan_code/_detach.py +0 -27
  27. witan_code-0.3.0/witan_code/elicit.py +0 -92
  28. witan_code-0.3.0/witan_code/graph.py +0 -271
  29. witan_code-0.3.0/witan_code/queries/code_mutations.gq +0 -81
  30. witan_code-0.3.0/witan_code/setup.py +0 -230
  31. {witan_code-0.3.0 → witan_code-0.4.0}/.gitignore +0 -0
  32. {witan_code-0.3.0 → witan_code-0.4.0}/config/claude.json +0 -0
  33. {witan_code-0.3.0 → witan_code-0.4.0}/config/copilot.json +0 -0
  34. {witan_code-0.3.0 → witan_code-0.4.0}/config/pi.json +0 -0
  35. {witan_code-0.3.0 → witan_code-0.4.0}/docs/BRANCH_INDEXING.md +0 -0
  36. {witan_code-0.3.0 → witan_code-0.4.0}/docs/EDGE_PRECISION_TIERS.md +0 -0
  37. {witan_code-0.3.0 → witan_code-0.4.0}/docs/PACKAGE_MAP.md +0 -0
  38. {witan_code-0.3.0 → witan_code-0.4.0}/docs/STAGE2_STITCHING.md +0 -0
  39. {witan_code-0.3.0 → witan_code-0.4.0}/docs/SYMBOL_FORMAT.md +0 -0
  40. {witan_code-0.3.0 → witan_code-0.4.0}/docs/SYMBOL_TABLE.md +0 -0
  41. {witan_code-0.3.0 → witan_code-0.4.0}/docs/USER_GUIDE.md +0 -0
  42. {witan_code-0.3.0 → witan_code-0.4.0}/install.sh +0 -0
  43. {witan_code-0.3.0 → witan_code-0.4.0}/tests/__init__.py +0 -0
  44. {witan_code-0.3.0 → witan_code-0.4.0}/tests/conftest.py +0 -0
  45. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_branches.py +0 -0
  46. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_bridge.py +0 -0
  47. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_cli_hooks.py +0 -0
  48. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_context.py +0 -0
  49. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_crossrepo.py +0 -0
  50. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_edges.py +0 -0
  51. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_enrichment.py +0 -0
  52. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_hooks.py +0 -0
  53. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_indexer.py +0 -0
  54. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_languages.py +0 -0
  55. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_maintenance.py +0 -0
  56. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_package_map.py +0 -0
  57. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_server.py +0 -0
  58. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_stitch.py +0 -0
  59. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_symbol_table.py +0 -0
  60. {witan_code-0.3.0 → witan_code-0.4.0}/tests/test_visualize.py +0 -0
  61. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/__init__.py +0 -0
  62. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/__main__.py +0 -0
  63. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/bridge_extractors.py +0 -0
  64. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/config.py +0 -0
  65. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/context.py +0 -0
  66. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/edges.py +0 -0
  67. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/extensions/pi/codegraph.ts +0 -0
  68. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/package_map.py +0 -0
  69. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/queries/bridge.gq +0 -0
  70. {witan_code-0.3.0 → witan_code-0.4.0/witan_code}/queries/code_mutations.gq +0 -0
  71. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/queries/code_read.gq +0 -0
  72. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/queries/delete.gq +0 -0
  73. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/queries_ts/bash.scm +0 -0
  74. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/queries_ts/hcl.scm +0 -0
  75. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/queries_ts/python.scm +0 -0
  76. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/queries_ts/sql.scm +0 -0
  77. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/queries_ts/typescript.scm +0 -0
  78. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/queries_ts/yaml.scm +0 -0
  79. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/schema/bridge-schema.pg +0 -0
  80. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/schema/code-schema.pg +0 -0
  81. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/server.py +0 -0
  82. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/skills/witan-code/SKILL.md +0 -0
  83. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/stitch.py +0 -0
  84. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/store.py +0 -0
  85. {witan_code-0.3.0 → witan_code-0.4.0}/witan_code/visualize.py +0 -0
@@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/) (pre-1.0:
7
7
  a MINOR bump may include breaking changes).
8
8
 
9
+ ## [0.4.0] - 2026-07-16
10
+
11
+ ### Added
12
+
13
+ - **`witan-code --output-format`**: `repos`, `symbols`, and `stitch` can now
14
+ render their table data as `json`, `toml`, or `yaml` in addition to the
15
+ default Rich table output. The same option is honored when mounted as
16
+ `witan --output-format … code …`.
17
+
18
+ ### Changed
19
+
20
+ - Adopted the shared `witan-core` package for the CLI scaffolding
21
+ (`make_app`/`report_install`/`resolve_author` + agent-name constants),
22
+ replacing the previously duplicated copies. No user-facing behavior change.
23
+ Now depends on `witan-core[cli]>=0.2,<1`.
24
+
9
25
  ## [0.3.0] - 2026-07-10
10
26
 
11
27
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: witan-code
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: witan-code — tree-sitter code graph + cross-repo bridge (mounts under `witan code`)
5
5
  Project-URL: Homepage, https://github.com/mitodl/agent-kit/tree/main/mcp/servers/witan-code
6
6
  Project-URL: Repository, https://github.com/mitodl/agent-kit
@@ -15,6 +15,7 @@ Requires-Dist: cyclopts<5,>=4
15
15
  Requires-Dist: fastmcp<4,>=3.4.2
16
16
  Requires-Dist: pyyaml<7,>=6
17
17
  Requires-Dist: rich>=13
18
+ Requires-Dist: tomli-w<2,>=1
18
19
  Requires-Dist: tree-sitter-bash<0.26,>=0.25
19
20
  Requires-Dist: tree-sitter-go<0.26,>=0.25
20
21
  Requires-Dist: tree-sitter-hcl<1.3,>=1.2
@@ -23,6 +24,7 @@ Requires-Dist: tree-sitter-sql<0.4,>=0.3
23
24
  Requires-Dist: tree-sitter-typescript<0.24,>=0.23
24
25
  Requires-Dist: tree-sitter-yaml<0.8,>=0.7
25
26
  Requires-Dist: tree-sitter<0.26,>=0.25
27
+ Requires-Dist: witan-core[cli]<1,>=0.2
26
28
  Description-Content-Type: text/markdown
27
29
 
28
30
  # witan-code
@@ -87,6 +89,22 @@ machine's local code index.
87
89
  - [CLI reference](https://github.com/mitodl/agent-kit/blob/main/mcp/servers/witan-code/docs/CLI_REFERENCE.md) — every `witan-code` command with
88
90
  its full flag table and an example invocation.
89
91
 
92
+ ## CLI structured output
93
+
94
+ `witan-code` table-producing commands can emit machine-readable output instead
95
+ of Rich tables:
96
+
97
+ ```bash
98
+ witan-code --output-format json repos
99
+ witan-code --output-format yaml symbols --role exported
100
+ witan --output-format toml code stitch --unresolved
101
+ ```
102
+
103
+ Supported formats are `txt` (default), `json`, `toml`, and `yaml`. The same
104
+ setting is available through `WITAN_OUTPUT_FORMAT`. It currently applies to
105
+ `repos`, `symbols`, and `stitch`; free-text commands such as `index` and hook
106
+ commands keep their existing output.
107
+
90
108
  ## Cross-repo context bridge (Layer 2.5)
91
109
 
92
110
  The per-repo graph stops at a repo boundary, but service-oriented architectures
@@ -283,11 +301,12 @@ witan's). Run `witan-code setup` on its own for a witan-code-only install, or
283
301
  when witan-code isn't importable from witan's environment. See
284
302
  [Hooks](#hooks) and [Skill](#skill).
285
303
 
286
- This downloads the omnigraph release pinned by `_OMNIGRAPH_VERSION` in
287
- [`witan_code/setup.py`](./witan_code/setup.py) — the same pin `witan`'s own
288
- `setup.py` uses, kept in lockstep by Renovate (see the repo-root
289
- `renovate.json`'s `omnigraph-version` customManager, which bumps both files
290
- in one PR). There is no build-time bundling of the binary into the wheel —
304
+ This downloads the omnigraph release pinned by `_OMNIGRAPH_VERSION` in the
305
+ shared installer
306
+ [`witan_core.omnigraph_install`](../../../packages/witan-core/witan_core/omnigraph_install.py)
307
+ — the same pin `witan` uses (both import it from `witan-core`), bumped by
308
+ Renovate (see the repo-root `renovate.json`'s `omnigraph-version`
309
+ customManager). There is no build-time bundling of the binary into the wheel —
291
310
  `witan-code setup` (or `witan setup`) is the only source of the binary, and
292
311
  re-running it is how you pick up a version bump.
293
312
 
@@ -60,6 +60,22 @@ machine's local code index.
60
60
  - [CLI reference](https://github.com/mitodl/agent-kit/blob/main/mcp/servers/witan-code/docs/CLI_REFERENCE.md) — every `witan-code` command with
61
61
  its full flag table and an example invocation.
62
62
 
63
+ ## CLI structured output
64
+
65
+ `witan-code` table-producing commands can emit machine-readable output instead
66
+ of Rich tables:
67
+
68
+ ```bash
69
+ witan-code --output-format json repos
70
+ witan-code --output-format yaml symbols --role exported
71
+ witan --output-format toml code stitch --unresolved
72
+ ```
73
+
74
+ Supported formats are `txt` (default), `json`, `toml`, and `yaml`. The same
75
+ setting is available through `WITAN_OUTPUT_FORMAT`. It currently applies to
76
+ `repos`, `symbols`, and `stitch`; free-text commands such as `index` and hook
77
+ commands keep their existing output.
78
+
63
79
  ## Cross-repo context bridge (Layer 2.5)
64
80
 
65
81
  The per-repo graph stops at a repo boundary, but service-oriented architectures
@@ -256,11 +272,12 @@ witan's). Run `witan-code setup` on its own for a witan-code-only install, or
256
272
  when witan-code isn't importable from witan's environment. See
257
273
  [Hooks](#hooks) and [Skill](#skill).
258
274
 
259
- This downloads the omnigraph release pinned by `_OMNIGRAPH_VERSION` in
260
- [`witan_code/setup.py`](./witan_code/setup.py) — the same pin `witan`'s own
261
- `setup.py` uses, kept in lockstep by Renovate (see the repo-root
262
- `renovate.json`'s `omnigraph-version` customManager, which bumps both files
263
- in one PR). There is no build-time bundling of the binary into the wheel —
275
+ This downloads the omnigraph release pinned by `_OMNIGRAPH_VERSION` in the
276
+ shared installer
277
+ [`witan_core.omnigraph_install`](../../../packages/witan-core/witan_core/omnigraph_install.py)
278
+ — the same pin `witan` uses (both import it from `witan-core`), bumped by
279
+ Renovate (see the repo-root `renovate.json`'s `omnigraph-version`
280
+ customManager). There is no build-time bundling of the binary into the wheel —
264
281
  `witan-code setup` (or `witan setup`) is the only source of the binary, and
265
282
  re-running it is how you pick up a version bump.
266
283
 
@@ -7,7 +7,13 @@ the `witan` umbrella package. Run `witan-code --help` or `witan-code
7
7
  <command> --help` for the live version of this table — it is generated from
8
8
  the same docstrings.
9
9
 
10
- Global flags: `--help` / `-h`, `--version`.
10
+ Global flags: `--help` / `-h`, `--version`, `--output-format txt|json|toml|yaml`.
11
+
12
+ `--output-format` controls table-producing commands (`repos`, `symbols`, and
13
+ `stitch`) and is also available as `WITAN_OUTPUT_FORMAT`. The default `txt`
14
+ renders the existing Rich tables; `json`, `toml`, and `yaml` emit a wrapper
15
+ object with `title` and `rows` for machine parsing. The same option works when
16
+ mounted under the umbrella CLI, e.g. `witan --output-format json code repos`.
11
17
 
12
18
  Boolean flags follow cyclopts' `--flag`/`--no-flag` convention; both forms are
13
19
  always available even where only `--flag` is shown below.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "witan-code"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "witan-code — tree-sitter code graph + cross-repo bridge (mounts under `witan code`)"
5
5
  readme = "README.md"
6
6
  license = "BSD-3-Clause"
@@ -11,6 +11,7 @@ classifiers = [
11
11
  ]
12
12
  dependencies = [
13
13
  "agent-config-kit>=0.4,<1",
14
+ "witan-core[cli]>=0.2,<1",
14
15
  "fastmcp>=3.4.2,<4",
15
16
  "cyclopts>=4,<5",
16
17
  # Pinned to 0.25.x: tree-sitter 0.26.0 has a use-after-free in its pyo3 Node
@@ -35,6 +36,7 @@ dependencies = [
35
36
  "tree-sitter-hcl>=1.2,<1.3",
36
37
  "tree-sitter-go>=0.25,<0.26",
37
38
  "pyyaml>=6,<7",
39
+ "tomli-w>=1,<2",
38
40
  "rich>=13",
39
41
  ]
40
42
 
@@ -57,6 +59,7 @@ build-backend = "hatchling.build"
57
59
  # has actually landed on PyPI. Published wheels only see the PyPI-range
58
60
  # dependency declared above.
59
61
  agent-config-kit = { path = "../../../packages/agent-config-kit", editable = true }
62
+ witan-core = { path = "../../../packages/witan-core", editable = true }
60
63
 
61
64
  [dependency-groups]
62
65
  test = ["pytest>=8"]
@@ -69,7 +72,7 @@ testpaths = ["tests"]
69
72
  packages = ["witan_code"]
70
73
 
71
74
  [tool.bumpversion]
72
- current_version = "0.3.0"
75
+ current_version = "0.4.0"
73
76
  allow_dirty = true
74
77
 
75
78
  [[tool.bumpversion.files]]
@@ -0,0 +1,91 @@
1
+ """Tests for the global ``--output-format`` option."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import pytest
8
+
9
+ from witan_code import cli as cli_module
10
+ from witan_code import output as output_module
11
+
12
+
13
+ @pytest.fixture(autouse=True)
14
+ def _reset_output_format():
15
+ """Every test starts and ends on the txt default regardless of order."""
16
+ output_module.set_output_format("txt")
17
+ yield
18
+ output_module.set_output_format("txt")
19
+
20
+
21
+ def test_render_table_json_dumps_normalized_rows(capsys):
22
+ output_module.set_output_format("json")
23
+
24
+ cli_module._render_table(
25
+ title="Indexed repositories",
26
+ columns=["repo", "files"],
27
+ rows=[{"repo": "https://github.com/test/repo", "files": None}],
28
+ )
29
+
30
+ payload = json.loads(capsys.readouterr().out)
31
+ assert payload == {
32
+ "title": "Indexed repositories",
33
+ "rows": [{"repo": "https://github.com/test/repo", "files": ""}],
34
+ }
35
+
36
+
37
+ def test_render_table_toml_dumps_normalized_rows(capsys):
38
+ import tomllib
39
+
40
+ output_module.set_output_format("toml")
41
+
42
+ cli_module._render_table(
43
+ title="Symbol table — https://github.com/test/repo",
44
+ columns=["role", "refs"],
45
+ rows=[{"role": "exported", "refs": 2}],
46
+ )
47
+
48
+ payload = tomllib.loads(capsys.readouterr().out)
49
+ assert payload["title"] == "Symbol table — https://github.com/test/repo"
50
+ assert payload["rows"] == [{"role": "exported", "refs": 2}]
51
+
52
+
53
+ def test_repos_honors_structured_output(tmp_path, monkeypatch, capsys):
54
+ code_dir = tmp_path / "code"
55
+ code_dir.mkdir()
56
+ store = code_dir / "https_github.com_test_repo.omni"
57
+ store.mkdir()
58
+ (code_dir / f"{store.name}.repo").write_text("https://github.com/test/repo")
59
+ monkeypatch.setenv("WITAN_CODE_DIR", str(code_dir))
60
+ monkeypatch.setattr(
61
+ cli_module,
62
+ "_code_store_stats",
63
+ lambda store: ("https://github.com/test/repo", "7"),
64
+ )
65
+ monkeypatch.setattr(cli_module, "_dir_stats", lambda path: (1024, "2026-07-13"))
66
+ output_module.set_output_format("json")
67
+
68
+ cli_module.repos()
69
+
70
+ payload = json.loads(capsys.readouterr().out)
71
+ assert payload == {
72
+ "title": "Indexed repositories",
73
+ "rows": [
74
+ {
75
+ "repo": "https://github.com/test/repo",
76
+ "files": "7",
77
+ "size": "1.0KB",
78
+ "last indexed": "2026-07-13",
79
+ }
80
+ ],
81
+ }
82
+
83
+
84
+ def test_launcher_sets_output_format_and_forwards_tokens(monkeypatch):
85
+ calls = []
86
+ monkeypatch.setattr(cli_module, "app", lambda tokens: calls.append(tokens))
87
+
88
+ cli_module._launcher("repos", output_format="yaml")
89
+
90
+ assert output_module.get_output_format() == "yaml"
91
+ assert calls == [("repos",)]
@@ -383,60 +383,9 @@ def test_find_definition_multi_repo_disambiguation(tmp_path, monkeypatch):
383
383
  assert {d["repo"] for d in r1} == {RA}
384
384
 
385
385
 
386
- # ── Direct unit tests for witan_code/elicit.py (no server/omnigraph needed) ──
387
-
388
-
389
- class _RaiseCtx:
390
- async def elicit(self, *args, **kwargs):
391
- raise RuntimeError("unsupported")
392
-
393
-
394
- def test_confirm_no_ctx_or_error_returns_default():
395
- from witan_code import elicit
396
-
397
- assert (
398
- asyncio.run(elicit.confirm(None, "q?", default_when_unsupported=True)) is True
399
- )
400
- assert (
401
- asyncio.run(elicit.confirm(None, "q?", default_when_unsupported=False)) is False
402
- )
403
- assert (
404
- asyncio.run(elicit.confirm(_RaiseCtx(), "q?", default_when_unsupported=True))
405
- is True
406
- )
407
-
408
-
409
- def test_confirm_accept_and_decline():
410
- from witan_code import elicit
411
-
412
- assert (
413
- asyncio.run(
414
- elicit.confirm(_AcceptCtx(True), "q?", default_when_unsupported=False)
415
- )
416
- is True
417
- )
418
- # accepting with a False value is still a "no"
419
- assert (
420
- asyncio.run(
421
- elicit.confirm(_AcceptCtx(False), "q?", default_when_unsupported=True)
422
- )
423
- is False
424
- )
425
- assert (
426
- asyncio.run(elicit.confirm(_DeclineCtx(), "q?", default_when_unsupported=True))
427
- is False
428
- )
429
-
430
-
431
- def test_text_no_ctx_error_or_empty_returns_default():
432
- from witan_code import elicit
433
-
434
- assert asyncio.run(elicit.text(None, "q?", default="d")) == "d"
435
- assert asyncio.run(elicit.text(_RaiseCtx(), "q?", default="d")) == "d"
436
- assert asyncio.run(elicit.text(_AcceptCtx(""), "q?", default="d")) == "d"
437
- # whitespace-only is treated as empty -> default; a real value is stripped
438
- assert asyncio.run(elicit.text(_AcceptCtx(" "), "q?", default="d")) == "d"
439
- assert asyncio.run(elicit.text(_AcceptCtx(" real "), "q?", default="d")) == "real"
386
+ # ── choose_repo unit tests (no server/omnigraph needed) ──
387
+ # The confirm/text primitives are covered in witan-core's own test suite
388
+ # (packages/witan-core/tests/test_elicit.py); only choose_repo is witan-code's.
440
389
 
441
390
 
442
391
  def test_choose_repo_exact_match_case_insensitive_and_stripped():
@@ -0,0 +1,23 @@
1
+ """witan-code-subclass-specific OmnigraphClient tests.
2
+
3
+ The generic base machinery (_find_binary lookup order, OCC conflict surfacing,
4
+ admission-cap backoff) is covered in packages/witan-core/tests/test_omnigraph.py.
5
+ Here we only assert witan-code's own subclass tail: the setup-hint in the
6
+ binary-not-found message. (branch ops + bulk load are exercised against a real
7
+ store in test_branches.py / test_indexer.py.)
8
+ """
9
+
10
+ import shutil
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ from witan_code.graph import OmnigraphClient
16
+
17
+
18
+ def test_find_binary_message_names_witan_code_setup(tmp_path, monkeypatch):
19
+ monkeypatch.setattr(shutil, "which", lambda name: None)
20
+ monkeypatch.setattr(Path, "home", lambda: tmp_path)
21
+
22
+ with pytest.raises(RuntimeError, match="witan-code setup"):
23
+ OmnigraphClient._find_binary()
@@ -8,18 +8,9 @@ import pytest
8
8
  from witan_code import repo
9
9
 
10
10
 
11
- @pytest.mark.parametrize(
12
- ("url", "expected"),
13
- [
14
- ("git@github.com:mitodl/ol-django.git", "https://github.com/mitodl/ol-django"),
15
- ("https://github.com/mitodl/ol-django", "https://github.com/mitodl/ol-django"),
16
- ("ssh://git@github.com/mitodl/repo.git", "https://github.com/mitodl/repo"),
17
- ],
18
- )
19
- def test_normalise_matches_memory_layer(url, expected):
20
- # Must stay identical to witan.repo._normalise so symbol ids and
21
- # the Layer-1 symbol_refs that point at them share one repo key.
22
- assert repo._normalise(url) == expected
11
+ # The repo-key canonicalizer (normalise) lives in witan_core.repo_key — a single
12
+ # source of truth shared with the memory layer (no more "must stay identical"
13
+ # copy). Its golden contract table is packages/witan-core/tests/test_repo_key.py.
23
14
 
24
15
 
25
16
  def test_detect_env_override(monkeypatch):
@@ -0,0 +1,103 @@
1
+ """Tests for witan-code's registration bundle.
2
+
3
+ Generic install-mechanics behavior (dry-run no-op, additive merge, JSON
4
+ skip-not-crash, hook dedup, ...) is covered by agent-config-kit's own test
5
+ suite (``packages/agent-config-kit/tests/``); the shared omnigraph installer is
6
+ covered by ``packages/witan-core/tests/test_omnigraph_install.py``. The bundle
7
+ tests here only assert that ``witan_code_bundle()`` + ``apply("claude", ...)``
8
+ produces witan-code's own MCP entry and hook commands, i.e. that the wiring is
9
+ correct.
10
+ """
11
+
12
+ import json
13
+ from pathlib import Path
14
+
15
+ from agent_config_kit import apply
16
+
17
+ from witan_code import setup
18
+
19
+
20
+ def test_witan_code_bundle_registers_mcp_server_and_hooks(tmp_path, monkeypatch):
21
+ monkeypatch.setattr(Path, "home", lambda: tmp_path)
22
+ pkg_dir = tmp_path / "pkg"
23
+ pkg_dir.mkdir()
24
+
25
+ bundle = setup.witan_code_bundle(pkg_dir, "tester")
26
+ apply("claude", bundle)
27
+
28
+ claude_json = json.loads((tmp_path / ".claude.json").read_text())
29
+ entry = claude_json["mcpServers"]["witan-code"]
30
+ assert entry["type"] == "stdio"
31
+ assert entry["command"] == "uvx"
32
+ assert entry["env"]["WITAN_AUTHOR"] == "tester"
33
+
34
+ settings = json.loads((tmp_path / ".claude" / "settings.json").read_text())
35
+ session_init = [
36
+ h
37
+ for e in settings["hooks"]["SessionStart"]
38
+ for h in e["hooks"]
39
+ if h["command"] == "witan-code session-init"
40
+ ]
41
+ reindex = [
42
+ h
43
+ for e in settings["hooks"]["PostToolUse"]
44
+ for h in e["hooks"]
45
+ if h["command"] == "witan-code reindex-hook"
46
+ ]
47
+ context = [
48
+ h
49
+ for e in settings["hooks"]["UserPromptSubmit"]
50
+ for h in e["hooks"]
51
+ if h["command"] == "witan-code inject-context"
52
+ ]
53
+ checkpoint = [
54
+ h
55
+ for e in settings["hooks"]["Stop"]
56
+ for h in e["hooks"]
57
+ if h["command"] == "witan-code checkpoint"
58
+ ]
59
+ assert session_init and reindex and context and checkpoint
60
+ # Both prompt-path hooks carry a timeout so a hung git/store can't stall.
61
+ assert context[0]["timeout"] == 15
62
+ assert checkpoint[0]["timeout"] == 15
63
+
64
+
65
+ def test_witan_code_bundle_honors_binary_override(tmp_path):
66
+ """witan.cli.setup_cmd passes binary="witan code" when folding this
67
+ bundle into witan's own, so hooks only need `witan` on PATH — not a
68
+ separately installed `witan-code` binary."""
69
+ pkg_dir = tmp_path / "pkg"
70
+ pkg_dir.mkdir()
71
+
72
+ bundle = setup.witan_code_bundle(pkg_dir, "tester", binary="witan code")
73
+
74
+ commands = {h.command for h in bundle.hooks if hasattr(h, "command")}
75
+ assert commands == {
76
+ "witan code session-init",
77
+ "witan code reindex-hook",
78
+ "witan code inject-context",
79
+ "witan code checkpoint",
80
+ }
81
+
82
+
83
+ def test_witan_code_bundle_includes_pi_extensions_as_plugin_hooks(tmp_path):
84
+ pkg_dir = tmp_path / "pkg"
85
+ ext_dir = pkg_dir / "extensions" / "pi"
86
+ ext_dir.mkdir(parents=True)
87
+ (ext_dir / "codegraph.ts").write_text("// stub")
88
+
89
+ bundle = setup.witan_code_bundle(pkg_dir, "tester")
90
+
91
+ plugin_hooks = [h for h in bundle.hooks if hasattr(h, "entry_path")]
92
+ assert any(h.entry_path.name == "codegraph.ts" for h in plugin_hooks)
93
+
94
+
95
+ def test_witan_code_bundle_includes_bundled_skills(tmp_path):
96
+ pkg_dir = tmp_path / "pkg"
97
+ skill_dir = pkg_dir / "skills" / "witan-code"
98
+ skill_dir.mkdir(parents=True)
99
+ (skill_dir / "SKILL.md").write_text("# witan-code")
100
+
101
+ bundle = setup.witan_code_bundle(pkg_dir, "tester")
102
+
103
+ assert any(s.name == "witan-code" for s in bundle.skills)