Capture ThinkStorm project: codebase state, workflows, and access control policies

This commit is contained in:
2026-08-21 07:57:51 -07:00
commit f57d5df088
34 changed files with 7806 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
"""
REST API Endpoint Tests
Tests Anonymous intake, auth sessions, claims, and role-based permissions.
"""
import pytest
from starlette.testclient import TestClient
from thinkstorm.main import app
from thinkstorm.database import init_db
from thinkstorm.config import config
@pytest.fixture(scope="module", autouse=True)
def setup_app():
init_db()
def test_api_anonymous_submission():
client = TestClient(app)
resp = client.post("/api/ideas", json={"text": "Decentralized task scheduler with automated cron jobs and webhooks."})
assert resp.status_code == 201
data = resp.json()
assert data["id"].startswith("TS-")
assert data["lifecycle_state"] == "SUBMITTED"
def test_api_ideas_list():
client = TestClient(app)
# 1. Unauthenticated -> 401 Unauthorized
resp_unauth = client.get("/api/ideas")
assert resp_unauth.status_code == 401
# 2. Authenticated -> 200 OK list
login_resp = client.post("/api/auth/login", json={"username": "researcher", "password": "thinkstorm_user_2026"})
assert login_resp.status_code == 200
token = login_resp.json()["token"]
resp_auth = client.get("/api/ideas", headers={"Authorization": f"Bearer {token}"})
assert resp_auth.status_code == 200
data = resp_auth.json()
assert isinstance(data, list)
def test_api_auth_and_claim_flow():
client = TestClient(app)
# 1. Submit idea
sub_resp = client.post("/api/ideas", json={"text": "Real-time vector search indexer for personal notes."})
idea_id = sub_resp.json()["id"]
# Make idea available in db
from thinkstorm.database import get_db
with get_db() as conn:
conn.execute("UPDATE ideas SET lifecycle_state = 'AVAILABLE' WHERE id = ?", (idea_id,))
# 2. Try to claim anonymously -> 401
claim_unauth = client.post(f"/api/ideas/{idea_id}/claim")
assert claim_unauth.status_code == 401
# 3. Login as researcher
login_resp = client.post("/api/auth/login", json={"username": "researcher", "password": "thinkstorm_user_2026"})
assert login_resp.status_code == 200
token = login_resp.json()["token"]
# 4. Claim idea with session
claim_auth = client.post(f"/api/ideas/{idea_id}/claim", headers={"Authorization": f"Bearer {token}"})
assert claim_auth.status_code == 200
assert "claimed" in claim_auth.json()["message"].lower()
def test_admin_endpoints_permission():
client = TestClient(app)
# 1. Anonymously access admin -> 401 or 403
adm_unauth = client.get("/api/admin/prompts")
assert adm_unauth.status_code in (401, 403)
# 2. Login as admin
login_adm = client.post("/api/auth/login", json={"username": "admin", "password": config.admin_bootstrap_key})
assert login_adm.status_code == 200
adm_token = login_adm.json()["token"]
# 3. Admin access prompts
adm_prompts = client.get("/api/admin/prompts", headers={"Authorization": f"Bearer {adm_token}"})
assert adm_prompts.status_code == 200
prompts = adm_prompts.json()
assert len(prompts) > 0
# Full prompt template visible to admin
assert prompts[0]["system_prompt"] != "[REDACTED: ADMIN ONLY]"
def test_unauthenticated_cannot_browse_ideas():
client = TestClient(app)
# 1. Anonymous submission is allowed
sub_resp = client.post("/api/ideas", json={"text": "New idea submitted by anonymous visitor."})
assert sub_resp.status_code == 201
idea_id = sub_resp.json()["id"]
# 2. Unauthenticated GET /ideas (HTML Browse page) -> Redirect to /login
html_ideas_resp = client.get("/ideas", follow_redirects=False)
assert html_ideas_resp.status_code in (302, 307)
assert html_ideas_resp.headers.get("location") == "/login"
# 3. Unauthenticated GET /ideas/{idea_id} (HTML Dossier page) -> Redirect to /login
html_detail_resp = client.get(f"/ideas/{idea_id}", follow_redirects=False)
assert html_detail_resp.status_code in (302, 307)
assert html_detail_resp.headers.get("location") == "/login"
# 4. Unauthenticated GET /api/ideas (API Browse list) -> 401 Unauthorized
api_ideas_resp = client.get("/api/ideas")
assert api_ideas_resp.status_code == 401
assert "authentication required" in api_ideas_resp.json()["detail"].lower()
# 5. Unauthenticated GET /api/ideas/{idea_id} (API Dossier detail) -> 401 Unauthorized
api_detail_resp = client.get(f"/api/ideas/{idea_id}")
assert api_detail_resp.status_code == 401
assert "authentication required" in api_detail_resp.json()["detail"].lower()
# 6. Authenticated user can browse and view ideas
login_resp = client.post("/api/auth/login", json={"username": "researcher", "password": "thinkstorm_user_2026"})
assert login_resp.status_code == 200
token = login_resp.json()["token"]
auth_headers = {"Authorization": f"Bearer {token}"}
# Authenticated API list -> 200
auth_list_resp = client.get("/api/ideas", headers=auth_headers)
assert auth_list_resp.status_code == 200
assert isinstance(auth_list_resp.json(), list)
# Authenticated API detail -> 200
auth_detail_resp = client.get(f"/api/ideas/{idea_id}", headers=auth_headers)
assert auth_detail_resp.status_code == 200
assert auth_detail_resp.json()["id"] == idea_id
+73
View File
@@ -0,0 +1,73 @@
import pytest
import asyncio
from fastapi.testclient import TestClient
from thinkstorm.main import app
from thinkstorm.database import init_db, get_db, get_utc_now
from thinkstorm.processors.pipeline import execute_work_track_workflow
@pytest.fixture(autouse=True)
def setup_database():
init_db()
client = TestClient(app)
def test_work_track_artifact_versioning_and_deletion():
# 1. Login as admin
login_res = client.post("/api/auth/login", json={"username": "admin", "password": "admin-thinkstorm-pass-2026"})
assert login_res.status_code == 200
# 2. Submit an idea
sub_res = client.post("/api/ideas", json={"text": "Test Idea for Artifact Versioning and Deletion System"})
assert sub_res.status_code == 201
idea_id = sub_res.json()["id"]
# 3. Create a work track
track_res = client.post(
f"/api/ideas/{idea_id}/work-tracks",
json={"work_type_id": "ARTICLE", "name": "Article Test Track", "model_override": "auto/best-fast"}
)
assert track_res.status_code == 200
track_id = track_res.json()["id"]
assert track_res.json()["model_override"] == "auto/best-fast"
# 4. Execute work track workflow (Run 1 -> Version 1)
asyncio.run(execute_work_track_workflow(track_id, model_override="auto/best-fast"))
with get_db() as conn:
outs_v1 = conn.execute("SELECT id, name, version, is_current, model_used FROM work_track_outputs WHERE work_track_id = ? ORDER BY version DESC", (track_id,)).fetchall()
assert len(outs_v1) >= 2
for o in outs_v1:
assert o["version"] == 1
assert o["is_current"] == 1
# 5. Execute work track workflow again (Run 2 -> Version 2)
asyncio.run(execute_work_track_workflow(track_id, model_override="auto/best-reasoning"))
with get_db() as conn:
outs_v2 = conn.execute("SELECT id, name, version, is_current, model_used FROM work_track_outputs WHERE work_track_id = ? ORDER BY name ASC, version DESC", (track_id,)).fetchall()
# Should now have 4 records (2 for article.md: v2 and v1; 2 for outline.md: v2 and v1)
assert len(outs_v2) == 4
article_v2 = [o for o in outs_v2 if o["name"] == "article.md" and o["version"] == 2][0]
article_v1 = [o for o in outs_v2 if o["name"] == "article.md" and o["version"] == 1][0]
assert article_v2["is_current"] == 1
assert article_v1["is_current"] == 0
# 6. Verify GET /api/ideas/{id} serializes version metadata
detail_res = client.get(f"/api/ideas/{idea_id}")
assert detail_res.status_code == 200
data = detail_res.json()
track_data = [t for t in data["work_tracks"] if t["id"] == track_id][0]
assert len(track_data["outputs"]) == 4
output_ids = [o["id"] for o in track_data["outputs"]]
# 7. Delete artifact version 2 of article.md
del_res = client.delete(f"/api/ideas/work-tracks/outputs/{article_v2['id']}")
assert del_res.status_code == 200
assert "deleted successfully" in del_res.json()["message"]
# 8. Verify article_v1 is now promoted to is_current = 1
with get_db() as conn:
remaining_article = conn.execute("SELECT id, version, is_current FROM work_track_outputs WHERE work_track_id = ? AND name = 'article.md'", (track_id,)).fetchall()
assert len(remaining_article) == 1
assert remaining_article[0]["version"] == 1
assert remaining_article[0]["is_current"] == 1
+36
View File
@@ -0,0 +1,36 @@
"""
Database & Sequence Generator Unit Tests
"""
import pytest
import sqlite3
from thinkstorm.database import init_db, get_db, next_sequence, hash_password
from thinkstorm.models import UserRole
def test_init_db_and_seeds():
init_db()
with get_db() as conn:
users = conn.execute("SELECT * FROM users").fetchall()
assert len(users) >= 2
categories = conn.execute("SELECT * FROM categories").fetchall()
assert len(categories) >= 5
work_types = conn.execute("SELECT * FROM work_types").fetchall()
assert len(work_types) >= 3
prompts = conn.execute("SELECT * FROM prompt_definitions").fetchall()
assert len(prompts) >= 5
def test_sequence_generators():
s1 = next_sequence("idea")
s2 = next_sequence("idea")
assert s1.startswith("TS-")
assert s2.startswith("TS-")
assert int(s2.split("-")[1]) == int(s1.split("-")[1]) + 1
w1 = next_sequence("work_track")
assert w1.startswith("WT-")
r1 = next_sequence("run")
assert r1.startswith("RUN-")
+93
View File
@@ -0,0 +1,93 @@
"""
Idea Lifecycle & Workflow State Machine Tests
PRD Section 10, 11, 28, 30, 31, 34:
- Separation of Lifecycle State and Processing State
- Claiming (AVAILABLE -> CLAIMED)
- Activation (CLAIMED -> ACTIVE)
- Work tracks & outputs
"""
import pytest
import asyncio
from thinkstorm.database import init_db, get_db, next_sequence, get_utc_now
from thinkstorm.processors.pipeline import execute_intake_pipeline, execute_work_track_workflow
from thinkstorm.models import LifecycleState, ProcessingState
@pytest.mark.asyncio
async def test_full_idea_intake_and_claim_lifecycle():
init_db()
idea_id = next_sequence("idea")
raw_text = f"Quantum biological sensor interface for monitoring mitochondrial ATP flux oscillations in real-time {idea_id} https://example.com/sensor"
now = get_utc_now()
# 1. Anonymous submission
with get_db() as conn:
conn.execute(
"""
INSERT INTO ideas (id, original_text, submitted_at, title, summary, lifecycle_state, processing_state, enrichment_level, created_at, updated_at)
VALUES (?, ?, ?, 'Pending', 'Pending', 'SUBMITTED', 'QUEUED', 0, ?, ?)
""",
(idea_id, raw_text, now, now, now)
)
# 2. Run intake pipeline
await execute_intake_pipeline(idea_id)
# 3. Verify Idea reached AVAILABLE
with get_db() as conn:
idea = conn.execute("SELECT * FROM ideas WHERE id = ?", (idea_id,)).fetchone()
assert idea["lifecycle_state"] == LifecycleState.AVAILABLE.value
assert idea["processing_state"] == ProcessingState.IDLE.value
assert idea["enrichment_level"] >= 4
assert len(idea["title"]) > 0
assert idea["opengist_id"] is not None
# Verify raw original text was NEVER mutated
assert idea["original_text"] == raw_text
# Verify runs were recorded
runs = conn.execute("SELECT * FROM processor_runs WHERE idea_id = ?", (idea_id,)).fetchall()
assert len(runs) >= 4
total_tokens = sum(r["total_tokens"] for r in runs)
assert total_tokens > 0
# 4. Claim Idea (AVAILABLE -> CLAIMED)
claim_time = get_utc_now()
with get_db() as conn:
conn.execute(
"UPDATE ideas SET lifecycle_state = 'CLAIMED', claimed_by = 'researcher', claimed_at = ? WHERE id = ?",
(claim_time, idea_id)
)
claimed_idea = conn.execute("SELECT * FROM ideas WHERE id = ?", (idea_id,)).fetchone()
assert claimed_idea["lifecycle_state"] == LifecycleState.CLAIMED.value
assert claimed_idea["claimed_by"] == "researcher"
# 5. Activate Idea (CLAIMED -> ACTIVE)
with get_db() as conn:
conn.execute(
"UPDATE ideas SET lifecycle_state = 'ACTIVE' WHERE id = ?",
(idea_id,)
)
active_idea = conn.execute("SELECT * FROM ideas WHERE id = ?", (idea_id,)).fetchone()
assert active_idea["lifecycle_state"] == LifecycleState.ACTIVE.value
# 6. Add Work Track
track_id = next_sequence("work_track")
with get_db() as conn:
conn.execute(
"""
INSERT INTO work_tracks (id, idea_id, work_type_id, name, state, created_at)
VALUES (?, ?, 'CODING_PROJECT', 'Core Architecture Blueprint', 'PLANNED', ?)
""",
(track_id, idea_id, get_utc_now())
)
# 7. Execute Work Track Workflow
await execute_work_track_workflow(track_id)
# 8. Verify Work Track completed and outputs produced
with get_db() as conn:
track = conn.execute("SELECT * FROM work_tracks WHERE id = ?", (track_id,)).fetchone()
assert track["state"] == "COMPLETED"
outputs = conn.execute("SELECT * FROM work_track_outputs WHERE work_track_id = ?", (track_id,)).fetchall()
assert len(outputs) >= 2
+67
View File
@@ -0,0 +1,67 @@
"""
Prompt Versioning & Role Visibility Tests
PRD Section 21, 23, 25:
- Editing a prompt creates a new immutable version without mutating historical provenance.
- Non-admin users cannot view full system/user prompt templates.
"""
import pytest
from thinkstorm.database import init_db
from thinkstorm.prompts.catalog import (
get_all_prompts, get_prompt_version, update_prompt, duplicate_prompt
)
def test_prompt_versioning_immutability():
init_db()
prompt_id = "test-versioning-prompt"
# 1. Create a fresh test prompt definition
from thinkstorm.database import get_db
with get_db() as conn:
conn.execute("DELETE FROM prompt_versions WHERE prompt_definition_id = ?", (prompt_id,))
conn.execute("DELETE FROM prompt_definitions WHERE id = ?", (prompt_id,))
duplicate_prompt("normalize-idea", prompt_id, "Test Versioning Prompt", created_by="test_user")
# Verify baseline v1
v1 = get_prompt_version(prompt_id, version=1, is_admin=True)
assert v1 is not None
assert v1["version"] == 1
old_hash = v1["prompt_hash"]
# 2. Update prompt -> creates v2
new_version = update_prompt(
prompt_id=prompt_id,
system_prompt="Updated system prompt for normalizer v2.",
user_prompt_template="Analyze: {{submission_text}}",
model_policy="fast",
updated_by="admin_test"
)
assert new_version == 2
# 3. Check current is v2
current = get_prompt_version(prompt_id, is_admin=True)
assert current["version"] == 2
assert current["system_prompt"] == "Updated system prompt for normalizer v2."
assert current["prompt_hash"] != old_hash
# 4. Verify historical v1 remains unchanged and intact
v1_recheck = get_prompt_version(prompt_id, version=1, is_admin=True)
assert v1_recheck["version"] == 1
assert v1_recheck["prompt_hash"] == old_hash
assert "Updated system prompt" not in v1_recheck["system_prompt"]
def test_non_admin_prompt_masking():
init_db()
prompt_id = "prior-art-search"
# Non-admin retrieval
public_view = get_prompt_version(prompt_id, is_admin=False)
assert public_view["system_prompt"] == "[REDACTED: ADMIN ONLY]"
assert public_view["user_prompt_template"] == "[REDACTED: ADMIN ONLY]"
assert public_view["name"] is not None
assert public_view["model_policy"] is not None
# Admin retrieval
admin_view = get_prompt_version(prompt_id, is_admin=True)
assert admin_view["system_prompt"] != "[REDACTED: ADMIN ONLY]"
assert admin_view["user_prompt_template"] != "[REDACTED: ADMIN ONLY]"
+133
View File
@@ -0,0 +1,133 @@
"""
ThinkStorm Trash & Empty Trash Tests
Tests moving ideas to trash, hiding from default browsing, restoring from trash,
permanently deleting single ideas, and emptying the entire trash bin with cascade cleanup.
"""
import pytest
from starlette.testclient import TestClient
from thinkstorm.main import app
from thinkstorm.database import init_db, get_db
@pytest.fixture(scope="module", autouse=True)
def setup_app():
init_db()
def get_auth_token(username="admin", password="admin-thinkstorm-pass-2026"):
client = TestClient(app)
resp = client.post("/api/auth/login", json={"username": username, "password": password})
assert resp.status_code == 200
return resp.json()["token"]
def test_trash_and_hide_from_default_catalog():
client = TestClient(app)
token = get_auth_token()
headers = {"Authorization": f"Bearer {token}"}
# 1. Create an idea and set to AVAILABLE
sub_resp = client.post("/api/ideas", json={"text": "A low-value idea that should be discarded: spam text test."})
idea_id = sub_resp.json()["id"]
with get_db() as conn:
conn.execute("UPDATE ideas SET lifecycle_state = 'AVAILABLE' WHERE id = ?", (idea_id,))
# Verify visible in default list
list_resp = client.get("/api/ideas", headers=headers)
idea_ids_default = [i["id"] for i in list_resp.json()]
assert idea_id in idea_ids_default
# 2. Move idea to Trash
trash_resp = client.post(f"/api/ideas/{idea_id}/trash", headers=headers)
assert trash_resp.status_code == 200
trash_data = trash_resp.json()
assert trash_data["lifecycle_state"] == "TRASHED"
assert trash_data["previous_lifecycle_state"] == "AVAILABLE"
# 3. Verify hidden from default ideas list and ALL filter
list_resp_after = client.get("/api/ideas", headers=headers)
idea_ids_after = [i["id"] for i in list_resp_after.json()]
assert idea_id not in idea_ids_after
list_all_resp = client.get("/api/ideas?state=ALL", headers=headers)
idea_ids_all = [i["id"] for i in list_all_resp.json()]
assert idea_id not in idea_ids_all
# 4. Verify visible when explicitly filtering by TRASHED
list_trashed_resp = client.get("/api/ideas?state=TRASHED", headers=headers)
idea_ids_trashed = [i["id"] for i in list_trashed_resp.json()]
assert idea_id in idea_ids_trashed
def test_restore_idea_from_trash():
client = TestClient(app)
token = get_auth_token()
headers = {"Authorization": f"Bearer {token}"}
# 1. Create idea, set to CLAIMED, then trash it
sub_resp = client.post("/api/ideas", json={"text": "Idea that was mistakenly trashed and needs retrieval."})
idea_id = sub_resp.json()["id"]
with get_db() as conn:
conn.execute("UPDATE ideas SET lifecycle_state = 'CLAIMED', claimed_by = 'admin' WHERE id = ?", (idea_id,))
trash_resp = client.post(f"/api/ideas/{idea_id}/trash", headers=headers)
assert trash_resp.status_code == 200
# 2. Restore idea
restore_resp = client.post(f"/api/ideas/{idea_id}/restore", headers=headers)
assert restore_resp.status_code == 200
assert restore_resp.json()["lifecycle_state"] == "CLAIMED"
# 3. Verify idea is restored and visible
detail_resp = client.get(f"/api/ideas/{idea_id}", headers=headers)
assert detail_resp.status_code == 200
assert detail_resp.json()["lifecycle_state"] == "CLAIMED"
def test_permanently_delete_single_idea():
client = TestClient(app)
token = get_auth_token()
headers = {"Authorization": f"Bearer {token}"}
# 1. Create idea and trash it
sub_resp = client.post("/api/ideas", json={"text": "Idea to be deleted permanently right now."})
idea_id = sub_resp.json()["id"]
client.post(f"/api/ideas/{idea_id}/trash", headers=headers)
# 2. Permanently delete
del_resp = client.delete(f"/api/ideas/{idea_id}/permanent", headers=headers)
assert del_resp.status_code == 200
assert "permanently deleted" in del_resp.json()["message"].lower()
# 3. Verify it no longer exists anywhere
detail_resp = client.get(f"/api/ideas/{idea_id}", headers=headers)
assert detail_resp.status_code == 404
def test_empty_trash_bulk():
client = TestClient(app)
token = get_auth_token()
headers = {"Authorization": f"Bearer {token}"}
# 1. Create 3 ideas and trash all 3
trash_ids = []
for i in range(3):
resp = client.post("/api/ideas", json={"text": f"Junk idea {i+1} destined for trash empty test."})
i_id = resp.json()["id"]
client.post(f"/api/ideas/{i_id}/trash", headers=headers)
trash_ids.append(i_id)
# Verify all 3 are currently in trash
trashed_list = client.get("/api/ideas?state=TRASHED", headers=headers).json()
trashed_ids_in_db = [x["id"] for x in trashed_list]
for tid in trash_ids:
assert tid in trashed_ids_in_db
# 2. Empty Trash
empty_resp = client.post("/api/trash/empty", headers=headers)
assert empty_resp.status_code == 200
data = empty_resp.json()
assert data["deleted_count"] >= 3
# 3. Verify trash list is now empty of those IDs
trashed_after = client.get("/api/ideas?state=TRASHED", headers=headers).json()
trashed_after_ids = [x["id"] for x in trashed_after]
for tid in trash_ids:
assert tid not in trashed_after_ids
+55
View File
@@ -0,0 +1,55 @@
"""
URL Extraction & VirusTotal Safety Rule Tests
PRD Section 14, 15:
- Any VirusTotal malicious detection (malicious > 0) blocks automation and quarantines idea.
- Safe URLs are approved.
"""
import pytest
import asyncio
from thinkstorm.processors.pipeline import process_url_safety, URL_REGEX
from thinkstorm.models import URLSafetyState, AutomationPolicy
from thinkstorm.database import init_db
@pytest.mark.asyncio
async def test_url_extraction():
text = "Check out this repo https://github.com/thinkstorm/core and also http://test.com/docs for details."
urls = URL_REGEX.findall(text)
assert len(urls) == 2
assert "https://github.com/thinkstorm/core" in urls
assert "http://test.com/docs" in urls
@pytest.mark.asyncio
async def test_safe_url_safety_check():
init_db()
idea_id = "TS-9901"
now = "2026-08-19T09:00:00Z"
from thinkstorm.database import get_db
with get_db() as conn:
conn.execute("INSERT OR IGNORE INTO ideas (id, original_text, submitted_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", (idea_id, "text", now, now, now))
text = "Great idea with safe link https://example.com/project"
url_records, quarantine_needed = await process_url_safety(idea_id, text)
assert len(url_records) == 1
assert quarantine_needed is False
assert url_records[0].safety_state == URLSafetyState.SAFE
assert url_records[0].automation_policy == AutomationPolicy.APPROVED
@pytest.mark.asyncio
async def test_malicious_url_safety_check_blocks_and_quarantines():
init_db()
idea_id = "TS-9902"
now = "2026-08-19T09:00:00Z"
from thinkstorm.database import get_db
with get_db() as conn:
conn.execute("INSERT OR IGNORE INTO ideas (id, original_text, submitted_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", (idea_id, "text", now, now, now))
text = "Suspicious submission with bad link https://malicious-site.testmalicious/payload"
url_records, quarantine_needed = await process_url_safety(idea_id, text)
assert len(url_records) == 1
assert quarantine_needed is True
assert url_records[0].safety_state == URLSafetyState.MALICIOUS
assert url_records[0].automation_policy == AutomationPolicy.BLOCKED
assert url_records[0].admin_review_required is True