From acf2ed809d8423f66d279a071eda659caa8f2eb8 Mon Sep 17 00:00:00 2001 From: "Kai (via Mike Darlington)" Date: Mon, 16 Feb 2026 19:17:42 +0000 Subject: [PATCH 1/4] feat: add persona-based prompt versioning - Add persona_prompts table with migration (007_persona_prompts.sql) - Implement PersonaPromptStore with CRUD operations - Add database models (PersonaPromptRow, PersonaPromptCreate/Response) - Create persona-prompts API endpoints: - GET /api/v1/persona-prompts/{persona} (latest version) - GET /api/v1/persona-prompts/{persona}/{version} (specific version) - POST /api/v1/persona-prompts/{persona} (create new version) - GET /api/v1/persona-prompts/{persona}/versions (list versions) - POST /api/v1/persona-prompts/seed (seed initial personas) - Seed 5 initial personas (researcher, developer, reviewer, tester, architect) - Add comprehensive unit tests for store and API endpoints (20 tests) - All existing tests pass (290 total tests) --- prompt_forge/api/models.py | 20 ++ prompt_forge/api/persona_prompts.py | 79 ++++++++ prompt_forge/api/personas.py | 89 +++++++++ prompt_forge/api/router.py | 2 + .../db/migrations/007_persona_prompts.sql | 11 + prompt_forge/db/models.py | 11 + prompt_forge/db/persona_store.py | 180 +++++++++++++++++ scripts/seed_personas.py | 152 ++++++++++++++ tests/conftest.py | 37 +++- tests/test_api_persona_prompts.py | 188 ++++++++++++++++++ tests/test_persona_store.py | 164 +++++++++++++++ 11 files changed, 932 insertions(+), 1 deletion(-) create mode 100644 prompt_forge/api/persona_prompts.py create mode 100644 prompt_forge/api/personas.py create mode 100644 prompt_forge/db/migrations/007_persona_prompts.sql create mode 100644 prompt_forge/db/persona_store.py create mode 100755 scripts/seed_personas.py create mode 100644 tests/test_api_persona_prompts.py create mode 100644 tests/test_persona_store.py diff --git a/prompt_forge/api/models.py b/prompt_forge/api/models.py index 9b26177..3955f86 100644 --- a/prompt_forge/api/models.py +++ b/prompt_forge/api/models.py @@ -351,3 +351,23 @@ class ModelEffectivenessResponse(BaseModel): economy: EffectivenessSummary | None = None standard: EffectivenessSummary | None = None premium: EffectivenessSummary | None = None + + +# --- Persona Prompts --- + + +class PersonaPromptCreate(BaseModel): + """Create a new persona prompt version.""" + + template: str = Field(..., min_length=1) + + +class PersonaPromptResponse(BaseModel): + """Persona prompt response.""" + + id: UUID + persona: str + version: int + template: str + is_latest: bool + created_at: datetime diff --git a/prompt_forge/api/persona_prompts.py b/prompt_forge/api/persona_prompts.py new file mode 100644 index 0000000..d821d79 --- /dev/null +++ b/prompt_forge/api/persona_prompts.py @@ -0,0 +1,79 @@ +"""Persona prompt versioning API endpoints.""" + +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException + +from prompt_forge.api.models import PersonaPromptCreate, PersonaPromptResponse +from prompt_forge.db.persona_store import PersonaPromptStore, get_persona_store + +router = APIRouter() + + +@router.get("/{persona}", response_model=PersonaPromptResponse) +async def get_persona_prompt_latest( + persona: str, + store: PersonaPromptStore = Depends(get_persona_store), +) -> PersonaPromptResponse: + """Get the latest version of a persona prompt.""" + prompt = store.get_latest_persona_prompt(persona) + if not prompt: + raise HTTPException(status_code=404, detail=f"Persona '{persona}' not found") + + return PersonaPromptResponse(**prompt.model_dump()) + + +@router.post("/seed", status_code=201) +async def seed_initial_personas( + store: PersonaPromptStore = Depends(get_persona_store), +) -> dict[str, str]: + """Seed initial personas with basic templates.""" + try: + store.seed_initial_personas() + return {"message": "Initial personas seeded successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to seed personas: {str(e)}") + + +@router.get("/{persona}/versions", response_model=list[PersonaPromptResponse]) +async def list_persona_prompt_versions( + persona: str, + store: PersonaPromptStore = Depends(get_persona_store), +) -> list[PersonaPromptResponse]: + """List all versions of a persona prompt.""" + prompts = store.list_persona_versions(persona) + if not prompts: + raise HTTPException(status_code=404, detail=f"Persona '{persona}' not found") + + return [PersonaPromptResponse(**prompt.model_dump()) for prompt in prompts] + + +@router.post("/{persona}", response_model=PersonaPromptResponse, status_code=201) +async def create_persona_prompt_version( + persona: str, + data: PersonaPromptCreate, + store: PersonaPromptStore = Depends(get_persona_store), +) -> PersonaPromptResponse: + """Create a new version of a persona prompt (auto-increments version, sets previous is_latest=false).""" + try: + prompt = store.create_persona_prompt_version(persona, data.template) + return PersonaPromptResponse(**prompt.model_dump()) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to create persona prompt: {str(e)}") + + +@router.get("/{persona}/{version}", response_model=PersonaPromptResponse) +async def get_persona_prompt_version( + persona: str, + version: int, + store: PersonaPromptStore = Depends(get_persona_store), +) -> PersonaPromptResponse: + """Get a specific version of a persona prompt.""" + prompt = store.get_persona_prompt_version(persona, version) + if not prompt: + raise HTTPException( + status_code=404, + detail=f"Persona '{persona}' version {version} not found" + ) + + return PersonaPromptResponse(**prompt.model_dump()) \ No newline at end of file diff --git a/prompt_forge/api/personas.py b/prompt_forge/api/personas.py new file mode 100644 index 0000000..7024b31 --- /dev/null +++ b/prompt_forge/api/personas.py @@ -0,0 +1,89 @@ +"""Persona convenience endpoints — thin wrappers over the prompt resolver.""" + +from __future__ import annotations + +import os +from typing import Any + +import httpx +import structlog +from fastapi import APIRouter, Depends, HTTPException, Query + +from prompt_forge.api.models import VersionResponse +from prompt_forge.core.resolver import PromptResolver, get_resolver + +logger = structlog.get_logger() + +router = APIRouter() + +ALEXANDRIA_URL = os.getenv("ALEXANDRIA_URL", "") + + +@router.get("/{persona}", response_model=VersionResponse) +async def get_persona( + persona: str, + branch: str = Query("main"), + strategy: str = Query("latest"), + resolver: PromptResolver = Depends(get_resolver), +) -> VersionResponse: + """Get the latest version of a persona prompt.""" + try: + version = resolver.resolve(slug=persona, branch=branch, strategy=strategy) + return VersionResponse(**version) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + +@router.get("/{persona}/{version}", response_model=VersionResponse) +async def get_persona_version( + persona: str, + version: int, + branch: str = Query("main"), + resolver: PromptResolver = Depends(get_resolver), +) -> VersionResponse: + """Get a specific version of a persona prompt.""" + try: + result = resolver.resolve( + slug=persona, branch=branch, version=version, strategy="pinned" + ) + return VersionResponse(**result) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + +@router.get("/{persona}/effective") +async def get_persona_effective( + persona: str, + branch: str = Query("main"), + strategy: str = Query("latest"), + resolver: PromptResolver = Depends(get_resolver), +) -> dict[str, Any]: + """Get latest persona version merged with Alexandria context (if configured).""" + try: + version = resolver.resolve(slug=persona, branch=branch, strategy=strategy) + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + result: dict[str, Any] = { + "version": VersionResponse(**version).model_dump(mode="json"), + "alexandria_context": None, + } + + if ALEXANDRIA_URL: + try: + async with httpx.AsyncClient(timeout=5.0) as client: + resp = await client.get( + f"{ALEXANDRIA_URL}/api/v1/context/{persona}", + ) + if resp.status_code == 200: + result["alexandria_context"] = resp.json() + else: + logger.warning( + "personas.alexandria_unavailable", + persona=persona, + status=resp.status_code, + ) + except httpx.HTTPError as exc: + logger.warning("personas.alexandria_error", persona=persona, error=str(exc)) + + return result diff --git a/prompt_forge/api/router.py b/prompt_forge/api/router.py index 610e1a4..b9ef5db 100644 --- a/prompt_forge/api/router.py +++ b/prompt_forge/api/router.py @@ -8,6 +8,7 @@ from prompt_forge.api.branches import router as branches_router from prompt_forge.api.compose import router as compose_router from prompt_forge.api.effectiveness import router as effectiveness_router +from prompt_forge.api.persona_prompts import router as persona_prompts_router from prompt_forge.api.prompts import router as prompts_router from prompt_forge.api.scan import router as scan_router from prompt_forge.api.subscriptions import router as subscriptions_router @@ -20,6 +21,7 @@ api_router.include_router(versions_router, prefix="/prompts", tags=["versions"]) api_router.include_router(branches_router, prefix="/prompts", tags=["branches"]) api_router.include_router(subscriptions_router, prefix="/prompts", tags=["subscriptions"]) +api_router.include_router(persona_prompts_router, prefix="/persona-prompts", tags=["persona-prompts"]) api_router.include_router(agents_router, prefix="/agents", tags=["agents"]) api_router.include_router(compose_router, tags=["composition"]) api_router.include_router(usage_router, prefix="/usage", tags=["usage"]) diff --git a/prompt_forge/db/migrations/007_persona_prompts.sql b/prompt_forge/db/migrations/007_persona_prompts.sql new file mode 100644 index 0000000..585ab94 --- /dev/null +++ b/prompt_forge/db/migrations/007_persona_prompts.sql @@ -0,0 +1,11 @@ +CREATE TABLE persona_prompts ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + persona TEXT NOT NULL, + version INT NOT NULL DEFAULT 1, + template TEXT NOT NULL, + is_latest BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(persona, version) +); + +CREATE INDEX idx_persona_prompts_latest ON persona_prompts(persona, is_latest) WHERE is_latest = TRUE; \ No newline at end of file diff --git a/prompt_forge/db/models.py b/prompt_forge/db/models.py index 2f5d01b..d4ede45 100644 --- a/prompt_forge/db/models.py +++ b/prompt_forge/db/models.py @@ -66,3 +66,14 @@ class UsageLogRow(BaseModel): outcome: str latency_ms: int | None feedback: dict[str, Any] | None + + +class PersonaPromptRow(BaseModel): + """Row from the persona_prompts table.""" + + id: UUID + persona: str + version: int + template: str + is_latest: bool + created_at: datetime diff --git a/prompt_forge/db/persona_store.py b/prompt_forge/db/persona_store.py new file mode 100644 index 0000000..0547b87 --- /dev/null +++ b/prompt_forge/db/persona_store.py @@ -0,0 +1,180 @@ +"""Store layer for persona prompt operations.""" + +from __future__ import annotations + +from typing import Any +from uuid import UUID + +import structlog + +from prompt_forge.db.client import SupabaseClient, get_supabase_client +from prompt_forge.db.models import PersonaPromptRow + +logger = structlog.get_logger() + + +class PersonaPromptStore: + """Store operations for persona prompts.""" + + def __init__(self, db: SupabaseClient) -> None: + self.db = db + + def get_latest_persona_prompt(self, persona: str) -> PersonaPromptRow | None: + """Get the latest version of a persona prompt.""" + rows = self.db.select( + "persona_prompts", + filters={"persona": persona, "is_latest": True}, + limit=1, + ) + if not rows: + return None + return PersonaPromptRow(**rows[0]) + + def get_persona_prompt_version(self, persona: str, version: int) -> PersonaPromptRow | None: + """Get a specific version of a persona prompt.""" + rows = self.db.select( + "persona_prompts", + filters={"persona": persona, "version": version}, + limit=1, + ) + if not rows: + return None + return PersonaPromptRow(**rows[0]) + + def create_persona_prompt_version(self, persona: str, template: str) -> PersonaPromptRow: + """Create a new version of a persona prompt.""" + # Get the next version number + existing_rows = self.db.select( + "persona_prompts", + filters={"persona": persona}, + order_by="version", + ascending=False, + limit=1, + ) + next_version = 1 if not existing_rows else existing_rows[0]["version"] + 1 + + # Mark all existing versions as not latest + if existing_rows: + # Update all existing versions to not be latest + query = self.db.client.table("persona_prompts").update( + {"is_latest": False} + ).eq("persona", persona) + query.execute() + + # Create the new version + data = { + "persona": persona, + "version": next_version, + "template": template, + "is_latest": True, + } + + row_data = self.db.insert("persona_prompts", data) + logger.info( + "persona_prompt.created", + persona=persona, + version=next_version, + id=row_data["id"], + ) + + return PersonaPromptRow(**row_data) + + def list_persona_versions(self, persona: str) -> list[PersonaPromptRow]: + """List all versions of a persona prompt.""" + rows = self.db.select( + "persona_prompts", + filters={"persona": persona}, + order_by="version", + ascending=False, + ) + return [PersonaPromptRow(**row) for row in rows] + + def seed_initial_personas(self) -> None: + """Seed initial personas with basic templates.""" + initial_personas = { + "researcher": """You are a Researcher persona with expertise in information gathering and analysis. + +Your objective: {{objective}} + +Context: {{context}} + +Constraints: {{constraints}} + +Scope paths: {{scope_paths}} + +Alexandria context: {{alexandria_context}} + +Focus on thorough research, fact-checking, and providing comprehensive information with proper citations and sources.""", + + "developer": """You are a Developer persona with expertise in software engineering and coding. + +Your objective: {{objective}} + +Context: {{context}} + +Constraints: {{constraints}} + +Scope paths: {{scope_paths}} + +Alexandria context: {{alexandria_context}} + +Focus on clean, efficient code, best practices, testing, and maintainable solutions. Provide working code examples and explanations.""", + + "reviewer": """You are a Reviewer persona with expertise in code review and quality assurance. + +Your objective: {{objective}} + +Context: {{context}} + +Constraints: {{constraints}} + +Scope paths: {{scope_paths}} + +Alexandria context: {{alexandria_context}} + +Focus on thorough code review, identifying issues, suggesting improvements, and ensuring quality standards are met.""", + + "tester": """You are a Tester persona with expertise in testing strategies and quality assurance. + +Your objective: {{objective}} + +Context: {{context}} + +Constraints: {{constraints}} + +Scope paths: {{scope_paths}} + +Alexandria context: {{alexandria_context}} + +Focus on comprehensive testing strategies, test case design, bug identification, and ensuring software quality through rigorous testing.""", + + "architect": """You are an Architect persona with expertise in system design and technical architecture. + +Your objective: {{objective}} + +Context: {{context}} + +Constraints: {{constraints}} + +Scope paths: {{scope_paths}} + +Alexandria context: {{alexandria_context}} + +Focus on high-level system design, scalability, performance, security considerations, and architectural best practices.""", + } + + for persona, template in initial_personas.items(): + # Check if persona already exists + existing = self.get_latest_persona_prompt(persona) + if existing: + logger.info("persona_prompt.seed_skip", persona=persona, reason="already_exists") + continue + + # Create initial version + self.create_persona_prompt_version(persona, template) + logger.info("persona_prompt.seeded", persona=persona) + + +def get_persona_store() -> PersonaPromptStore: + """Get PersonaPromptStore instance.""" + return PersonaPromptStore(get_supabase_client()) \ No newline at end of file diff --git a/scripts/seed_personas.py b/scripts/seed_personas.py new file mode 100755 index 0000000..7bf898d --- /dev/null +++ b/scripts/seed_personas.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Seed initial persona prompt templates into PromptForge. + +Usage: + python scripts/seed_personas.py # against real DB + python scripts/seed_personas.py --base-url http://localhost:8083 # against running server +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +import httpx + +PERSONAS = [ + { + "slug": "researcher", + "name": "Researcher", + "description": "Deep-dive investigation and analysis persona", + "content": { + "system_prompt": ( + "You are {{persona}}, an expert research agent (v{{version}}).\n\n" + "## Objective\n{{objective}}\n\n" + "## Approach\n" + "- Search broadly, then narrow down to the most relevant sources\n" + "- Cross-reference findings across multiple files and documents\n" + "- Summarize key insights with evidence and file references\n" + "- Flag uncertainties and knowledge gaps explicitly\n\n" + "## Constraints\n{{constraints}}\n\n" + "## Working Directory\n{{workdir}}" + ), + }, + }, + { + "slug": "developer", + "name": "Developer", + "description": "Implementation and coding persona", + "content": { + "system_prompt": ( + "You are {{persona}}, a skilled software developer (v{{version}}).\n\n" + "## Objective\n{{objective}}\n\n" + "## Approach\n" + "- Read existing code before modifying it\n" + "- Follow the project's established patterns and conventions\n" + "- Write minimal, focused changes that solve the stated objective\n" + "- Ensure all changes compile/parse correctly before finishing\n\n" + "## Constraints\n{{constraints}}\n\n" + "## Working Directory\n{{workdir}}" + ), + }, + }, + { + "slug": "reviewer", + "name": "Reviewer", + "description": "Code review and quality assessment persona", + "content": { + "system_prompt": ( + "You are {{persona}}, a thorough code reviewer (v{{version}}).\n\n" + "## Objective\n{{objective}}\n\n" + "## Approach\n" + "- Check for correctness, security vulnerabilities, and edge cases\n" + "- Evaluate code clarity, naming, and adherence to project conventions\n" + "- Identify potential performance issues or resource leaks\n" + "- Provide actionable feedback with specific file and line references\n\n" + "## Constraints\n{{constraints}}\n\n" + "## Working Directory\n{{workdir}}" + ), + }, + }, + { + "slug": "tester", + "name": "Tester", + "description": "Test creation and validation persona", + "content": { + "system_prompt": ( + "You are {{persona}}, a meticulous test engineer (v{{version}}).\n\n" + "## Objective\n{{objective}}\n\n" + "## Approach\n" + "- Identify the critical paths and edge cases to cover\n" + "- Write tests that match the project's existing test framework and style\n" + "- Include both positive and negative test cases\n" + "- Ensure tests are deterministic and independent\n\n" + "## Constraints\n{{constraints}}\n\n" + "## Working Directory\n{{workdir}}" + ), + }, + }, + { + "slug": "architect", + "name": "Architect", + "description": "System design and planning persona", + "content": { + "system_prompt": ( + "You are {{persona}}, a systems architect (v{{version}}).\n\n" + "## Objective\n{{objective}}\n\n" + "## Approach\n" + "- Analyze the existing architecture before proposing changes\n" + "- Consider scalability, maintainability, and operational concerns\n" + "- Propose concrete file and module structure, not just abstract ideas\n" + "- Document trade-offs and rationale for key decisions\n\n" + "## Constraints\n{{constraints}}\n\n" + "## Working Directory\n{{workdir}}" + ), + }, + }, +] + + +def seed_via_api(base_url: str) -> None: + """Seed personas by POSTing to the PromptForge API.""" + with httpx.Client(base_url=base_url, timeout=10.0) as client: + for persona in PERSONAS: + payload = { + "slug": persona["slug"], + "name": persona["name"], + "type": "persona", + "description": persona["description"], + "tags": ["persona", "seed"], + "metadata": {"seeded": True}, + "content": persona["content"], + "initial_message": "Seed persona template", + } + resp = client.post("/api/v1/prompts", json=payload) + if resp.status_code == 201: + print(f" Created persona: {persona['slug']}") + elif resp.status_code == 409: + print(f" Skipped (exists): {persona['slug']}") + else: + print( + f" FAILED {persona['slug']}: {resp.status_code} {resp.text}", + file=sys.stderr, + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Seed persona prompts into PromptForge") + parser.add_argument( + "--base-url", + default="http://localhost:8400", + help="PromptForge API base URL (default: http://localhost:8400)", + ) + args = parser.parse_args() + + print(f"Seeding {len(PERSONAS)} personas to {args.base_url} ...") + seed_via_api(args.base_url) + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/tests/conftest.py b/tests/conftest.py index 981303a..72d9582 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,11 +24,42 @@ def __init__(self): "prompt_usage_log": [], "audit_log": [], "prompt_subscriptions": [], + "persona_prompts": [], } @property def client(self): - return MagicMock() + """Mock the client.table().update().eq() pattern used in persona store.""" + mock_client = MagicMock() + + def mock_table(table_name): + table_mock = MagicMock() + + def mock_update(data): + update_mock = MagicMock() + + def mock_eq(column, value): + eq_mock = MagicMock() + + def mock_execute(): + # Handle bulk updates for persona_prompts + if table_name == "persona_prompts" and column == "persona": + for row in self._tables.get(table_name, []): + if row.get(column) == value: + row.update(data) + return MagicMock() + + eq_mock.execute = mock_execute + return eq_mock + + update_mock.eq = mock_eq + return update_mock + + table_mock.update = mock_update + return table_mock + + mock_client.table = mock_table + return mock_client def insert(self, table: str, data: dict[str, Any]) -> dict[str, Any]: record = { @@ -117,6 +148,9 @@ def app(mock_db): from prompt_forge.core.resolver import get_resolver from prompt_forge.core.composer import get_composer from prompt_forge.db.client import get_supabase_client + from prompt_forge.db.persona_store import get_persona_store, PersonaPromptStore + + persona_store = PersonaPromptStore(mock_db) _app.dependency_overrides[get_registry] = lambda: registry _app.dependency_overrides[get_vcs] = lambda: vcs @@ -124,6 +158,7 @@ def app(mock_db): _app.dependency_overrides[get_composer] = lambda: composer _app.dependency_overrides[get_supabase_client] = lambda: mock_db _app.dependency_overrides[get_audit_logger] = lambda: audit + _app.dependency_overrides[get_persona_store] = lambda: persona_store yield _app diff --git a/tests/test_api_persona_prompts.py b/tests/test_api_persona_prompts.py new file mode 100644 index 0000000..59eeb6c --- /dev/null +++ b/tests/test_api_persona_prompts.py @@ -0,0 +1,188 @@ +"""Tests for persona prompt API endpoints.""" + +from __future__ import annotations + +from fastapi.testclient import TestClient + +from prompt_forge.db.persona_store import get_persona_store + + +def test_get_persona_prompt_latest_not_found(client: TestClient): + """Test getting latest persona prompt when it doesn't exist.""" + response = client.get("/api/v1/persona-prompts/nonexistent") + assert response.status_code == 404 + assert response.json()["detail"] == "Persona 'nonexistent' not found" + + +def test_get_persona_prompt_version_not_found(client: TestClient): + """Test getting specific persona prompt version when it doesn't exist.""" + response = client.get("/api/v1/persona-prompts/nonexistent/1") + assert response.status_code == 404 + assert response.json()["detail"] == "Persona 'nonexistent' version 1 not found" + + +def test_create_persona_prompt_version(client: TestClient): + """Test creating a persona prompt version.""" + template = "You are a developer. Context: {{context}}" + + response = client.post( + "/api/v1/persona-prompts/developer", + json={"template": template} + ) + + assert response.status_code == 201 + data = response.json() + assert data["persona"] == "developer" + assert data["version"] == 1 + assert data["template"] == template + assert data["is_latest"] is True + assert "id" in data + assert "created_at" in data + + +def test_get_persona_prompt_latest_after_create(client: TestClient): + """Test getting latest persona prompt after creating one.""" + template = "You are a tester. Context: {{context}}" + + # Create the persona prompt + create_response = client.post( + "/api/v1/persona-prompts/tester", + json={"template": template} + ) + assert create_response.status_code == 201 + + # Get the latest version + response = client.get("/api/v1/persona-prompts/tester") + assert response.status_code == 200 + data = response.json() + assert data["persona"] == "tester" + assert data["version"] == 1 + assert data["template"] == template + assert data["is_latest"] is True + + +def test_get_persona_prompt_specific_version(client: TestClient): + """Test getting a specific version of a persona prompt.""" + template = "You are a reviewer. Context: {{context}}" + + # Create the persona prompt + client.post("/api/v1/persona-prompts/reviewer", json={"template": template}) + + # Get the specific version + response = client.get("/api/v1/persona-prompts/reviewer/1") + assert response.status_code == 200 + data = response.json() + assert data["persona"] == "reviewer" + assert data["version"] == 1 + assert data["template"] == template + + +def test_create_multiple_versions(client: TestClient, app): + """Test creating multiple versions of a persona prompt.""" + # We need to mock the bulk update operation for this test + from prompt_forge.db.client import get_supabase_client + + def mock_update_previous_versions(client, mock_db): + """Helper to mock the bulk update operation.""" + for row in mock_db._tables["persona_prompts"]: + if row["persona"] == "architect" and not row.get("version") == 2: + row["is_latest"] = False + + mock_db = app.dependency_overrides[get_supabase_client]() + + template1 = "You are an architect v1. Context: {{context}}" + template2 = "You are an architect v2. Context: {{context}}" + + # Create first version + response1 = client.post("/api/v1/persona-prompts/architect", json={"template": template1}) + assert response1.status_code == 201 + data1 = response1.json() + assert data1["version"] == 1 + + # Mock the bulk update + mock_update_previous_versions(client, mock_db) + + # Create second version + response2 = client.post("/api/v1/persona-prompts/architect", json={"template": template2}) + assert response2.status_code == 201 + data2 = response2.json() + assert data2["version"] == 2 + + # Latest should be version 2 + latest_response = client.get("/api/v1/persona-prompts/architect") + assert latest_response.status_code == 200 + latest_data = latest_response.json() + assert latest_data["version"] == 2 + assert latest_data["template"] == template2 + + +def test_list_persona_prompt_versions(client: TestClient, app): + """Test listing all versions of a persona prompt.""" + from prompt_forge.db.client import get_supabase_client + mock_db = app.dependency_overrides[get_supabase_client]() + + templates = ["Version 1", "Version 2", "Version 3"] + + for i, template in enumerate(templates, 1): + # Mock the bulk update for previous versions + for row in mock_db._tables["persona_prompts"]: + if row["persona"] == "researcher" and row["version"] < i: + row["is_latest"] = False + + client.post("/api/v1/persona-prompts/researcher", json={"template": template}) + + response = client.get("/api/v1/persona-prompts/researcher/versions") + assert response.status_code == 200 + data = response.json() + assert len(data) == 3 + + # Should be ordered by version descending + assert data[0]["version"] == 3 + assert data[1]["version"] == 2 + assert data[2]["version"] == 1 + + +def test_list_persona_prompt_versions_not_found(client: TestClient): + """Test listing versions for non-existent persona.""" + response = client.get("/api/v1/persona-prompts/nonexistent/versions") + assert response.status_code == 404 + assert response.json()["detail"] == "Persona 'nonexistent' not found" + + +def test_seed_initial_personas(client: TestClient): + """Test seeding initial personas.""" + response = client.post("/api/v1/persona-prompts/seed") + assert response.status_code == 201 + assert response.json()["message"] == "Initial personas seeded successfully" + + # Verify that personas were created + expected_personas = ["researcher", "developer", "reviewer", "tester", "architect"] + + for persona in expected_personas: + get_response = client.get(f"/api/v1/persona-prompts/{persona}") + assert get_response.status_code == 200 + data = get_response.json() + assert data["version"] == 1 + assert data["is_latest"] is True + # Check that template contains expected placeholders + template = data["template"] + assert "{{objective}}" in template + assert "{{context}}" in template + assert "{{constraints}}" in template + assert "{{scope_paths}}" in template + assert "{{alexandria_context}}" in template + + +def test_create_persona_prompt_empty_template(client: TestClient): + """Test creating persona prompt with empty template fails validation.""" + response = client.post( + "/api/v1/persona-prompts/empty", + json={"template": ""} + ) + assert response.status_code == 422 # Validation error + + +def test_create_persona_prompt_missing_template(client: TestClient): + """Test creating persona prompt without template fails validation.""" + response = client.post("/api/v1/persona-prompts/missing", json={}) + assert response.status_code == 422 # Validation error \ No newline at end of file diff --git a/tests/test_persona_store.py b/tests/test_persona_store.py new file mode 100644 index 0000000..f3b809d --- /dev/null +++ b/tests/test_persona_store.py @@ -0,0 +1,164 @@ +"""Tests for persona prompt store operations.""" + +from __future__ import annotations + +import pytest + +from prompt_forge.db.persona_store import PersonaPromptStore + + +@pytest.fixture +def persona_store(mock_db) -> PersonaPromptStore: + """PersonaPromptStore with mock database.""" + return PersonaPromptStore(mock_db) + + +def test_get_latest_persona_prompt_not_found(persona_store: PersonaPromptStore): + """Test getting latest persona prompt when it doesn't exist.""" + result = persona_store.get_latest_persona_prompt("nonexistent") + assert result is None + + +def test_get_persona_prompt_version_not_found(persona_store: PersonaPromptStore): + """Test getting specific persona prompt version when it doesn't exist.""" + result = persona_store.get_persona_prompt_version("nonexistent", 1) + assert result is None + + +def test_create_first_persona_prompt_version(persona_store: PersonaPromptStore): + """Test creating the first version of a persona prompt.""" + template = "You are a developer. Context: {{context}}" + + result = persona_store.create_persona_prompt_version("developer", template) + + assert result.persona == "developer" + assert result.version == 1 + assert result.template == template + assert result.is_latest is True + assert result.id is not None + assert result.created_at is not None + + +def test_create_second_persona_prompt_version(persona_store: PersonaPromptStore, mock_db): + """Test creating a second version marks previous as not latest.""" + template1 = "You are a developer v1. Context: {{context}}" + template2 = "You are a developer v2. Context: {{context}}" + + # Create first version + first = persona_store.create_persona_prompt_version("developer", template1) + assert first.version == 1 + assert first.is_latest is True + + # Mock the bulk update operation that sets is_latest=False for previous versions + # Since our mock client doesn't handle the complex query, we'll manually update + for row in mock_db._tables["persona_prompts"]: + if row["persona"] == "developer" and row["version"] < 2: + row["is_latest"] = False + + # Create second version + second = persona_store.create_persona_prompt_version("developer", template2) + assert second.version == 2 + assert second.is_latest is True + + # Verify first version is no longer latest + first_updated = persona_store.get_persona_prompt_version("developer", 1) + assert first_updated is not None + assert first_updated.is_latest is False + + # Verify second version is latest + latest = persona_store.get_latest_persona_prompt("developer") + assert latest is not None + assert latest.version == 2 + assert latest.is_latest is True + + +def test_get_latest_after_multiple_versions(persona_store: PersonaPromptStore, mock_db): + """Test getting latest version after creating multiple versions.""" + templates = [ + "Template v1", + "Template v2", + "Template v3" + ] + + for i, template in enumerate(templates, 1): + # Mock the bulk update for previous versions + for row in mock_db._tables["persona_prompts"]: + if row["persona"] == "tester" and row["version"] < i: + row["is_latest"] = False + + persona_store.create_persona_prompt_version("tester", template) + + latest = persona_store.get_latest_persona_prompt("tester") + assert latest is not None + assert latest.version == 3 + assert latest.template == "Template v3" + assert latest.is_latest is True + + +def test_list_persona_versions(persona_store: PersonaPromptStore, mock_db): + """Test listing all versions of a persona.""" + templates = ["Version 1", "Version 2", "Version 3"] + + for i, template in enumerate(templates, 1): + # Mock the bulk update for previous versions + for row in mock_db._tables["persona_prompts"]: + if row["persona"] == "reviewer" and row["version"] < i: + row["is_latest"] = False + + persona_store.create_persona_prompt_version("reviewer", template) + + versions = persona_store.list_persona_versions("reviewer") + assert len(versions) == 3 + + # Should be ordered by version descending + assert versions[0].version == 3 + assert versions[1].version == 2 + assert versions[2].version == 1 + + # Only latest should have is_latest=True + assert versions[0].is_latest is True + assert versions[1].is_latest is False + assert versions[2].is_latest is False + + +def test_list_persona_versions_empty(persona_store: PersonaPromptStore): + """Test listing versions for non-existent persona.""" + versions = persona_store.list_persona_versions("nonexistent") + assert versions == [] + + +def test_seed_initial_personas(persona_store: PersonaPromptStore): + """Test seeding initial personas.""" + persona_store.seed_initial_personas() + + expected_personas = ["researcher", "developer", "reviewer", "tester", "architect"] + + for persona in expected_personas: + result = persona_store.get_latest_persona_prompt(persona) + assert result is not None + assert result.version == 1 + assert result.is_latest is True + assert "{{objective}}" in result.template + assert "{{context}}" in result.template + assert "{{constraints}}" in result.template + assert "{{scope_paths}}" in result.template + assert "{{alexandria_context}}" in result.template + + +def test_seed_initial_personas_skip_existing(persona_store: PersonaPromptStore): + """Test that seeding skips existing personas.""" + # Create a persona first + persona_store.create_persona_prompt_version("developer", "Custom template") + + # Now seed - should skip the existing one + persona_store.seed_initial_personas() + + # Developer should still have the custom template + result = persona_store.get_latest_persona_prompt("developer") + assert result is not None + assert result.template == "Custom template" + + # But other personas should be seeded + result = persona_store.get_latest_persona_prompt("researcher") + assert result is not None + assert "research" in result.template.lower() \ No newline at end of file From 26895ff84adb9f417c7b3556c87ac49d7be4cc0e Mon Sep 17 00:00:00 2001 From: "Kai (via Mike Darlington)" Date: Mon, 16 Feb 2026 19:18:07 +0000 Subject: [PATCH 2/4] feat: add seed personas script - Add scripts/seed_persona_prompts.py for manual seeding - Make script executable --- scripts/seed_persona_prompts.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100755 scripts/seed_persona_prompts.py diff --git a/scripts/seed_persona_prompts.py b/scripts/seed_persona_prompts.py new file mode 100755 index 0000000..8190f9a --- /dev/null +++ b/scripts/seed_persona_prompts.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Script to seed initial persona prompts.""" + +import sys +from pathlib import Path + +# Add the project root to the Python path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from prompt_forge.db.persona_store import get_persona_store + + +def main(): + """Seed initial persona prompts.""" + print("Seeding initial persona prompts...") + + store = get_persona_store() + store.seed_initial_personas() + + print("✅ Initial persona prompts seeded successfully!") + print("Available personas: researcher, developer, reviewer, tester, architect") + + +if __name__ == "__main__": + main() \ No newline at end of file From 08184a2cb823b6e745fb5ef4f27b2308d31591d5 Mon Sep 17 00:00:00 2001 From: "Kai (via Mike Darlington)" Date: Mon, 16 Feb 2026 19:18:41 +0000 Subject: [PATCH 3/4] fix: format and lint persona prompt files - Remove unused imports from persona_store.py and test files - Apply ruff format to all modified files - All 290 tests continue to pass --- prompt_forge/api/persona_prompts.py | 11 +++--- prompt_forge/api/personas.py | 4 +- prompt_forge/api/router.py | 4 +- prompt_forge/db/persona_store.py | 18 ++++----- tests/conftest.py | 16 ++++---- tests/test_api_persona_prompts.py | 60 ++++++++++++----------------- tests/test_persona_store.py | 48 +++++++++++------------ 7 files changed, 71 insertions(+), 90 deletions(-) diff --git a/prompt_forge/api/persona_prompts.py b/prompt_forge/api/persona_prompts.py index d821d79..bd09f20 100644 --- a/prompt_forge/api/persona_prompts.py +++ b/prompt_forge/api/persona_prompts.py @@ -19,7 +19,7 @@ async def get_persona_prompt_latest( prompt = store.get_latest_persona_prompt(persona) if not prompt: raise HTTPException(status_code=404, detail=f"Persona '{persona}' not found") - + return PersonaPromptResponse(**prompt.model_dump()) @@ -44,7 +44,7 @@ async def list_persona_prompt_versions( prompts = store.list_persona_versions(persona) if not prompts: raise HTTPException(status_code=404, detail=f"Persona '{persona}' not found") - + return [PersonaPromptResponse(**prompt.model_dump()) for prompt in prompts] @@ -72,8 +72,7 @@ async def get_persona_prompt_version( prompt = store.get_persona_prompt_version(persona, version) if not prompt: raise HTTPException( - status_code=404, - detail=f"Persona '{persona}' version {version} not found" + status_code=404, detail=f"Persona '{persona}' version {version} not found" ) - - return PersonaPromptResponse(**prompt.model_dump()) \ No newline at end of file + + return PersonaPromptResponse(**prompt.model_dump()) diff --git a/prompt_forge/api/personas.py b/prompt_forge/api/personas.py index 7024b31..fccda45 100644 --- a/prompt_forge/api/personas.py +++ b/prompt_forge/api/personas.py @@ -43,9 +43,7 @@ async def get_persona_version( ) -> VersionResponse: """Get a specific version of a persona prompt.""" try: - result = resolver.resolve( - slug=persona, branch=branch, version=version, strategy="pinned" - ) + result = resolver.resolve(slug=persona, branch=branch, version=version, strategy="pinned") return VersionResponse(**result) except ValueError as e: raise HTTPException(status_code=404, detail=str(e)) diff --git a/prompt_forge/api/router.py b/prompt_forge/api/router.py index b9ef5db..632eb29 100644 --- a/prompt_forge/api/router.py +++ b/prompt_forge/api/router.py @@ -21,7 +21,9 @@ api_router.include_router(versions_router, prefix="/prompts", tags=["versions"]) api_router.include_router(branches_router, prefix="/prompts", tags=["branches"]) api_router.include_router(subscriptions_router, prefix="/prompts", tags=["subscriptions"]) -api_router.include_router(persona_prompts_router, prefix="/persona-prompts", tags=["persona-prompts"]) +api_router.include_router( + persona_prompts_router, prefix="/persona-prompts", tags=["persona-prompts"] +) api_router.include_router(agents_router, prefix="/agents", tags=["agents"]) api_router.include_router(compose_router, tags=["composition"]) api_router.include_router(usage_router, prefix="/usage", tags=["usage"]) diff --git a/prompt_forge/db/persona_store.py b/prompt_forge/db/persona_store.py index 0547b87..ffa6ca1 100644 --- a/prompt_forge/db/persona_store.py +++ b/prompt_forge/db/persona_store.py @@ -2,8 +2,6 @@ from __future__ import annotations -from typing import Any -from uuid import UUID import structlog @@ -56,9 +54,11 @@ def create_persona_prompt_version(self, persona: str, template: str) -> PersonaP # Mark all existing versions as not latest if existing_rows: # Update all existing versions to not be latest - query = self.db.client.table("persona_prompts").update( - {"is_latest": False} - ).eq("persona", persona) + query = ( + self.db.client.table("persona_prompts") + .update({"is_latest": False}) + .eq("persona", persona) + ) query.execute() # Create the new version @@ -76,7 +76,7 @@ def create_persona_prompt_version(self, persona: str, template: str) -> PersonaP version=next_version, id=row_data["id"], ) - + return PersonaPromptRow(**row_data) def list_persona_versions(self, persona: str) -> list[PersonaPromptRow]: @@ -105,7 +105,6 @@ def seed_initial_personas(self) -> None: Alexandria context: {{alexandria_context}} Focus on thorough research, fact-checking, and providing comprehensive information with proper citations and sources.""", - "developer": """You are a Developer persona with expertise in software engineering and coding. Your objective: {{objective}} @@ -119,7 +118,6 @@ def seed_initial_personas(self) -> None: Alexandria context: {{alexandria_context}} Focus on clean, efficient code, best practices, testing, and maintainable solutions. Provide working code examples and explanations.""", - "reviewer": """You are a Reviewer persona with expertise in code review and quality assurance. Your objective: {{objective}} @@ -133,7 +131,6 @@ def seed_initial_personas(self) -> None: Alexandria context: {{alexandria_context}} Focus on thorough code review, identifying issues, suggesting improvements, and ensuring quality standards are met.""", - "tester": """You are a Tester persona with expertise in testing strategies and quality assurance. Your objective: {{objective}} @@ -147,7 +144,6 @@ def seed_initial_personas(self) -> None: Alexandria context: {{alexandria_context}} Focus on comprehensive testing strategies, test case design, bug identification, and ensuring software quality through rigorous testing.""", - "architect": """You are an Architect persona with expertise in system design and technical architecture. Your objective: {{objective}} @@ -177,4 +173,4 @@ def seed_initial_personas(self) -> None: def get_persona_store() -> PersonaPromptStore: """Get PersonaPromptStore instance.""" - return PersonaPromptStore(get_supabase_client()) \ No newline at end of file + return PersonaPromptStore(get_supabase_client()) diff --git a/tests/conftest.py b/tests/conftest.py index 72d9582..21fe7a2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,16 +31,16 @@ def __init__(self): def client(self): """Mock the client.table().update().eq() pattern used in persona store.""" mock_client = MagicMock() - + def mock_table(table_name): table_mock = MagicMock() - + def mock_update(data): update_mock = MagicMock() - + def mock_eq(column, value): eq_mock = MagicMock() - + def mock_execute(): # Handle bulk updates for persona_prompts if table_name == "persona_prompts" and column == "persona": @@ -48,16 +48,16 @@ def mock_execute(): if row.get(column) == value: row.update(data) return MagicMock() - + eq_mock.execute = mock_execute return eq_mock - + update_mock.eq = mock_eq return update_mock - + table_mock.update = mock_update return table_mock - + mock_client.table = mock_table return mock_client diff --git a/tests/test_api_persona_prompts.py b/tests/test_api_persona_prompts.py index 59eeb6c..fe05be9 100644 --- a/tests/test_api_persona_prompts.py +++ b/tests/test_api_persona_prompts.py @@ -4,8 +4,6 @@ from fastapi.testclient import TestClient -from prompt_forge.db.persona_store import get_persona_store - def test_get_persona_prompt_latest_not_found(client: TestClient): """Test getting latest persona prompt when it doesn't exist.""" @@ -24,12 +22,9 @@ def test_get_persona_prompt_version_not_found(client: TestClient): def test_create_persona_prompt_version(client: TestClient): """Test creating a persona prompt version.""" template = "You are a developer. Context: {{context}}" - - response = client.post( - "/api/v1/persona-prompts/developer", - json={"template": template} - ) - + + response = client.post("/api/v1/persona-prompts/developer", json={"template": template}) + assert response.status_code == 201 data = response.json() assert data["persona"] == "developer" @@ -43,14 +38,11 @@ def test_create_persona_prompt_version(client: TestClient): def test_get_persona_prompt_latest_after_create(client: TestClient): """Test getting latest persona prompt after creating one.""" template = "You are a tester. Context: {{context}}" - + # Create the persona prompt - create_response = client.post( - "/api/v1/persona-prompts/tester", - json={"template": template} - ) + create_response = client.post("/api/v1/persona-prompts/tester", json={"template": template}) assert create_response.status_code == 201 - + # Get the latest version response = client.get("/api/v1/persona-prompts/tester") assert response.status_code == 200 @@ -64,10 +56,10 @@ def test_get_persona_prompt_latest_after_create(client: TestClient): def test_get_persona_prompt_specific_version(client: TestClient): """Test getting a specific version of a persona prompt.""" template = "You are a reviewer. Context: {{context}}" - + # Create the persona prompt client.post("/api/v1/persona-prompts/reviewer", json={"template": template}) - + # Get the specific version response = client.get("/api/v1/persona-prompts/reviewer/1") assert response.status_code == 200 @@ -81,33 +73,33 @@ def test_create_multiple_versions(client: TestClient, app): """Test creating multiple versions of a persona prompt.""" # We need to mock the bulk update operation for this test from prompt_forge.db.client import get_supabase_client - + def mock_update_previous_versions(client, mock_db): """Helper to mock the bulk update operation.""" for row in mock_db._tables["persona_prompts"]: if row["persona"] == "architect" and not row.get("version") == 2: row["is_latest"] = False - + mock_db = app.dependency_overrides[get_supabase_client]() - + template1 = "You are an architect v1. Context: {{context}}" template2 = "You are an architect v2. Context: {{context}}" - + # Create first version response1 = client.post("/api/v1/persona-prompts/architect", json={"template": template1}) assert response1.status_code == 201 data1 = response1.json() assert data1["version"] == 1 - + # Mock the bulk update mock_update_previous_versions(client, mock_db) - + # Create second version response2 = client.post("/api/v1/persona-prompts/architect", json={"template": template2}) assert response2.status_code == 201 data2 = response2.json() assert data2["version"] == 2 - + # Latest should be version 2 latest_response = client.get("/api/v1/persona-prompts/architect") assert latest_response.status_code == 200 @@ -119,23 +111,24 @@ def mock_update_previous_versions(client, mock_db): def test_list_persona_prompt_versions(client: TestClient, app): """Test listing all versions of a persona prompt.""" from prompt_forge.db.client import get_supabase_client + mock_db = app.dependency_overrides[get_supabase_client]() - + templates = ["Version 1", "Version 2", "Version 3"] - + for i, template in enumerate(templates, 1): # Mock the bulk update for previous versions for row in mock_db._tables["persona_prompts"]: if row["persona"] == "researcher" and row["version"] < i: row["is_latest"] = False - + client.post("/api/v1/persona-prompts/researcher", json={"template": template}) - + response = client.get("/api/v1/persona-prompts/researcher/versions") assert response.status_code == 200 data = response.json() assert len(data) == 3 - + # Should be ordered by version descending assert data[0]["version"] == 3 assert data[1]["version"] == 2 @@ -154,10 +147,10 @@ def test_seed_initial_personas(client: TestClient): response = client.post("/api/v1/persona-prompts/seed") assert response.status_code == 201 assert response.json()["message"] == "Initial personas seeded successfully" - + # Verify that personas were created expected_personas = ["researcher", "developer", "reviewer", "tester", "architect"] - + for persona in expected_personas: get_response = client.get(f"/api/v1/persona-prompts/{persona}") assert get_response.status_code == 200 @@ -175,14 +168,11 @@ def test_seed_initial_personas(client: TestClient): def test_create_persona_prompt_empty_template(client: TestClient): """Test creating persona prompt with empty template fails validation.""" - response = client.post( - "/api/v1/persona-prompts/empty", - json={"template": ""} - ) + response = client.post("/api/v1/persona-prompts/empty", json={"template": ""}) assert response.status_code == 422 # Validation error def test_create_persona_prompt_missing_template(client: TestClient): """Test creating persona prompt without template fails validation.""" response = client.post("/api/v1/persona-prompts/missing", json={}) - assert response.status_code == 422 # Validation error \ No newline at end of file + assert response.status_code == 422 # Validation error diff --git a/tests/test_persona_store.py b/tests/test_persona_store.py index f3b809d..79a7aae 100644 --- a/tests/test_persona_store.py +++ b/tests/test_persona_store.py @@ -28,9 +28,9 @@ def test_get_persona_prompt_version_not_found(persona_store: PersonaPromptStore) def test_create_first_persona_prompt_version(persona_store: PersonaPromptStore): """Test creating the first version of a persona prompt.""" template = "You are a developer. Context: {{context}}" - + result = persona_store.create_persona_prompt_version("developer", template) - + assert result.persona == "developer" assert result.version == 1 assert result.template == template @@ -43,28 +43,28 @@ def test_create_second_persona_prompt_version(persona_store: PersonaPromptStore, """Test creating a second version marks previous as not latest.""" template1 = "You are a developer v1. Context: {{context}}" template2 = "You are a developer v2. Context: {{context}}" - + # Create first version first = persona_store.create_persona_prompt_version("developer", template1) assert first.version == 1 assert first.is_latest is True - + # Mock the bulk update operation that sets is_latest=False for previous versions # Since our mock client doesn't handle the complex query, we'll manually update for row in mock_db._tables["persona_prompts"]: if row["persona"] == "developer" and row["version"] < 2: row["is_latest"] = False - + # Create second version second = persona_store.create_persona_prompt_version("developer", template2) assert second.version == 2 assert second.is_latest is True - + # Verify first version is no longer latest first_updated = persona_store.get_persona_prompt_version("developer", 1) assert first_updated is not None assert first_updated.is_latest is False - + # Verify second version is latest latest = persona_store.get_latest_persona_prompt("developer") assert latest is not None @@ -74,20 +74,16 @@ def test_create_second_persona_prompt_version(persona_store: PersonaPromptStore, def test_get_latest_after_multiple_versions(persona_store: PersonaPromptStore, mock_db): """Test getting latest version after creating multiple versions.""" - templates = [ - "Template v1", - "Template v2", - "Template v3" - ] - + templates = ["Template v1", "Template v2", "Template v3"] + for i, template in enumerate(templates, 1): # Mock the bulk update for previous versions for row in mock_db._tables["persona_prompts"]: if row["persona"] == "tester" and row["version"] < i: row["is_latest"] = False - + persona_store.create_persona_prompt_version("tester", template) - + latest = persona_store.get_latest_persona_prompt("tester") assert latest is not None assert latest.version == 3 @@ -98,23 +94,23 @@ def test_get_latest_after_multiple_versions(persona_store: PersonaPromptStore, m def test_list_persona_versions(persona_store: PersonaPromptStore, mock_db): """Test listing all versions of a persona.""" templates = ["Version 1", "Version 2", "Version 3"] - + for i, template in enumerate(templates, 1): # Mock the bulk update for previous versions for row in mock_db._tables["persona_prompts"]: if row["persona"] == "reviewer" and row["version"] < i: row["is_latest"] = False - + persona_store.create_persona_prompt_version("reviewer", template) - + versions = persona_store.list_persona_versions("reviewer") assert len(versions) == 3 - + # Should be ordered by version descending assert versions[0].version == 3 assert versions[1].version == 2 assert versions[2].version == 1 - + # Only latest should have is_latest=True assert versions[0].is_latest is True assert versions[1].is_latest is False @@ -130,9 +126,9 @@ def test_list_persona_versions_empty(persona_store: PersonaPromptStore): def test_seed_initial_personas(persona_store: PersonaPromptStore): """Test seeding initial personas.""" persona_store.seed_initial_personas() - + expected_personas = ["researcher", "developer", "reviewer", "tester", "architect"] - + for persona in expected_personas: result = persona_store.get_latest_persona_prompt(persona) assert result is not None @@ -149,16 +145,16 @@ def test_seed_initial_personas_skip_existing(persona_store: PersonaPromptStore): """Test that seeding skips existing personas.""" # Create a persona first persona_store.create_persona_prompt_version("developer", "Custom template") - + # Now seed - should skip the existing one persona_store.seed_initial_personas() - + # Developer should still have the custom template result = persona_store.get_latest_persona_prompt("developer") assert result is not None assert result.template == "Custom template" - + # But other personas should be seeded result = persona_store.get_latest_persona_prompt("researcher") assert result is not None - assert "research" in result.template.lower() \ No newline at end of file + assert "research" in result.template.lower() From acfaed3c95647c783c2dec2037c7592c2e292b63 Mon Sep 17 00:00:00 2001 From: "Kai (via Mike Darlington)" Date: Mon, 16 Feb 2026 19:50:26 +0000 Subject: [PATCH 4/4] fix: remove unused Path import in seed_personas --- scripts/seed_personas.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/seed_personas.py b/scripts/seed_personas.py index 7bf898d..8165b9a 100755 --- a/scripts/seed_personas.py +++ b/scripts/seed_personas.py @@ -10,7 +10,6 @@ import argparse import sys -from pathlib import Path import httpx