@mnemora/testkit
v1.2.0
Published
adapter(MemoryStore/VectorStore/EventStore の実装)が満たすべき適合テスト一式(conformance suite)。
Readme
@mnemora/testkit
adapter(MemoryStore / VectorStore / EventStore / OutboxStore /
TenantSettingsStore の実装)が満たすべき適合テスト一式(conformance suite)と、
決定的な擬似 LLMProvider / EmbeddingProvider。
インストール
pnpm add -D @mnemora/testkit @mnemora/core vitest
# または
npm i -D @mnemora/testkit @mnemora/core vitest@mnemora/testkit は vitest に依存している(describe/it/expect を内部で呼ぶ)ため、
vitest から実行するコードとして使う。テスト対象の adapter を書く側の devDependency として入れる。
vitest は peerDependencies である(ADR 0066)——このパッケージは vitest を同梱せず、
**使う側が入れた vitest をそのまま使う。**そうしないと、使う側の vitest と
このパッケージが引き込む vitest の2つが node_modules に並び、describe の実体が
食い違って「テストが1本も見つからない」形の壊れ方をしうる。
前提
- Node.js >= 22
- ESM のみ(
"type": "module")。CommonJS からは Node 22.12 以降のrequire(esm)で読み込める(TypeScript はmodule/moduleResolutionをnodenextにし、TypeScript 5.8 以降を使うこと。 5.7 以前のnodenextと、どの版のnode16もTS1479になる。node10は TypeScript 5.x なら パッケージの入口の型を解決できるが、exportsを読まないので@mnemora/testkit/fixturesのような subpath は解決できず、TypeScript 6 で非推奨・7 で廃止された。2026-09-27 に TypeScript 5.0〜7.0 で実測) - 呼び出し側が vitest を使っていること(
describeXxxConformanceは 内部でdescribe/it/expectを呼ぶ)
動く最小の例(実際に vitest で実行して確認済み)
**adapter 作者は、自分の実装を conformance suite に食わせるだけで、テナント分離・
append-only・並び順・外部キー相当の契約などを検査できる。**以下は EventStore を
自作した場合の例(describeEventStoreConformance を使う。他に
describeMemoryStoreConformance / describeVectorStoreConformance /
describeLexicalStoreConformance / describeRelationStoreConformance / describeOutboxStoreConformance /
describeTenantSettingsStoreConformance / describeEmbeddingProviderConformance / describeLLMProviderConformance がある)。
// my-event-store.test.ts
import { randomUUID } from "node:crypto";
import type {
Ctx,
EventFilter,
EventId,
EventStore,
MemoryEvent,
NewMemoryEvent,
} from "@mnemora/core";
import { assertWellFormedCtx } from "@mnemora/core";
import { describeEventStoreConformance } from "@mnemora/testkit";
// 適合テストは「memoryId が実在の Memory を指しているか」(外部キー相当)と、その Memory が
// ctx のテナントのものか(ADR 0436)も検査する。ここでは実際の MemoryStore を持たない最小の例なので、
// prepareMemoryId が登録した id だけを、登録したテナントの Memory として「実在する」ことにする素朴な実装にしてある。
const knownMemoryIds = new Map<string, string>(); // memoryId -> tenantId
class MyEventStore implements EventStore {
private rows: MemoryEvent[] = [];
async append(ctx: Ctx, event: NewMemoryEvent): Promise<MemoryEvent> {
// 孤立サロゲート・NUL を含む識別子は入口で断る(適合テストが検査する約束。ADR 0423)。
assertWellFormedCtx(ctx);
// 実在しない Memory も、別のテナントの Memory も、同じ例外で断る(行は書かない)。
if (event.memoryId !== null && knownMemoryIds.get(event.memoryId) !== ctx.tenantId) {
throw new Error(`MyEventStore: memory not found for tenant: ${event.memoryId}`);
}
// structuredClone で複製して持つ——受け取った入力(meta の配列・オブジェクト)を
// 呼び手が後から書き換えても、store の中身は変わらない(適合テストが検査する約束)。
const row: MemoryEvent = structuredClone({
id: randomUUID(),
at: event.at ?? new Date(),
...event,
});
this.rows.push(row);
return structuredClone(row);
}
async get(ctx: Ctx, id: EventId): Promise<MemoryEvent | null> {
assertWellFormedCtx(ctx);
const row = this.rows.find((row) => row.tenantId === ctx.tenantId && row.id === id);
// 返す値も複製する——呼び手が受け取った値を書き換えても、次の get は影響を受けない。
return row ? structuredClone(row) : null;
}
async list(ctx: Ctx, filter: EventFilter): Promise<MemoryEvent[]> {
assertWellFormedCtx(ctx);
return this.rows
.filter((row) => row.tenantId === ctx.tenantId)
.filter((row) => filter.kind === undefined || row.kind === filter.kind)
.filter((row) => filter.memoryId === undefined || row.memoryId === filter.memoryId)
.filter((row) => filter.since === undefined || row.at >= filter.since)
.filter((row) => filter.until === undefined || row.at <= filter.until)
.sort((a, b) => a.at.getTime() - b.at.getTime())
.slice(0, filter.limit);
}
}
describeEventStoreConformance({
name: "my-event-store",
createStore: () => new MyEventStore(),
prepareMemoryId: async (ctx) => {
const id = randomUUID();
knownMemoryIds.set(id, ctx.tenantId);
return id;
},
});npx vitest run my-event-store.test.ts決定的な擬似 provider
LLMProvider / EmbeddingProvider の本物(@mnemora/openai)を
CI で叩けない(API キーが無い)場合に備えて、決定的な擬似実装を export している。
import { DeterministicLLMProvider, DeterministicEmbeddingProvider } from "@mnemora/testkit";
const llmProvider = new DeterministicLLMProvider();
const embeddingProvider = new DeterministicEmbeddingProvider(); // 既定で 8次元**⚠ これは本物の LLM・埋め込みモデルを模したものではない。**文字コードや文字列長から
機械的に出力を作るだけで、意味的な類似度・言語理解は一切表現しない。配線・契約・
適合テストのための stubであり、想起の質を測る物差しにはならない
(recorded 層・openai 層との違いは ADR 0051 を参照)。
ほかに export しているもの(約束は各 TSDoc)
- 記録を再生する provider(ADR 0051):
RecordedLLMProvider・RecordedEmbeddingProviderはカセット(Cassette)に記録した実 API の応答を再生する。 **記録に無い入力は例外にする。**録る側はCassetteRecorderと、実 provider を包むRecordingLLMProvider・RecordingEmbeddingProvider。カセットの形の検査はassertCassette(CASSETTE_FORMAT_VERSION)、鍵はllmCassetteKey・embeddingCassetteKey。 壊れたカセットは読んだ時点で落ちる(成分が有限でない・embedding.space.dimensionsが正の整数でない・鍵がtext/promptから導いた値と一致しない。 ADR 0452)。CassetteRecorderは、違う埋め込み空間・モデル名の2回目以降の記録を断る。RecordingEmbeddingProviderは、delegate の壊れた戻り(次元違い・有限でない成分)を記録せずに落ち、同じ入力の並列の呼びでも delegate を1回だけ呼ぶ。 - 種カセットから返す provider:
SeededLLMProvider・SeededEmbeddingProvider。種に在る入力は種から返し、 種に無い入力だけ実 provider(delegate)へ流す(Recorded*とは逆の規律)。種のモデル名・埋め込み空間がexpectedModel・expectedSpace(必須)と食い違えば構築時に落ちる(埋め込みはdelegate.spaceとも照合する。ADR 0452)。 - テストデータのひな型:
buildNewMemoryFixture・buildNewObservationFixture・buildNewMemoryEventFixture・buildProvenanceFixture。⚠ 実時計でrecall()を通すなら、recordedAt(必要ならdecayFloorAt)を明示して 渡すこと(既定値のままだと減衰の床を越えて0件になる。buildNewMemoryFixtureの TSDoc)。 - 適合スイートの各 options の型(
MemoryStoreConformanceOptionsなど)は、対応するdescribe*Conformanceの引数。
@mnemora/testkit/fixtures(インメモリの store。適合スイートの入力にしない)
@mnemora/testkit/fixtures は、InMemoryMemoryStore・InMemoryVectorStore・InMemoryLexicalStore・
InMemoryRelationStore・InMemoryEventStore・InMemoryOutboxStore・InMemoryTenantSettingsStore を export する別の入口である。
DB 無しで createRuntime を組み立てて、本物の provider を通しで動かすためにある。
⛔ これを describe*Conformance の createStore に渡してはいけない——自分の adapter を1文字も測らないまま
緑になる(そのため @mnemora/testkit の入口からは export していない)。Postgres が拒む入力を同じく拒むが、
例外の顔は違う(packages/postgres/README.md「例外の見分け方」)。揃えてあるものと揃えていないものの一覧は
src/fixtures.ts の冒頭にある。
もっと詳しく
- docs/architecture.md §5 — 各 interface の契約
- ADR 0051 — provider の3層
(
deterministic/recorded/openai)の使い分け - ADR 0047 — 擬似実装の外部キー相当の扱い
- リポジトリ: https://github.com/takecchi/mnemora
