fix(ci): restore green CI — libsqlcipher-dev, prep/survey test drift
Backend: add apt-get install libsqlcipher-dev before pip install so pysqlcipher3 builds in the runner image. Frontend: prep.test.ts was missing a qa mock (fetchFor now calls 5 endpoints in parallel; tests only mocked 4 — 5th returned undefined, threw in catch, research.value never set). survey.test.ts: analyze() was refactored from sync-result to async-task+poll; update test to mock POST then poll completion. Also remove Classic UI (Streamlit) button from AppNav — Streamlit is deprecated and the button caused an unrecoverable redirect loop.
This commit is contained in:
parent
0d6ddd35cf
commit
b44a7975bc
4 changed files with 19 additions and 45 deletions
|
|
@ -23,6 +23,9 @@ jobs:
|
||||||
python-version: '3.12'
|
python-version: '3.12'
|
||||||
cache: pip
|
cache: pip
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: sudo apt-get update -q && sudo apt-get install -y libsqlcipher-dev
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install -r requirements.txt
|
run: pip install -r requirements.txt
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,6 @@
|
||||||
<Cog6ToothIcon class="sidebar__icon" aria-hidden="true" />
|
<Cog6ToothIcon class="sidebar__icon" aria-hidden="true" />
|
||||||
<span class="sidebar__label">Settings</span>
|
<span class="sidebar__label">Settings</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<button class="sidebar__classic-btn" @click="switchToClassic" title="Switch to Classic (Streamlit) UI">
|
|
||||||
⚡ Classic
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
@ -134,23 +131,6 @@ function exitHackerMode() {
|
||||||
restoreTheme()
|
restoreTheme()
|
||||||
}
|
}
|
||||||
|
|
||||||
const _apiBase = import.meta.env.BASE_URL.replace(/\/$/, '')
|
|
||||||
|
|
||||||
async function switchToClassic() {
|
|
||||||
// Persist preference via API so Streamlit reads streamlit from user.yaml
|
|
||||||
// and won't re-set the cookie back to vue (avoids the ?prgn_switch rerun cycle)
|
|
||||||
try {
|
|
||||||
await fetch(_apiBase + '/api/settings/ui-preference', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ preference: 'streamlit' }),
|
|
||||||
})
|
|
||||||
} catch { /* non-fatal — cookie below is enough for immediate redirect */ }
|
|
||||||
document.cookie = 'prgn_ui=streamlit; path=/; SameSite=Lax'
|
|
||||||
// Navigate to root (no query params) — Caddy routes to Streamlit based on cookie
|
|
||||||
window.location.href = window.location.origin + '/'
|
|
||||||
}
|
|
||||||
|
|
||||||
const navLinks = computed(() => [
|
const navLinks = computed(() => [
|
||||||
{ to: '/', icon: HomeIcon, label: 'Home' },
|
{ to: '/', icon: HomeIcon, label: 'Home' },
|
||||||
{ to: '/review', icon: ClipboardDocumentListIcon, label: 'Job Review' },
|
{ to: '/review', icon: ClipboardDocumentListIcon, label: 'Job Review' },
|
||||||
|
|
@ -321,29 +301,6 @@ const mobileLinks = [
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__classic-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
padding: var(--space-2) var(--space-3);
|
|
||||||
margin-top: var(--space-1);
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: var(--text-xs);
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.6;
|
|
||||||
transition: opacity 150ms, background 150ms;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar__classic-btn:hover {
|
|
||||||
opacity: 1;
|
|
||||||
background: var(--color-surface-alt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Theme picker ───────────────────────────────────── */
|
/* ── Theme picker ───────────────────────────────────── */
|
||||||
.sidebar__theme {
|
.sidebar__theme {
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ describe('usePrepStore', () => {
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: '2026-03-20T12:00:00' }, error: null }) // research
|
generated_at: '2026-03-20T12:00:00' }, error: null }) // research
|
||||||
.mockResolvedValueOnce({ data: [], error: null }) // contacts
|
.mockResolvedValueOnce({ data: [], error: null }) // contacts
|
||||||
|
.mockResolvedValueOnce({ data: [], error: null }) // qa
|
||||||
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) // task
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) // task
|
||||||
.mockResolvedValueOnce({ data: { id: 1, title: 'Engineer', company: 'Acme', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Engineer', company: 'Acme', url: null,
|
||||||
description: 'Build things.', cover_letter: null, match_score: 80,
|
description: 'Build things.', cover_letter: null, match_score: 80,
|
||||||
|
|
@ -50,6 +51,7 @@ describe('usePrepStore', () => {
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: null }, error: null })
|
generated_at: null }, error: null })
|
||||||
.mockResolvedValueOnce({ data: [], error: null })
|
.mockResolvedValueOnce({ data: [], error: null })
|
||||||
|
.mockResolvedValueOnce({ data: [], error: null }) // qa
|
||||||
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
||||||
.mockResolvedValueOnce({ data: { id: 1, title: 'Old Job', company: 'OldCo', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Old Job', company: 'OldCo', url: null,
|
||||||
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
||||||
|
|
@ -62,6 +64,7 @@ describe('usePrepStore', () => {
|
||||||
mockApiFetch
|
mockApiFetch
|
||||||
.mockResolvedValueOnce({ data: null, error: { kind: 'http', status: 404, detail: '' } }) // 404 → null
|
.mockResolvedValueOnce({ data: null, error: { kind: 'http', status: 404, detail: '' } }) // 404 → null
|
||||||
.mockResolvedValueOnce({ data: [], error: null })
|
.mockResolvedValueOnce({ data: [], error: null })
|
||||||
|
.mockResolvedValueOnce({ data: [], error: null }) // qa
|
||||||
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
||||||
.mockResolvedValueOnce({ data: { id: 2, title: 'New Job', company: 'NewCo', url: null,
|
.mockResolvedValueOnce({ data: { id: 2, title: 'New Job', company: 'NewCo', url: null,
|
||||||
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
||||||
|
|
@ -102,6 +105,7 @@ describe('usePrepStore', () => {
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: null }, error: null })
|
generated_at: null }, error: null })
|
||||||
.mockResolvedValueOnce({ data: [], error: null })
|
.mockResolvedValueOnce({ data: [], error: null })
|
||||||
|
.mockResolvedValueOnce({ data: [], error: null }) // qa
|
||||||
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
||||||
.mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null,
|
||||||
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
||||||
|
|
@ -112,11 +116,12 @@ describe('usePrepStore', () => {
|
||||||
// Mock first poll → completed
|
// Mock first poll → completed
|
||||||
mockApiFetch
|
mockApiFetch
|
||||||
.mockResolvedValueOnce({ data: { status: 'completed', stage: null, message: null }, error: null })
|
.mockResolvedValueOnce({ data: { status: 'completed', stage: null, message: null }, error: null })
|
||||||
// re-fetch on completed: research, contacts, task, fullJob
|
// re-fetch on completed: research, contacts, qa, task, fullJob
|
||||||
.mockResolvedValueOnce({ data: { company_brief: 'Updated!', ceo_brief: null, talking_points: null,
|
.mockResolvedValueOnce({ data: { company_brief: 'Updated!', ceo_brief: null, talking_points: null,
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: '2026-03-20T13:00:00' }, error: null })
|
generated_at: '2026-03-20T13:00:00' }, error: null })
|
||||||
.mockResolvedValueOnce({ data: [], error: null })
|
.mockResolvedValueOnce({ data: [], error: null })
|
||||||
|
.mockResolvedValueOnce({ data: [], error: null }) // qa
|
||||||
.mockResolvedValueOnce({ data: { status: 'completed', stage: null, message: null }, error: null })
|
.mockResolvedValueOnce({ data: { status: 'completed', stage: null, message: null }, error: null })
|
||||||
.mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null,
|
||||||
description: 'Now with content', cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
description: 'Now with content', cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
||||||
|
|
@ -134,6 +139,7 @@ describe('usePrepStore', () => {
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: null }, error: null })
|
generated_at: null }, error: null })
|
||||||
.mockResolvedValueOnce({ data: [], error: null })
|
.mockResolvedValueOnce({ data: [], error: null })
|
||||||
|
.mockResolvedValueOnce({ data: [], error: null }) // qa
|
||||||
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null })
|
||||||
.mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Eng', company: 'Acme', url: null,
|
||||||
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
description: null, cover_letter: null, match_score: null, keyword_gaps: null }, error: null })
|
||||||
|
|
@ -162,6 +168,7 @@ describe('usePrepStore', () => {
|
||||||
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
tech_brief: null, funding_brief: null, red_flags: null, accessibility_brief: null,
|
||||||
generated_at: '2026-03-20T12:00:00' }, error: null }) // research OK
|
generated_at: '2026-03-20T12:00:00' }, error: null }) // research OK
|
||||||
.mockResolvedValueOnce({ data: null, error: { kind: 'http', status: 500, detail: 'DB error' } }) // contacts fail
|
.mockResolvedValueOnce({ data: null, error: { kind: 'http', status: 500, detail: 'DB error' } }) // contacts fail
|
||||||
|
.mockResolvedValueOnce({ data: [], error: null }) // qa
|
||||||
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) // task OK
|
.mockResolvedValueOnce({ data: { status: 'none', stage: null, message: null }, error: null }) // task OK
|
||||||
.mockResolvedValueOnce({ data: { id: 1, title: 'Engineer', company: 'Acme', url: null,
|
.mockResolvedValueOnce({ data: { id: 1, title: 'Engineer', company: 'Acme', url: null,
|
||||||
description: 'Build things.', cover_letter: null, match_score: 80,
|
description: 'Build things.', cover_letter: null, match_score: 80,
|
||||||
|
|
|
||||||
|
|
@ -54,14 +54,20 @@ describe('useSurveyStore', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('analyze stores result including mode and rawInput', async () => {
|
it('analyze stores result including mode and rawInput', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
const mockApiFetch = vi.mocked(useApiFetch)
|
const mockApiFetch = vi.mocked(useApiFetch)
|
||||||
|
// POST → task accepted
|
||||||
|
mockApiFetch.mockResolvedValueOnce({ data: { task_id: 7, is_new: true }, error: null })
|
||||||
|
// Poll → completed with result
|
||||||
mockApiFetch.mockResolvedValueOnce({
|
mockApiFetch.mockResolvedValueOnce({
|
||||||
data: { output: '1. B — reason', source: 'text_paste' },
|
data: { status: 'completed', stage: null, message: null,
|
||||||
|
result: { output: '1. B — reason', source: 'text_paste' } },
|
||||||
error: null,
|
error: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = useSurveyStore()
|
const store = useSurveyStore()
|
||||||
await store.analyze(1, { text: 'Q1: test', mode: 'quick' })
|
await store.analyze(1, { text: 'Q1: test', mode: 'quick' })
|
||||||
|
await vi.advanceTimersByTimeAsync(3000)
|
||||||
|
|
||||||
expect(store.analysis).not.toBeNull()
|
expect(store.analysis).not.toBeNull()
|
||||||
expect(store.analysis!.output).toBe('1. B — reason')
|
expect(store.analysis!.output).toBe('1. B — reason')
|
||||||
|
|
@ -69,6 +75,7 @@ describe('useSurveyStore', () => {
|
||||||
expect(store.analysis!.mode).toBe('quick')
|
expect(store.analysis!.mode).toBe('quick')
|
||||||
expect(store.analysis!.rawInput).toBe('Q1: test')
|
expect(store.analysis!.rawInput).toBe('Q1: test')
|
||||||
expect(store.loading).toBe(false)
|
expect(store.loading).toBe(false)
|
||||||
|
vi.useRealTimers()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('analyze sets error on failure', async () => {
|
it('analyze sets error on failure', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue