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 { makeSqliteAppsStore } from "../backing/sqlite-apps-store";import { syncGitHubSource } from "../source/github-source";import { makeTestResolver } from "../testing";import { makeSelfHostAppsRuntime } from "./self-host-runtime"; const run = <A, E>(effect: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(effect); const toolSource = (name: string): string => `import { defineTool } from "executor:app";import { z } from "zod";export default defineTool({ description: "${name}", input: z.object({}), output: z.object({ ok: z.boolean(), name: z.string() }), async handler() { return { ok: true, name: ${JSON.stringify(name)} }; },});`; const json = (value: unknown, status = 200): Response => new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" }, }); const makeGitHubFetch = (input: { readonly toolName: string; readonly upstreamSha: string }) => { const repoPath = "/repos/acme/tools"; const treeSha = `${input.upstreamSha}-tree`; const content = toolSource(input.toolName); const fetch = (async (rawUrl: string) => { const url = new URL(rawUrl); if (url.pathname === repoPath) return json({ default_branch: "main" }); if (url.pathname === `${repoPath}/git/ref/heads%2Fmain`) { return json({ object: { sha: input.upstreamSha } }); } if (url.pathname === `${repoPath}/git/commits/${input.upstreamSha}`) { return json({ sha: input.upstreamSha, tree: { sha: treeSha } }); } if (url.pathname === `${repoPath}/git/trees/${treeSha}`) { return json({ tree: [ { path: `tools/${input.toolName}.ts`, type: "blob", mode: "100644", sha: `${input.upstreamSha}-blob`, size: Buffer.byteLength(content, "utf8"), }, ], }); } if (url.pathname === `${repoPath}/git/blobs/${input.upstreamSha}-blob`) { return json({ encoding: "base64", content: Buffer.from(content, "utf8").toString("base64"), }); } return json({ message: "not found" }, 404); }) as typeof globalThis.fetch; return fetch;}; const makeTenantRuntime = (dataDir: string, tenant: string, storePath: string) => makeSelfHostAppsRuntime({ dataDir, tenant, store: makeSqliteAppsStore({ path: storePath }), resolver: makeTestResolver({}), }); describe("tenant scope isolation", () => { it("keeps the same source-shaped scope distinct across tenants", async () => { const dataDir = mkdtempSync(join(tmpdir(), "apps-tenant-")); const storePath = join(dataDir, "store.sqlite"); const scope = "githubSourceSameRepoConnection"; const tenantA = "org-a"; const tenantB = "org-b"; const hostA = makeTenantRuntime(dataDir, tenantA, storePath); const hostB = makeTenantRuntime(dataDir, tenantB, storePath); const publishedA = await run( syncGitHubSource({ runtime: hostA.runtime, tenant: tenantA, scope, url: "https://github.com/acme/tools", fetch: makeGitHubFetch({ toolName: "alpha", upstreamSha: "sha-a" }), }), ); const publishedB = await run( syncGitHubSource({ runtime: hostB.runtime, tenant: tenantB, scope, url: "https://github.com/acme/tools", fetch: makeGitHubFetch({ toolName: "beta", upstreamSha: "sha-b" }), }), ); expect(publishedA.status).toBe("published"); expect(publishedB.status).toBe("published"); const artifactDirs = readdirSync(join(dataDir, "artifacts")).filter((name) => name.endsWith(".git"), ); expect(artifactDirs.length).toBe(2); const descriptorA = await run(hostA.runtime.getDescriptor(tenantA, scope)); const descriptorB = await run(hostB.runtime.getDescriptor(tenantB, scope)); expect(descriptorA?.tools.map((tool) => tool.name)).toEqual(["alpha"]); expect(descriptorB?.tools.map((tool) => tool.name)).toEqual(["beta"]); await hostA.close(); await hostB.close(); });});
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 { makeSqliteAppsStore } from "../backing/sqlite-apps-store";
import { syncGitHubSource } from "../source/github-source";
import { makeTestResolver } from "../testing";
import { makeSelfHostAppsRuntime } from "./self-host-runtime";
const run = <A, E>(effect: Effect.Effect<A, E>): Promise<A> => Effect.runPromise(effect);
const toolSource = (name: string): string => `import { defineTool } from "executor:app";
import { z } from "zod";
export default defineTool({
description: "${name}",
input: z.object({}),
output: z.object({ ok: z.boolean(), name: z.string() }),
async handler() {
return { ok: true, name: ${JSON.stringify(name)} };
},
});`;
const json = (value: unknown, status = 200): Response =>
new Response(JSON.stringify(value), {
status,
headers: { "content-type": "application/json" },
});
const makeGitHubFetch = (input: { readonly toolName: string; readonly upstreamSha: string }) => {
const repoPath = "/repos/acme/tools";
const treeSha = `${input.upstreamSha}-tree`;
const content = toolSource(input.toolName);
const fetch = (async (rawUrl: string) => {
const url = new URL(rawUrl);
if (url.pathname === repoPath) return json({ default_branch: "main" });
if (url.pathname === `${repoPath}/git/ref/heads%2Fmain`) {
return json({ object: { sha: input.upstreamSha } });
}
if (url.pathname === `${repoPath}/git/commits/${input.upstreamSha}`) {
return json({ sha: input.upstreamSha, tree: { sha: treeSha } });
}
if (url.pathname === `${repoPath}/git/trees/${treeSha}`) {
return json({
tree: [
{
path: `tools/${input.toolName}.ts`,
type: "blob",
mode: "100644",
sha: `${input.upstreamSha}-blob`,
size: Buffer.byteLength(content, "utf8"),
},
],
});
}
if (url.pathname === `${repoPath}/git/blobs/${input.upstreamSha}-blob`) {
return json({
encoding: "base64",
content: Buffer.from(content, "utf8").toString("base64"),
});
}
return json({ message: "not found" }, 404);
}) as typeof globalThis.fetch;
return fetch;
};
const makeTenantRuntime = (dataDir: string, tenant: string, storePath: string) =>
makeSelfHostAppsRuntime({
dataDir,
tenant,
store: makeSqliteAppsStore({ path: storePath }),
resolver: makeTestResolver({}),
});
describe("tenant scope isolation", () => {
it("keeps the same source-shaped scope distinct across tenants", async () => {
const dataDir = mkdtempSync(join(tmpdir(), "apps-tenant-"));
const storePath = join(dataDir, "store.sqlite");
const scope = "githubSourceSameRepoConnection";
const tenantA = "org-a";
const tenantB = "org-b";
const hostA = makeTenantRuntime(dataDir, tenantA, storePath);
const hostB = makeTenantRuntime(dataDir, tenantB, storePath);
const publishedA = await run(
syncGitHubSource({
runtime: hostA.runtime,
tenant: tenantA,
scope,
url: "https://github.com/acme/tools",
fetch: makeGitHubFetch({ toolName: "alpha", upstreamSha: "sha-a" }),
}),
);
const publishedB = await run(
syncGitHubSource({
runtime: hostB.runtime,
tenant: tenantB,
scope,
url: "https://github.com/acme/tools",
fetch: makeGitHubFetch({ toolName: "beta", upstreamSha: "sha-b" }),
}),
);
expect(publishedA.status).toBe("published");
expect(publishedB.status).toBe("published");
const artifactDirs = readdirSync(join(dataDir, "artifacts")).filter((name) =>
name.endsWith(".git"),
);
expect(artifactDirs.length).toBe(2);
const descriptorA = await run(hostA.runtime.getDescriptor(tenantA, scope));
const descriptorB = await run(hostB.runtime.getDescriptor(tenantB, scope));
expect(descriptorA?.tools.map((tool) => tool.name)).toEqual(["alpha"]);
expect(descriptorB?.tools.map((tool) => tool.name)).toEqual(["beta"]);
await hostA.close();
await hostB.close();
});
});