@retinue/agentkit 0.1.0 → 0.3.0
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/README.md +59 -277
- package/dist/adapters/audio/openai.d.ts +36 -0
- package/dist/adapters/audio/openai.js +136 -0
- package/dist/adapters/bullmq/lock.d.ts +26 -2
- package/dist/adapters/embeddings/openai.d.ts +45 -0
- package/dist/adapters/embeddings/openai.js +109 -0
- package/dist/adapters/memory/connections.d.ts +10 -0
- package/dist/adapters/memory/connections.js +87 -0
- package/dist/adapters/memory/graph.d.ts +17 -0
- package/dist/adapters/memory/graph.js +267 -0
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.js +3 -0
- package/dist/adapters/memory/rate-limit.d.ts +14 -0
- package/dist/adapters/memory/rate-limit.js +28 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/postgres/connections.d.ts +15 -0
- package/dist/adapters/postgres/connections.js +134 -0
- package/dist/adapters/postgres/graph.d.ts +24 -0
- package/dist/adapters/postgres/graph.js +325 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.js +198 -0
- package/dist/adapters/postgres/run-store.js +26 -2
- package/dist/adapters/postgres/schema.d.ts +61 -2
- package/dist/adapters/postgres/schema.js +95 -10
- package/dist/adapters/redis/index.d.ts +1 -0
- package/dist/adapters/redis/index.js +1 -0
- package/dist/adapters/redis/rate-limit.d.ts +46 -0
- package/dist/adapters/redis/rate-limit.js +55 -0
- package/dist/adapters/supabase/index.d.ts +15 -0
- package/dist/adapters/supabase/index.js +15 -0
- package/dist/adapters/supabase/rls.js +33 -0
- package/dist/agents/agent.d.ts +99 -1
- package/dist/agents/agent.js +215 -16
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +48 -0
- package/dist/agents/engine.js +370 -13
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/capabilities/index.d.ts +5 -1
- package/dist/capabilities/index.js +23 -0
- package/dist/capabilities/runtime.d.ts +8 -0
- package/dist/connections/cipher.d.ts +103 -0
- package/dist/connections/cipher.js +141 -0
- package/dist/connections/index.d.ts +156 -0
- package/dist/connections/index.js +72 -0
- package/dist/connections/oauth/client.d.ts +84 -0
- package/dist/connections/oauth/client.js +141 -0
- package/dist/connections/oauth/index.d.ts +166 -0
- package/dist/connections/oauth/index.js +226 -0
- package/dist/connections/oauth/service.d.ts +90 -0
- package/dist/connections/oauth/service.js +132 -0
- package/dist/connections/pause.d.ts +68 -0
- package/dist/connections/pause.js +97 -0
- package/dist/connections/resolver.d.ts +59 -0
- package/dist/connections/resolver.js +138 -0
- package/dist/connections/resume.d.ts +64 -0
- package/dist/connections/resume.js +99 -0
- package/dist/connections/vault-cipher.d.ts +95 -0
- package/dist/connections/vault-cipher.js +206 -0
- package/dist/core/budget.d.ts +55 -0
- package/dist/core/budget.js +56 -0
- package/dist/core/content-parts.d.ts +26 -2
- package/dist/core/content-parts.js +1 -0
- package/dist/core/context.d.ts +25 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +15 -0
- package/dist/core/events.d.ts +110 -2
- package/dist/core/events.js +33 -2
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/validation.js +19 -0
- package/dist/documents/index.d.ts +14 -0
- package/dist/documents/parsers/text.d.ts +16 -0
- package/dist/documents/parsers/text.js +54 -2
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/guardrails.d.ts +14 -0
- package/dist/entries/guardrails.js +14 -0
- package/dist/entries/knowledge.d.ts +17 -0
- package/dist/entries/knowledge.js +16 -0
- package/dist/entries/mcp-server.d.ts +9 -0
- package/dist/entries/mcp-server.js +9 -0
- package/dist/entries/runtime.d.ts +10 -0
- package/dist/entries/runtime.js +8 -0
- package/dist/entries/testing.d.ts +28 -0
- package/dist/entries/testing.js +28 -0
- package/dist/graphql/resolvers.d.ts +7 -1
- package/dist/graphql/resolvers.js +20 -0
- package/dist/graphql/schema.d.ts +1 -1
- package/dist/graphql/schema.js +44 -0
- package/dist/guardrails/index.d.ts +115 -0
- package/dist/guardrails/index.js +108 -0
- package/dist/guardrails/moderation.d.ts +53 -0
- package/dist/guardrails/moderation.js +75 -0
- package/dist/guardrails/pii.d.ts +75 -0
- package/dist/guardrails/pii.js +193 -0
- package/dist/knowledge/communities.d.ts +166 -0
- package/dist/knowledge/communities.js +377 -0
- package/dist/knowledge/graph-global.d.ts +153 -0
- package/dist/knowledge/graph-global.js +246 -0
- package/dist/knowledge/graph-retrieval.d.ts +150 -0
- package/dist/knowledge/graph-retrieval.js +303 -0
- package/dist/knowledge/graph.d.ts +223 -0
- package/dist/knowledge/graph.js +369 -0
- package/dist/knowledge/index.d.ts +25 -0
- package/dist/knowledge/index.js +37 -1
- package/dist/knowledge/navigate.d.ts +89 -0
- package/dist/knowledge/navigate.js +107 -0
- package/dist/knowledge/retrieval.d.ts +98 -5
- package/dist/knowledge/retrieval.js +158 -28
- package/dist/mcp-server/index.d.ts +168 -0
- package/dist/mcp-server/index.js +175 -0
- package/dist/models/extraction.d.ts +73 -0
- package/dist/models/extraction.js +118 -0
- package/dist/models/index.d.ts +40 -1
- package/dist/models/index.js +22 -1
- package/dist/models/pricing.d.ts +2 -0
- package/dist/models/pricing.js +17 -1
- package/dist/models/provider-factory.js +9 -7
- package/dist/models/streaming.d.ts +111 -1
- package/dist/models/streaming.js +184 -3
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +2 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/worker.d.ts +36 -1
- package/dist/runtime/worker.js +37 -4
- package/dist/security/checklist.js +55 -0
- package/dist/security/findings.js +18 -9
- package/dist/server/bin.d.ts +19 -0
- package/dist/server/bin.js +226 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +7 -4
- package/dist/server/doctor.d.ts +74 -0
- package/dist/server/doctor.js +280 -0
- package/dist/skills/catalogue.d.ts +49 -0
- package/dist/skills/catalogue.js +61 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/telemetry/spans.js +17 -0
- package/dist/testing/conformance/artifact-exports.d.ts +20 -0
- package/dist/testing/conformance/artifact-exports.js +172 -0
- package/dist/testing/conformance/artifacts.d.ts +11 -0
- package/dist/testing/conformance/artifacts.js +316 -0
- package/dist/testing/conformance/audio.d.ts +38 -0
- package/dist/testing/conformance/audio.js +145 -0
- package/dist/testing/conformance/capability.d.ts +23 -0
- package/dist/testing/conformance/capability.js +27 -0
- package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
- package/dist/testing/conformance/checkpoint-store.js +72 -0
- package/dist/testing/conformance/connections.d.ts +14 -0
- package/dist/testing/conformance/connections.js +171 -0
- package/dist/testing/conformance/conversation-store.d.ts +10 -0
- package/dist/testing/conformance/conversation-store.js +60 -0
- package/dist/testing/conformance/evaluation.d.ts +10 -0
- package/dist/testing/conformance/evaluation.js +295 -0
- package/dist/testing/conformance/files.d.ts +19 -0
- package/dist/testing/conformance/files.js +454 -0
- package/dist/testing/conformance/flows.d.ts +16 -0
- package/dist/testing/conformance/flows.js +193 -0
- package/dist/testing/conformance/graph.d.ts +22 -0
- package/dist/testing/conformance/graph.js +500 -0
- package/dist/testing/conformance/hitl.d.ts +25 -0
- package/dist/testing/conformance/hitl.js +523 -0
- package/dist/testing/conformance/index.d.ts +163 -0
- package/dist/testing/conformance/index.js +321 -0
- package/dist/testing/conformance/invariants.d.ts +23 -0
- package/dist/testing/conformance/invariants.js +80 -0
- package/dist/testing/conformance/knowledge.d.ts +41 -0
- package/dist/testing/conformance/knowledge.js +592 -0
- package/dist/testing/conformance/parents.d.ts +50 -0
- package/dist/testing/conformance/parents.js +39 -0
- package/dist/testing/conformance/rate-limit.d.ts +25 -0
- package/dist/testing/conformance/rate-limit.js +71 -0
- package/dist/testing/conformance/records.d.ts +36 -0
- package/dist/testing/conformance/records.js +400 -0
- package/dist/testing/conformance/rollups.d.ts +30 -0
- package/dist/testing/conformance/rollups.js +425 -0
- package/dist/testing/conformance/run-coordinator.d.ts +27 -0
- package/dist/testing/conformance/run-coordinator.js +150 -0
- package/dist/testing/conformance/run-event-log.d.ts +9 -0
- package/dist/testing/conformance/run-event-log.js +138 -0
- package/dist/testing/conformance/run-store.d.ts +12 -0
- package/dist/testing/conformance/run-store.js +294 -0
- package/dist/testing/conformance/session-state.d.ts +38 -0
- package/dist/testing/conformance/session-state.js +198 -0
- package/dist/testing/conformance/usage-limits.d.ts +10 -0
- package/dist/testing/conformance/usage-limits.js +309 -0
- package/dist/testing/conformance.d.ts +8 -0
- package/dist/testing/conformance.js +8 -0
- package/dist/testing/memory-backend.d.ts +39 -0
- package/dist/testing/memory-backend.js +41 -0
- package/dist/testing/pglite.d.ts +55 -0
- package/dist/testing/pglite.js +78 -0
- package/dist/testing/stub-model.d.ts +58 -0
- package/dist/testing/stub-model.js +71 -0
- package/dist/testing/supabase-storage-double.d.ts +35 -0
- package/dist/testing/supabase-storage-double.js +101 -0
- package/dist/toolkit/files.d.ts +125 -0
- package/dist/toolkit/files.js +320 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +31 -1
- package/dist/toolkit/index.js +26 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/sandbox.d.ts +119 -0
- package/dist/toolkit/sandbox.js +239 -0
- package/dist/toolkit/ssrf.d.ts +139 -0
- package/dist/toolkit/ssrf.js +349 -0
- package/dist/toolkit/vendor.d.ts +91 -0
- package/dist/toolkit/vendor.js +153 -0
- package/dist/toolkit/web.d.ts +13 -0
- package/dist/toolkit/web.js +7 -1
- package/dist/tools/budget.d.ts +28 -0
- package/dist/tools/budget.js +35 -0
- package/dist/tools/credentials.d.ts +316 -0
- package/dist/tools/credentials.js +369 -0
- package/dist/tools/define.d.ts +45 -0
- package/dist/tools/define.js +26 -0
- package/dist/tools/find.d.ts +109 -0
- package/dist/tools/find.js +210 -0
- package/dist/tools/index.d.ts +35 -2
- package/dist/tools/index.js +11 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/fs.d.ts +24 -0
- package/dist/tools/library/fs.js +102 -0
- package/dist/tools/library/index.d.ts +37 -2
- package/dist/tools/library/index.js +47 -0
- package/dist/tools/library/shell.d.ts +45 -0
- package/dist/tools/library/shell.js +70 -0
- package/dist/tools/meta-tools.js +8 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +250 -4
- package/dist/usage/index.d.ts +21 -0
- package/dist/usage/index.js +1 -0
- package/dist/usage/rate-limit.d.ts +122 -0
- package/dist/usage/rate-limit.js +131 -0
- package/dist/usage/recorder.js +3 -0
- package/package.json +33 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio provider conformance — REQ-062 (#257), task #258, AC-7.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is not in the storage suite
|
|
5
|
+
*
|
|
6
|
+
* AC-7 asks for these ports to be "covered by the conformance suite, consistent with the other 31". The other
|
|
7
|
+
* 31 are the **storage** conformance suite — `conformance/index.ts` says so in its first line, and every
|
|
8
|
+
* harness there is handed a store and asserts persistence semantics: a claim, a lease, a version check, a
|
|
9
|
+
* tenant boundary.
|
|
10
|
+
*
|
|
11
|
+
* A transcription provider is not a store. Adding it to that suite would put a non-storage port in a list whose
|
|
12
|
+
* name and coverage report both say storage, and the next reader would reasonably conclude the audio ports have
|
|
13
|
+
* a durable backing they do not have.
|
|
14
|
+
*
|
|
15
|
+
* So: the same *mechanism* — one shared harness, any adapter must pass it — in its own file, following the
|
|
16
|
+
* precedent `tools-scrape` already set with `CONTRACT_KEYS`. The intent of the AC is that no adapter gets to
|
|
17
|
+
* define its own contract, and that is what this enforces.
|
|
18
|
+
*
|
|
19
|
+
* ## What it can and cannot assert
|
|
20
|
+
*
|
|
21
|
+
* It runs against a **fake** provider by default and against a real one when a caller passes one. That split
|
|
22
|
+
* is deliberate: the contract is about shape and bounds, which a fake can exercise exhaustively and cheaply,
|
|
23
|
+
* while "does Whisper actually transcribe" is a live-call question answered once in `audio-live` rather than on
|
|
24
|
+
* every test run. `#268` is the precedent in the other direction — a *crypto* implementation must be run for
|
|
25
|
+
* real before shipping, because an untested one is worse than none. A transcription adapter that returns the
|
|
26
|
+
* wrong text is visibly wrong; one that silently reports no duration is not, which is why duration is the thing
|
|
27
|
+
* asserted hardest here.
|
|
28
|
+
*/
|
|
29
|
+
import { describe, expect, it } from "vitest";
|
|
30
|
+
import { boundTranscript, MAX_SPEECH_CHARS, MAX_TRANSCRIPT_CHARS } from "../../audio/index.js";
|
|
31
|
+
/** Every key a transcription result must carry. A provider cannot quietly return less. */
|
|
32
|
+
export const TRANSCRIPTION_CONTRACT_KEYS = ["text", "truncated"];
|
|
33
|
+
/** And a speech result. `durationSeconds` is optional on both — absent is a legitimate answer. */
|
|
34
|
+
export const SPEECH_CONTRACT_KEYS = ["audio", "mediaType"];
|
|
35
|
+
/** A tiny valid-looking payload. No provider here decodes it; the fakes do not, and the real one is not run. */
|
|
36
|
+
const SILENCE = new Uint8Array([0xff, 0xfb, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
|
37
|
+
export function transcriptionProviderConformance(make) {
|
|
38
|
+
describe("TranscriptionProvider conformance", () => {
|
|
39
|
+
it("names itself, so a usage record can say which provider ran", () => {
|
|
40
|
+
expect(make().id).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
it("returns every contract key", async () => {
|
|
43
|
+
const result = await make().transcribe({ audio: SILENCE, mediaType: "audio/mpeg" });
|
|
44
|
+
for (const key of TRANSCRIPTION_CONTRACT_KEYS) {
|
|
45
|
+
expect(result, `missing ${key}`).toHaveProperty(key);
|
|
46
|
+
}
|
|
47
|
+
expect(typeof result.text).toBe("string");
|
|
48
|
+
expect(typeof result.truncated).toBe("boolean");
|
|
49
|
+
});
|
|
50
|
+
it("reports a duration, because usage is charged on it", async () => {
|
|
51
|
+
/**
|
|
52
|
+
* The assertion that matters most, and the one a provider is most likely to fail quietly.
|
|
53
|
+
*
|
|
54
|
+
* Audio is billed per second. A provider that omits `durationSeconds` makes every transcription cost
|
|
55
|
+
* **nothing** in the ledger — under-billing that scales with usage and shows up as a revenue shortfall
|
|
56
|
+
* rather than as a bug. The OpenAI adapter asks for `verbose_json` specifically to get this field.
|
|
57
|
+
*
|
|
58
|
+
* `undefined` is permitted by the type, because some providers genuinely do not report it — but a
|
|
59
|
+
* provider claiming conformance has to state which it is rather than leaving a caller to find out from an
|
|
60
|
+
* invoice.
|
|
61
|
+
*/
|
|
62
|
+
const result = await make().transcribe({ audio: SILENCE, mediaType: "audio/mpeg" });
|
|
63
|
+
if (result.durationSeconds === undefined) {
|
|
64
|
+
// Made loud on purpose: this is a costing decision, not a detail.
|
|
65
|
+
throw new Error("this provider reports no durationSeconds, so its transcriptions will be billed as free. If that is " +
|
|
66
|
+
"correct for it, exclude this case explicitly and say why.");
|
|
67
|
+
}
|
|
68
|
+
expect(result.durationSeconds).toBeGreaterThan(0);
|
|
69
|
+
});
|
|
70
|
+
it("refuses a media type it does not accept, rather than sending it", async () => {
|
|
71
|
+
// The useful refusal happens locally: a provider answers an unknown container with a message about the
|
|
72
|
+
// request, not about the format.
|
|
73
|
+
await expect(make().transcribe({ audio: SILENCE, mediaType: "application/zip" })).rejects.toThrow();
|
|
74
|
+
});
|
|
75
|
+
it("bounds the transcript and reports the truncation", async () => {
|
|
76
|
+
const provider = make();
|
|
77
|
+
const result = await provider.transcribe({
|
|
78
|
+
audio: SILENCE,
|
|
79
|
+
mediaType: "audio/mpeg",
|
|
80
|
+
maxTranscriptChars: 20,
|
|
81
|
+
});
|
|
82
|
+
expect(result.text.length).toBeLessThanOrEqual(20);
|
|
83
|
+
// If it was cut, it must say so. A shortened transcript claiming to be whole would have a model
|
|
84
|
+
// summarise a fragment as if it were the entire recording.
|
|
85
|
+
if (result.text.length === 20)
|
|
86
|
+
expect(result.truncated).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function speechProviderConformance(make) {
|
|
91
|
+
describe("SpeechProvider conformance", () => {
|
|
92
|
+
it("names itself", () => {
|
|
93
|
+
expect(make().id).toBeTruthy();
|
|
94
|
+
});
|
|
95
|
+
it("returns every contract key, with audio as bytes", async () => {
|
|
96
|
+
const result = await make().speak({ text: "hello" });
|
|
97
|
+
for (const key of SPEECH_CONTRACT_KEYS) {
|
|
98
|
+
expect(result, `missing ${key}`).toHaveProperty(key);
|
|
99
|
+
}
|
|
100
|
+
expect(result.audio).toBeInstanceOf(Uint8Array);
|
|
101
|
+
expect(result.audio.byteLength).toBeGreaterThan(0);
|
|
102
|
+
expect(result.mediaType).toMatch(/^audio\//);
|
|
103
|
+
});
|
|
104
|
+
it("refuses empty text rather than producing silence", async () => {
|
|
105
|
+
// A zero-length recording is a bill and a file for nothing, and it reads to a user as a broken player.
|
|
106
|
+
await expect(make().speak({ text: " " })).rejects.toThrow();
|
|
107
|
+
});
|
|
108
|
+
it("refuses text over the ceiling before the call", async () => {
|
|
109
|
+
// Speech is billed per character, so a runaway prompt is a runaway bill. Refused locally, not by the
|
|
110
|
+
// provider, because the provider's refusal costs the round trip.
|
|
111
|
+
await expect(make().speak({ text: "a".repeat(MAX_SPEECH_CHARS + 1) })).rejects.toThrow();
|
|
112
|
+
});
|
|
113
|
+
it("honours the requested format in the media type it reports", async () => {
|
|
114
|
+
const result = await make().speak({ text: "hello", format: "wav" });
|
|
115
|
+
// Reported, not assumed: a caller storing this as an artifact writes the media type into the file record,
|
|
116
|
+
// and a wrong one makes the browser refuse to play a file that is perfectly good.
|
|
117
|
+
expect(result.mediaType).toBe("audio/wav");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** The transcript bounding, on its own — shared by both suites and worth asserting once. */
|
|
122
|
+
export function transcriptBoundingConformance() {
|
|
123
|
+
describe("transcript bounding", () => {
|
|
124
|
+
it("leaves a short transcript alone", () => {
|
|
125
|
+
expect(boundTranscript("short")).toEqual({ text: "short", truncated: false });
|
|
126
|
+
});
|
|
127
|
+
it("cuts at a word boundary when one is near the ceiling", () => {
|
|
128
|
+
const text = `${"word ".repeat(20)}finalword`;
|
|
129
|
+
const bounded = boundTranscript(text, 50);
|
|
130
|
+
expect(bounded.truncated).toBe(true);
|
|
131
|
+
// Not mid-word: a sliced word reads as a transcription error rather than as a truncation.
|
|
132
|
+
expect(bounded.text.endsWith(" ")).toBe(false);
|
|
133
|
+
expect(text.startsWith(bounded.text)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
it("still cuts when there is no word boundary to use", () => {
|
|
136
|
+
const bounded = boundTranscript("a".repeat(200), 50);
|
|
137
|
+
expect(bounded.text).toHaveLength(50);
|
|
138
|
+
expect(bounded.truncated).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
it("defaults to the documented ceiling", () => {
|
|
141
|
+
expect(boundTranscript("x".repeat(MAX_TRANSCRIPT_CHARS + 10)).text).toHaveLength(MAX_TRANSCRIPT_CHARS);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=audio.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability gating for the conformance suite — `docs/02` → capability declarations.
|
|
3
|
+
*
|
|
4
|
+
* A harness block may be skipped **only** when the adapter declares the capability absent via
|
|
5
|
+
* `AdapterCapability`. An unconditional `it.skip` is not permitted: a silent skip is how a suite
|
|
6
|
+
* comes to look green while verifying nothing, which is exactly the failure #91 exists to close.
|
|
7
|
+
*
|
|
8
|
+
* `gatedIt` therefore always registers a test. When the capability is missing it registers a
|
|
9
|
+
* *passing* test whose name carries the declared reason, so the skip is visible in the report
|
|
10
|
+
* rather than absent from it.
|
|
11
|
+
*/
|
|
12
|
+
import type { AdapterCapability } from "../../persistence/index.js";
|
|
13
|
+
/** What an adapter tells the suite about itself. Absent ⇒ treated as "declares nothing". */
|
|
14
|
+
export type AdapterDeclaration = {
|
|
15
|
+
readonly capabilities?: readonly AdapterCapability[];
|
|
16
|
+
};
|
|
17
|
+
export declare const declares: (declaration: AdapterDeclaration | undefined, capability: AdapterCapability) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Register a test that runs only when the adapter declares `capability`. When it does not, the
|
|
20
|
+
* test still appears in the report, named with the reason — a visible, attributable skip.
|
|
21
|
+
*/
|
|
22
|
+
export declare const gatedIt: (declaration: AdapterDeclaration | undefined, capability: AdapterCapability, name: string, fn: () => Promise<void> | void) => void;
|
|
23
|
+
//# sourceMappingURL=capability.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability gating for the conformance suite — `docs/02` → capability declarations.
|
|
3
|
+
*
|
|
4
|
+
* A harness block may be skipped **only** when the adapter declares the capability absent via
|
|
5
|
+
* `AdapterCapability`. An unconditional `it.skip` is not permitted: a silent skip is how a suite
|
|
6
|
+
* comes to look green while verifying nothing, which is exactly the failure #91 exists to close.
|
|
7
|
+
*
|
|
8
|
+
* `gatedIt` therefore always registers a test. When the capability is missing it registers a
|
|
9
|
+
* *passing* test whose name carries the declared reason, so the skip is visible in the report
|
|
10
|
+
* rather than absent from it.
|
|
11
|
+
*/
|
|
12
|
+
import { it } from "vitest";
|
|
13
|
+
export const declares = (declaration, capability) => (declaration?.capabilities ?? []).includes(capability);
|
|
14
|
+
/**
|
|
15
|
+
* Register a test that runs only when the adapter declares `capability`. When it does not, the
|
|
16
|
+
* test still appears in the report, named with the reason — a visible, attributable skip.
|
|
17
|
+
*/
|
|
18
|
+
export const gatedIt = (declaration, capability, name, fn) => {
|
|
19
|
+
if (declares(declaration, capability)) {
|
|
20
|
+
it(name, fn);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
it(`${name} [skipped: adapter does not declare "${capability}"]`, () => {
|
|
24
|
+
// Intentionally empty: the assertion is the printed reason. See the module docstring.
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=capability.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CheckpointStore` conformance — `docs/04` → durable execution. The port documents `save` as
|
|
3
|
+
* **monotonic**: "a save with a lower sequence is ignored". That is what stops a slow in-flight
|
|
4
|
+
* write from rewinding a recovered run, so every adapter must agree on it.
|
|
5
|
+
*
|
|
6
|
+
* **Referential integrity (#95).** A checkpoint belongs to a run, and an adapter is entitled to
|
|
7
|
+
* enforce that — the Postgres schema does, with a foreign key, because an orphan checkpoint is
|
|
8
|
+
* meaningless. The in-memory adapter holds no such constraint. The fixture may therefore supply
|
|
9
|
+
* `seedRun`, via the shared `parents` helper (#96 generalised what started here); adapters without
|
|
10
|
+
* referential integrity omit it and nothing changes for them.
|
|
11
|
+
*/
|
|
12
|
+
import type { CheckpointStore } from "../../persistence/index.js";
|
|
13
|
+
import { type FixtureOrStore } from "./parents.js";
|
|
14
|
+
export declare function checkpointStoreConformance(makeFixture: () => FixtureOrStore<CheckpointStore>): void;
|
|
15
|
+
//# sourceMappingURL=checkpoint-store.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `CheckpointStore` conformance — `docs/04` → durable execution. The port documents `save` as
|
|
3
|
+
* **monotonic**: "a save with a lower sequence is ignored". That is what stops a slow in-flight
|
|
4
|
+
* write from rewinding a recovered run, so every adapter must agree on it.
|
|
5
|
+
*
|
|
6
|
+
* **Referential integrity (#95).** A checkpoint belongs to a run, and an adapter is entitled to
|
|
7
|
+
* enforce that — the Postgres schema does, with a foreign key, because an orphan checkpoint is
|
|
8
|
+
* meaningless. The in-memory adapter holds no such constraint. The fixture may therefore supply
|
|
9
|
+
* `seedRun`, via the shared `parents` helper (#96 generalised what started here); adapters without
|
|
10
|
+
* referential integrity omit it and nothing changes for them.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { asId } from "../../core/ids.js";
|
|
14
|
+
import { emptyCheckpoint } from "../../runtime/checkpoint.js";
|
|
15
|
+
import { withRun } from "./parents.js";
|
|
16
|
+
const T1 = asId("conf-tenant-1");
|
|
17
|
+
const T2 = asId("conf-tenant-2");
|
|
18
|
+
const RUN = asId("conf-run-1");
|
|
19
|
+
const at = (runId, sequence, step = 0) => ({
|
|
20
|
+
...emptyCheckpoint(runId, `2020-01-01T00:00:${String(sequence).padStart(2, "0")}.000Z`),
|
|
21
|
+
sequence,
|
|
22
|
+
step,
|
|
23
|
+
});
|
|
24
|
+
export function checkpointStoreConformance(makeFixture) {
|
|
25
|
+
const open = (runIds = [{ tenantId: T1, runId: RUN }]) => withRun(makeFixture(), runIds);
|
|
26
|
+
describe("CheckpointStore conformance", () => {
|
|
27
|
+
it("returns null before anything is saved", async () => {
|
|
28
|
+
const store = await open();
|
|
29
|
+
expect(await store.latest({ tenantId: T1, runId: RUN })).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
it("saves and returns the checkpoint", async () => {
|
|
32
|
+
const store = await open();
|
|
33
|
+
await store.save({ tenantId: T1, checkpoint: at(RUN, 5, 2) });
|
|
34
|
+
expect(await store.latest({ tenantId: T1, runId: RUN })).toMatchObject({ sequence: 5, step: 2 });
|
|
35
|
+
});
|
|
36
|
+
it("advances on a higher sequence", async () => {
|
|
37
|
+
const store = await open();
|
|
38
|
+
await store.save({ tenantId: T1, checkpoint: at(RUN, 5) });
|
|
39
|
+
await store.save({ tenantId: T1, checkpoint: at(RUN, 9) });
|
|
40
|
+
expect((await store.latest({ tenantId: T1, runId: RUN }))?.sequence).toBe(9);
|
|
41
|
+
});
|
|
42
|
+
it("ignores a save with a lower sequence — monotonic, so recovery never rewinds", async () => {
|
|
43
|
+
const store = await open();
|
|
44
|
+
await store.save({ tenantId: T1, checkpoint: at(RUN, 9) });
|
|
45
|
+
await store.save({ tenantId: T1, checkpoint: at(RUN, 4) });
|
|
46
|
+
expect((await store.latest({ tenantId: T1, runId: RUN }))?.sequence).toBe(9);
|
|
47
|
+
});
|
|
48
|
+
it("treats an equal sequence as a no-op rather than an error", async () => {
|
|
49
|
+
const store = await open();
|
|
50
|
+
await store.save({ tenantId: T1, checkpoint: at(RUN, 7, 1) });
|
|
51
|
+
await store.save({ tenantId: T1, checkpoint: at(RUN, 7, 1) });
|
|
52
|
+
expect((await store.latest({ tenantId: T1, runId: RUN }))?.sequence).toBe(7);
|
|
53
|
+
});
|
|
54
|
+
it("round-trips pendingToolCalls, which recovery reconciles against", async () => {
|
|
55
|
+
const store = await open();
|
|
56
|
+
const checkpoint = {
|
|
57
|
+
...at(RUN, 3),
|
|
58
|
+
pendingToolCalls: [{ toolCallId: asId("tc1"), toolName: "search_web", startedAt: "2020-01-01T00:00:03.000Z" }],
|
|
59
|
+
};
|
|
60
|
+
await store.save({ tenantId: T1, checkpoint });
|
|
61
|
+
const latest = await store.latest({ tenantId: T1, runId: RUN });
|
|
62
|
+
expect(latest?.pendingToolCalls).toHaveLength(1);
|
|
63
|
+
expect(latest?.pendingToolCalls[0]?.toolName).toBe("search_web");
|
|
64
|
+
});
|
|
65
|
+
it("enforces tenant isolation", async () => {
|
|
66
|
+
const store = await open();
|
|
67
|
+
await store.save({ tenantId: T1, checkpoint: at(RUN, 5) });
|
|
68
|
+
expect(await store.latest({ tenantId: T2, runId: RUN })).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=checkpoint-store.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ConnectionStore` conformance — REQ-063 (#259), task #261.
|
|
3
|
+
*
|
|
4
|
+
* Every clause here is a defect somebody would otherwise ship, and two of them are the reason the port exists
|
|
5
|
+
* at all: a store that leaked across tenants would hand one customer another's credential, and a store that
|
|
6
|
+
* kept a revoked connection readable would let a withdrawn credential be used once more.
|
|
7
|
+
*
|
|
8
|
+
* The store never sees plaintext, so this suite never supplies any: it seals nothing and asserts nothing about
|
|
9
|
+
* encryption. That belongs to `SecretCipher`'s own tests and to the raw-row assertion, which needs a real
|
|
10
|
+
* database and lives with the adapter.
|
|
11
|
+
*/
|
|
12
|
+
import type { ConnectionStore } from "../../connections/index.js";
|
|
13
|
+
export declare function connectionStoreConformance(makeStore: () => ConnectionStore | Promise<ConnectionStore>): void;
|
|
14
|
+
//# sourceMappingURL=connections.d.ts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ConnectionStore` conformance — REQ-063 (#259), task #261.
|
|
3
|
+
*
|
|
4
|
+
* Every clause here is a defect somebody would otherwise ship, and two of them are the reason the port exists
|
|
5
|
+
* at all: a store that leaked across tenants would hand one customer another's credential, and a store that
|
|
6
|
+
* kept a revoked connection readable would let a withdrawn credential be used once more.
|
|
7
|
+
*
|
|
8
|
+
* The store never sees plaintext, so this suite never supplies any: it seals nothing and asserts nothing about
|
|
9
|
+
* encryption. That belongs to `SecretCipher`'s own tests and to the raw-row assertion, which needs a real
|
|
10
|
+
* database and lives with the adapter.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { asId } from "../../core/ids.js";
|
|
14
|
+
const sealed = (marker) => ({
|
|
15
|
+
keyId: "k1",
|
|
16
|
+
algorithm: "aes-256-gcm",
|
|
17
|
+
nonce: Buffer.alloc(12, 1).toString("base64"),
|
|
18
|
+
ciphertext: Buffer.from(`sealed-${marker}`).toString("base64"),
|
|
19
|
+
});
|
|
20
|
+
const connection = (over) => ({
|
|
21
|
+
provider: "github",
|
|
22
|
+
mode: "token",
|
|
23
|
+
scheme: "bearer",
|
|
24
|
+
sealed: sealed(over.id),
|
|
25
|
+
...over,
|
|
26
|
+
});
|
|
27
|
+
export function connectionStoreConformance(makeStore) {
|
|
28
|
+
describe("ConnectionStore conformance", () => {
|
|
29
|
+
const T1 = asId("conn-t1");
|
|
30
|
+
const T2 = asId("conn-t2");
|
|
31
|
+
it("creates and reads back, scoped to its tenant", async () => {
|
|
32
|
+
const store = await makeStore();
|
|
33
|
+
await store.create({ tenantId: T1, connection: connection({ id: "c1", label: "Acme org" }) });
|
|
34
|
+
const read = await store.get({ tenantId: T1, id: "c1" });
|
|
35
|
+
expect(read).toMatchObject({ id: "c1", provider: "github", label: "Acme org", scheme: "bearer" });
|
|
36
|
+
expect(read?.sealed.ciphertext).toBe(sealed("c1").ciphertext);
|
|
37
|
+
expect(read?.createdAt).toBeTruthy();
|
|
38
|
+
});
|
|
39
|
+
it("returns null for another tenant's connection", async () => {
|
|
40
|
+
// Indistinguishable from absent, so the store cannot be used to probe which ids exist — and, far worse,
|
|
41
|
+
// cannot hand T2 a credential belonging to T1.
|
|
42
|
+
const store = await makeStore();
|
|
43
|
+
await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
|
|
44
|
+
expect(await store.get({ tenantId: T2, id: "c1" })).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
it("lists only this tenant's live connections", async () => {
|
|
47
|
+
const store = await makeStore();
|
|
48
|
+
await store.create({ tenantId: T1, connection: connection({ id: "a" }) });
|
|
49
|
+
await store.create({ tenantId: T2, connection: connection({ id: "b" }) });
|
|
50
|
+
expect((await store.list({ tenantId: T1 })).map((c) => c.id)).toEqual(["a"]);
|
|
51
|
+
});
|
|
52
|
+
it("allows several connections to one provider for one tenant", async () => {
|
|
53
|
+
// A customer with three GitHub organisations is the normal case, not an edge one.
|
|
54
|
+
const store = await makeStore();
|
|
55
|
+
await store.create({ tenantId: T1, connection: connection({ id: "org-a", label: "A" }) });
|
|
56
|
+
await store.create({ tenantId: T1, connection: connection({ id: "org-b", label: "B" }) });
|
|
57
|
+
expect((await store.list({ tenantId: T1, provider: "github" })).map((c) => c.id)).toEqual(["org-a", "org-b"]);
|
|
58
|
+
});
|
|
59
|
+
it("narrows a listing by provider", async () => {
|
|
60
|
+
const store = await makeStore();
|
|
61
|
+
await store.create({ tenantId: T1, connection: connection({ id: "gh" }) });
|
|
62
|
+
await store.create({ tenantId: T1, connection: connection({ id: "sl", provider: "slack" }) });
|
|
63
|
+
expect((await store.list({ tenantId: T1, provider: "slack" })).map((c) => c.id)).toEqual(["sl"]);
|
|
64
|
+
});
|
|
65
|
+
it("orders a listing stably, so a default connection does not move", async () => {
|
|
66
|
+
// The resolver's default is "the oldest", which is only stable if the order is. Newest-first would
|
|
67
|
+
// silently re-point every agent the moment somebody connects a second account.
|
|
68
|
+
const store = await makeStore();
|
|
69
|
+
for (const id of ["first", "second", "third"]) {
|
|
70
|
+
await store.create({ tenantId: T1, connection: connection({ id }) });
|
|
71
|
+
}
|
|
72
|
+
const once = (await store.list({ tenantId: T1 })).map((c) => c.id);
|
|
73
|
+
const twice = (await store.list({ tenantId: T1 })).map((c) => c.id);
|
|
74
|
+
expect(once).toEqual(twice);
|
|
75
|
+
expect(once[0]).toBe("first");
|
|
76
|
+
});
|
|
77
|
+
it("refuses to create the same id twice rather than overwriting", async () => {
|
|
78
|
+
// Silently overwriting is the worst resolution of a duplicate: it replaces a working credential with
|
|
79
|
+
// whatever the second caller had.
|
|
80
|
+
const store = await makeStore();
|
|
81
|
+
await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
|
|
82
|
+
await expect(store.create({ tenantId: T1, connection: connection({ id: "c1" }) })).rejects.toThrow();
|
|
83
|
+
});
|
|
84
|
+
it("updates the secret, which is what a refresh does", async () => {
|
|
85
|
+
const store = await makeStore();
|
|
86
|
+
await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
|
|
87
|
+
const updated = await store.update({ tenantId: T1, id: "c1", patch: { sealed: sealed("rotated") } });
|
|
88
|
+
expect(updated.sealed.ciphertext).toBe(sealed("rotated").ciphertext);
|
|
89
|
+
expect((await store.get({ tenantId: T1, id: "c1" }))?.sealed.ciphertext).toBe(sealed("rotated").ciphertext);
|
|
90
|
+
});
|
|
91
|
+
it("leaves unpatched fields alone", async () => {
|
|
92
|
+
// An absent field means "leave it", not "set it to null" — otherwise a label-only rename would wipe the
|
|
93
|
+
// credential.
|
|
94
|
+
const store = await makeStore();
|
|
95
|
+
await store.create({
|
|
96
|
+
tenantId: T1,
|
|
97
|
+
connection: connection({ id: "c1", label: "before", metadata: { cloudId: "x" } }),
|
|
98
|
+
});
|
|
99
|
+
const updated = await store.update({ tenantId: T1, id: "c1", patch: { label: "after" } });
|
|
100
|
+
expect(updated.label).toBe("after");
|
|
101
|
+
expect(updated.metadata?.cloudId).toBe("x");
|
|
102
|
+
expect(updated.sealed.ciphertext).toBe(sealed("c1").ciphertext);
|
|
103
|
+
});
|
|
104
|
+
it("refuses to update another tenant's connection", async () => {
|
|
105
|
+
const store = await makeStore();
|
|
106
|
+
await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
|
|
107
|
+
await expect(store.update({ tenantId: T2, id: "c1", patch: { label: "mine now" } })).rejects.toThrow();
|
|
108
|
+
});
|
|
109
|
+
it("makes a revoked connection unreadable, while keeping the row", async () => {
|
|
110
|
+
// The audit trail survives — "who connected this and when was it removed" is a question a security review
|
|
111
|
+
// asks — and the credential does not resolve.
|
|
112
|
+
const store = await makeStore();
|
|
113
|
+
await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
|
|
114
|
+
await store.revoke({ tenantId: T1, id: "c1" });
|
|
115
|
+
expect(await store.get({ tenantId: T1, id: "c1" })).toBeNull();
|
|
116
|
+
expect(await store.list({ tenantId: T1 })).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
it("treats revoking twice as success", async () => {
|
|
119
|
+
// A retried disconnect should not have to distinguish "already gone" from "failed".
|
|
120
|
+
const store = await makeStore();
|
|
121
|
+
await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
|
|
122
|
+
await store.revoke({ tenantId: T1, id: "c1" });
|
|
123
|
+
await expect(store.revoke({ tenantId: T1, id: "c1" })).resolves.toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
it("refuses to revoke another tenant's connection", async () => {
|
|
126
|
+
const store = await makeStore();
|
|
127
|
+
await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
|
|
128
|
+
await expect(store.revoke({ tenantId: T2, id: "c1" })).rejects.toThrow();
|
|
129
|
+
});
|
|
130
|
+
it("refuses to update a revoked connection", async () => {
|
|
131
|
+
const store = await makeStore();
|
|
132
|
+
await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
|
|
133
|
+
await store.revoke({ tenantId: T1, id: "c1" });
|
|
134
|
+
await expect(store.update({ tenantId: T1, id: "c1", patch: { label: "back" } })).rejects.toThrow();
|
|
135
|
+
});
|
|
136
|
+
it("purges one tenant's connections and only that tenant's", async () => {
|
|
137
|
+
// `docs/18`. A soft-deleted credential is still a credential, so deletion has to be real — and it has to
|
|
138
|
+
// stop at the tenant boundary, or a retention job for one customer removes another's access.
|
|
139
|
+
const store = await makeStore();
|
|
140
|
+
await store.create({ tenantId: T1, connection: connection({ id: "a" }) });
|
|
141
|
+
await store.create({ tenantId: T1, connection: connection({ id: "b" }) });
|
|
142
|
+
await store.create({ tenantId: T2, connection: connection({ id: "c" }) });
|
|
143
|
+
expect(await store.purge({ tenantId: T1 })).toBe(2);
|
|
144
|
+
expect(await store.list({ tenantId: T1 })).toEqual([]);
|
|
145
|
+
expect((await store.list({ tenantId: T2 })).map((x) => x.id)).toEqual(["c"]);
|
|
146
|
+
});
|
|
147
|
+
it("purges revoked connections too, since a retention promise covers them", async () => {
|
|
148
|
+
const store = await makeStore();
|
|
149
|
+
await store.create({ tenantId: T1, connection: connection({ id: "a" }) });
|
|
150
|
+
await store.revoke({ tenantId: T1, id: "a" });
|
|
151
|
+
expect(await store.purge({ tenantId: T1 })).toBe(1);
|
|
152
|
+
});
|
|
153
|
+
it("round-trips metadata and granted scopes without reshaping them", async () => {
|
|
154
|
+
// Both are read on the request path — the cloud id goes into the URL, the scopes decide whether to tell
|
|
155
|
+
// somebody to reconnect — so a store that dropped or reordered them would break the call, not the listing.
|
|
156
|
+
const store = await makeStore();
|
|
157
|
+
await store.create({
|
|
158
|
+
tenantId: T1,
|
|
159
|
+
connection: connection({
|
|
160
|
+
id: "c1",
|
|
161
|
+
metadata: { cloudId: "abc", username: "me@example.com" },
|
|
162
|
+
grantedScopes: ["read:issues", "write:issues"],
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
const read = await store.get({ tenantId: T1, id: "c1" });
|
|
166
|
+
expect(read?.metadata).toEqual({ cloudId: "abc", username: "me@example.com" });
|
|
167
|
+
expect(read?.grantedScopes).toEqual(["read:issues", "write:issues"]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=connections.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared storage conformance harness — `docs/02` "Conformance suite".
|
|
3
|
+
*
|
|
4
|
+
* Every `ConversationStore` adapter (memory today, Postgres/Supabase later) must pass the same
|
|
5
|
+
* suite, so "swap the database" is provably safe. Adapter packages call this from a test file.
|
|
6
|
+
* Not part of the published build (excluded in tsconfig); imported by tests only.
|
|
7
|
+
*/
|
|
8
|
+
import type { ConversationStore } from "../../persistence/index.js";
|
|
9
|
+
export declare function conversationStoreConformance(makeStore: () => ConversationStore): void;
|
|
10
|
+
//# sourceMappingURL=conversation-store.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared storage conformance harness — `docs/02` "Conformance suite".
|
|
3
|
+
*
|
|
4
|
+
* Every `ConversationStore` adapter (memory today, Postgres/Supabase later) must pass the same
|
|
5
|
+
* suite, so "swap the database" is provably safe. Adapter packages call this from a test file.
|
|
6
|
+
* Not part of the published build (excluded in tsconfig); imported by tests only.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
export function conversationStoreConformance(makeStore) {
|
|
10
|
+
const t1 = "tenant-1";
|
|
11
|
+
const t2 = "tenant-2";
|
|
12
|
+
const cid = (s) => s;
|
|
13
|
+
describe("ConversationStore conformance", () => {
|
|
14
|
+
it("create then findById returns the row, scoped to its tenant", async () => {
|
|
15
|
+
const store = makeStore();
|
|
16
|
+
const created = await store.create({ tenantId: t1, id: cid("c1"), title: "Hello" });
|
|
17
|
+
expect(created).toMatchObject({ id: "c1", tenantId: t1, title: "Hello", version: 1 });
|
|
18
|
+
expect(await store.findById({ tenantId: t1, id: cid("c1") })).toMatchObject({ id: "c1" });
|
|
19
|
+
});
|
|
20
|
+
it("enforces tenant isolation", async () => {
|
|
21
|
+
const store = makeStore();
|
|
22
|
+
await store.create({ tenantId: t1, id: cid("c1"), title: "t1 only" });
|
|
23
|
+
expect(await store.findById({ tenantId: t2, id: cid("c1") })).toBeNull();
|
|
24
|
+
expect((await store.list({ tenantId: t2, limit: 10 })).items).toHaveLength(0);
|
|
25
|
+
});
|
|
26
|
+
it("rejects a duplicate id", async () => {
|
|
27
|
+
const store = makeStore();
|
|
28
|
+
await store.create({ tenantId: t1, id: cid("c1"), title: "a" });
|
|
29
|
+
await expect(store.create({ tenantId: t1, id: cid("c1"), title: "b" })).rejects.toThrow();
|
|
30
|
+
});
|
|
31
|
+
it("paginates by stable cursor", async () => {
|
|
32
|
+
const store = makeStore();
|
|
33
|
+
for (const n of [1, 2, 3, 4, 5])
|
|
34
|
+
await store.create({ tenantId: t1, id: cid(`c${n}`), title: `#${n}` });
|
|
35
|
+
const first = await store.list({ tenantId: t1, limit: 2 });
|
|
36
|
+
expect(first.items).toHaveLength(2);
|
|
37
|
+
expect(first.nextCursor).toBeDefined();
|
|
38
|
+
const second = await store.list({ tenantId: t1, limit: 2, cursor: first.nextCursor });
|
|
39
|
+
expect(second.items).toHaveLength(2);
|
|
40
|
+
// no overlap between pages
|
|
41
|
+
const ids = new Set(first.items.map((c) => c.id));
|
|
42
|
+
expect(second.items.some((c) => ids.has(c.id))).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it("optimistic concurrency: a stale expectedVersion is rejected", async () => {
|
|
45
|
+
const store = makeStore();
|
|
46
|
+
await store.create({ tenantId: t1, id: cid("c1"), title: "v1" });
|
|
47
|
+
const updated = await store.update({ tenantId: t1, id: cid("c1"), expectedVersion: 1, patch: { title: "v2" } });
|
|
48
|
+
expect(updated.version).toBe(2);
|
|
49
|
+
await expect(store.update({ tenantId: t1, id: cid("c1"), expectedVersion: 1, patch: { title: "stale" } })).rejects.toThrow();
|
|
50
|
+
});
|
|
51
|
+
it("soft delete hides the row from findById and list", async () => {
|
|
52
|
+
const store = makeStore();
|
|
53
|
+
await store.create({ tenantId: t1, id: cid("c1"), title: "bye" });
|
|
54
|
+
await store.softDelete({ tenantId: t1, id: cid("c1") });
|
|
55
|
+
expect(await store.findById({ tenantId: t1, id: cid("c1") })).toBeNull();
|
|
56
|
+
expect((await store.list({ tenantId: t1, limit: 10 })).items).toHaveLength(0);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=conversation-store.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EvaluationStore` conformance (#141).
|
|
3
|
+
*
|
|
4
|
+
* The cases are mostly about *aggregates agreeing with their evidence* and about *idempotency*, because those
|
|
5
|
+
* are what a release gate depends on: a mean that disagrees with its case rows is a number nobody can defend,
|
|
6
|
+
* and a double-counted case makes the gate pass or fail for the wrong reason.
|
|
7
|
+
*/
|
|
8
|
+
import type { EvaluationStore } from "../../persistence/index.js";
|
|
9
|
+
export declare function evaluationStoreConformance(make: () => EvaluationStore | Promise<EvaluationStore>): void;
|
|
10
|
+
//# sourceMappingURL=evaluation.d.ts.map
|