personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_describe_config.py
9.3 kB 201 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Tests for observe/describe.py category discovery and configuration.""" 5 6from pathlib import Path 7 8from solstone.observe import describe as describe_module 9from solstone.observe.describe import _build_redact_instruction 10from solstone.think.prompts import load_prompt 11 12 13def test_categories_discovered(): 14 """Test that categories are discovered on import.""" 15 CATEGORIES = describe_module.CATEGORIES 16 17 # Should have discovered at least some categories 18 assert isinstance(CATEGORIES, dict) 19 assert len(CATEGORIES) > 0 20 21 22def test_categories_have_required_fields(): 23 """Test that all categories have required metadata.""" 24 CATEGORIES = describe_module.CATEGORIES 25 26 for category, metadata in CATEGORIES.items(): 27 # Every category must have description 28 assert "description" in metadata, f"Category {category} missing 'description'" 29 assert isinstance(metadata["description"], str) 30 assert len(metadata["description"]) > 0 31 32 # Every category should have output field (defaulted if not set) 33 assert "output" in metadata, f"Category {category} missing 'output'" 34 assert metadata["output"] in ("json", "markdown") 35 36 # Every category should have context field (for provider resolution) 37 assert "context" in metadata, f"Category {category} missing 'context'" 38 assert metadata["context"].startswith("observe.describe.") 39 40 # Every category should have an output-token budget 41 assert "max_output_tokens" in metadata 42 assert isinstance(metadata["max_output_tokens"], int) 43 assert metadata["max_output_tokens"] > 0 44 45 46def test_extractable_categories_have_prompts(): 47 """Test that extractable categories have valid prompts loaded.""" 48 CATEGORIES = describe_module.CATEGORIES 49 50 extractable_count = 0 51 for category, metadata in CATEGORIES.items(): 52 if "prompt" in metadata: 53 extractable_count += 1 54 assert isinstance(metadata["prompt"], str) 55 assert len(metadata["prompt"]) > 0, f"Category {category} has empty prompt" 56 57 # Sanity check: we should have at least some extractable categories 58 assert extractable_count > 0, "No extractable categories found" 59 60 61def test_category_max_output_token_defaults_and_overrides(): 62 """Test category output-token defaults and explicit overrides.""" 63 categories_dir = Path(describe_module.__file__).resolve().parent / "categories" 64 declared_overrides = [] 65 defaulted_categories = [] 66 67 for md_path in categories_dir.glob("*.md"): 68 category = md_path.stem 69 prompt_content = load_prompt(category, base_dir=categories_dir) 70 metadata = describe_module.CATEGORIES[category] 71 72 if "max_output_tokens" in prompt_content.metadata: 73 declared_overrides.append(category) 74 assert ( 75 metadata["max_output_tokens"] 76 == prompt_content.metadata["max_output_tokens"] 77 ) 78 else: 79 defaulted_categories.append(category) 80 assert metadata["max_output_tokens"] == 4096 81 82 assert declared_overrides 83 assert defaulted_categories 84 85 86def test_categorization_prompt_built(): 87 """Test that categorization prompt is built correctly.""" 88 prompt = describe_module.CATEGORIZATION_PROMPT 89 90 # Should contain all category descriptions 91 for category, metadata in describe_module.CATEGORIES.items(): 92 assert f"- {category}:" in prompt 93 assert metadata["description"] in prompt 94 95 # Should have the template structure 96 assert "primary" in prompt 97 assert "secondary" in prompt 98 assert "overlap" in prompt 99 assert "Categories (choose one):" in prompt 100 101 102def test_categorization_prompt_alphabetical(): 103 """Test that categories in prompt are alphabetically ordered.""" 104 prompt = describe_module.CATEGORIZATION_PROMPT 105 106 # Extract category lines from prompt 107 lines = prompt.split("\n") 108 category_lines = [line for line in lines if line.startswith("- ") and ":" in line] 109 110 # Extract category names 111 categories = [line.split(":")[0].replace("- ", "") for line in category_lines] 112 113 # Should be sorted 114 assert categories == sorted(categories) 115 116 117def test_categorization_prompt_is_frozen_literal(): 118 """Test that categorization prompt matches the hand-authored literal.""" 119 expected = """You have one job: identify the primary foreground and (if present) secondary app categories in this desktop screenshot, and return ONLY this JSON: 120 121{ 122 "visual_description":"<1–2 sentences describing what is visible>", 123 "primary": "<largest and most visible app category>", 124 "secondary": "<second most visible app category or 'none'>", 125 "overlap": <boolean, does the primary overlap or cover the secondary, or is it fully standalone and separate> 126} 127 128Rules: 129- For visual_description summarize the **overall desktop view** in **1–2 sentences** for a visually impaired person, first state what kind of content dominates the screen (app UI, photo/video, feed/thread, text document, terminal, or meeting), then summarize layout and window arrangement. 130- For the most visible primary foreground app choose the best category from the list below. 131- Set "secondary" to "none" and "overlap" to true if the primary effectively fills the screen or no distinct second category/window is visible. 132- Set overlap to true if the primary app overlaps, covers, clips, or obscures the secondary in any way. 133- Only set a category for secondary if it is very visible and occupies more than 30% of the screen. 134 135Categories (choose one): 136- browsing: General web browsing, news, shopping, or reference pages without a dominant social feed or media viewer 137- calendar: Calendar and scheduling interfaces: day/week/month views, agenda lists, event details, event creation forms, booking pages, availability grids, and RSVP/scheduling workflows 138- code: Code editors and IDEs 139- gaming: Video games, puzzles, idle games 140- media: Photos, video players, image galleries, or visual media dominating the view, even when displayed inside a browser tab 141- meeting: Video calls/conferencing (Zoom, Meet, Teams, Webex, etc.) 142- messaging: Chat or email apps (Slack, Discord, Messages/iMessage, Gmail, etc.) 143- productivity: Spreadsheets, slides, document editors, task and issue tracking tools, dashboards, other workplace desktop or web apps and professional tools that are not primarily calendar/scheduling views 144- reading: Documents, articles, PDFs, documentation 145- social: Social platforms with feeds, threads, profiles, posts, comments, or timelines (X, Bluesky, Reddit, Instagram, TikTok, LinkedIn, Mastodon, HN) 146- terminal: Command line interfaces, logs, shell 147 148Tie-break rules: 149- If a photo, video, image gallery, or visual media fills most of the screen, choose media even when it is inside a browser. 150- If the dominant surface is a feed, thread, profile, posts, comments, or timeline, choose social rather than browsing. 151- Choose browsing for ordinary web pages, search, news, shopping, or documentation when no social feed or media viewer dominates. 152- Choose calendar for calendar grids, agenda views, event detail/edit forms, availability pickers, booking pages, and scheduling assistants, even when they appear inside a browser or productivity suite. 153- Choose meeting only for an active live call/conference UI; a calendar event for a meeting is calendar, not meeting. 154- Choose messaging/email when the dominant surface is an email or chat conversation, even if it discusses scheduling; choose calendar when an invite/event editor, RSVP pane, availability grid, or booking flow is dominant.""" 155 prompt = describe_module.CATEGORIZATION_PROMPT 156 157 assert prompt == expected 158 assert "" in prompt 159 assert "1-2 sentences" not in prompt 160 assert "" not in prompt 161 assert "Tie-break rules:" in prompt 162 assert prompt.index("- terminal:") < prompt.index("Tie-break rules:") 163 assert prompt.rstrip().endswith( 164 "choose calendar when an invite/event editor, RSVP pane, availability grid, or booking flow is dominant." 165 ) 166 167 168def test_redact_instruction_empty(): 169 """Test that empty/missing redact list returns empty string.""" 170 assert _build_redact_instruction([]) == "" 171 assert _build_redact_instruction(None) == "" 172 173 174def test_redact_instruction_format(): 175 """Test that redact instruction formats rules correctly.""" 176 rules = [ 177 "use *** instead of any visible passwords", 178 "replace personal email addresses with [redacted]", 179 ] 180 result = _build_redact_instruction(rules) 181 182 # Should contain the header 183 assert "Redaction rules" in result 184 assert "do not generalize" in result 185 186 # Should contain each rule as a bullet 187 for rule in rules: 188 assert f"- {rule}" in result 189 190 191def test_redact_instruction_no_vague_language(): 192 """Test that redact instruction doesn't add vague privacy language.""" 193 rules = ["replace bank account numbers with ***"] 194 result = _build_redact_instruction(rules) 195 196 # Should not contain vague directives the model could over-apply 197 lower = result.lower() 198 assert "sensitive" not in lower 199 assert "confidential" not in lower 200 assert "personally identifiable" not in lower 201 assert "pii" not in lower