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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ jobs:

- run: pnpm install --frozen-lockfile

# The other two jobs set ELECTRON_SKIP_BINARY_DOWNLOAD, and all three
# share one pnpm store cache keyed on the lockfile, so whichever of them
# saves that cache first can hand this job an electron package whose
# postinstall was skipped and whose binary is therefore missing. The
# symptom is "Electron failed to install correctly" out of
# electron.launch, and because it depends on which parallel job wins the
# cache write, it appears as flake rather than as a broken job.
#
# install.js is the package's own postinstall. It checks the installed
# version first and downloads only what is missing, reusing the binary
# cache restored above, so running it here is idempotent and costs
# nothing when the store cache was clean.
- name: Repair the Electron binary if the shared store cache skipped it
run: node node_modules/electron/install.js

- run: pnpm e2e --project=electron

- uses: actions/upload-artifact@v4
Expand Down
22 changes: 17 additions & 5 deletions e2e/journeys/create-book.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,28 @@ test('revises the table of contents, persists it, and generates chapter 1 on app

// Persistence, checked on disk rather than on screen: the revision wrote
// through to toc.yml, not just into the streaming buffer the page renders.
const toc = await bookRepository(app.dataDir).getToc(id)
expect(toc.chapters.map(chapter => chapter.title)).toEqual(TOC_REVISED_CHAPTERS.map(chapter => chapter.title))
//
// Polled rather than read once, because the two are not simultaneous.
// Titles appear on screen while `revise-toc.ts` is still streaming, and the
// parse and the save both happen after the stream ends, so a bare read here
// races a write that is strictly later. Polling waits for that write on a
// deadline, the same way a web-first assertion waits for the screen, and it
// weakens nothing because the expected value is unchanged.
await expect.poll(async () => (await bookRepository(app.dataDir).getToc(id)).chapters.map(chapter => chapter.title))
.toEqual(TOC_REVISED_CHAPTERS.map(chapter => chapter.title))

await wizard(page).approveToc()

await expect(page.getByText(chapterMarker(1)).first()).toBeVisible()

const book = await readBook(app.dataDir, id)
expect(book.generatedUpTo).toBe(1)
expect(book.status).toBe('reading')
// Same reasoning, and this is the one that actually caught it. `start-book.ts`
// streams the chapter, saves it, generates its quiz, and only then writes
// generatedUpTo and status in its finalize step. Chapter 1's prose is on
// screen well before that, so reading the book once at this point asks a
// question the server has not answered yet. It won on every local run and on
// the pull request, and lost on the busier post-merge runner.
await expect.poll(async () => (await readBook(app.dataDir, id)).generatedUpTo).toBe(1)
expect((await readBook(app.dataDir, id)).status).toBe('reading')

// And the model path /start actually drives: a skill classification call
// and a chapter-1 stream, not just the revise-toc stream from earlier.
Expand Down
Loading