* fix(kv): keep disableSharing entries out of other apps' events (PUT-1876) Private kv keys and values are no longer delivered to cross-app subscriptions, including forwarded and durable deliveries. * fix(fs): refuse usernames whose home path still holds another account's rows (PUT-1877) Username claims (signup, save_account, change-username, OIDC, team provisioning) now also check for leftover rows under /<name>/, and the root listing only returns the actor's own home. * fix(puter-js): reconnect the events channel after a server-side disconnect (PUT-1881) A server hang-up is retried with a bounded backoff; persistent handlers survive it and get an optional onError when the connection can't be restored. * fix(backend): let the graceful shutdown finish before telemetry exits (PUT-1948) The telemetry preload no longer exits on signals; the entry point drains, tears down top-down, flushes telemetry last, then exits. DB pools stay open until teardown. * fix(fs): reserve quota for signed uploads in progress (PUT-1880) startWrite/startBatchWrite hold each upload's declared size in a per-owner cache reservation until it completes, aborts or expires, so a burst of starts can't all read the same committed usage. Caps in-flight uploads at 10,000 per account. * fix(fs): refuse to complete a signed upload whose object never arrived (PUT-1951) Completion now 400s when the object store reports the key missing (transient errors stay lenient), abort no longer deletes an object a live entry still uses, and a ghost entry is only removed after a second miss at its recorded location. * fix(metering): move usage to v2 keys with a small totals item, per-model detail shards and a 3-month TTL (PUT-1949) The billed item holds only totals; per-model detail lives in 100 hash-sharded items read in one batch and cached for 60s, and more than 5,000 distinct usage types in a month fold into other. Global and per-app aggregates store totals only, appTotals comes from a prefix listing, a refused path only skips the key that refused it, exact reads near the allowance are throttled per key, and every metering record expires after 3 months. September usage restarts at deploy; the monthly-charge claim stays on the v1 key through September so recurring charges don't fire twice. * test(events): let the worker backoff test tolerate a loaded run * feat(events): scale KV event fan-out with the key owner's plan A KV change delivers to up to 512 matching subscriptions (2,048 filter evaluations) for a paid key owner and 128 (512) otherwise, inline values are dropped once more than 128 match, and paid accounts may hold up to 512 share handles per app. The owner's plan is only looked up when a change has more candidates than any cap. * test(metering): seed the 150-app listing test directly so it fits under coverage
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.
- 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 contextt. - New area (e.g. hosting): create
suites/hosting.suite.tsand register it insuites/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), globalfetch, andt.assert(ok/equal/deepEqual/rejects). - Admin or cross-user assertions go through plain
fetchwitht.env.users.admin.token(seeauth.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.