Files
turnstone/tests/test_storage_skill_spec_uplift.py
T
Patrick Buckley 06e16de066 chore(skills): drop Anthropic attribution from SKILL.md spec references
Two related cleanups landed together because they touch the same surface
(skill-spec uplift PRs #569/#570/#571/#572):

  1. Wording: replace "Anthropic spec" / "Anthropic Claude Code skill spec"
     with "SKILL.md spec" across admin UI tooltips, code comments, test
     docstrings, migration 056's module docstring, and the user-facing
     `arguments` description in tools/skills.json.  Renames a parser test
     `test_anthropic_tags` -> `test_nested_metadata_tags` and consolidates
     a parse-API test of the same shape; fixture author renamed
     `Anthropic` -> `Acme` to keep the fixture neutral.  Legitimate
     provider/SDK/API references (provider name, api.anthropic.com,
     `_anthropic.py`, capability comments) are intentionally untouched.

  2. Admin UX: in the Create + Edit Skill modals, six fields per modal
     (Compatibility, Paths, Hide-from-skill-picker, Arguments, Argument
     hint, Activation) had long uppercase label-hint spans crammed into
     the visible label.  Migrated each to the existing
     `.settings-help-btn` + `.settings-help-popover` pattern already used
     in the Settings tab — short label + inline `?` button that opens a
     styled popover with proper `<code>` formatting for technical tokens.

     Pattern reuse required two small generalisations in admin.js:

       * `_toggleSettingsHelp` now looks up the popover via a new
         `data-help-target="<id>"` attribute first, falling back to the
         settings-tab `.settings-label-col` ancestor lookup.
       * `_closeAllSettingsHelp` mirrors the same dual-path lookup when
         resetting `aria-expanded`, so modal buttons don't get stuck on
         `aria-expanded="true"` after another popover opens.
       * Added a document-delegated click handler that fires only for
         buttons with `data-help-target`; existing per-button binding
         in the settings-tab render path is unchanged.

     CSS: `.settings-help-btn` now paints its `?` via `::after` with the
     button's own `font-size: 0`, so prettier-introduced whitespace
     inside the new HTML buttons can't off-center the glyph.  The same
     rule applies to existing admin.js-generated buttons (text content
     hidden, pseudo identical).  Small additions for
     `.settings-help-popover code` / `strong` styling so technical
     tokens render with the same monospace pill treatment used elsewhere
     in skill UI.

Known follow-ups (intentionally NOT in this PR):
  * Migrate the settings-tab `_renderSettingRow` button assembly to the
    empty-`<button>` + `data-help-target` form so the per-button
    addEventListener loop can be dropped in favour of pure document
    delegation, and the `font-size: 0` rule stops being a workaround for
    two markup styles.
  * The 12 new popover blocks are duplicated verbatim between the
    Create and Edit modals (same as the rest of the create/edit modal
    pair).  A small renderer that emits popovers from a shared data
    object would eliminate the drift risk but is unrelated cleanup.
2026-05-24 14:56:43 -07:00

120 lines
4.3 KiB
Python

"""Storage round-trip for the SKILL.md spec-uplift columns (migration 056).
Each column is parsed/stored/editable in PR1 (#569); the consumers
(autoload filter / menu hide / argument substitution) land in
follow-up PRs. These tests cover only the persistence layer — that
the four new fields survive create + read + update without loss.
"""
from __future__ import annotations
import json
from typing import Any
def _create(storage: Any, **kw: Any) -> str:
template_id = kw.pop("template_id", "spec1")
storage.create_prompt_template(
template_id=template_id,
name=kw.pop("name", "skill-one"),
category="general",
content="",
variables="[]",
is_default=False,
org_id="",
created_by="test",
**kw,
)
return template_id
class TestPathsRoundTrip:
def test_default_empty_array(self, storage: Any) -> None:
_create(storage)
row = storage.get_prompt_template("spec1")
assert row is not None
assert row["paths"] == "[]"
def test_create_with_paths(self, storage: Any) -> None:
_create(storage, paths=json.dumps(["**/*.py", "packages/api/**"]))
row = storage.get_prompt_template("spec1")
assert row is not None
assert json.loads(row["paths"]) == ["**/*.py", "packages/api/**"]
def test_update_paths(self, storage: Any) -> None:
_create(storage)
ok = storage.update_prompt_template("spec1", paths=json.dumps(["docs/**"]))
assert ok is True
row = storage.get_prompt_template("spec1")
assert row is not None
assert json.loads(row["paths"]) == ["docs/**"]
class TestHiddenFromMenu:
def test_default_false(self, storage: Any) -> None:
_create(storage)
row = storage.get_prompt_template("spec1")
assert row is not None
assert row["hidden_from_menu"] is False
def test_create_hidden(self, storage: Any) -> None:
_create(storage, hidden_from_menu=True)
row = storage.get_prompt_template("spec1")
assert row is not None
assert row["hidden_from_menu"] is True
def test_update_hidden(self, storage: Any) -> None:
_create(storage)
ok = storage.update_prompt_template("spec1", hidden_from_menu=1)
assert ok is True
row = storage.get_prompt_template("spec1")
assert row is not None
assert row["hidden_from_menu"] is True
def test_update_hidden_with_bool(self, storage: Any) -> None:
"""``hidden_from_menu`` lives on an INTEGER column but the wire type
from JSON / Pydantic is ``bool``. ``update_prompt_template`` must
coerce explicitly — without coercion, a PG INSERT of ``True`` into
an Integer column is driver-dependent and was the gap Copilot
review on PR #574 flagged."""
_create(storage)
ok = storage.update_prompt_template("spec1", hidden_from_menu=True)
assert ok is True
row = storage.get_prompt_template("spec1")
assert row is not None
assert row["hidden_from_menu"] is True
# Also round-trips the false transition.
ok = storage.update_prompt_template("spec1", hidden_from_menu=False)
assert ok is True
row = storage.get_prompt_template("spec1")
assert row is not None
assert row["hidden_from_menu"] is False
class TestArguments:
def test_default_empty_array(self, storage: Any) -> None:
_create(storage)
row = storage.get_prompt_template("spec1")
assert row is not None
assert row["arguments"] == "[]"
def test_create_with_arguments(self, storage: Any) -> None:
_create(storage, arguments=json.dumps(["issue", "branch"]))
row = storage.get_prompt_template("spec1")
assert row is not None
assert json.loads(row["arguments"]) == ["issue", "branch"]
class TestArgumentHint:
def test_default_empty_string(self, storage: Any) -> None:
_create(storage)
row = storage.get_prompt_template("spec1")
assert row is not None
assert row["argument_hint"] == ""
def test_create_with_argument_hint(self, storage: Any) -> None:
_create(storage, argument_hint="[issue-number]")
row = storage.get_prompt_template("spec1")
assert row is not None
assert row["argument_hint"] == "[issue-number]"