Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
381cef3
feat: persist per-entry cache lives and serve stale on tag invalidation
jjohnson-hdb Sep 22, 2026
2ebb947
feat: back the "use cache" directive with Harper
jjohnson-hdb Sep 22, 2026
4eebc83
test: add a "use cache" fixture and document the two handler interfaces
jjohnson-hdb Sep 22, 2026
1442b59
fix: store integer cache lives and disable the sweep by default
jjohnson-hdb Sep 22, 2026
7bc7675
test: verify both handlers on a real two-node cluster
jjohnson-hdb Sep 22, 2026
ff7e895
fix: address Gemini review findings and de-time-bomb a unit test
jjohnson-hdb Sep 22, 2026
9edbd6f
fix: keep oversized "use cache" keys under Harper's primary-key limit
jjohnson-hdb Sep 24, 2026
0d42441
fix: restore the playwright-report ignore rule
jjohnson-hdb Sep 28, 2026
ab2d0e6
fix: follow Next's stale/expired tag semantics and make tombstones ou…
jjohnson-hdb Sep 28, 2026
826ef5f
docs: record the Harper and Next.js behaviour the cache handlers rely on
jjohnson-hdb Sep 28, 2026
919d31c
fix: set each cache entry's Harper expiry from Next's expire, capped …
jjohnson-hdb Sep 28, 2026
c1d8250
feat: keep cache entries up to a year, and optionally clear earlier b…
jjohnson-hdb Sep 29, 2026
ab8c7b0
Merge remote-tracking branch 'origin/main' into feat/use-cache-handle…
jjohnson-hdb Oct 1, 2026
cd4e2d8
fix: fail closed until tombstones are hydrated, re-subscribe when the…
jjohnson-hdb Oct 5, 2026
e03b4b0
test: pin the four invalidation defects with independent regression t…
Ethan-Arrowood Oct 5, 2026
1fba8ff
test: pin the same-millisecond ordering gap the write-version fix lea…
Ethan-Arrowood Oct 5, 2026
f952676
test: pin the lone-surrogate cache-key collision, and rule out the ch…
Ethan-Arrowood Oct 5, 2026
ea56382
test: write the regression-test comments for the next reader, not the…
Ethan-Arrowood Oct 5, 2026
c5c3365
ci: run the unit suite on every pull request
Ethan-Arrowood Oct 5, 2026
fd6f5a9
fix: separate oversized "use cache" keys that differ only by a lone s…
Ethan-Arrowood Oct 5, 2026
201b33c
fix: resolve a same-millisecond invalidation tie by strictness, not a…
Ethan-Arrowood Oct 5, 2026
0410543
fix: leave an unbounded cache life to the table's expiration instead …
Ethan-Arrowood Oct 5, 2026
e807099
test: run the cluster suite on any container runtime, and make it lou…
Ethan-Arrowood Oct 5, 2026
f09a64f
docs: point at the packaged loopback setup script instead of hand-rol…
Ethan-Arrowood Oct 5, 2026
db1cc89
fix(test): make the cluster rig's staged tree writable by the image's…
Ethan-Arrowood Oct 5, 2026
1391c8f
fix(test): reopen the staged rig from inside the image, which owns it…
Ethan-Arrowood Oct 5, 2026
64afdb1
fix(test): pin the cluster rig's build containers to the host uid
Ethan-Arrowood Oct 5, 2026
31782ca
fix(test): fingerprint every staged input, so the cluster rig cannot …
Ethan-Arrowood Oct 6, 2026
c364be2
fix: keep a passed expiry irrevocable, so a later profiled invalidati…
Ethan-Arrowood Oct 6, 2026
239684b
docs: state the constraint in the cluster rig's comments, not how it …
Ethan-Arrowood Oct 7, 2026
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
8 changes: 7 additions & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,16 @@ jobs:

- name: Run integration tests
run: npm run test:integration
timeout-minutes: 15
# The cluster suite stages its rig by running `npm install` and `next build` inside the Pro
# image before any test runs, which dominates a cold run.
timeout-minutes: 30
env:
HARPER_INTEGRATION_TEST_LOG_DIR: /tmp/harper-test-logs
FORCE_COLOR: '1'
# The runner has Docker and the Pro image is public, so the cluster suite is expected to run
# here. Without this it would skip itself on any setup problem and the job would still be
# green β€” which is exactly how it went unexercised before.
HARPER_CLUSTER_REQUIRED: '1'

- name: Upload Harper logs on failure
if: failure()
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Unit Tests

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
unit-tests:
name: Unit Tests (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest

strategy:
fail-fast: false
matrix:
node-version: [22, 24, 26]

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Set up Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: ${{ matrix.node-version }}
cache: npm

- name: Install dependencies
run: npm ci

# No separate build step: `npm test` compiles first, because the suite runs the emitted
# `dist/**/*.test.js` rather than the TypeScript sources. Unlike the integration suite this
# needs no Harper instance, fixtures or browsers, so it stays a seconds-long check.
- name: Run unit tests
run: npm test
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ node_modules/
test-results/
.DS_Store
dist/
playwright-report/
playwright-report/
15 changes: 14 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,21 @@ Review the `README.md` and `CONTRIBUTING.md` for all relevant repository informa

- Use `npm link` in this directory and `npm link @harperfast/nextjs` in other project directories to test out changes locally
- Run `npm run install:fixtures` before running tests for the first time, and again after changing any fixture's `package.json` **or any plugin source under `src/`**. Fixtures install the plugin with `--install-links`, so each one holds a *copy* of `dist/`, not a symlink β€” `npm run build` alone does not reach them.
- Run `npm test` for the unit suite. It is `npm run build` plus `node --test` over the emitted `dist/**/*.test.js`, so it tests compiled output, not `src/` β€” a stale `dist/` tests stale code.
- Run `npm run test:integration` to run all tests, or `npm run test:integration -- integrationTests/next-15.pw.ts` for a single file.
- Test startup is slow by design β€” each test file starts a real Harper instance and waits for Next.js to build (up to 2 minutes). A slow start is not a failure.
- The ISR cache tests in `integrationTests/next-16.pw.ts` are intentionally skipped; `CacheHandler.cts` is a work in progress.
- Integration tests need the macOS loopback alias pool (127.0.0.2+). Without it every fixture fails at startup with `LoopbackAddressValidationError` / `EADDRNOTAVAIL` before any assertion runs β€” that is a machine-setup gap, not a test failure. `ifconfig lo0 | grep 'inet '` shows what is configured.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to detail this; lets also detail that the @harperfast/integration-testing module has a loopback alias script included in it.

- `@harperfast/integration-testing` ships the setup script; do not hand-roll `ifconfig` loops. Run `npx harper-integration-test-setup-loopback` (it prompts for sudo). `HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START` (default 2) and `HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT` (default 32) size the pool. Aliases do not survive a reboot; the package also ships `scripts/io.harperdb.loopback-setup.plist`, a launchd job that re-adds them at boot.
- There is no `format:fix` script in `package.json` despite the Code Style note below; match the surrounding file style (tabs, semicolons).
- `next-16-static-data` is run by two test files: `next-16-static-data.pw.ts` on the default VM module loader (where Harper's component `harper` allowlist omits `flushDatabases`, so the plugin's pre-build flush is a no-op and a read-only build child can't see unflushed writes) and `next-16-static-data-native.pw.ts` under `applications.moduleLoader: native`, where the flush does run. The pair is what pins that behavior down β€” keep both.
- CI is currently disabled (`if: false` in `.github/workflows/integration-tests.yml`). Run tests locally.
- The cluster suite (`integrationTests/cluster.pw.ts`) needs a container runtime and a **harper-pro** image (default `harperfast/harper-pro:5.3.1`, public). OSS `harper` has no replication module and rejects `add_node` as an unknown operation, so it cannot form a cluster. Docker is **not** required: the rig probes for a `docker` binary, then `podman`, and `HARPER_CONTAINER_CLI` forces one. On macOS `docker` is usually a *shell alias* for podman, which a direct `execFileSync` never sees β€” hence the probe. The image is pulled when missing rather than being a precondition.
- Cluster-suite environment: `HARPER_PRO_IMAGE` overrides the image, `HARPER_CONTAINER_CLI` the runtime, `HARPER_CLUSTER_KEEP=1` leaves the containers up for inspection, and `HARPER_CLUSTER_REQUIRED=1` turns "cannot run here" from a skip into a failure. CI sets the last one β€” without it the suite skips itself on any setup problem and the job stays green, which is how it went unexercised.
- `add_node` is retried while the replication listener comes up. `waitForNode` only proves the operations API answers; port 9933 binds later, and a too-early `add_node` fails with `connect ECONNREFUSED <ip>:9933 and connection was required to sign certificate`. Only that failure is retried β€” bad credentials or an OSS image surface immediately.
- `add_node` needs an explicit `authorization: "Basic <base64>"`. Passing `username`/`password` alone fails the cert-signing handshake with "No authorization provided".
- Each node needs a distinct `node.hostname` (`NODE_HOSTNAME`); it defaults to `localhost`, so two nodes otherwise collide.
- Do not `cpSync` a staged rig per node: npm links a `file:` dependency as a relative symlink and Node rewrites it to an absolute host path, which does not exist in the container. Mount one staged directory into every node instead.
- Playwright's Chromium cannot launch inside the Claude Code sandbox (`bootstrap_check_in … Permission denied`): every integration test then fails at browser launch before any assertion. Run integration tests outside the sandbox.
- Harper facts the cache handlers depend on (verified in `node_modules/harper` 5.1.23): each element of an `@indexed` array attribute is indexed, so `{attribute: 'tags', comparator: 'equals', value}` is an index lookup, while `contains` is a full scan doing a *substring* match on `String(array)`. Every write β€” `patch` included β€” resets a record's expiry to now + the table's `expiration` unless `put(id, value, { expiresAt })` overrides it, and a sourceless table returns `null` for an expired record. TTL eviction emits no subscription `delete` event. `patch` on a missing record creates a partial one. Adding `@indexed` to an existing attribute backfills in the background, and on 5.2.0 the worker threads that did not run the backfill were observed to keep throwing `IndexRebuildingError` for that attribute after "Finished indexing" was logged, until a restart β€” which is why the sweep falls back to an exact-match scan.
- Next.js `IncrementalCache` treats a handler-returned `lastModified: -1` as an on-demand (blocking) revalidation, not stale-while-revalidate. Next 16 serves tag-stale FETCH/APP_PAGE/APP_ROUTE entries stale from its `tagsManifest` (`{stale, expired}` per tag); Next 15's manifest holds a number per tag; Next 14 has none.
- CI runs on every pull request: `unit-tests.yml` (`npm test`, Node 22/24/26) and `integration-tests.yml` (`npm run test:integration`, Node 22/24/26), plus commitlint. The integration suite needs the machine setup described above, so it is the one to re-run locally; the unit suite is self-contained.
Loading
Loading