mirror of
https://github.com/computerim/impactflow-discovery.git
synced 2026-08-27 07:10:37 +00:00
Complete Phase 1: goals, cookie auth, profile editing
Close the remaining Phase 1 DoD gaps and reconcile the browser flow with the auth layer. Goals (5 -> 7 prompts): - Add near-term (6-12mo) and long-term (3-5yr) goal prompts; collect raw text on the conversation and store AI-articulated goal summaries on the profile. Extractor articulates the person's own stated goals (mirror, not compass) and never fabricates. Alembic 003 adds the four columns. Cookie-based browser sessions (fixes frontend<->auth desync): - OAuth callback now sets httpOnly session cookies and redirects into the app instead of returning JSON. get_current_user gains a cookie fallback (X-API-Key -> Bearer -> cookie). refresh/logout read the refresh cookie and set/clear cookies. New shared auth.js (authedFetch) sends cookies and silently refreshes on 401. Static pages drop the bogus user_id and call the correct /me endpoints. Profile editing (read/edit/affirm): - PATCH /discovery/profile/me edits the prose (Ikigai summaries, overlap narrative, goals); owner-scoped, partial update, 409 when locked. Edit mode in profile.html with Save/Cancel. Also: bump default model to claude-sonnet-4-6, align ports to 8011 (OAuth redirect, CORS), add COOKIE_SECURE/POST_LOGIN_REDIRECT config, and refresh the README to match the shipped behavior. Tests: 33 passing (added cookie-auth, profile-edit, goal-extraction cases; factored a shared app_client fixture into conftest.py). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""Tests for PATCH /discovery/profile/me (profile editing).
|
||||
|
||||
These run under the X-API-Key admin identity, editing a profile seeded for
|
||||
that user, so they don't depend on the Google OAuth flow.
|
||||
"""
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
API_KEY = {"X-API-Key": "test-api-key"}
|
||||
|
||||
|
||||
async def _seed_profile(locked: bool = False) -> str:
|
||||
"""Insert a conversation + profile for the API-key admin user."""
|
||||
from app.auth import API_KEY_ADMIN_ID, ensure_api_key_admin
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models import DiscoveryConversation, DiscoveryProfile
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
await ensure_api_key_admin(db)
|
||||
conv = DiscoveryConversation(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=API_KEY_ADMIN_ID,
|
||||
started_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(conv)
|
||||
await db.commit()
|
||||
|
||||
profile = DiscoveryProfile(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=API_KEY_ADMIN_ID,
|
||||
conversation_id=conv.id,
|
||||
generated_at=datetime.now(timezone.utc),
|
||||
triad="gut",
|
||||
probable_type=8,
|
||||
wing=9,
|
||||
instinctual_variant="sp",
|
||||
love_summary="orig love",
|
||||
strength_summary="orig strength",
|
||||
mission_summary="orig mission",
|
||||
vocation_summary="orig vocation",
|
||||
overlap_narrative="orig narrative",
|
||||
short_term_goals="orig short",
|
||||
long_term_goals="orig long",
|
||||
locked=locked,
|
||||
)
|
||||
db.add(profile)
|
||||
await db.commit()
|
||||
return profile.id
|
||||
|
||||
|
||||
async def test_patch_updates_prose(app_client):
|
||||
await _seed_profile()
|
||||
r = await app_client.patch(
|
||||
"/discovery/profile/me",
|
||||
headers=API_KEY,
|
||||
json={"love_summary": "new love", "overlap_narrative": "new narrative"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["love_summary"] == "new love"
|
||||
assert body["overlap_narrative"] == "new narrative"
|
||||
# Untouched prose preserved; structural inference is never editable here.
|
||||
assert body["strength_summary"] == "orig strength"
|
||||
assert body["triad"] == "gut"
|
||||
assert body["probable_type"] == 8
|
||||
|
||||
|
||||
async def test_patch_partial_does_not_clear_other_fields(app_client):
|
||||
await _seed_profile()
|
||||
r = await app_client.patch(
|
||||
"/discovery/profile/me",
|
||||
headers=API_KEY,
|
||||
json={"short_term_goals": "updated goal"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["short_term_goals"] == "updated goal"
|
||||
assert body["long_term_goals"] == "orig long"
|
||||
|
||||
|
||||
async def test_patch_locked_profile_returns_409(app_client):
|
||||
await _seed_profile(locked=True)
|
||||
r = await app_client.patch(
|
||||
"/discovery/profile/me",
|
||||
headers=API_KEY,
|
||||
json={"love_summary": "x"},
|
||||
)
|
||||
assert r.status_code == 409
|
||||
|
||||
|
||||
async def test_patch_without_profile_returns_404(app_client):
|
||||
r = await app_client.patch(
|
||||
"/discovery/profile/me",
|
||||
headers=API_KEY,
|
||||
json={"love_summary": "x"},
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
async def test_patch_empty_payload_returns_400(app_client):
|
||||
await _seed_profile()
|
||||
r = await app_client.patch(
|
||||
"/discovery/profile/me", headers=API_KEY, json={}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
async def test_patch_requires_auth(app_client):
|
||||
r = await app_client.patch(
|
||||
"/discovery/profile/me", json={"love_summary": "x"}
|
||||
)
|
||||
assert r.status_code == 401
|
||||
Reference in New Issue
Block a user