feat(§8.12/§8.3): main-view Ask cuts an edit branch and runs the question there (v0.54.0)
The AI "Ask" affordance (selection tooltip + prompt bar) had no response surface on an entry's canonical `main` view: RFCView renders the human- discussion panel there, not the chat panel, so a chat turn posted from the reading view had nowhere to render — asking-while-reading silently did nothing. AI chat is an editing activity (a turn can emit <change> proposals) and only runs on an edit branch. Option B: when Ask is invoked from main, transparently cut an edit branch via the same dispatch as Start Contributing (promote-to-branch for active, start-edit-branch for super-draft; idempotent — reuse an existing edit branch), navigate onto it so the chat panel mounts, and run the question (text + selected quote) as the branch's first chat turn once its view and main_thread_id resolve. The turn fires from the message-load effect's continuation (not a parallel effect) so a late message load can't clobber the optimistic turn; a live ref keeps the latest submitChatTurn closure in reach. Degrades gracefully: signed-out → login; a viewer who can't contribute has the cut rejected server-side and the error surfaced (no spurious branch); asking from a branch already, and Flag on main (human discussion thread), unchanged. Frontend-only; the branch chat endpoints already work on a branch. SPEC §8.3 records the behavior; new RFCView unit tests + an e2e spec cover the flow. backend 677 / frontend 66 / e2e 5 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import { test, expect } from './lib/fixtures.js'
|
||||
import { signIn, OWNER_EMAIL } from './lib/auth.js'
|
||||
import { dismissCookies } from './lib/ui.js'
|
||||
|
||||
// §8.12 Option B — asking-while-reading on the canonical `main` view.
|
||||
//
|
||||
// Before this fix, the AI "Ask" affordance had no chat surface on main (the
|
||||
// human-discussion panel renders there, not the chat panel), so asking from the
|
||||
// reading view silently did nothing. Option B transparently cuts an edit branch,
|
||||
// navigates to it, and runs the question there.
|
||||
//
|
||||
// This spec asserts the model-independent core: the branch cut + navigation
|
||||
// (old code did NOTHING here — the silent no-op), and that the question turn
|
||||
// actually FIRED against the branch. The turn's outcome is environment-specific
|
||||
// — PPE/prod (a model is configured) streams an answer and the optimistic
|
||||
// question persists; the Tier-1 stack has no AI provider, so the turn errors
|
||||
// with "Chat failed". Either outcome proves the turn fired; we match both so the
|
||||
// one spec passes in both environments. The quote-passthrough + answer rendering
|
||||
// are covered deterministically by the RFCView unit tests (model mocked).
|
||||
//
|
||||
// Driven through the prompt bar, which shares handlePrompt → handleMainAsk with
|
||||
// the selection tooltip's onAsk. Runs against the faceted `bdd` collection entry
|
||||
// (collection-scoped — the same three-tier path G-15 hardened).
|
||||
const ENTRY = '/p/ohm/c/bdd/e/checkout-returning'
|
||||
|
||||
test('Ask from the canonical view cuts an edit branch and lands the question in chat', async ({ page }) => {
|
||||
await signIn(page, OWNER_EMAIL)
|
||||
await page.goto(ENTRY)
|
||||
await dismissCookies(page)
|
||||
|
||||
// We start on the canonical (main) view: read-only, no chat surface — the
|
||||
// discuss-mode banner is the canonical-view tell, and the URL has no branch.
|
||||
await expect(page.locator('.discuss-mode-banner')).toContainText('read-only')
|
||||
expect(new URL(page.url()).searchParams.get('branch')).toBeNull()
|
||||
|
||||
// Ask a question from the prompt bar.
|
||||
const question = `What problem does this entry solve? (${Date.now()})`
|
||||
await page.locator('.prompt-input').fill(question)
|
||||
await page.getByRole('button', { name: 'Ask', exact: true }).click()
|
||||
|
||||
// Option B: an edit branch is cut and we navigate onto it (no silent no-op).
|
||||
await expect(async () => {
|
||||
expect(new URL(page.url()).searchParams.get('branch')).toMatch(/^edit/)
|
||||
}).toPass({ timeout: 30_000 })
|
||||
|
||||
// The question turn fired against the new branch: either the optimistic
|
||||
// question is showing (model answered / answering) or the turn surfaced a
|
||||
// chat error (no AI provider in Tier-1). Both prove it ran — the pre-fix
|
||||
// silent no-op showed neither.
|
||||
await expect(
|
||||
page.getByText(question).or(page.getByText(/Chat failed|No AI providers/i)),
|
||||
).toBeVisible({ timeout: 30_000 })
|
||||
})
|
||||
Reference in New Issue
Block a user