Skip to Content
SDK & PluginsShared State

Shared State

Glubean provides two scopes for sharing state between tests: module scope (within a file) and session scope (across files).

Session scope is per run. It exists only for a single glubean run execution and is not persisted across runs.

Module scope — within a file

Tests in the same file run sequentially in a single process. Module-level variables persist between tests:

import { test } from "@glubean/sdk"; let authToken: string; export const login = test("login", async (ctx) => { const res = await ctx.http.post("/auth/login", { json: { user: ctx.vars.require("USER"), pass: ctx.secrets.require("PASS") }, }); const body = await res.json(); authToken = body.access_token; ctx.assert(!!authToken, "should receive token"); }); export const getProfile = test("get-profile", async (ctx) => { // authToken is available — same process, same module const res = await ctx.http.get("/me", { headers: { Authorization: `Bearer ${authToken}` }, }); ctx.expect(res).toHaveStatus(200); });

Module scope is the simplest way to share state. Use it when:

  • Tests in the same file share setup (login, seed data, etc.)
  • You want ordered execution with shared context
  • The shared value doesn’t need to cross file boundaries

Session scope — across files

When you need to share state across multiple test files (e.g., authenticate once, use the token everywhere), use session scope.

1. Create session.ts

Place a session.ts file at your test root. The runner auto-discovers it:

// session.ts import { defineSession } from "@glubean/sdk"; export default defineSession({ async setup(ctx) { const res = await ctx.http.post("/auth/login", { json: { user: ctx.vars.require("TEST_USER"), pass: ctx.secrets.require("TEST_PASS"), }, }); const { access_token, user_id } = await res.json(); ctx.session.set("token", access_token); ctx.session.set("userId", user_id); ctx.log("Authenticated as user " + user_id); }, async teardown(ctx) { const token = ctx.session.get("token"); if (token) { await ctx.http.post("/auth/logout", { headers: { Authorization: `Bearer ${token}` }, }); } }, });

2. Read session values in tests

Any test file can read session values via ctx.session:

// tests/users.test.ts import { test } from "@glubean/sdk"; export const listUsers = test("list-users", async (ctx) => { const token = ctx.session.require("token"); // throws if missing const res = await ctx.http.get("/users", { headers: { Authorization: `Bearer ${token}` }, }); ctx.expect(res).toHaveStatus(200); });
// tests/orders.test.ts import { test } from "@glubean/sdk"; export const createOrder = test("create-order", async (ctx) => { const token = ctx.session.require("token"); const userId = ctx.session.require("userId"); const res = await ctx.http.post("/orders", { json: { userId, items: [{ sku: "TEST-001", qty: 1 }] }, headers: { Authorization: `Bearer ${token}` }, }); ctx.expect(res).toHaveStatus(201); // Write back to session for downstream files const body = await res.json(); ctx.session.set("orderId", body.id); });

3. Session lifecycle

glubean run tests/ ├─ 1. Discover session.ts ├─ 2. Run setup() │ └─ Sets token, userId ├─ 3. Run test files (sequential) │ ├─ users.test.ts → reads token ✓ │ ├─ orders.test.ts → reads token, userId ✓ │ │ writes orderId │ └─ billing.test.ts → reads orderId ✓ ├─ 4. Run teardown() │ └─ Logs out └─ Done

Session API

ctx.session.get(key)

Returns the value if present, otherwise undefined.

const version = ctx.session.get("apiVersion") ?? "v1";

ctx.session.require(key)

Returns the value or throws a clear error if missing.

const token = ctx.session.require("token"); // throws: "Session key 'token' is required but not set. Check your session.ts setup."

ctx.session.set(key, value)

Sets a value. It is available immediately within the current file, and to subsequent test files in sequential mode. Any JSON-serializable value is supported.

ctx.session.set("orderId", createdOrder.id); ctx.session.set("user", { id: 42, name: "test" }); ctx.session.set("tags", ["smoke", "api"]); ctx.session.set("retryCount", 3);

ctx.session.entries()

Returns all session key-value pairs as a plain object.

ctx.log("Session state", ctx.session.entries());

Session setup context

The setup() and teardown() functions receive a context with:

PropertyDescription
ctx.varsEnvironment variables
ctx.secretsSecrets (auto-redacted)
ctx.httpPre-configured HTTP client
ctx.sessionSession accessor (get/set/require)
ctx.log()Logging

No assertions are available in session setup/teardown — it’s for initialization only.

File discovery

The runner looks for session.ts or session.setup.ts starting from your test directory and walking up toward the project root. This mirrors how config files are resolved.

This also applies when you run a single file. For example, glubean run tests/api/users.test.ts still walks up to find the nearest session.ts.

project/ ├── session.ts ← found here ├── tests/ │ ├── api/ │ │ ├── users.test.ts │ │ └── orders.test.ts │ └── e2e/ │ └── checkout.test.ts

Skipping session

Use --no-session to run tests without session setup/teardown:

glubean run tests/users.test.ts --no-session

Useful for debugging individual tests in isolation. ctx.session will still exist but will be empty — get() returns undefined, require() throws.

When to use which

NeedUse
Share state within one fileModule-level let variables
Share state across filessession.ts + ctx.session
Share config (baseUrl, headers)configure() in a shared config file
Share secrets.env.secrets + ctx.secrets

Current limitations

  • Sequential mode only. Session state propagates file-to-file in discovery order. This is the current v1 behavior, not a long-term parallel execution contract.
  • JSON-serializable values only. Strings, numbers, booleans, arrays, plain objects — anything that survives JSON.stringify().
  • No parallel guarantees. If parallel execution is added in the future, cross-file session writes will require explicit file dependency configuration.

What not to store in session

  • Long-term config. Use configure() or env files for stable configuration.
  • Source-of-truth secrets. Read secrets from .env.secrets and copy only the runtime values you actually need.
  • Large payloads. Session crosses process boundaries via JSON — prefer IDs, tokens, and small objects over large datasets.
Last updated on