Stevesolun commited on
Commit
92f06d8
·
verified ·
1 Parent(s): 6d83007

Sync ctx b1e7e1e (part 3)

Browse files

GitHub commit: b1e7e1ee28ac2147594e3d1f59009fbd0dd74647

src/mcp_add.py CHANGED
@@ -719,7 +719,7 @@ def main() -> None:
719
  print(f"Error: {json_path} does not exist.", file=sys.stderr)
720
  sys.exit(1)
721
  try:
722
- raw_records = [json.loads(json_path.read_text(encoding="utf-8"))]
723
  except json.JSONDecodeError as exc:
724
  print(f"Error: failed to parse JSON: {exc}", file=sys.stderr)
725
  sys.exit(1)
@@ -730,9 +730,9 @@ def main() -> None:
730
  print(f"Error: {jsonl_path} does not exist.", file=sys.stderr)
731
  sys.exit(1)
732
  for lineno, line in enumerate(
733
- jsonl_path.read_text(encoding="utf-8").splitlines(), 1
734
  ):
735
- line = line.strip()
736
  if not line:
737
  continue
738
  try:
@@ -742,7 +742,7 @@ def main() -> None:
742
 
743
  elif args.from_stdin:
744
  for lineno, line in enumerate(sys.stdin, 1):
745
- line = line.strip()
746
  if not line:
747
  continue
748
  try:
@@ -767,6 +767,8 @@ def main() -> None:
767
  f"\nDone{dry_label}: {added} added, {merged} updated, "
768
  f"{reviewed} reviewed, {rejected} rejected, {errors} errors"
769
  )
 
 
770
 
771
 
772
  if __name__ == "__main__":
 
719
  print(f"Error: {json_path} does not exist.", file=sys.stderr)
720
  sys.exit(1)
721
  try:
722
+ raw_records = [json.loads(json_path.read_text(encoding="utf-8-sig"))]
723
  except json.JSONDecodeError as exc:
724
  print(f"Error: failed to parse JSON: {exc}", file=sys.stderr)
725
  sys.exit(1)
 
730
  print(f"Error: {jsonl_path} does not exist.", file=sys.stderr)
731
  sys.exit(1)
732
  for lineno, line in enumerate(
733
+ jsonl_path.read_text(encoding="utf-8-sig").splitlines(), 1
734
  ):
735
+ line = line.lstrip("\ufeff").strip()
736
  if not line:
737
  continue
738
  try:
 
742
 
743
  elif args.from_stdin:
744
  for lineno, line in enumerate(sys.stdin, 1):
745
+ line = line.lstrip("\ufeff").strip()
746
  if not line:
747
  continue
748
  try:
 
767
  f"\nDone{dry_label}: {added} added, {merged} updated, "
768
  f"{reviewed} reviewed, {rejected} rejected, {errors} errors"
769
  )
770
+ if rejected or errors:
771
+ sys.exit(1)
772
 
773
 
774
  if __name__ == "__main__":
src/scan_repo.py CHANGED
@@ -132,12 +132,15 @@ def read_json_safe(path: str) -> dict | None:
132
  return None
133
 
134
 
135
- def read_toml_deps(path: str) -> list[str]:
136
  """Extract dependency names from pyproject.toml.
137
 
138
  Covers PEP 621 ``[project].dependencies`` / ``optional-dependencies`` and
139
  Poetry-style ``[tool.poetry].dependencies`` / ``dev-dependencies``. Version
140
- specifiers and extras are stripped via PEP 508 splitting.
 
 
 
141
  """
142
  try:
143
  data = tomllib.loads(Path(path).read_text(encoding="utf-8-sig"))
@@ -152,15 +155,17 @@ def read_toml_deps(path: str) -> list[str]:
152
  deps = project.get("dependencies", [])
153
  if isinstance(deps, list):
154
  raw.extend(d for d in deps if isinstance(d, str))
155
- opt = project.get("optional-dependencies", {})
156
- if isinstance(opt, dict):
157
- for group in opt.values():
158
- if isinstance(group, list):
159
- raw.extend(d for d in group if isinstance(d, str))
 
160
 
161
  poetry = data.get("tool", {}).get("poetry", {}) if isinstance(data.get("tool"), dict) else {}
162
  if isinstance(poetry, dict):
163
- for key in ("dependencies", "dev-dependencies"):
 
164
  deps = poetry.get(key, {})
165
  if isinstance(deps, dict):
166
  raw.extend(k for k in deps.keys() if isinstance(k, str) and k.lower() != "python")
@@ -222,6 +227,7 @@ def detect_stack(repo_path: str, signals: dict) -> dict:
222
 
223
  # Collect all deps from Python and JS configs
224
  all_py_deps: list[str] = []
 
225
  all_js_deps: list[str] = []
226
  pkg_json = None
227
 
@@ -229,10 +235,15 @@ def detect_stack(repo_path: str, signals: dict) -> dict:
229
  base = os.path.basename(cfg)
230
  if base == "pyproject.toml":
231
  all_py_deps.extend(read_toml_deps(cfg))
 
232
  elif base == "requirements.txt":
233
- all_py_deps.extend(read_requirements(cfg))
 
 
234
  elif base == "Pipfile":
235
- all_py_deps.extend(read_requirements(cfg))
 
 
236
  elif base == "package.json":
237
  data = read_json_safe(cfg)
238
  if data:
@@ -242,6 +253,7 @@ def detect_stack(repo_path: str, signals: dict) -> dict:
242
  all_js_deps.extend(k.lower() for k in data[section])
243
 
244
  py_dep_set = set(all_py_deps)
 
245
  js_dep_set = set(all_js_deps)
246
 
247
  # --- LANGUAGES ---
@@ -540,6 +552,14 @@ def detect_stack(repo_path: str, signals: dict) -> dict:
540
 
541
  # --- PROJECT TYPE ---
542
  fw_names = {f["name"] for f in profile["frameworks"]}
 
 
 
 
 
 
 
 
543
  if fw_names & {"react", "vue", "angular", "svelte", "nextjs", "nuxt"}:
544
  if fw_names & {"fastapi", "django", "flask", "express", "nestjs"}:
545
  profile["project_type"] = "fullstack"
@@ -547,9 +567,9 @@ def detect_stack(repo_path: str, signals: dict) -> dict:
547
  profile["project_type"] = "frontend"
548
  elif fw_names & {"fastapi", "django", "flask", "express", "nestjs", "gin", "actix"}:
549
  profile["project_type"] = "api-service"
550
- elif fw_names & {"pytorch", "tensorflow", "huggingface"}:
551
  profile["project_type"] = "ml-project"
552
- elif fw_names & {"langchain", "llamaindex", "crewai"}:
553
  profile["project_type"] = "ai-agent"
554
  elif profile["infrastructure"]:
555
  profile["project_type"] = "infrastructure"
 
132
  return None
133
 
134
 
135
+ def read_toml_deps(path: str, *, include_optional: bool = True) -> list[str]:
136
  """Extract dependency names from pyproject.toml.
137
 
138
  Covers PEP 621 ``[project].dependencies`` / ``optional-dependencies`` and
139
  Poetry-style ``[tool.poetry].dependencies`` / ``dev-dependencies``. Version
140
+ specifiers and extras are stripped via PEP 508 splitting. Callers can set
141
+ ``include_optional=False`` when deciding the primary project type; optional
142
+ extras should remain searchable signals but should not make a docs/tooling
143
+ repo look like an ML project just because it offers an embeddings extra.
144
  """
145
  try:
146
  data = tomllib.loads(Path(path).read_text(encoding="utf-8-sig"))
 
155
  deps = project.get("dependencies", [])
156
  if isinstance(deps, list):
157
  raw.extend(d for d in deps if isinstance(d, str))
158
+ if include_optional:
159
+ opt = project.get("optional-dependencies", {})
160
+ if isinstance(opt, dict):
161
+ for group in opt.values():
162
+ if isinstance(group, list):
163
+ raw.extend(d for d in group if isinstance(d, str))
164
 
165
  poetry = data.get("tool", {}).get("poetry", {}) if isinstance(data.get("tool"), dict) else {}
166
  if isinstance(poetry, dict):
167
+ keys = ("dependencies", "dev-dependencies") if include_optional else ("dependencies",)
168
+ for key in keys:
169
  deps = poetry.get(key, {})
170
  if isinstance(deps, dict):
171
  raw.extend(k for k in deps.keys() if isinstance(k, str) and k.lower() != "python")
 
227
 
228
  # Collect all deps from Python and JS configs
229
  all_py_deps: list[str] = []
230
+ core_py_deps: list[str] = []
231
  all_js_deps: list[str] = []
232
  pkg_json = None
233
 
 
235
  base = os.path.basename(cfg)
236
  if base == "pyproject.toml":
237
  all_py_deps.extend(read_toml_deps(cfg))
238
+ core_py_deps.extend(read_toml_deps(cfg, include_optional=False))
239
  elif base == "requirements.txt":
240
+ deps = read_requirements(cfg)
241
+ all_py_deps.extend(deps)
242
+ core_py_deps.extend(deps)
243
  elif base == "Pipfile":
244
+ deps = read_requirements(cfg)
245
+ all_py_deps.extend(deps)
246
+ core_py_deps.extend(deps)
247
  elif base == "package.json":
248
  data = read_json_safe(cfg)
249
  if data:
 
253
  all_js_deps.extend(k.lower() for k in data[section])
254
 
255
  py_dep_set = set(all_py_deps)
256
+ py_core_dep_set = set(core_py_deps)
257
  js_dep_set = set(all_js_deps)
258
 
259
  # --- LANGUAGES ---
 
552
 
553
  # --- PROJECT TYPE ---
554
  fw_names = {f["name"] for f in profile["frameworks"]}
555
+ core_ml_deps = py_core_dep_set & {"torch", "pytorch", "tensorflow", "transformers"}
556
+ core_ai_deps = py_core_dep_set & {
557
+ "langchain",
558
+ "langchain-core",
559
+ "llama-index",
560
+ "crewai",
561
+ "dspy-ai",
562
+ }
563
  if fw_names & {"react", "vue", "angular", "svelte", "nextjs", "nuxt"}:
564
  if fw_names & {"fastapi", "django", "flask", "express", "nestjs"}:
565
  profile["project_type"] = "fullstack"
 
567
  profile["project_type"] = "frontend"
568
  elif fw_names & {"fastapi", "django", "flask", "express", "nestjs", "gin", "actix"}:
569
  profile["project_type"] = "api-service"
570
+ elif (fw_names & {"pytorch", "tensorflow", "huggingface"}) and core_ml_deps:
571
  profile["project_type"] = "ml-project"
572
+ elif (fw_names & {"langchain", "llamaindex", "crewai"}) and core_ai_deps:
573
  profile["project_type"] = "ai-agent"
574
  elif profile["infrastructure"]:
575
  profile["project_type"] = "infrastructure"
src/skill_add.py CHANGED
@@ -394,7 +394,7 @@ def add_skill(
394
  f"Split the skill or trim content before ingestion."
395
  )
396
 
397
- content = source_path.read_text(encoding="utf-8", errors="replace")
398
  line_count = len(content.splitlines())
399
 
400
  installed_path = skills_dir / name / "SKILL.md"
@@ -711,6 +711,8 @@ def main() -> None:
711
  f"\nDone: {added} added, {updated} updated, {converted} converted, "
712
  f"{skipped} skipped, {errors} errors"
713
  )
 
 
714
 
715
 
716
  if __name__ == "__main__":
 
394
  f"Split the skill or trim content before ingestion."
395
  )
396
 
397
+ content = source_path.read_text(encoding="utf-8-sig", errors="replace")
398
  line_count = len(content.splitlines())
399
 
400
  installed_path = skills_dir / name / "SKILL.md"
 
711
  f"\nDone: {added} added, {updated} updated, {converted} converted, "
712
  f"{skipped} skipped, {errors} errors"
713
  )
714
+ if errors:
715
+ sys.exit(1)
716
 
717
 
718
  if __name__ == "__main__":
src/tests/test_agent_add.py CHANGED
@@ -206,6 +206,25 @@ def test_new_agent_add_writes_converted_agent_mirror(
206
  assert mirror.read_text(encoding="utf-8") == source_text
207
 
208
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
  def test_existing_agent_update_refreshes_converted_agent_mirror(
210
  tmp_path: Path,
211
  monkeypatch: Any,
 
206
  assert mirror.read_text(encoding="utf-8") == source_text
207
 
208
 
209
+ def test_bom_prefixed_agent_frontmatter_is_accepted(
210
+ tmp_path: Path,
211
+ monkeypatch: Any,
212
+ ) -> None:
213
+ wiki, agents_dir, source = _setup_paths(tmp_path)
214
+ source.write_text("\ufeff" + _agent_text(), encoding="utf-8")
215
+ _patch_side_effects(monkeypatch)
216
+
217
+ result = agent_add.add_agent(
218
+ source_path=source,
219
+ name="reviewer-agent",
220
+ wiki_path=wiki,
221
+ agents_dir=agents_dir,
222
+ )
223
+
224
+ assert result["is_new_page"] is True
225
+ assert (wiki / "entities" / "agents" / "reviewer-agent.md").exists()
226
+
227
+
228
  def test_existing_agent_update_refreshes_converted_agent_mirror(
229
  tmp_path: Path,
230
  monkeypatch: Any,
src/tests/test_ctx_monitor.py CHANGED
@@ -3918,6 +3918,43 @@ def test_render_home_defers_sidecar_grade_scan(
3918
  assert "data-home-grade='A'" in html_out
3919
 
3920
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3921
  def test_render_skills_emits_sidebar_filters(fake_claude: Path) -> None:
3922
  _write_sidecar(fake_claude, "a", {"slug": "a", "grade": "A", "raw_score": 0.9,
3923
  "subject_type": "skill"})
@@ -4239,6 +4276,7 @@ def test_render_wiki_index_wrapper_matches_extracted_orchestration(
4239
  write_html_disk_cache=mt.cache_service.write_html_disk_cache,
4240
  wiki_render_disk_cache_path=mt.wiki_render_disk_cache_path,
4241
  wiki_index_entries=mt.wiki_index_entries,
 
4242
  wiki_stats=mt.wiki_stats,
4243
  load_sidecar=mt.load_sidecar,
4244
  dashboard_entity_types=mt.DASHBOARD_ENTITY_TYPES,
@@ -4354,6 +4392,33 @@ def test_wiki_index_entries_use_dashboard_index_without_markdown_pages(
4354
  assert mt.render_wiki_index(query="python") == html_out
4355
 
4356
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4357
  def test_render_wiki_index_supports_type_query_and_autocomplete(fake_claude: Path) -> None:
4358
  skills_dir = fake_claude / "skill-wiki" / "entities" / "skills"
4359
  agents_dir = fake_claude / "skill-wiki" / "entities" / "agents"
 
3918
  assert "data-home-grade='A'" in html_out
3919
 
3920
 
3921
+ def test_api_grades_reuses_kpi_summary_without_sidecar_scan() -> None:
3922
+ class Summary:
3923
+ def to_dict(self) -> dict[str, object]:
3924
+ return {"grade_counts": {"A": 2, "B": 1, "F": 4}}
3925
+
3926
+ def fail_scan() -> dict[str, object]:
3927
+ raise AssertionError("grades API should reuse the cached KPI summary")
3928
+
3929
+ response = readonly_api.handle_readonly_route(
3930
+ "api_grades",
3931
+ {},
3932
+ {},
3933
+ readonly_api.ReadOnlyApiDeps(
3934
+ summarize_sessions=lambda: {},
3935
+ read_manifest=lambda: {},
3936
+ status_payload=lambda: {},
3937
+ kpi_summary=lambda: Summary(),
3938
+ grade_distribution_payload=fail_scan,
3939
+ sidecar_page_payload=lambda _qs: {},
3940
+ runtime_lifecycle_summary=lambda: {},
3941
+ skillspector_audit_payload=lambda _qs: {},
3942
+ effective_config_payload=lambda: {},
3943
+ search_wiki_entities=lambda _q, _type, _limit: [],
3944
+ wiki_entity_detail=lambda _slug, _type: None,
3945
+ load_sidecar=lambda _slug, _type: None,
3946
+ graph_neighborhood=lambda _slug, _hops, _limit, _type: {},
3947
+ normalize_dashboard_entity_type=lambda raw: raw,
3948
+ ),
3949
+ )
3950
+
3951
+ assert response is not None
3952
+ assert response.payload == {
3953
+ "grades": {"A": 2, "B": 1, "C": 0, "D": 0, "F": 4},
3954
+ "total": 7,
3955
+ }
3956
+
3957
+
3958
  def test_render_skills_emits_sidebar_filters(fake_claude: Path) -> None:
3959
  _write_sidecar(fake_claude, "a", {"slug": "a", "grade": "A", "raw_score": 0.9,
3960
  "subject_type": "skill"})
 
4276
  write_html_disk_cache=mt.cache_service.write_html_disk_cache,
4277
  wiki_render_disk_cache_path=mt.wiki_render_disk_cache_path,
4278
  wiki_index_entries=mt.wiki_index_entries,
4279
+ search_wiki_entities=mt.search_wiki_entities,
4280
  wiki_stats=mt.wiki_stats,
4281
  load_sidecar=mt.load_sidecar,
4282
  dashboard_entity_types=mt.DASHBOARD_ENTITY_TYPES,
 
4392
  assert mt.render_wiki_index(query="python") == html_out
4393
 
4394
 
4395
+ def test_render_wiki_index_query_uses_bounded_search(
4396
+ monkeypatch: pytest.MonkeyPatch,
4397
+ ) -> None:
4398
+ def fail_catalog_rebuild(*args: object, **kwargs: object) -> object:
4399
+ raise AssertionError("queried catalog should use bounded search results")
4400
+
4401
+ monkeypatch.setattr(mt, "wiki_index_entries", fail_catalog_rebuild)
4402
+ monkeypatch.setattr(
4403
+ mt,
4404
+ "search_wiki_entities",
4405
+ lambda query, entity_type=None, limit=80: [{
4406
+ "slug": "code-reviewer",
4407
+ "display_slug": "code-reviewer",
4408
+ "type": entity_type or "agent",
4409
+ "description": f"{query} result",
4410
+ "tags": ["review"],
4411
+ "search_tags": ["review"],
4412
+ }],
4413
+ )
4414
+
4415
+ html_out = mt.render_wiki_index(entity_type="agent", query="review")
4416
+
4417
+ assert "code-reviewer" in html_out
4418
+ assert "review result" in html_out
4419
+ assert "href='/wiki/code-reviewer?type=agent'" in html_out
4420
+
4421
+
4422
  def test_render_wiki_index_supports_type_query_and_autocomplete(fake_claude: Path) -> None:
4423
  skills_dir = fake_claude / "skill-wiki" / "entities" / "skills"
4424
  agents_dir = fake_claude / "skill-wiki" / "entities" / "agents"
src/tests/test_harness_add.py CHANGED
@@ -230,3 +230,27 @@ def test_cli_from_json_adds_harness(
230
 
231
  assert (wiki / "entities" / "harnesses" / "text-to-cad.md").exists()
232
  assert "added: text-to-cad" in capsys.readouterr().out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  assert (wiki / "entities" / "harnesses" / "text-to-cad.md").exists()
232
  assert "added: text-to-cad" in capsys.readouterr().out
233
+
234
+
235
+ def test_cli_from_bom_json_adds_harness(
236
+ tmp_path: Path,
237
+ capsys: Any,
238
+ ) -> None:
239
+ record_path = tmp_path / "harness.json"
240
+ record_path.write_text(
241
+ "\ufeff"
242
+ + json.dumps(
243
+ {
244
+ "repo_url": "https://github.com/example/bom-harness",
245
+ "description": "Harness loaded from a Windows UTF-8 BOM JSON file.",
246
+ "tags": ["llm", "testing"],
247
+ }
248
+ ),
249
+ encoding="utf-8",
250
+ )
251
+ wiki = tmp_path / "wiki"
252
+
253
+ harness_add.main(["--from-json", str(record_path), "--wiki", str(wiki)])
254
+
255
+ assert (wiki / "entities" / "harnesses" / "bom-harness.md").exists()
256
+ assert "added: bom-harness" in capsys.readouterr().out
src/tests/test_harness_recommendations.py CHANGED
@@ -273,6 +273,21 @@ def test_ctx_init_keeps_domain_goal_strong_with_install_requirements(
273
  assert "gpt-5" not in results[0]["fit_signals"]
274
 
275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  def test_ctx_init_recommends_harness_from_wiki_frontmatter_only(
277
  monkeypatch: pytest.MonkeyPatch,
278
  ) -> None:
 
273
  assert "gpt-5" not in results[0]["fit_signals"]
274
 
275
 
276
+ def test_ctx_init_rejects_narrow_domain_harness_for_generic_coding_agent(
277
+ monkeypatch: pytest.MonkeyPatch,
278
+ ) -> None:
279
+ monkeypatch.setattr(ctx_init, "_load_recommendation_graph", _harness_graph)
280
+
281
+ results = ctx_init.recommend_harnesses(
282
+ "build an openai python coding agent with filesystem and test verification",
283
+ top_k=5,
284
+ model_provider="openai",
285
+ model="openai/gpt-4.1",
286
+ )
287
+
288
+ assert "text-to-cad" not in [row["name"] for row in results]
289
+
290
+
291
  def test_ctx_init_recommends_harness_from_wiki_frontmatter_only(
292
  monkeypatch: pytest.MonkeyPatch,
293
  ) -> None:
src/tests/test_mcp_add.py CHANGED
@@ -832,3 +832,120 @@ class TestAddMcpFromFixtures:
832
  record = McpRecord.from_dict(data)
833
  result = patched_mcp_add.add_mcp(record=record, wiki_path=wiki_dir)
834
  assert result["is_new_page"] is True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
832
  record = McpRecord.from_dict(data)
833
  result = patched_mcp_add.add_mcp(record=record, wiki_path=wiki_dir)
834
  assert result["is_new_page"] is True
835
+
836
+
837
+ class TestAddMcpCliInput:
838
+ def test_from_json_accepts_utf8_bom(
839
+ self,
840
+ monkeypatch: pytest.MonkeyPatch,
841
+ tmp_path: Path,
842
+ ) -> None:
843
+ import mcp_add # noqa: PLC0415
844
+
845
+ record_path = tmp_path / "mcp.json"
846
+ record_path.write_text(
847
+ "\ufeff"
848
+ + json.dumps(
849
+ {
850
+ "name": "bom-json-mcp",
851
+ "description": "MCP loaded from a Windows UTF-8 BOM JSON file.",
852
+ "sources": ["test"],
853
+ "github_url": "https://github.com/example/bom-json-mcp",
854
+ "tags": ["testing"],
855
+ }
856
+ ),
857
+ encoding="utf-8",
858
+ )
859
+ wiki = tmp_path / "wiki"
860
+ monkeypatch.setattr(sys, "argv", [
861
+ "mcp_add.py",
862
+ "--from-json", str(record_path),
863
+ "--wiki", str(wiki),
864
+ ])
865
+ monkeypatch.setattr("mcp_add.check_intake", _fake_allow)
866
+ monkeypatch.setattr("mcp_add.record_embedding", _fake_record_embedding)
867
+ monkeypatch.setattr("mcp_add.update_index", lambda *a, **k: None)
868
+ monkeypatch.setattr("mcp_add.append_log", lambda *a, **k: None)
869
+
870
+ mcp_add.main()
871
+
872
+ assert (
873
+ wiki / "entities" / "mcp-servers" / "b" / "bom-json-mcp.md"
874
+ ).exists()
875
+
876
+ def test_from_stdin_accepts_utf8_bom(
877
+ self,
878
+ monkeypatch: pytest.MonkeyPatch,
879
+ tmp_path: Path,
880
+ ) -> None:
881
+ import io
882
+ import mcp_add # noqa: PLC0415
883
+
884
+ wiki = tmp_path / "wiki"
885
+ monkeypatch.setattr(sys, "argv", [
886
+ "mcp_add.py",
887
+ "--from-stdin",
888
+ "--wiki", str(wiki),
889
+ ])
890
+ monkeypatch.setattr(
891
+ sys,
892
+ "stdin",
893
+ io.StringIO(
894
+ "\ufeff"
895
+ + json.dumps(
896
+ {
897
+ "name": "bom-stdin-mcp",
898
+ "description": (
899
+ "MCP loaded from a Windows UTF-8 BOM stdin stream."
900
+ ),
901
+ "sources": ["test"],
902
+ "github_url": "https://github.com/example/bom-stdin-mcp",
903
+ "tags": ["testing"],
904
+ }
905
+ )
906
+ + "\n"
907
+ ),
908
+ )
909
+ monkeypatch.setattr("mcp_add.check_intake", _fake_allow)
910
+ monkeypatch.setattr("mcp_add.record_embedding", _fake_record_embedding)
911
+ monkeypatch.setattr("mcp_add.update_index", lambda *a, **k: None)
912
+ monkeypatch.setattr("mcp_add.append_log", lambda *a, **k: None)
913
+
914
+ mcp_add.main()
915
+
916
+ assert (
917
+ wiki / "entities" / "mcp-servers" / "b" / "bom-stdin-mcp.md"
918
+ ).exists()
919
+
920
+ def test_rejected_batch_exits_nonzero(
921
+ self,
922
+ monkeypatch: pytest.MonkeyPatch,
923
+ tmp_path: Path,
924
+ ) -> None:
925
+ import mcp_add # noqa: PLC0415
926
+
927
+ record_path = tmp_path / "mcp.json"
928
+ record_path.write_text(
929
+ json.dumps(
930
+ {
931
+ "name": "rejected-cli-mcp",
932
+ "description": "Rejected MCP candidate for exit-code regression.",
933
+ "sources": ["test"],
934
+ "github_url": "https://github.com/example/rejected-cli-mcp",
935
+ "tags": ["testing"],
936
+ }
937
+ ),
938
+ encoding="utf-8",
939
+ )
940
+ monkeypatch.setattr(sys, "argv", [
941
+ "mcp_add.py",
942
+ "--from-json", str(record_path),
943
+ "--wiki", str(tmp_path / "wiki"),
944
+ ])
945
+ monkeypatch.setattr("mcp_add.check_intake", _fake_reject)
946
+ monkeypatch.setattr("mcp_add.record_embedding", _fake_record_embedding)
947
+
948
+ with pytest.raises(SystemExit) as exc:
949
+ mcp_add.main()
950
+
951
+ assert exc.value.code == 1
src/tests/test_scan_repo.py CHANGED
@@ -303,6 +303,19 @@ dev = ["black"]
303
  assert "pytest-cov" in deps
304
  assert "black" in deps
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  def test_extracts_poetry_deps(self, tmp_path: Path) -> None:
307
  p = _write(tmp_path / "pyproject.toml", """
308
  [tool.poetry]
@@ -671,6 +684,20 @@ class TestDetectStackProjectType:
671
  profile = sr.detect_stack(str(tmp_path), signals)
672
  assert profile["project_type"] == "ml-project"
673
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
674
  def test_ai_agent_when_langchain_present(self, tmp_path: Path) -> None:
675
  py = _write(tmp_path / "requirements.txt", "langchain\n")
676
  signals = _make_signals(config_files=[str(py)])
 
303
  assert "pytest-cov" in deps
304
  assert "black" in deps
305
 
306
+ def test_can_ignore_optional_deps_for_primary_stack(self, tmp_path: Path) -> None:
307
+ p = _write(tmp_path / "pyproject.toml", """
308
+ [project]
309
+ dependencies = ["core-dep"]
310
+
311
+ [project.optional-dependencies]
312
+ embeddings = ["torch>=2"]
313
+ """)
314
+
315
+ deps = sr.read_toml_deps(str(p), include_optional=False)
316
+
317
+ assert deps == ["core-dep"]
318
+
319
  def test_extracts_poetry_deps(self, tmp_path: Path) -> None:
320
  p = _write(tmp_path / "pyproject.toml", """
321
  [tool.poetry]
 
684
  profile = sr.detect_stack(str(tmp_path), signals)
685
  assert profile["project_type"] == "ml-project"
686
 
687
+ def test_optional_torch_does_not_make_project_ml(self, tmp_path: Path) -> None:
688
+ py = _write(tmp_path / "pyproject.toml", """
689
+ [project]
690
+ dependencies = ["networkx"]
691
+
692
+ [project.optional-dependencies]
693
+ embeddings = ["torch>=2"]
694
+ """)
695
+ signals = _make_signals(config_files=[str(py)])
696
+ profile = sr.detect_stack(str(tmp_path), signals)
697
+
698
+ assert any(f["name"] == "pytorch" for f in profile["frameworks"])
699
+ assert profile["project_type"] == "unknown"
700
+
701
  def test_ai_agent_when_langchain_present(self, tmp_path: Path) -> None:
702
  py = _write(tmp_path / "requirements.txt", "langchain\n")
703
  signals = _make_signals(config_files=[str(py)])
src/tests/test_skill_add.py CHANGED
@@ -633,6 +633,27 @@ class TestAddSkill:
633
  assert result["converted"] is False
634
  assert result["is_new_page"] is True
635
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  def test_required_security_scan_blocks_before_install(
637
  self, tmp_path, monkeypatch
638
  ):
 
633
  assert result["converted"] is False
634
  assert result["is_new_page"] is True
635
 
636
+ def test_bom_prefixed_skill_frontmatter_is_accepted(self, tmp_path, monkeypatch):
637
+ wiki = self._setup_wiki(tmp_path)
638
+ skills_dir = tmp_path / "skills"
639
+ self._setup_intake_allow()
640
+ _fake_batch_convert.convert_skill.return_value = {"status": "error"}
641
+ monkeypatch.setattr(_sa, "update_index", MagicMock())
642
+ monkeypatch.setattr(_sa, "append_log", MagicMock())
643
+
644
+ source = tmp_path / "SKILL.md"
645
+ source.write_text("\ufeff" + self._skill_text(), encoding="utf-8")
646
+
647
+ result = add_skill(
648
+ source_path=source,
649
+ name="myskill",
650
+ wiki_path=wiki,
651
+ skills_dir=skills_dir,
652
+ )
653
+
654
+ assert result["name"] == "myskill"
655
+ assert result["is_new_page"] is True
656
+
657
  def test_required_security_scan_blocks_before_install(
658
  self, tmp_path, monkeypatch
659
  ):