mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
93af031fc7
Implements PR1 of issue #569 — parser + storage + admin UI for the Anthropic Claude Code skill spec `paths:` SKILL.md frontmatter field (glob patterns gating model-initiated autoload). The autoload filter that consumes `paths` is deferred to a follow-up PR pending the workstream-CWD design discussion. Migration 056 bundles three additional columns whose consumer PRs are filed but not yet implemented: * `hidden_from_menu` (boolean) — backs the spec's `user-invocable: false` (issue #571). * `arguments` (JSON list) — backs spec `arguments:` named arg slots (issue #572). * `argument_hint` (string) — autocomplete display string (issue #572). The deferred columns surface in `SkillInfo` (response) so consumers can read them, but are deliberately absent from `CreateSkillRequest` and `UpdateSkillRequest` — the create/update handlers don't yet read them and advertising a writable field the handler would silently ignore would be an OpenAPI lie. Surface - Parser: `ParsedSkill.paths` populated from frontmatter; accepts the spec's YAML-list-or-CSV-string shape via the existing `_extract_list` machinery. - Storage: 4 new columns on `prompt_templates`; `SKILL_MUTABLE` extended; `_row_to_dict` calls extended to cast the new bool; protocol + SQLite + PostgreSQL `create_prompt_template` signatures threaded. - HTTP: admin create/update/install/parse handlers plumb `paths` through. Pydantic schemas extended accordingly. - Admin UI: `skill-paths` and `etm-paths` inputs on the create + edit modals; field map and read/write helpers wired across paste-parse, reset, create-send, edit-load, edit-send, and the readonly-disable list. Notable - `_canonicalize_skill_string_list` collapses the list-or-CSV-or-JSON- string normalization shared between admin_create_skill and admin_update_skill. Treats `None` as no-value so a body containing `{"paths": null}` doesn't CSV-split through `str(None)` and store the literal `["None"]`. Will back `arguments` once #572 wires its consumer. Tests - Parser: TestPaths covers YAML list, CSV string, empty, full- frontmatter integration (tests/test_skill_parser.py). - Storage: round-trip suite covers create + read + update for each of the four new columns on both backends (tests/test_storage_skill_spec_uplift.py). - Helper: focused unit tests for the canonicalizer including the regression-net case for the null-corruption bug (tests/test_canonicalize_skill_string_list.py). - HTTP boundary: extended test_parses_full_frontmatter + test_parses_minimal_frontmatter to assert `paths` survives the admin parse endpoint.
73 lines
2.9 KiB
Python
73 lines
2.9 KiB
Python
"""Unit tests for ``_canonicalize_skill_string_list`` in console.server.
|
|
|
|
Backs the admin create/update handlers' wire-shape normalization for
|
|
JSON-array-string skill fields (``paths`` today; ``arguments`` once
|
|
#572 wires its consumer). The interesting cases are the corruption
|
|
paths the regex / split previously took on ``None``/empty input —
|
|
without explicit None handling, ``str(None)`` slid through CSV-split
|
|
and stored the literal value ``["None"]``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from turnstone.console.server import _canonicalize_skill_string_list
|
|
|
|
|
|
class TestList:
|
|
def test_list_of_strings(self) -> None:
|
|
assert _canonicalize_skill_string_list(["**/*.py", "docs/**"]) == '["**/*.py", "docs/**"]'
|
|
|
|
def test_list_trims_and_drops_blank(self) -> None:
|
|
assert _canonicalize_skill_string_list([" a ", "", "b"]) == '["a", "b"]'
|
|
|
|
def test_empty_list(self) -> None:
|
|
assert _canonicalize_skill_string_list([]) == "[]"
|
|
|
|
|
|
class TestJsonString:
|
|
def test_valid_json_array(self) -> None:
|
|
assert _canonicalize_skill_string_list('["**/*.py", "docs/**"]') == '["**/*.py", "docs/**"]'
|
|
|
|
def test_json_array_trims_elements(self) -> None:
|
|
assert _canonicalize_skill_string_list('[" a ", " ", "b"]') == '["a", "b"]'
|
|
|
|
def test_malformed_json_array_collapses_to_empty(self) -> None:
|
|
"""``[``-prefixed unparseable input → empty array, not CSV-split."""
|
|
assert _canonicalize_skill_string_list("[not-json") == "[]"
|
|
|
|
def test_non_array_json_treated_as_csv(self) -> None:
|
|
"""A string that doesn't start with ``[`` is CSV input by contract,
|
|
even if it happens to be valid JSON for some other shape. No commas
|
|
means a single-element list. Pragmatic over strict — the admin UI
|
|
round-trips through this helper and a typo doesn't need to error."""
|
|
assert _canonicalize_skill_string_list('{"k": "v"}') == '["{\\"k\\": \\"v\\"}"]'
|
|
|
|
|
|
class TestCsvString:
|
|
def test_comma_separated(self) -> None:
|
|
assert (
|
|
_canonicalize_skill_string_list("**/*.py, docs/**, src/api/**")
|
|
== '["**/*.py", "docs/**", "src/api/**"]'
|
|
)
|
|
|
|
def test_csv_trims_and_drops_blank(self) -> None:
|
|
assert _canonicalize_skill_string_list("a , , b ,") == '["a", "b"]'
|
|
|
|
def test_single_value_no_comma(self) -> None:
|
|
assert _canonicalize_skill_string_list("**/*.py") == '["**/*.py"]'
|
|
|
|
|
|
class TestNullAndEmpty:
|
|
def test_none_returns_empty_array(self) -> None:
|
|
"""``None`` must NOT corrupt into ``'["None"]'`` (regression bug-1/bug-2)."""
|
|
assert _canonicalize_skill_string_list(None) == "[]"
|
|
|
|
def test_empty_string(self) -> None:
|
|
assert _canonicalize_skill_string_list("") == "[]"
|
|
|
|
def test_whitespace_only_string(self) -> None:
|
|
assert _canonicalize_skill_string_list(" ") == "[]"
|
|
|
|
def test_empty_json_array_string(self) -> None:
|
|
assert _canonicalize_skill_string_list("[]") == "[]"
|