Files
puter/src/puter-js/tests/api
Daniel Salazar b784b51cf3 fix: harden the events stack for flag-on (#3752)
* fix: harden events dispatch, single delivery and KV share handles

Dispatch: a filtered subscription used the anchor path stored at subscribe
time, so renaming or moving the anchor folder silently ended its deliveries;
dispatch now resolves the anchor's live path from the event's own ancestor
chain. A move out of a watched folder now reaches that folder's subscribers,
with `from` only for rows that watched the source side. Gap markers are
authorized like deliveries and coalesced per subscription and subject instead
of fanning per lost event. Session subscriptions: the per-socket cap decides
on the write, not before it; an orphaned watched-set token heals on refresh;
durable rows keep their watch window when a session subscribe touches the
same keys. `self` is false when the acting user is unknown.

Single delivery: a subscription in backoff or suspended with a backlog pinned
the sweeper's head and starved everyone behind it — the sweep now defers it.
Only a settled handler run bills a delivery. A socket-only account row no
longer wedges after two attempts nobody received. The lease is twice the
handler timeout; remote candidates have their own attempt counter; the region
depth reconcile runs once a minute region-wide with a bounded scan.

KV share handles: a grantee no longer sees the owner's namespace and absolute
prefix on the subscribe answer or listing, nor in the delivery token; revoking
a wider handle retires the handles it covers; minting the same handle twice
returns the existing one, after the delegation check; a row whose event
cannot be re-based onto its handle is dropped rather than delivered raw.

* fix: presence survives replication, long sessions and region churn

One presence item per (user, app) with per-region map fields lost a region
whenever two regions joined inside the replication window, and nothing ever
put it back. Presence is now one item per (user, app, region): each region
writes only its own, a leave or repair retires it conditionally on its own
write stamp, and a read is a prefix query. Items carry a 48 h ttl refreshed by
a claim-gated write off the existing socket renew path, at most once per
12 h, so a tab that stays connected keeps its region in the row. A region
that answered "no socket" or completed a leave releases a shared pin, so a
reconnect on another node rejoins and a flapping client cannot force a
replicated write per cycle. Cached rows expire after a minute; unaddressable
region names are filtered and pruned; relayed acks settle under a bounded
concurrency; the forward queue is bounded in bytes as well as items.

* feat: indexes for the event_subscriptions hot queries

Handler publish, remove and listing, and the hourly expiry and suspension
sweeps, all scanned `event_subscriptions`. Adds (app_uid, handler_name),
(expires_at) and (suspended_at, id), guarded on every engine. Existing
migrations: the postgres widens are now guarded so a boot does not take an
exclusive lock for a no-op, the kv_share_handles grantee FK gets an index,
the sqlite notification rebuild is transactional and idempotent.

* fix: notification writes go through the registry

The driver's `create` bypassed the type registry, producing uncatalogued
rows with no size bound; it now requires a registered type, caps the payload,
and answers 400 rather than 500 for a bad one. `mark_acknowledged` emits the
ack other tabs listen for, and only when a row was actually changed.

* fix: the handler scanner, unsubscribe, and the in-tab handler environment

The free-variable scanner skipped arrows inside a declaration's initializer,
so `const ids = event.items.map(x => x.id)` was refused, and treated a name
after a comma in a nested initializer as bound, so a real free variable slipped
through to fail on first delivery. `unsubscribe()` now drops the durable
routing entry so the events socket can close. A broadcast handler running in
the tab gets `user` and `fetch` like the worker gives it. `single` without
a handler name is refused before the round trip.

* docs: events limits, error codes and the background-workers section

Retention is deployment-configured rather than a fixed 14 days, and the
template no longer ships it armed. Documents `events_terminal`, the two
per-event gap reasons, the subject length and listing caps, the `from` field
on moves, and the handle-relative anchor. The sessions manager hides the
background-workers section when the server has none to show.

* feat: a background handler acts as the app does for its user

A handler's `user` was a five-minute access token scoped to the subscription's
`list` grant, which could stat the changed file but not read it, and could
not reach the app's KV or AppData — so an app told that a file was written
could do nothing with it. It now runs with the same authority the app has for
that user in a tab: an app-under-user worker session, one row per (user, app)
named `events:handlers`, visible and revocable in the sessions list. The
`events:background` consent is what authorizes running it unattended, and is
re-checked before every mint.

The wider token exposed two things: puter.js opens a filesystem socket the
moment it has a token, which would have parked the isolate in the app's own
delivery room and steered deliveries at it; the events client now opts out of
sockets (and the per-open bookkeeping) before construction, and is memoized
per token in the isolate. And four filesystem operations assumed a socket
exists; they no longer do.
2026-09-04 17:32:57 -07:00
..
2026-07-16 15:52:37 -07:00

puter.js API test environment

Client-agnostic test suites for puter.js, run against a self-contained in-memory Puter server — no external server, no stdout password scraping, no .env. The same suite files execute on three platforms through thin adapters:

Runner Platform How the SDK runs
runners/node.test.ts node.js Built SDK bundle loaded into a fresh vm context per test (like src/init.cjs)
runners/browser.test.ts headless Chromium (playwright) Fixture page served same-origin on the API host loads /puter.js/v2 from the server itself
runners/workerd.test.ts local workerd (Miniflare) Suite bundle deployed as a real Puter worker via puter.workers.create, dispatched through the local worker proxy

Running

Build the SDK bundle and worker preamble once (repeat after SDK changes):

npm run build:workerLib

Then, from the package root:

npm run test:puterjs            # all three platforms
npm run test:puterjs:node
npm run test:puterjs:browser    # needs `npx playwright install chromium` once
npm run test:puterjs:workerd

How it works

Each runner boots setupPuterTestEnv() (from src/backend/testUtil.ts) in beforeAll: a fully in-memory backend (sqlite / dynalite / redis-mock / fauxqs S3) listening on a real ephemeral port, with the production extensions loaded and two deterministic users seeded:

  • admin — member of the admin group,
  • testuser — a regular, non-privileged user (what suites run as).

The env manifest ({ origin, apiOrigin, users } with fixed passwords and pre-minted session tokens) is JSON-serializable and crosses into whatever runtime executes the tests. Root-only routes (e.g. POST /login) live on origin; the SDK talks to apiOrigin (the api. subdomain host).

Adding tests

Tests are added once and run on all three platforms — never write a per-platform test here.

  1. Existing area (apps, auth, fs, kv): add a test to the matching suites/<name>.suite.ts — one entry in the object, key is the test name, value gets the context t.
  2. New area (e.g. hosting): create suites/hosting.suite.ts and register it in suites/index.ts (explicit list, no globbing — esbuild bundles exactly this list for the browser/workerd runners).
import { suite } from '../harness/types.ts';

export default suite('example', {
    'does the thing': async (t) => {
        await t.puter.fs.write(`/${t.env.users.user.username}/x.txt`, 'hi');
        t.assert.ok(await t.puter.fs.stat(/* … */));
    },
});

Rules that keep a suite runnable everywhere:

  • Platform-agnostic only. No node/browser/workerd-specific imports — a suite may use the SDK instance (t.puter, authed as the regular user), global fetch, and t.assert (ok/equal/deepEqual/rejects).
  • Admin or cross-user assertions go through plain fetch with t.env.users.admin.token (see auth.suite.ts) — that works identically on every platform.
  • Unique resource names per test (file paths, kv keys): tests in a suite share one server and one user, so don't reuse names across tests.
  • The runners in runners/ enumerate suites automatically — adding a suite requires no runner changes.

Iterate fast with npm run test:puterjs:node (boots in ~3s); run npm run test:puterjs before pushing to cover browser and workerd too.