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
│
└─ DoneSession 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:
| Property | Description |
|---|---|
ctx.vars | Environment variables |
ctx.secrets | Secrets (auto-redacted) |
ctx.http | Pre-configured HTTP client |
ctx.session | Session 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.tsSkipping session
Use --no-session to run tests without session setup/teardown:
glubean run tests/users.test.ts --no-sessionUseful for debugging individual tests in isolation. ctx.session will still exist but will be empty — get() returns undefined, require() throws.
When to use which
| Need | Use |
|---|---|
| Share state within one file | Module-level let variables |
| Share state across files | session.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.secretsand 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.