mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
06e16de066
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.
120 lines
4.3 KiB
Python
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]"
|