Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/cloud/src/mcp-session.e2e.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ import {
makePostgresAdapter,
makePostgresBlobStore,
} from "@executor-js/storage-postgres";
import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing";
import executorConfig from "../executor.config";
import { DbService } from "./services/db";
import { makeFakeVaultClient } from "./services/__test-harness__/api-harness";

// ---------------------------------------------------------------------------
// Test-only plugin: exposes one in-memory tool that elicits once. Lets the
Expand Down Expand Up @@ -102,7 +102,7 @@ const buildScopedExecutor = (
Effect.gen(function* () {
const { db } = yield* DbService;
const basePlugins = executorConfig.plugins({
workosVaultClient: makeFakeVaultClient(),
workosVaultClient: makeTestWorkOSVaultClient(),
});
const plugins = options.withElicitingPlugin
? ([...basePlugins, elicitingTestPlugin()] as const)
Expand Down
103 changes: 4 additions & 99 deletions apps/cloud/src/services/__test-harness__/api-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,7 @@ import {
makePostgresAdapter,
makePostgresBlobStore,
} from "@executor-js/storage-postgres";
import {
WorkOSVaultClientError,
type WorkOSVaultClient,
type WorkOSVaultObject,
type WorkOSVaultObjectMetadata,
} from "@executor-js/plugin-workos-vault";
import { makeTestWorkOSVaultClient } from "@executor-js/plugin-workos-vault/testing";

import executorConfig from "../../../executor.config";
import { AuthContext } from "../../auth/middleware";
Expand All @@ -73,103 +68,13 @@ const userOrgScopeId = (userId: string, orgId: string) =>
// across calls within a single test.
const defaultUserFor = (orgId: string) => `default_user_${orgId}`;

// ---------------------------------------------------------------------------
// Fake WorkOS Vault client — in-memory map keyed by name.
// ---------------------------------------------------------------------------

export const makeFakeVaultClient = (): WorkOSVaultClient => {
const byName = new Map<string, WorkOSVaultObject>();
let seq = 0;
const nextId = () => `vault_${++seq}_${crypto.randomUUID().slice(0, 8)}`;

const create = (opts: { name: string; value: string; context: Record<string, string> }) => {
const id = nextId();
const metadata: WorkOSVaultObjectMetadata = {
context: opts.context,
id,
updatedAt: new Date(),
versionId: `v_${seq}`,
};
byName.set(opts.name, { id, name: opts.name, value: opts.value, metadata });
return metadata;
};

const notFound = (name: string) =>
Object.assign(new Error(`not found: ${name}`), { status: 404 });

const read = (name: string): WorkOSVaultObject => {
const obj = byName.get(name);
if (!obj) throw notFound(name);
return obj;
};

const update = (opts: { id: string; value: string }): WorkOSVaultObject => {
for (const [name, obj] of byName.entries()) {
if (obj.id === opts.id) {
const updated: WorkOSVaultObject = {
...obj,
value: opts.value,
metadata: { ...obj.metadata, updatedAt: new Date(), versionId: `v_${++seq}` },
};
byName.set(name, updated);
return updated;
}
}
throw notFound(opts.id);
};

const remove = (opts: { id: string }) => {
for (const [name, obj] of byName.entries()) {
if (obj.id === opts.id) byName.delete(name);
}
};

return {
use: (_op, fn) =>
Effect.tryPromise({
try: () =>
fn({
createObject: async (opts) => create(opts),
readObjectByName: async (name) => read(name),
updateObject: async (opts) => update(opts),
deleteObject: async (opts) => remove(opts),
}),
catch: (cause) =>
new WorkOSVaultClientError({ cause, operation: _op }),
}),
// The real client wraps SDK rejections in WorkOSVaultClientError so
// provider-side `isStatusError` checks can introspect `cause.status`.
// Mirror that here so our 404s flow through the same unwrap path.
createObject: (opts) =>
Effect.try({
try: () => create(opts),
catch: (cause) => new WorkOSVaultClientError({ cause, operation: "create_object" }),
}),
readObjectByName: (name) =>
Effect.try({
try: () => read(name),
catch: (cause) =>
new WorkOSVaultClientError({ cause, operation: "read_object_by_name" }),
}),
updateObject: (opts) =>
Effect.try({
try: () => update(opts),
catch: (cause) => new WorkOSVaultClientError({ cause, operation: "update_object" }),
}),
deleteObject: (opts) =>
Effect.try({
try: () => remove(opts),
catch: (cause) => new WorkOSVaultClientError({ cause, operation: "delete_object" }),
}),
};
};

// ---------------------------------------------------------------------------
// Executor factory — mirrors apps/cloud/services/executor#createScopedExecutor
// but with a fake vault client.
// but with an in-memory test vault client (see
// `@executor-js/plugin-workos-vault/testing`).
// ---------------------------------------------------------------------------

const fakeVault = makeFakeVaultClient();
const fakeVault = makeTestWorkOSVaultClient();
const testPlugins = executorConfig.plugins({ workosVaultClient: fakeVault });

const createTestScopedExecutor = (
Expand Down
9 changes: 8 additions & 1 deletion packages/plugins/workos-vault/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
".": "./src/sdk/index.ts",
"./promise": "./src/promise.ts",
"./react": "./src/react/index.ts",
"./client": "./src/react/plugin-client.tsx"
"./client": "./src/react/plugin-client.tsx",
"./testing": "./src/sdk/testing.ts"
},
"publishConfig": {
"access": "public",
Expand All @@ -35,6 +36,12 @@
"types": "./dist/sdk/index.d.ts",
"default": "./dist/core.js"
}
},
"./testing": {
"import": {
"types": "./dist/sdk/testing.d.ts",
"default": "./dist/testing.js"
}
}
}
},
Expand Down
231 changes: 231 additions & 0 deletions packages/plugins/workos-vault/src/sdk/testing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// In-memory test double for `WorkOSVaultClient`.
//
// Mirrors the Effect-shaped surface of the real client (see ./client.ts) but
// stores objects in a `Map<string, WorkOSVaultObject>` keyed by name so tests
// never hit WorkOS. Errors carry a numeric `status` on `cause` so the
// production `isStatusError` checks in `secret-store.ts` match the same
// 404/409/400 paths the real SDK exercises.

import { Data, Effect } from "effect";

import {
WorkOSVaultClientError,
type WorkOSVaultClient,
type WorkOSVaultObject,
type WorkOSVaultObjectMetadata,
type WorkOSVaultPromiseApi,
} from "./client";

export class TestWorkOSVaultNotFoundError extends Data.TaggedError(
"TestWorkOSVaultNotFoundError",
)<{
readonly message: string;
readonly status: 404;
}> {}

export class TestWorkOSVaultConflictError extends Data.TaggedError(
"TestWorkOSVaultConflictError",
)<{
readonly message: string;
readonly status: 409;
}> {}

export class TestWorkOSVaultInvalidRequestError extends Data.TaggedError(
"TestWorkOSVaultInvalidRequestError",
)<{
readonly message: string;
readonly status: 400;
}> {}

type TestWorkOSVaultError =
| TestWorkOSVaultNotFoundError
| TestWorkOSVaultConflictError
| TestWorkOSVaultInvalidRequestError;

export interface TestWorkOSVaultClientOptions {
/**
* Injects a single 409 on the next update against an object whose name
* ends in `/secrets/conflict`. The retry path in the secret store should
* then re-read and succeed on the second attempt.
*/
readonly conflictOnNextSecretUpdate?: boolean;
/**
* Reject create/read with a 400 when the name contains a colon. Useful
* for exercising the secret store's invalid-name fallback paths.
*/
readonly rejectNamesWithColon?: boolean;
/**
* Reject reads with a 400 when the requested name is longer than this
* threshold. Mirrors WorkOS's own length cap on object names.
*/
readonly rejectReadNamesLongerThan?: number;
}

const notFound = (message: string) =>
new TestWorkOSVaultNotFoundError({ message, status: 404 });

const conflict = (message: string) =>
new TestWorkOSVaultConflictError({ message, status: 409 });

const invalidRequest = (message: string) =>
new TestWorkOSVaultInvalidRequestError({ message, status: 400 });

const makeMetadata = (
id: string,
context: Record<string, string>,
versionId: string,
): WorkOSVaultObjectMetadata => ({
id,
context,
updatedAt: new Date(),
versionId,
});

export const makeTestWorkOSVaultClient = (
options?: TestWorkOSVaultClientOptions,
): WorkOSVaultClient => {
const objects = new Map<string, WorkOSVaultObject>();
let sequence = 0;
let conflictPending = options?.conflictOnNextSecretUpdate ?? false;

const nextId = () =>
`vault_${(sequence += 1)}_${crypto.randomUUID().slice(0, 8)}`;

const validateObjectName = (
name: string,
): Effect.Effect<void, TestWorkOSVaultError> => {
if (options?.rejectNamesWithColon && name.includes(":")) {
return Effect.fail(invalidRequest(`Invalid object name "${name}"`));
}
return Effect.void;
};

const validateReadName = (
name: string,
): Effect.Effect<void, TestWorkOSVaultError> =>
Effect.gen(function* () {
yield* validateObjectName(name);
if (
options?.rejectReadNamesLongerThan !== undefined &&
name.length > options.rejectReadNamesLongerThan
) {
return yield* invalidRequest(`Invalid object name "${name}"`);
}
});

const createObject = (opts: {
readonly name: string;
readonly value: string;
readonly context: Record<string, string>;
}): Effect.Effect<WorkOSVaultObjectMetadata, TestWorkOSVaultError> =>
Effect.gen(function* () {
yield* validateObjectName(opts.name);
if (objects.has(opts.name)) {
return yield* conflict(`Object "${opts.name}" already exists`);
}
const id = nextId();
const metadata = makeMetadata(id, opts.context, `${id}-v1`);
objects.set(opts.name, {
id,
name: opts.name,
value: opts.value,
metadata,
});
return metadata;
});

const readObjectByName = (
name: string,
): Effect.Effect<WorkOSVaultObject, TestWorkOSVaultError> =>
Effect.gen(function* () {
yield* validateReadName(name);
const object = objects.get(name);
if (!object) {
return yield* notFound(`Object "${name}" not found`);
}
return object;
});

const updateObject = (opts: {
readonly id: string;
readonly value: string;
readonly versionCheck?: string;
}): Effect.Effect<WorkOSVaultObject, TestWorkOSVaultError> =>
Effect.gen(function* () {
const current = [...objects.values()].find((o) => o.id === opts.id);
if (!current) {
return yield* notFound(`Object "${opts.id}" not found`);
}
if (conflictPending && current.name.endsWith("/secrets/conflict")) {
conflictPending = false;
return yield* conflict(`Injected conflict for "${opts.id}"`);
}
if (
opts.versionCheck &&
current.metadata.versionId !== opts.versionCheck
) {
return yield* conflict(`Version mismatch for "${opts.id}"`);
}

const nextVersion = current.metadata.versionId.replace(
/v(\d+)$/,
(_, version) => `v${Number(version) + 1}`,
);
const next: WorkOSVaultObject = {
...current,
value: opts.value,
metadata: {
...current.metadata,
updatedAt: new Date(),
versionId: nextVersion,
},
};
objects.set(current.name, next);
return next;
});

const deleteObject = (opts: {
readonly id: string;
}): Effect.Effect<void, TestWorkOSVaultError> =>
Effect.gen(function* () {
const entry = [...objects.entries()].find(
([, object]) => object.id === opts.id,
);
if (!entry) {
return yield* notFound(`Object "${opts.id}" not found`);
}
objects.delete(entry[0]);
});

const wrap = <A>(
operation: string,
effect: Effect.Effect<A, TestWorkOSVaultError>,
): Effect.Effect<A, WorkOSVaultClientError> =>
effect.pipe(
Effect.mapError((cause) => new WorkOSVaultClientError({ cause, operation })),
Effect.withSpan(`workos_vault.test.${operation}`),
);

// Promise-shaped facade exposed to `use` callers, which may be plugin code
// that still calls into the underlying SDK directly via `client.use(...)`.
// Each method runs the in-memory effect and rethrows the tagged error so
// callers see the same `.status` shape they would from a real SDK rejection.
const rawClient: WorkOSVaultPromiseApi = {
createObject: (opts) => Effect.runPromise(createObject(opts)),
readObjectByName: (name) => Effect.runPromise(readObjectByName(name)),
updateObject: (opts) => Effect.runPromise(updateObject(opts)),
deleteObject: (opts) => Effect.runPromise(deleteObject(opts)),
};

return {
use: (operation, fn) =>
Effect.tryPromise({
try: () => fn(rawClient),
catch: (cause) => new WorkOSVaultClientError({ cause, operation }),
}).pipe(Effect.withSpan(`workos_vault.test.${operation}`)),
createObject: (opts) => wrap("create_object", createObject(opts)),
readObjectByName: (name) => wrap("read_object_by_name", readObjectByName(name)),
updateObject: (opts) => wrap("update_object", updateObject(opts)),
deleteObject: (opts) => wrap("delete_object", deleteObject(opts)),
};
};
Loading
Loading