Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions __tests__/components/build/ArtifactFrame-keep-going.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// @vitest-environment jsdom
import { describe, it, expect, vi, beforeAll, afterEach } from 'vitest'
import React from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { act } from 'react'

beforeAll(() => {
;(globalThis as any).React = React
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
})

/**
* #1038: once state.auto becomes false (via "Take the wheel", or via resuming
* an in-progress build — START_BUILD's resume path per #1033 deliberately
* leaves auto:false), there was NO UI affordance anywhere that dispatched
* KEEP_GOING (auto: true) — the reducer case existed but had zero callers.
* Autoplay's own gate (useAutoplay.ts:82-83) means a paused-but-unfinished
* build silently stopped generating forever.
*
* This test asserts ArtifactFrame renders a "Keep going" button, wired to
* dispatch({ type: 'KEEP_GOING' }), exactly when the build is paused
* (!state.auto) AND the active track still has undone/non-errored views
* remaining — mirroring useAutoplay.ts's own trackViews(state.track) +
* state.done/state.genError "next" lookup, not a reinvented check.
*/

let mockState: any
const dispatchMock = vi.fn()
const goViewMock = vi.fn()
vi.mock('@/contexts/build-context', () => ({
useBuild: () => ({
state: mockState,
views: ['thesis', 'wedge', 'businessModel', 'positioning', 'landing', 'plan30'],
goView: goViewMock,
dispatch: dispatchMock,
}),
}))

import { ArtifactFrame } from '@/components/build/ArtifactFrame'

let host: HTMLElement
let root: Root

function render(view = 'businessModel') {
host = document.createElement('div')
document.body.appendChild(host)
root = createRoot(host)
act(() => {
root.render(
React.createElement(ArtifactFrame, { title: 'Business Model', status: 'done', view }),
)
})
}

function baseState(overrides: Partial<any> = {}) {
return {
companyName: 'Acme',
idea: 'A coffee subscription platform',
track: 'company',
auto: false,
paused: false,
generated: {},
done: { thesis: true, wedge: true },
genError: {},
appChatId: 'acme-co',
sawPreview: false,
conflictResolved: false,
...overrides,
}
}

function keepGoingButton() {
return Array.from(host.querySelectorAll('button')).find((b) => /keep going/i.test(b.textContent || ''))
}

afterEach(() => {
act(() => { root?.unmount() })
host?.remove()
vi.clearAllMocks()
vi.unstubAllGlobals()
})

describe('ArtifactFrame "Keep going" resume affordance (#1038)', () => {
it('renders a Keep going button when paused (auto:false) with unfinished track views', () => {
mockState = baseState() // auto:false, businessModel/positioning/landing/plan30 all undone
render('businessModel')

const btn = keepGoingButton()
expect(btn).toBeTruthy()
})

it('dispatches KEEP_GOING when clicked, restoring auto:true via the reducer', () => {
mockState = baseState()
render('businessModel')

const btn = keepGoingButton()!
act(() => { btn.dispatchEvent(new MouseEvent('click', { bubbles: true })) })

expect(dispatchMock).toHaveBeenCalledWith({ type: 'KEEP_GOING' })
})

it('does NOT render Keep going when auto is already true', () => {
mockState = baseState({ auto: true })
render('businessModel')

expect(keepGoingButton()).toBeFalsy()
})

it('does NOT render Keep going when the track is actually finished (no undone/non-errored views remain)', () => {
mockState = baseState({
auto: false,
done: {
design: true, thesis: true, wedge: true, businessModel: true,
positioning: true, landing: true, plan30: true,
},
})
render('plan30')

expect(keepGoingButton()).toBeFalsy()
})

it('still renders the manual ‹ Back / Next › pager alongside Keep going when paused', () => {
mockState = baseState()
render('businessModel')

const backBtn = Array.from(host.querySelectorAll('button')).find((b) => /back/i.test(b.textContent || ''))
const nextBtn = Array.from(host.querySelectorAll('button')).find((b) => /next/i.test(b.textContent || ''))
expect(backBtn).toBeTruthy()
expect(nextBtn).toBeTruthy()
expect(keepGoingButton()).toBeTruthy()
})
})
25 changes: 24 additions & 1 deletion components/build/ArtifactFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { useBuild } from '@/contexts/build-context'
import { SHARED_LATE_VIEWS } from '@/lib/build/state'
import { SHARED_LATE_VIEWS, trackViews } from '@/lib/build/state'
import { collectPrior, serializeArtifact, applyEdit } from '@/lib/build/artifact-edit'
import { CompanyNameEdit } from '@/components/build/CompanyNameEdit'
import { useState, type ReactNode } from 'react'
Expand Down Expand Up @@ -62,6 +62,19 @@ export function ArtifactFrame({
const isLate = isSharedLateView(view)
const companyLabel = state.companyName || 'company'

// #1038: "Take the wheel" (and resuming an in-progress build, which per
// #1033's START_BUILD comment deliberately leaves auto:false) sets
// state.auto to false, which is the ONLY gate useAutoplay.ts checks before
// it will fetch another artifact. KEEP_GOING (auto: true) has existed in
// the reducer the whole time but had no caller anywhere in the component
// tree — so once paused, a build could never resume on its own. Surface a
// "Keep going" affordance whenever paused AND the active track still has
// real work left, using the exact same "next undone, non-errored view"
// lookup useAutoplay.ts itself uses to decide whether a track is complete,
// so this never appears once the track has actually finished.
const hasUnfinishedWork = trackViews(state.track).some((v) => !state.done?.[v] && !state.genError?.[v])
const canKeepGoing = !isLate && !state.auto && hasUnfinishedWork

// ── Per-artifact review actions (GR-16 #329) ──────────────────────────────
// Shown on every artifact that has GENERATED content (prose views after
// GEN_DONE). Build views (swarm/infra/preview) and late shared views have no
Expand Down Expand Up @@ -259,6 +272,16 @@ export function ArtifactFrame({
) : !state.auto ? (
<div className="m-artifact-nav">
<button className="btn-ghost" disabled={!prev} onClick={() => prev && goView(prev as never)}>‹ Back</button>
{/* #1038: restores the dead KEEP_GOING path — the only way back to
autoplay once paused (via "Take the wheel" or a resumed build)
was previously nonexistent. Hidden once the track has actually
finished (hasUnfinishedWork is false), since there's nothing
left for autoplay to resume. */}
{canKeepGoing && (
<button className="btn-secondary" onClick={() => dispatch({ type: 'KEEP_GOING' })}>
Keep going →
</button>
)}
{/* On the LAST artifact (e.g. preview) there is no next artifact, so the
pager 'Next' used to be a disabled dead-end — users clicked it and
nothing happened. Instead, advance to the pricing/pay-gate (the real
Expand Down
Loading