import { mkdtempSync, readdirSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import { describe, expect, it } from "@effect/vitest";import { Effect } from "effect";import { makeLibsqlScopeDb } from "./libsql-scope-db";import { makeSqliteAppsStore } from "./sqlite-apps-store";const run = <A, E>(effect: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(effect);// ---------------------------------------------------------------------------// Finding 9 regression: scopes that differ only by a character the old naming// collapsed ("my-scope" vs "my_scope") must NOT share a database, and the// connection->scope mapping must route each to its own scope. Before the fix the// scope-db filename collapsed `[^A-Za-z0-9._-]` to `_` (colliding two scopes onto// one file) and the connection name was reverse-parsed (colliding two scopes onto// one normalized identifier).// ---------------------------------------------------------------------------describe("scope collision (Fix 9)", () => { it("gives my-scope and my_scope DISTINCT scope-db files with isolated data", async () => { const root = mkdtempSync(join(tmpdir(), "apps-scopecol-")); const db = makeLibsqlScopeDb({ root }); const a = await run(db.forScope("my-scope")); await run(a.exec("CREATE TABLE t (v TEXT)")); await run(a.exec("INSERT INTO t (v) VALUES ('a')")); const b = await run(db.forScope("my_scope")); await run(b.exec("CREATE TABLE t (v TEXT)")); await run(b.exec("INSERT INTO t (v) VALUES ('b')")); // Each scope sees ONLY its own row (no shared file). const aRows = await run(a.exec<{ v: string }>("SELECT v FROM t")); const bRows = await run(b.exec<{ v: string }>("SELECT v FROM t")); expect(aRows.map((r) => r.v)).toEqual(["a"]); expect(bRows.map((r) => r.v)).toEqual(["b"]); // Two distinct .db files exist on disk. const dbFiles = readdirSync(root).filter((f) => f.endsWith(".db")); expect(dbFiles.length).toBe(2); await run(db.close()); }); it("routes distinct scopes through the explicit connection->scope mapping", async () => { const store = makeSqliteAppsStore({ path: ":memory:" }); // Both scopes' connection names normalize to the SAME identifier // ("appsMyScope"), but the explicit mapping keys by the ACTUAL stored name, // so each scope's connection maps to the right scope. Simulate two DISTINCT // connection names being recorded for two distinct scopes. await run(store.putScopeForConnection("appsMyScopeDash", "my-scope")); await run(store.putScopeForConnection("appsMyScopeUnderscore", "my_scope")); expect(await run(store.getScopeForConnection("appsMyScopeDash"))).toBe("my-scope"); expect(await run(store.getScopeForConnection("appsMyScopeUnderscore"))).toBe("my_scope"); // An unmapped name returns null (caller falls back to legacy parse). expect(await run(store.getScopeForConnection("unknown"))).toBeNull(); });});
import { mkdtempSync, readdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "@effect/vitest";
import { Effect } from "effect";
import { makeLibsqlScopeDb } from "./libsql-scope-db";
import { makeSqliteAppsStore } from "./sqlite-apps-store";
const run = <A, E>(effect: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(effect);
// ---------------------------------------------------------------------------
// Finding 9 regression: scopes that differ only by a character the old naming
// collapsed ("my-scope" vs "my_scope") must NOT share a database, and the
// connection->scope mapping must route each to its own scope. Before the fix the
// scope-db filename collapsed `[^A-Za-z0-9._-]` to `_` (colliding two scopes onto
// one file) and the connection name was reverse-parsed (colliding two scopes onto
// one normalized identifier).
// ---------------------------------------------------------------------------
describe("scope collision (Fix 9)", () => {
it("gives my-scope and my_scope DISTINCT scope-db files with isolated data", async () => {
const root = mkdtempSync(join(tmpdir(), "apps-scopecol-"));
const db = makeLibsqlScopeDb({ root });
const a = await run(db.forScope("my-scope"));
await run(a.exec("CREATE TABLE t (v TEXT)"));
await run(a.exec("INSERT INTO t (v) VALUES ('a')"));
const b = await run(db.forScope("my_scope"));
await run(b.exec("CREATE TABLE t (v TEXT)"));
await run(b.exec("INSERT INTO t (v) VALUES ('b')"));
// Each scope sees ONLY its own row (no shared file).
const aRows = await run(a.exec<{ v: string }>("SELECT v FROM t"));
const bRows = await run(b.exec<{ v: string }>("SELECT v FROM t"));
expect(aRows.map((r) => r.v)).toEqual(["a"]);
expect(bRows.map((r) => r.v)).toEqual(["b"]);
// Two distinct .db files exist on disk.
const dbFiles = readdirSync(root).filter((f) => f.endsWith(".db"));
expect(dbFiles.length).toBe(2);
await run(db.close());
});
it("routes distinct scopes through the explicit connection->scope mapping", async () => {
const store = makeSqliteAppsStore({ path: ":memory:" });
// Both scopes' connection names normalize to the SAME identifier
// ("appsMyScope"), but the explicit mapping keys by the ACTUAL stored name,
// so each scope's connection maps to the right scope. Simulate two DISTINCT
// connection names being recorded for two distinct scopes.
await run(store.putScopeForConnection("appsMyScopeDash", "my-scope"));
await run(store.putScopeForConnection("appsMyScopeUnderscore", "my_scope"));
expect(await run(store.getScopeForConnection("appsMyScopeDash"))).toBe("my-scope");
expect(await run(store.getScopeForConnection("appsMyScopeUnderscore"))).toBe("my_scope");
// An unmapped name returns null (caller falls back to legacy parse).
expect(await run(store.getScopeForConnection("unknown"))).toBeNull();
});
});