@suluk/journeys 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suluk/journeys",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Intuitive, runnable BDD over a v4 'Suluk' contract. Projects a deterministic Gherkin step VOCABULARY from the contract (Given from x-suluk-access, When from each operation, Then from declared statuses + x-suluk-store + x-suluk-cost), binds authored .feature stories EXACT-or-UNBOUND with outcome steps resolved relative to the scenario's When-subject, and emits a BIDIRECTIONAL tri-state gap report (PARAPHRASE / NEEDS-DEV-GLUE / NEEDS-CONTRACT) + contract->authored coverage holes. A pure function of the document. CANDIDATE tooling.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -13,6 +13,7 @@
13
13
  *
14
14
  * Runtime IO only — provably outside the deterministic core (bind.ts / vocabulary.ts), enforced by hatch-wall.test.ts.
15
15
  */
16
+ export { runScope, type RunScope, type RunScopeOptions } from "./runscope";
16
17
  export { resolveBackend, localD1, remoteD1, type BackendSpec } from "./backends";
17
18
  export { stateHatch, type StateHatchOptions } from "./state";
18
19
  export { signInAs, type SignInAsOptions, type SignedInSession, type CleanupTarget } from "./auth";
@@ -0,0 +1,40 @@
1
+ /**
2
+ * runScope (C039) — a per-process RUN SCOPE so parallel git worktrees / CI shards / agents can run the SAME BDD suite
3
+ * without clashing. The only things that clash are SHARED mutable state with a FIXED name; this makes both unique:
4
+ *
5
+ * - `d1Path` is a unique file in the OS temp dir (NOT in the repo), so git/worktrees/.wrangler never see or contend
6
+ * on it — each run opens its own local sqlite.
7
+ * - `scopeId`/`email` are unique, so even against a SHARED backend (prod-as-test-users) the hatch's test-user scoping
8
+ * means a run only ever seeds/cleans ITS OWN rows.
9
+ *
10
+ * `runId = <pid>_<uuid8>` is globally unique across processes and worktrees. Pair it with a hatch `scope: { value:
11
+ * scopeId }` (state hatch) and the `email` (auth hatch) for a fully isolated, parallel-safe run.
12
+ */
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+
16
+ export interface RunScope {
17
+ runId: string;
18
+ /** the test-user id — the state hatch forces every seeded row's owner to this; cleanup deletes only these rows. */
19
+ scopeId: string;
20
+ email: string;
21
+ /** an isolated local sqlite path in the OS temp dir (outside any worktree). */
22
+ d1Path: string;
23
+ }
24
+
25
+ export interface RunScopeOptions {
26
+ /** filename prefix for the temp DB (default "suluk-bdd"). */
27
+ prefix?: string;
28
+ /** email domain for the synthetic test user (default "example.test"). */
29
+ emailDomain?: string;
30
+ }
31
+
32
+ export function runScope(opts: RunScopeOptions = {}): RunScope {
33
+ const runId = `${process.pid}_${crypto.randomUUID().slice(0, 8)}`;
34
+ return {
35
+ runId,
36
+ scopeId: `testuser_${runId}`,
37
+ email: `bdd+${runId}@${opts.emailDomain ?? "example.test"}`,
38
+ d1Path: join(tmpdir(), `${opts.prefix ?? "suluk-bdd"}-${runId}.sqlite`),
39
+ };
40
+ }
@@ -0,0 +1,53 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { localD1 } from "../src/hatch/backends";
3
+ import { stateHatch } from "../src/hatch/state";
4
+ import { signInAs } from "../src/hatch/auth";
5
+ import type { TestUser } from "../src/hatch/types";
6
+
7
+ /**
8
+ * Durable package witness for the AUTH HATCH contract (C039) — the orchestration + the FAIL-CLOSED guarantee, proven
9
+ * without any real Better Auth (the consumer supplies mintSession/verify; here they are stubs). The end-to-end mint
10
+ * against toolfactory's real Better Auth is witnessed separately by toolfactory's local self-test
11
+ * (scripts/journeys-hatch-selftest.ts). This pins the property that matters most: signInAs NEVER returns a session the
12
+ * app rejected — it can never manufacture a false green — and teardown is test-user-scoped.
13
+ */
14
+ async function fixture() {
15
+ const d1 = await localD1(":memory:");
16
+ await d1.run("CREATE TABLE user (id TEXT PRIMARY KEY, email TEXT)");
17
+ await d1.run("CREATE TABLE session (id TEXT PRIMARY KEY, userId TEXT, token TEXT)");
18
+ const state = stateHatch(d1, { write: true, scope: { value: "testuser_1" } });
19
+ return { d1, state };
20
+ }
21
+ const ensureUser = async (state: Awaited<ReturnType<typeof fixture>>["state"], user: TestUser) => {
22
+ await state.d1.seed("user", "id", [{ email: user.email }], { kind: "auth", because: "OAuth-only; no API seeds a verified user", userPathChecked: true });
23
+ return "testuser_1"; // seed forced user.id = the scope value
24
+ };
25
+ const mintSession = async (userId: string) => ({ cookie: `better-auth.session_token=${userId}.sig` });
26
+
27
+ describe("auth hatch — signInAs (fail-closed, never a false green)", () => {
28
+ test("returns the session + seeds the user when the app ACCEPTS the minted cookie", async () => {
29
+ const { state } = await fixture();
30
+ const s = await signInAs({ state, user: { email: "bdd@example.test" }, ensureUser, mintSession, verify: async () => true });
31
+ expect(s.userId).toBe("testuser_1");
32
+ expect(s.cookie).toBe("better-auth.session_token=testuser_1.sig");
33
+ expect(await state.d1.get("SELECT id FROM user WHERE id = ?", ["testuser_1"])).toEqual({ id: "testuser_1" });
34
+ });
35
+
36
+ test("THROWS when the app REJECTS the minted session — fails closed, never returns an unverified credential", async () => {
37
+ const { state } = await fixture();
38
+ await expect(
39
+ signInAs({ state, user: { email: "bdd@example.test" }, ensureUser, mintSession, verify: async () => false }),
40
+ ).rejects.toThrow(/REJECTED|fails closed/);
41
+ });
42
+
43
+ test("teardown scoped-deletes only the test user's user + session rows", async () => {
44
+ const { d1, state } = await fixture();
45
+ await d1.run("INSERT INTO session (id, userId, token) VALUES (?,?,?)", ["s1", "testuser_1", "t"]);
46
+ await d1.run("INSERT INTO user (id, email) VALUES (?,?)", ["real_1", "real@customer.com"]); // a co-resident real user
47
+ const s = await signInAs({ state, user: { email: "bdd@example.test" }, ensureUser, mintSession, verify: async () => true });
48
+ await s.teardown(); // default targets: session.userId + user.id
49
+ expect(await state.d1.select("SELECT id FROM user WHERE id = ?", ["testuser_1"])).toEqual([]);
50
+ expect(await state.d1.select("SELECT id FROM session WHERE userId = ?", ["testuser_1"])).toEqual([]);
51
+ expect(await state.d1.select("SELECT id FROM user WHERE id = ?", ["real_1"])).toEqual([{ id: "real_1" }]); // untouched
52
+ });
53
+ });
@@ -0,0 +1,28 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { tmpdir } from "node:os";
3
+ import { runScope } from "../src/hatch/runscope";
4
+
5
+ /**
6
+ * runScope is the parallel-safety primitive (C039): every call yields a UNIQUE test-user identity + an isolated temp DB
7
+ * path, so parallel worktrees / CI shards running the same BDD suite never clash.
8
+ */
9
+ describe("runScope — unique-per-call, isolated, parallel-safe", () => {
10
+ test("each call is globally unique (id, scopeId, email, d1Path)", () => {
11
+ const a = runScope();
12
+ const b = runScope();
13
+ expect(a.runId).not.toBe(b.runId);
14
+ expect(a.scopeId).not.toBe(b.scopeId);
15
+ expect(a.email).not.toBe(b.email);
16
+ expect(a.d1Path).not.toBe(b.d1Path);
17
+ // 50 calls → 50 distinct ids
18
+ expect(new Set(Array.from({ length: 50 }, () => runScope().runId)).size).toBe(50);
19
+ });
20
+
21
+ test("the DB path is in the OS temp dir (outside any worktree/repo) and the scope ties everything to one test user", () => {
22
+ const s = runScope({ prefix: "tf-bdd" });
23
+ expect(s.d1Path.startsWith(tmpdir())).toBe(true);
24
+ expect(s.d1Path).toContain("tf-bdd-");
25
+ expect(s.scopeId).toBe(`testuser_${s.runId}`);
26
+ expect(s.email).toContain(s.runId);
27
+ });
28
+ });