Compare commits
3 Commits
d386d11af2
...
pre-ai-han
| Author | SHA1 | Date | |
|---|---|---|---|
| e110fedfe4 | |||
| dab740ddf7 | |||
| 24972e8444 |
@@ -14,21 +14,33 @@ from sqlalchemy.pool import NullPool
|
||||
|
||||
from app.main import app
|
||||
from app.core.database import Base, get_db
|
||||
from app.core.admin_database import get_admin_db
|
||||
from app.core.config import settings
|
||||
|
||||
# Disable invite code requirement for tests
|
||||
settings.REQUIRE_INVITE_CODE = False
|
||||
|
||||
# Test database URL (separate from production)
|
||||
# Use DATABASE_TEST_URL env var if set (e.g. inside Docker where host is 'db'),
|
||||
# otherwise fall back to localhost for local development.
|
||||
# Test database URL — NEVER reuse DATABASE_URL. The test_db fixture does
|
||||
# `DROP SCHEMA public CASCADE` on every test; if DATABASE_URL (which normally
|
||||
# points at the dev/prod DB) leaked into this value, running `pytest tests/`
|
||||
# would silently nuke the dev database. Only DATABASE_TEST_URL is honored,
|
||||
# and the safety assertion below refuses to run against a DB whose name
|
||||
# doesn't contain "test".
|
||||
import os
|
||||
TEST_DATABASE_URL = os.environ.get(
|
||||
"DATABASE_URL",
|
||||
os.environ.get(
|
||||
"DATABASE_TEST_URL",
|
||||
"postgresql+asyncpg://postgres:postgres@localhost:5432/patherly_test",
|
||||
),
|
||||
"DATABASE_TEST_URL",
|
||||
"postgresql+asyncpg://postgres:postgres@localhost:5432/resolutionflow_test",
|
||||
)
|
||||
|
||||
# Belt-and-suspenders: refuse to run tests against a DB whose name doesn't
|
||||
# contain "test". Parses the last path segment of the URL (everything after
|
||||
# the final '/', with query string stripped) so credentials / hosts that
|
||||
# happen to contain "test" can't bypass the check.
|
||||
_test_db_name = TEST_DATABASE_URL.rsplit("/", 1)[-1].split("?", 1)[0].lower()
|
||||
assert "test" in _test_db_name, (
|
||||
f"Refusing to run tests against database {_test_db_name!r} — "
|
||||
f"the DB name must contain 'test'. Set DATABASE_TEST_URL to a dedicated "
|
||||
f"test database (e.g. resolutionflow_test)."
|
||||
)
|
||||
|
||||
|
||||
@@ -131,6 +143,11 @@ async def client(test_db: AsyncSession):
|
||||
yield test_db
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
# Endpoints that use get_admin_db (register, admin routes, service accounts)
|
||||
# must also hit the test DB; otherwise they leak into the real admin DB.
|
||||
# RLS is not enabled in the test schema (create_all, not alembic), so sharing
|
||||
# the same session is safe.
|
||||
app.dependency_overrides[get_admin_db] = override_get_db
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
|
||||
@@ -33,6 +33,9 @@ services:
|
||||
- DEBUG=true
|
||||
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/resolutionflow
|
||||
- DATABASE_URL_SYNC=postgresql://postgres:postgres@db:5432/resolutionflow
|
||||
# Dedicated test database — pytest will refuse to run against any DB
|
||||
# whose name doesn't contain 'test' (conftest.py safety assertion).
|
||||
- DATABASE_TEST_URL=postgresql+asyncpg://postgres:postgres@db:5432/resolutionflow_test
|
||||
- SECRET_KEY=${SECRET_KEY}
|
||||
- ALGORITHM=HS256
|
||||
- ACCESS_TOKEN_EXPIRE_MINUTES=15
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
* Visual reference: docs/FlowAssist_Migration/mockups/07-verify-states.html
|
||||
* (panel C).
|
||||
*/
|
||||
import { useState } from 'react'
|
||||
import { X, AlertCircle, Check, Info } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { FixOutcome } from '@/api/sessionSuggestedFixes'
|
||||
|
||||
export type InterceptChoice = FixOutcome | 'never_applied'
|
||||
|
||||
export interface EscalateInterceptDialogProps {
|
||||
fixTitle: string
|
||||
onChoose: (choice: InterceptChoice) => void
|
||||
onChoose: (choice: InterceptChoice, notes?: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
@@ -24,6 +26,11 @@ export function EscalateInterceptDialog({
|
||||
onChoose,
|
||||
onClose,
|
||||
}: EscalateInterceptDialogProps) {
|
||||
const [partialStep, setPartialStep] = useState(false)
|
||||
const [partialNotes, setPartialNotes] = useState('')
|
||||
|
||||
const notesValid = partialNotes.trim().length > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -36,45 +43,86 @@ export function EscalateInterceptDialog({
|
||||
aria-label="Capture fix outcome before escalating"
|
||||
className="absolute bottom-full mb-2 left-0 z-50 w-[340px] rounded-lg border border-white/15 bg-card p-3.5 shadow-[0_18px_40px_rgba(0,0,0,0.55)]"
|
||||
>
|
||||
<div className="font-heading font-semibold text-[13px] text-heading mb-1">
|
||||
Before escalating — what happened with the fix?
|
||||
</div>
|
||||
<div className="text-[12px] text-muted-foreground leading-[1.5] mb-3">
|
||||
“{fixTitle}” is still in the Verifying state. Tag its outcome so
|
||||
the senior picking this up knows what's been tried.
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<button
|
||||
autoFocus
|
||||
onClick={() => onChoose('applied_failed')}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-danger/30 bg-danger-dim text-[12.5px] text-primary hover:bg-danger-dim/80 hover:border-danger transition-colors text-left"
|
||||
>
|
||||
<X size={13} strokeWidth={2.5} className="text-danger" />
|
||||
<span className="flex-1">The fix didn't work</span>
|
||||
<span className="text-[10.5px] text-muted-foreground font-mono px-1.5 py-[2px] rounded bg-white/[0.05]">↵</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onChoose('applied_partial')}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
|
||||
>
|
||||
<Info size={13} strokeWidth={2} />
|
||||
<span className="flex-1">I applied some of it — partial</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onChoose('applied_success')}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
|
||||
>
|
||||
<Check size={13} strokeWidth={2} />
|
||||
<span className="flex-1">It worked — escalating for another reason</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onChoose('never_applied')}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
|
||||
>
|
||||
<AlertCircle size={13} strokeWidth={2} />
|
||||
<span className="flex-1">Never actually applied it</span>
|
||||
</button>
|
||||
</div>
|
||||
{!partialStep ? (
|
||||
<>
|
||||
<div className="font-heading font-semibold text-[13px] text-heading mb-1">
|
||||
Before escalating — what happened with the fix?
|
||||
</div>
|
||||
<div className="text-[12px] text-muted-foreground leading-[1.5] mb-3">
|
||||
“{fixTitle}” is still in the Verifying state. Tag its outcome so
|
||||
the senior picking this up knows what's been tried.
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<button
|
||||
autoFocus
|
||||
onClick={() => onChoose('applied_failed')}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-danger/30 bg-danger-dim text-[12.5px] text-primary hover:bg-danger-dim/80 hover:border-danger transition-colors text-left"
|
||||
>
|
||||
<X size={13} strokeWidth={2.5} className="text-danger" />
|
||||
<span className="flex-1">The fix didn't work</span>
|
||||
<span className="text-[10.5px] text-muted-foreground font-mono px-1.5 py-[2px] rounded bg-white/[0.05]">↵</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPartialStep(true)}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
|
||||
>
|
||||
<Info size={13} strokeWidth={2} />
|
||||
<span className="flex-1">I applied some of it — partial</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onChoose('applied_success')}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
|
||||
>
|
||||
<Check size={13} strokeWidth={2} />
|
||||
<span className="flex-1">It worked — escalating for another reason</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onChoose('never_applied')}
|
||||
className="flex items-center gap-2.5 px-3 py-2.5 rounded-lg border border-white/10 bg-elevated text-[12.5px] text-primary hover:bg-sidebar transition-colors text-left"
|
||||
>
|
||||
<AlertCircle size={13} strokeWidth={2} />
|
||||
<span className="flex-1">Never actually applied it</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="font-heading font-semibold text-[13px] text-heading mb-1">
|
||||
What partially worked?
|
||||
</div>
|
||||
<div className="text-[12px] text-muted-foreground leading-[1.5] mb-3">
|
||||
A short note for whoever picks this up — what you tried, what worked, what's still broken.
|
||||
</div>
|
||||
<textarea
|
||||
autoFocus
|
||||
value={partialNotes}
|
||||
onChange={(e) => setPartialNotes(e.target.value)}
|
||||
placeholder="e.g. Cleared cached creds; rebuild profile still hung on sync."
|
||||
className="w-full h-24 resize-none rounded-md border border-white/10 bg-elevated px-2.5 py-2 text-[12.5px] text-primary placeholder:text-muted-foreground focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2 mt-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
setPartialStep(false)
|
||||
setPartialNotes('')
|
||||
}}
|
||||
className="text-[12px] text-muted-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onChoose('applied_partial', partialNotes.trim())}
|
||||
disabled={!notesValid}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded text-[12.5px] font-semibold transition-colors',
|
||||
'bg-accent text-[#0a0d14] hover:brightness-110 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
)}
|
||||
>
|
||||
Record partial & escalate
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -656,19 +656,33 @@ export default function AssistantChatPage() {
|
||||
setShowConclude(true)
|
||||
}, [activeFix])
|
||||
|
||||
const handleInterceptChoice = useCallback(async (choice: InterceptChoice) => {
|
||||
const handleInterceptChoice = useCallback(async (choice: InterceptChoice, notes?: string) => {
|
||||
const stored = escalateIntercept
|
||||
setEscalateIntercept(null)
|
||||
if (!stored || !activeChatId) return
|
||||
if (!stored || !activeChatId) {
|
||||
setEscalateIntercept(null)
|
||||
return
|
||||
}
|
||||
const outcomeToSend: FixOutcome =
|
||||
choice === 'never_applied' ? 'dismissed' : choice
|
||||
try {
|
||||
const updated = await sessionSuggestedFixesApi.patchOutcome(
|
||||
activeChatId, stored.fixId, outcomeToSend,
|
||||
activeChatId, stored.fixId, outcomeToSend, notes,
|
||||
)
|
||||
setActiveFix(updated)
|
||||
} catch { /* non-fatal — engineer can still escalate */ }
|
||||
setShowConclude(true)
|
||||
setEscalateIntercept(null)
|
||||
setShowConclude(true)
|
||||
} catch (err) {
|
||||
// applied_partial without notes (or any other 4xx) must surface — the
|
||||
// previous silent catch let engineers believe the partial outcome was
|
||||
// recorded while it was rejected server-side.
|
||||
const message = choice === 'applied_partial'
|
||||
? 'Couldn’t record the partial outcome. Add a short note and try again.'
|
||||
: 'Couldn’t record the outcome before escalating. Try again.'
|
||||
toast.error(message)
|
||||
// Keep the intercept open so the engineer can retry (partial path can
|
||||
// re-enter the notes step from the dialog).
|
||||
if (import.meta.env.DEV) console.warn('[AssistantChat] intercept outcome failed:', err)
|
||||
}
|
||||
}, [activeChatId, escalateIntercept])
|
||||
|
||||
// Phase 8: Resolve click — auto-mark applied_success if in verifying state
|
||||
@@ -1629,7 +1643,12 @@ export default function AssistantChatPage() {
|
||||
so both scroll positions and state are preserved across tab switches. */}
|
||||
{showTabStrip && activeFix && activeChatId && (
|
||||
<div className={cn('flex-1 min-h-0 flex flex-col', chatTab !== 'script_builder' && 'hidden')}>
|
||||
{/* key={activeFix.id} forces a fresh mount when the active
|
||||
fix changes within the same pilot session — otherwise
|
||||
stale messages / editorBuffer / latestScript from the
|
||||
prior fix could submit against the new fix.id. */}
|
||||
<ScriptBuilderTab
|
||||
key={activeFix.id}
|
||||
fix={activeFix}
|
||||
pilotSessionId={activeChatId}
|
||||
onProgressChange={setScriptBuilderHasProgress}
|
||||
|
||||
Reference in New Issue
Block a user