Skip to content

Commit 973917c

Browse files
test(plugin-auth): the last three #23184 test-budget rows (org-create-posture-gate, implicit-account-linking, dev-admin-seed-credential-gate) (#24626)
Fixes #23184 Clause-②: no The three `@objectstack/plugin-auth` rows that were still open on #23184. Every other row on the card has landed (#23243, #23300, #23697, #23711, #23721, #23748), so this PR closes the card. Tests only: no source file, option or export changes, and nothing is published (`skip-changeset`). ## The readings (A1) Each reading is the vitest module line (`✓ src/FILE.test.ts (N tests) MS`), the number the hourly battery's per-file timing reads. Base and head ran interleaved, three rounds, under `scripts/pm/os-verify-lock.sh`, on a shared box. Base is the file at `de4352460`, run under a temporary name. Head is `6c63f499e` (the merge of `main` after it touches none of these files). | file | base runs (ms) | head runs (ms) | median base → head | card CI reading → projected | |:--|:--|:--|:--|:--| | `org-create-posture-gate.test.ts` | 4756 / 4856 / 4317 | 1309 / 1175 / 1112 | 4756 → 1175 (−75 %) | 11.6 s → 2.9 s | | `implicit-account-linking.test.ts` | 3365 / 3538 / 3449 | 1051 / 1324 / 1288 | 3449 → 1288 (−63 %) | 11.7 s → 4.4 s | | `dev-admin-seed-credential-gate.test.ts` | 4824 / 4560 / 4504 | 2741 / 2740 / 2753 | 4560 → 2741 (−40 %) | 10.3 s → 6.2 s | The projection scales the card's CI reading by head ÷ base. That is the card's own worst CI/local ratio applied to the head median (2.44, 3.39 and 2.26). No file declares a budget header: all three come in under the 10 s line. **Where the time went (per-case probe at base, scrypt calls counted and timed):** - `org-create-posture-gate`: the slowest phase was per-test. 25 sign-ups each ran one production-parameter scrypt derivation under a fresh salt, 3.5 s of 4.9 s. The first case also loaded better-auth's vendor modules inside the clock, about 0.8 s. - `implicit-account-linking`: also per-test. 14 sign-ups took 1.9 s of 3.8 s, and the first manager's vendor load took about 0.75 s. - `dev-admin-seed-credential-gate`: per-test again. A sqlite boot (`init()` plus DDL for 30 objects, about 70 ms) ran in each of 15 cases. There were 12 derivations (1.8 s), and case ⓪ paid about 1.1 s of vendor load. **CI readings (A2): NOT MEASURED.** Two sources were tried. The `test-core-timings-K-of-6` artifacts of the hourly runs (latest `39265242003`) and the Test Core job logs both redirect to `productionresultssa13.blob.core.windows.net`, which answers 403 from this container. The `Test Core` check-run annotations name only the slowest file of a package that is over its ceiling. The open refresh #24467 carries no per-file readings ("Regression readings: NOT MEASURED"). So the card's 2026-10-13 readings stay the CI numbers, and the pin is the next measured refresh's list. ## What changed - **All three files: vendor modules load at module top.** `AuthManager` reaches better-auth through a lazy `await import()`, so the first case paid the vendor's module evaluation inside its clock. Side-effect imports at module top follow AGENTS.md ("clocked windows measure behaviour, never loading"). They copy the shape `admin-impersonate-endpoint.test.ts` already uses, and each list is the set of modules a probe saw that file's managers load. - better-auth's one load-time read is `NODE_ENV`, which its env module keeps on first evaluation. Measured inside vitest: that module is already evaluated when the `./auth-manager` import runs. It is reached via `./objectql-adapter.js` → `better-auth/adapters`. The vendor reads `isTest() = true, isDevelopment() = false`, even after a case sets `NODE_ENV=development`. So moving the rest of the load changes no captured value, and `dev-admin-seed-credential-gate` gets the move too. An earlier note on this card said that could not happen; it does not hold. - **`org-create-posture-gate`: one founder sign-up per file.** It goes through the real `/sign-up/email` route in a `beforeAll`, with both knobs unset. `runGuidedWorkspaceCreation` copies its tables (`structuredClone`) onto each case's own engine, builds the case's own manager under the case's own env and tenancy service, and presents that sign-up's cookie. No case is about the sign-up: each one is about what `organization/create` and `/auth/config` answer under a scenario. The two cases that sign up inline still do. Derivations: 25 → 3. - **`implicit-account-linking`: one sign-up per file, copied per case.** `signUp` gives a case's first sign-up on a still-empty engine a copy of one real sign-up's tables, with the user row's address set to the case's own, and answers that sign-up's cookies. Two kinds of sign-up still run live, as before: - one on an engine that already holds a user; - one under a `secondaryStorage` manager, where the session goes to the cache. `makeManager` records which engine a manager runs over. Derivations: 14 → 2. - **`dev-admin-seed-credential-gate`: pooled engine, scrypt memo.** It reuses the landed `__tests__/sqlite-engine-pool.testkit.ts`: one booted engine per file, emptied before each case, refused if its schema changed or it holds an organization row. It also reuses `__tests__/scrypt-memo.testkit.ts`: a sign-in after the seed, and the second seed run, re-derive the same password against the same salt and are answered from the first derivation's bytes. Every fresh salt is still derived for real. Derivations: 12 → 9. Engine boots: 15 → 1. Every `it`, `describe` and `expect` line of the base files is present unchanged in the head, compared line by line: | file | `expect(` lines | `it(` lines | `describe(` lines | |:--|:--|:--|:--| | org-create | 53/53 | 13/13 | 3/3 | | implicit | 83/83, plus one | 23/23 | 3/3 | | dev-admin | 50/50 | 15/15 | 1/1 | The one added `expect` asserts the shared sign-up's own 200, the same check `signUp` already made. In org-create, `expect(cookie).not.toBe('')` is the same line; it now reads the copied sign-up's cookie. The test lists match too: 25/25, 23/23 and 15/15 pass at both base and head. ## Equivalence and isolation (measured once, nothing kept) - **Equivalence probe** (a temporary test file, deleted, never committed). For every arrangement the head copies, it compared a live sign-up (the base arrangement) with the copy (the head arrangement), normalising ids, tokens, hashes and timestamps. - `implicit-account-linking`: all 13 addresses, including the `accountLinking` opt-out manager. The copied tables equal a live sign-up of that address on that manager, and `get-session` with the copied cookie answers 200 for that user and address. - `org-create-posture-gate`: all 23 scenarios `runGuidedWorkspaceCreation` runs. A sign-up under the scenario's own env and tenancy service gives the same tables before `organization/create`, then the same status, body, three `/auth/config` flags and tables after it. - 36 of 36 equal. No copied row carries a fake-engine sequence id (`row_N`): 0 in all 36. So a copied table cannot collide with an id the case's engine assigns later. - **Leak mutations**, each through `scripts/ablation-replace.mjs` (anchor hit 1 → 0, blob changed, restored blob == HEAD, `git diff HEAD` empty): - Dropping `structuredClone` in `implicit-account-linking` turns the file red: 12 failed / 23. - Disabling the pool's row reset in `sqlite-engine-pool.testkit.ts` turns `dev-admin-seed-credential-gate` red: 13 failed / 15. - Dropping `structuredClone` in `org-create-posture-gate` leaves the file green (25/25). Its assertions read `sys_organization`, which each case's own engine creates, so a leak into the sign-up's tables is invisible to them. So isolation there was measured directly. A snapshot of the shared sign-up's tables taken after the `beforeAll` is byte-identical after the last case with the copy, 4 rows before and after. Without the copy the snapshot changes: 4 → 15 rows. The same snapshot in `implicit-account-linking` is identical with the copy and changed without it. - **Shuffled order:** `--sequence.shuffle` with seeds 1, 2 and 3, all three files green (25/25, 23/23, 15/15). The verbose order shows different first cases per seed. ## Tests and gates, at head `54067dc35` (after merging `main` at `5a871e4e4`) - **The three files at the final head**, one more interleaved run on the module line: org-create 1274 ms, implicit 1234 ms, dev-admin 2492 ms. Results: 25/25, 23/23 and 15/15. - **Whole package:** all 165 `@objectstack/plugin-auth` test files ran in 8 chunks with `vitest run --maxWorkers=2`, each under the lock. Every chunk exited 0 (`VERDICT command-exit 0`): 165/165 files passed, with 3165 tests passed and 10 skipped. The skips were already there at base. - **Typecheck:** `pnpm --filter @objectstack/plugin-auth run typecheck` gave `VERDICT command-exit 0`. `check:test-typecheck` reports OK and the debt ledger is unchanged (10 files / 88 errors / 23 signatures). `tsc -p tsconfig.test.json --listFilesOnly` lists all three files, and none of them is in the ledger. - **ESLint** on the three files (`--no-inline-config`): exit 0, no findings. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, with no paths, at `54067dc35` gave 55 commands. All 55 exited 0, each exit captured before any pipe. `check:dual-build-cjs-loads` first exited 3 (`PREREQUISITE NOT MET`, no workspace `dist/`). It exited 0 when re-run after a full `turbo run build` (77/77, `VERDICT command-exit 0`). `dispatch-gates --ran` reports 55 derived, 55 run, 0 NOT-MEASURED, 0 UNRUN. - **CI families** run on this PR's check-runs. `test:repo` was not run locally (CI-owned; the diff touches no repo-project test). - **No budget header was added**, so `measure-test-shard-timings --self-test` is not owed. ## Acceptance notes - `org-create-posture-gate`'s assertions cannot see a write that leaks into tables its cases do not read. The copy holds by construction and by the snapshot above. This is an observation about the file's reach, not a defect. - Each of these files spends about 30 to 35 s in vitest transform and import on this box before its first case, at base and at head. That is outside the module line the budget reads. --- _Generated by [Claude Code](https://claude.ai/code/session_01LR1cD9JqgkeqY7k9rrZH8s)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 65a8eeb commit 973917c

3 files changed

Lines changed: 195 additions & 31 deletions

File tree

‎packages/plugins/plugin-auth/src/dev-admin-seed-credential-gate.test.ts‎

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
* console reads the route and not the predicate.
5050
*/
5151

52-
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
52+
import { describe, it, expect, afterAll, afterEach, beforeEach, vi } from 'vitest';
5353
import { Hono } from 'hono';
5454
import { ObjectQL } from '@objectstack/objectql';
5555
import { SqlDriver } from '@objectstack/driver-sql';
@@ -60,6 +60,29 @@ import { AuthPlugin } from './auth-plugin.js';
6060
import { SELF_REGISTRATION_CLOSED } from './audience-posture.js';
6161
import { decideDevAdminSeedGate } from './dev-admin-seed-gate.js';
6262
import { recoverInternalFieldsForSystemRead } from './internal-field-readback.js';
63+
import { memoizeScrypt } from './__tests__/scrypt-memo.testkit';
64+
import { sqliteEnginePool } from './__tests__/sqlite-engine-pool.testkit';
65+
// [#23184] better-auth's first module load is paid HERE, at module load, and
66+
// not inside the first clocked case: clocked windows measure behaviour, never
67+
// loading (AGENTS.md, Build & Test). `AuthManager` reaches these vendor modules
68+
// through a lazy `await import()`, so without these lines case ⓪ paid about a
69+
// second of vendor module evaluation on a dev container. The module cache hands
70+
// `AuthManager` these same instances. The one vendor value read at load is
71+
// `NODE_ENV` (better-auth's env module keeps the value it sees when it is first
72+
// evaluated), and that module is already evaluated before this point: the
73+
// `./auth-manager.js` import above reaches `better-auth/adapters` through
74+
// `./objectql-adapter.js`, and that import evaluates it. So these lines move no
75+
// capture: the vendor reads the same `NODE_ENV` as before, whatever the
76+
// `beforeEach` below sets, and nothing a case runs is different.
77+
import 'better-auth';
78+
import 'better-auth/api';
79+
import 'better-auth/plugins';
80+
import 'better-auth/plugins/bearer';
81+
import 'better-auth/plugins/custom-session';
82+
import 'better-auth/plugins/organization';
83+
import 'better-auth/plugins/organization/access';
84+
import '@better-auth/oauth-provider';
85+
import '@better-auth/core/error';
6386

6487
const BASE = 'http://localhost:3000';
6588
const AUTH_BASE = '/api/v1/auth';
@@ -85,46 +108,61 @@ const SEED_ENV_KEYS = [
85108
] as const;
86109
let savedEnv: Record<string, string | undefined> = {};
87110

88-
const engines: ObjectQL[] = [];
89-
90111
beforeEach(() => {
91112
savedEnv = Object.fromEntries(SEED_ENV_KEYS.map((k) => [k, process.env[k]]));
92113
for (const k of SEED_ENV_KEYS) delete process.env[k];
93114
process.env.NODE_ENV = 'development';
94115
});
95116

96-
afterEach(async () => {
117+
afterEach(() => {
97118
for (const [k, v] of Object.entries(savedEnv)) {
98119
if (v === undefined) delete process.env[k];
99120
else process.env[k] = v;
100121
}
101-
while (engines.length) {
102-
const e = engines.pop();
103-
try {
104-
await (e as unknown as { destroy?(): Promise<void> })?.destroy?.();
105-
} catch {
106-
/* noop */
107-
}
108-
}
109122
});
110123

111-
async function bootEngine(): Promise<ObjectQL> {
124+
/**
125+
* [#23184] The engine every case starts from: booted ONCE per file and emptied
126+
* before each case that takes it (`__tests__/sqlite-engine-pool.testkit.ts`),
127+
* instead of booted per case. A boot here is `init()` plus the DDL for the 30
128+
* objects plugin-auth registers, about 70 ms on a dev container and the
129+
* costliest step of the cases that derive no password. The engine a case
130+
* takes was built by the same steps a fresh boot runs, and the pool hands it
131+
* over with every table empty, so each case still starts from the empty
132+
* database it always did. Nothing in this file adds a hook, an object or a
133+
* table to an engine; the pool refuses an engine whose schema changed, and one
134+
* left holding an organization row.
135+
*/
136+
const engines = sqliteEnginePool(async () => {
112137
const engine = new ObjectQL();
113-
engines.push(engine);
114-
engine.registerDriver(
115-
new SqlDriver({
116-
client: 'better-sqlite3',
117-
connection: { filename: ':memory:' },
118-
useNullAsDefault: true,
119-
}),
120-
true,
121-
);
138+
const driver = new SqlDriver({
139+
client: 'better-sqlite3',
140+
connection: { filename: ':memory:' },
141+
useNullAsDefault: true,
142+
});
143+
engine.registerDriver(driver, true);
122144
await engine.init();
123145
for (const object of AUTH_OBJECTS) {
124146
engine.registry.registerObject(object as never, '@objectstack/plugin-auth');
125147
}
126148
await engine.syncSchemas();
127-
return engine;
149+
return { engine, driver };
150+
});
151+
afterEach(engines.release);
152+
afterAll(engines.destroy);
153+
154+
/**
155+
* [#23184] A sign-in that follows the seed verifies the password against the
156+
* hash the seed just stored, and a second seed run re-checks that same hash:
157+
* the same password against the same salt, which the memo answers from the
158+
* bytes the first derivation produced. Every fresh salt is still derived for
159+
* real (`__tests__/scrypt-memo.testkit.ts`).
160+
*/
161+
const scrypt = memoizeScrypt();
162+
afterAll(scrypt.restore);
163+
164+
async function bootEngine(): Promise<ObjectQL> {
165+
return engines.take();
128166
}
129167

130168
/** The population that makes this card: people, no credentials. */

‎packages/plugins/plugin-auth/src/implicit-account-linking.test.ts‎

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ import {
2424
unlinkTombstoneIdentifier,
2525
unlinkTombstoneUserPrefix,
2626
} from './implicit-account-linking';
27+
// [#23184] better-auth's first module load is paid HERE, at module load, and
28+
// not inside the first clocked case: clocked windows measure behaviour, never
29+
// loading (AGENTS.md, Build & Test). `AuthManager` reaches these vendor modules
30+
// through a lazy `await import()`, so without these lines the first case that
31+
// builds an instance paid about 0.7 s of vendor module evaluation on a dev
32+
// container. The module cache hands `AuthManager` these same instances, and
33+
// better-auth's one load-time read (`NODE_ENV`, kept by its env module when
34+
// first evaluated) already happened at the `./auth-manager` import above,
35+
// through `better-auth/adapters`, so nothing a case runs is different.
36+
import 'better-auth';
37+
import 'better-auth/api';
38+
import 'better-auth/plugins';
39+
import 'better-auth/plugins/bearer';
40+
import 'better-auth/plugins/custom-session';
41+
import 'better-auth/plugins/generic-oauth';
42+
import 'better-auth/plugins/organization';
43+
import 'better-auth/plugins/organization/access';
44+
import '@better-auth/oauth-provider';
2745

2846
// ── In-memory IDataEngine (the audience-posture harness shape) ───────────────
2947

@@ -122,8 +140,16 @@ const provider = (providerId: string) => ({
122140
pkce: false,
123141
});
124142

125-
const makeManager = (engine: any, config: Record<string, unknown> = {}) =>
126-
new AuthManager({
143+
/**
144+
* [#23184] The engine under each manager this file builds, for the managers a
145+
* copied sign-up stands for (see {@link signUp}): every one but a manager with
146+
* a `secondaryStorage`, where a sign-up writes its session to the cache rather
147+
* than to the engine.
148+
*/
149+
const copyTargets = new WeakMap<AuthManager, any>();
150+
151+
const makeManager = (engine: any, config: Record<string, unknown> = {}) => {
152+
const manager = new AuthManager({
127153
secret: SECRET,
128154
baseUrl: BASE,
129155
dataEngine: engine,
@@ -157,6 +183,9 @@ const makeManager = (engine: any, config: Record<string, unknown> = {}) =>
157183
},
158184
...config,
159185
} as any);
186+
if (!('secondaryStorage' in config)) copyTargets.set(manager, engine);
187+
return manager;
188+
};
160189

161190
// ── The stubbed identity provider ────────────────────────────────────────────
162191

@@ -221,8 +250,52 @@ const post = (manager: AuthManager, path: string, body: unknown, cookie?: string
221250
}),
222251
);
223252

224-
/** Sign up with email + password; answers the session cookies. */
253+
/**
254+
* [#23184] One sign-up, made ONCE per file through the real `/sign-up/email`
255+
* route, on an engine and a manager of its own.
256+
*
257+
* A sign-up is the costly step of every end-to-end case here (one
258+
* production-parameter scrypt derivation under a fresh salt, most of the
259+
* case's wall), and no case is about it: each case is about what an external
260+
* sign-in, a link or an unlink does to a local row that a sign-up made. So
261+
* {@link signUp} gives a case's first sign-up a deep copy of the tables this
262+
* one wrote, on the case's own engine, with the user row's address set to the
263+
* case's own, and answers this sign-up's session cookies.
264+
*/
265+
const TEMPLATE_EMAIL = 'founder@example.test';
266+
let signedUp: Promise<{ tables: Map<string, any[]>; cookies: string[] }> | undefined;
267+
const signedUpOnce = () =>
268+
(signedUp ??= (async () => {
269+
const template = createMemoryEngine();
270+
const res = await post(makeManager(template), 'sign-up/email', {
271+
email: TEMPLATE_EMAIL,
272+
password: PASSWORD,
273+
name: 'Synthetic Person',
274+
});
275+
expect(res.status).toBe(200);
276+
return { tables: template.tables, cookies: cookiesFrom(res) };
277+
})());
278+
279+
/**
280+
* Sign up with email + password; answers the session cookies.
281+
*
282+
* [#23184] The first sign-up on a manager's still-empty engine is the one
283+
* above, copied (see {@link signedUpOnce}); a sign-up on an engine that already
284+
* holds a user, or under a `secondaryStorage` manager, runs live as before.
285+
* Measured for every address and manager configuration this file signs up
286+
* with, the copy holds the same rows a live sign-up of that address writes
287+
* (ids, tokens, the hash and timestamps aside), and its cookie opens a session
288+
* for that user. The copy is `structuredClone`, so no case's write reaches the
289+
* template or another case.
290+
*/
225291
const signUp = async (manager: AuthManager, email: string): Promise<string[]> => {
292+
const engine = copyTargets.get(manager);
293+
if (engine && (engine.tables.get('sys_user') ?? []).length === 0) {
294+
const { tables, cookies } = await signedUpOnce();
295+
for (const [name, rows] of tables) engine.tables.set(name, structuredClone(rows));
296+
userRow(engine, TEMPLATE_EMAIL).email = email.toLowerCase();
297+
return cookies;
298+
}
226299
const res = await post(manager, 'sign-up/email', { email, password: PASSWORD, name: 'Synthetic Person' });
227300
expect(res.status).toBe(200);
228301
return cookiesFrom(res);

‎packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts‎

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,27 @@
5454
// Real better-auth pipeline throughout (the #3585 EdDSA / #4785 session-of-record
5555
// precedent: patch the real thing, never stub our own code).
5656

57-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
57+
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from 'vitest';
5858
import { assertEngineDeleteDispatch, assertEngineFindOnePredicate } from '@objectstack/objectql';
5959
import { AuthManager } from './auth-manager';
6060
import { createTenancyService, type TenancyService, type TenancyPosture } from './tenancy-service';
61+
// [#23184] better-auth's first module load is paid HERE, at module load, and
62+
// not inside the first clocked case: clocked windows measure behaviour, never
63+
// loading (AGENTS.md, Build & Test). `AuthManager` reaches these vendor modules
64+
// through a lazy `await import()`, so without these lines the first case paid
65+
// about 0.8 s of vendor module evaluation on a dev container. The module cache
66+
// hands `AuthManager` these same instances, and better-auth's one load-time
67+
// read (`NODE_ENV`, kept by its env module when first evaluated) already
68+
// happened at the `./auth-manager` import above, through
69+
// `better-auth/adapters`, so nothing a case runs is different.
70+
import 'better-auth';
71+
import 'better-auth/api';
72+
import 'better-auth/plugins';
73+
import 'better-auth/plugins/bearer';
74+
import 'better-auth/plugins/custom-session';
75+
import 'better-auth/plugins/organization';
76+
import 'better-auth/plugins/organization/access';
77+
import '@better-auth/oauth-provider';
6178

6279
/**
6380
* In-memory IDataEngine — same shape as the #4785 harness (`fields` really
@@ -195,9 +212,44 @@ interface Scenario {
195212
}
196213

197214
/**
198-
* Boot a manager under a scenario and run the whole guided path: sign up, then
199-
* ask for a workspace. Returns the real HTTP answer plus the `/auth/config`
200-
* flag the console would have rendered its button from.
215+
* [#23184] The founder's sign-up, made ONCE per file through the real
216+
* `/sign-up/email` route, on an engine and a manager of its own.
217+
*
218+
* The sign-up is the costly step of a guided run (one production-parameter
219+
* scrypt derivation under a fresh salt, most of each case's wall), and no case
220+
* is about it: each case is about what `organization/create` and
221+
* `/auth/config` answer for a signed-in founder under a scenario. So
222+
* {@link runGuidedWorkspaceCreation} starts each case from a deep copy of the
223+
* tables this sign-up wrote, on the case's own engine, with the case's own
224+
* manager built under the case's own scenario, and presents the session cookie
225+
* this sign-up answered. Both knobs are unset while it runs. Measured across
226+
* every scenario in this file, a sign-up run under the scenario itself writes
227+
* the same rows (ids, tokens, hashes and timestamps aside) and the case then
228+
* answers the same status, body, flags and organization rows. The copy is
229+
* `structuredClone`, so no case's write reaches the template or another case.
230+
* The two cases that sign up inline below still do, under their own env.
231+
*/
232+
let signedUp: { tables: Map<string, any[]>; cookie: string } | undefined;
233+
beforeAll(async () => {
234+
delete process.env.OS_TENANCY_POSTURE;
235+
delete process.env.OS_MULTI_ORG_ENABLED;
236+
try {
237+
const template = createMemoryEngine();
238+
const cookie = cookieFrom(await signUp(makeManager(template), 'founder@example.com'));
239+
signedUp = { tables: template.tables, cookie };
240+
} finally {
241+
if (OLD_POSTURE === undefined) delete process.env.OS_TENANCY_POSTURE;
242+
else process.env.OS_TENANCY_POSTURE = OLD_POSTURE;
243+
if (OLD_LEGACY === undefined) delete process.env.OS_MULTI_ORG_ENABLED;
244+
else process.env.OS_MULTI_ORG_ENABLED = OLD_LEGACY;
245+
}
246+
});
247+
248+
/**
249+
* Boot a manager under a scenario and run the guided path from a signed-in
250+
* founder (the sign-up above): ask for a workspace. Returns the real HTTP
251+
* answer plus the `/auth/config` flag the console would have rendered its
252+
* button from.
201253
*/
202254
const runGuidedWorkspaceCreation = async (scenario: Scenario, slug = 'acme') => {
203255
if (scenario.posture === undefined) delete process.env.OS_TENANCY_POSTURE;
@@ -206,11 +258,12 @@ const runGuidedWorkspaceCreation = async (scenario: Scenario, slug = 'acme') =>
206258
else process.env.OS_MULTI_ORG_ENABLED = scenario.legacy;
207259

208260
const engine = createMemoryEngine();
261+
for (const [name, rows] of signedUp!.tables) engine.tables.set(name, structuredClone(rows));
209262
const manager = makeManager(
210263
engine,
211264
scenario.tenancy ? { getTenancy: () => scenario.tenancy } : {},
212265
);
213-
const cookie = cookieFrom(await signUp(manager, `founder-${slug}@example.com`));
266+
const cookie = signedUp!.cookie;
214267
expect(cookie).not.toBe('');
215268

216269
const response = await createOrganization(manager, cookie, slug);

0 commit comments

Comments
 (0)