-
Notifications
You must be signed in to change notification settings - Fork 2
fix: unbreak integration tests on the read-only build branch, and cover it with a test #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cb1kenobi
merged 5 commits into
use-readonly
from
fix/readonly-flush-databases-sandbox-import
Jul 27, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
b0fb9be
fix: import harper as a namespace so a missing flushDatabases isn't f…
dawsontoth c8b83e9
fix(test): refresh the copied plugin when installing fixtures
dawsontoth 427c656
test: cover static generation against a database Harper holds open
dawsontoth 5aed595
fix: don't let a failed pre-build flush leak HARPER_READONLY
dawsontoth a32871b
fix: point the skipped-flush warning at the config that enables it
dawsontoth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| package-lock=false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // A statically generated page that reads a Harper table at build time. No `force-dynamic`, no | ||
| // dynamic APIs, so Next.js prerenders it during `next build` — and Next.js runs static generation in | ||
| // a *child process*. Loading Harper there opens the same RocksDB databases the parent Harper process | ||
| // already has open, which without `HARPER_READONLY` fails with: | ||
| // | ||
| // Error: IO error: While lock file: <root>/database/data/LOCK: Resource temporarily unavailable | ||
| // | ||
| // Real apps declare `harper` as a dependency and use the bare `import('harper')` specifier (see | ||
| // HarperFast/nextjs-example). Fixtures can't: the harness deep-copies the fixture directory into the | ||
| // Harper root and `harper` installs to ~577MB. So the test passes harper's already-resolved entry | ||
| // path in via `HARPER_FIXTURE_HARPER_ENTRY`. The mechanism under test is unchanged — a build child | ||
| // process loading Harper's storage layer against a database the parent holds locked. | ||
| const harperEntry = process.env.HARPER_FIXTURE_HARPER_ENTRY; | ||
|
|
||
| async function listDogs() { | ||
| // Served from a Harper worker thread, the `tables` global is already there. In the build child it | ||
| // is not, and loading Harper is what installs it (and opens the databases). | ||
| // | ||
| // The ignore comments are required: both bundlers otherwise try to resolve this specifier at build | ||
| // time and fail with "Cannot find module as expression is too dynamic". They tell the bundler to | ||
| // leave the import alone and let Node resolve the absolute path at runtime. | ||
| if (typeof globalThis.tables === 'undefined') { | ||
| if (!harperEntry) { | ||
| throw new Error( | ||
| 'HARPER_FIXTURE_HARPER_ENTRY is not set, so this fixture cannot load Harper outside a Harper thread. ' + | ||
| 'It is passed in by integrationTests/next-16-static-data.pw.ts.' | ||
| ); | ||
| } | ||
| await import(/* webpackIgnore: true */ /* turbopackIgnore: true */ harperEntry); | ||
| } | ||
|
|
||
| // No fallback if `tables.Dog` is missing, deliberately. An empty list is a *meaningful* result here | ||
| // — it is what a read-only child renders when it can't see the parent's unflushed writes — so | ||
| // swallowing a failure to load Harper as `[]` would make this test pass for the wrong reason. | ||
| const dogs = []; | ||
| for await (const dog of globalThis.tables.Dog.search()) { | ||
| dogs.push({ id: dog.id, name: dog.name }); | ||
| } | ||
| return dogs; | ||
| } | ||
|
|
||
| export default async function Page() { | ||
| const dogs = await listDogs(); | ||
|
|
||
| return ( | ||
| <ul data-testid="dogs"> | ||
| {dogs.map((dog) => ( | ||
| <li key={dog.id} data-testid={`dog-${dog.id}`}> | ||
| {dog.name} | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| export const metadata = { | ||
| title: 'Harper - Next.js v16 Static Data App', | ||
| }; | ||
|
|
||
| export default function RootLayout({ children }) { | ||
| return ( | ||
| <html> | ||
| <body>{children}</body> | ||
| </html> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export default function Page() { | ||
| return <h1>Next.js v16 Static Data</h1>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| rest: true | ||
|
|
||
| graphqlSchema: | ||
| files: schema.graphql | ||
|
|
||
| # Seeds the Dog table. Declared before @harperfast/nextjs so the row is committed by the time the | ||
| # plugin runs `next build` — the statically generated /dogs page reads it during prerender. | ||
| jsResource: | ||
| files: seed.js | ||
|
|
||
| '@harperfast/nextjs': | ||
| package: '@harperfast/nextjs' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { withHarper } from '@harperfast/nextjs'; | ||
|
|
||
| export default withHarper({}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "name": "next-16-static-data", | ||
| "private": true, | ||
| "scripts": { | ||
| "dev": "next dev", | ||
| "build": "next build", | ||
| "start": "next start", | ||
| "lint": "next lint" | ||
| }, | ||
| "dependencies": { | ||
| "@harperfast/nextjs": "file:../../", | ||
| "react": "^19", | ||
| "react-dom": "^19", | ||
| "next": "^16" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| type Dog @table @export { | ||
| id: ID @primaryKey | ||
| name: String | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { tables } from 'harper'; | ||
|
|
||
| // Top-level await: Harper awaits component module evaluation, so the row is committed before the | ||
| // '@harperfast/nextjs' plugin (declared after this file in config.yaml) starts `next build`. | ||
| await tables.Dog.put({ id: 'rex', name: 'Rex' }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { fixture, harperModuleEntry } from './fixture.ts'; | ||
|
|
||
| // The same fixture as next-16-static-data.pw.ts, under `applications.moduleLoader: native`. | ||
| // | ||
| // Harper's default VM loader hands components a `harper` module built from a fixed allowlist | ||
| // (`getHarperExports` in harper's security/jsLoader.ts) that omits `flushDatabases`, so the plugin's | ||
| // pre-build flush is a no-op and a read-only build child can't see writes the parent hasn't flushed. | ||
| // `native` mode skips the VM loader entirely and resolves the real package, so the flush runs — which | ||
| // makes this the only place the suite actually covers `flushDatabases()` doing its job. | ||
| // | ||
| // The trade-off is real and documented: native mode gives up per-app tagged logging and `config`. | ||
| const { test, expect } = fixture('next-16-static-data', { | ||
| env: { HARPER_FIXTURE_HARPER_ENTRY: harperModuleEntry() }, | ||
| applications: { moduleLoader: 'native' }, | ||
| }); | ||
|
|
||
| // The counterpart to 'without a reachable flush...' in next-16-static-data.pw.ts: same page, same | ||
| // seeded row, but the flush ran before `next build`, so the statically generated page sees the row. | ||
| test('a reachable flush lets the build child see writes committed before the build', async ({ page, harper }) => { | ||
| await page.goto(`${harper.httpURL}/dogs`); | ||
| await expect(page.getByTestId('dog-rex')).toHaveText('Rex'); | ||
| }); | ||
|
|
||
| test('Harper still accepts writes after the build', async ({ request, harper }) => { | ||
| const headers = { | ||
| authorization: `Basic ${Buffer.from(`${harper.admin.username}:${harper.admin.password}`).toString('base64')}`, | ||
| 'content-type': 'application/json', | ||
| }; | ||
|
|
||
| const response = await request.put(`${harper.httpURL}/Dog/fido`, { data: { name: 'Fido' }, headers }); | ||
| expect(response.ok()).toBe(true); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { fixture, harperModuleEntry } from './fixture.ts'; | ||
|
|
||
| // Regression coverage for Harper read-only mode during `next build`. | ||
| // | ||
| // Next.js runs static generation in child processes. When a prerendered page reads Harper data, that | ||
| // child loads Harper's storage layer and opens the same RocksDB databases the parent Harper process | ||
| // already holds — which fails on the LOCK file unless the build sets `HARPER_READONLY`. Every other | ||
| // fixture either never touches Harper data or marks its data pages `force-dynamic`, so nothing else | ||
| // in this suite covers the build-time path. | ||
| const { test, expect } = fixture('next-16-static-data', { | ||
| env: { HARPER_FIXTURE_HARPER_ENTRY: harperModuleEntry() }, | ||
| }); | ||
|
|
||
| test('home page renders', async ({ page, harper }) => { | ||
| await page.goto(harper.httpURL); | ||
| await expect(page.locator('h1')).toHaveText('Next.js v16 Static Data'); | ||
| }); | ||
|
|
||
| // The core assertion. Without `HARPER_READONLY` the prerender of /dogs throws on the RocksDB LOCK, | ||
| // `next build` exits non-zero, and the plugin never reaches `serve()` — so no route exists at all and | ||
| // this fails on a connection error rather than a missing element. Reaching a rendered list means the | ||
| // build child opened the locked databases read-only. | ||
| test('statically generated page builds while Harper holds the databases open', async ({ page, harper }) => { | ||
| await page.goto(`${harper.httpURL}/dogs`); | ||
| await expect(page.getByTestId('dogs')).toBeAttached(); | ||
| }); | ||
|
|
||
| // Documents the flush gap under the default (VM) loader, rather than skipping it. Harper's component | ||
| // `harper` module is a fixed allowlist (security/jsLoader.ts) that omits `flushDatabases`, so the | ||
| // pre-build flush is a no-op here: the read-only child reads on-disk state and prerenders an empty | ||
| // list even though `GET /Dog/rex` returns Rex. next-16-static-data-native.pw.ts runs the same fixture | ||
| // with `moduleLoader: native`, where the flush does run and the row *is* visible. | ||
| test('without a reachable flush, the build child does not see unflushed writes', async ({ page, harper }) => { | ||
| await page.goto(`${harper.httpURL}/dogs`); | ||
| await expect(page.getByTestId('dogs')).toBeEmpty(); | ||
| }); | ||
|
|
||
| // The build must not leave the serving process read-only, or writes after the build fail. | ||
| test('Harper still accepts writes after the build', async ({ request, harper }) => { | ||
| const headers = { | ||
| authorization: `Basic ${Buffer.from(`${harper.admin.username}:${harper.admin.password}`).toString('base64')}`, | ||
| 'content-type': 'application/json', | ||
| }; | ||
|
|
||
| const response = await request.put(`${harper.httpURL}/Dog/fido`, { data: { name: 'Fido' }, headers }); | ||
| expect(response.ok()).toBe(true); | ||
|
|
||
| const created = await request.get(`${harper.httpURL}/Dog/fido`, { headers }); | ||
| expect(created.status()).toBe(200); | ||
| expect((await created.json()).name).toBe('Fido'); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.