@retinue/agentkit 0.2.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 +1 -1
- 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/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 +78 -1
- package/dist/agents/agent.js +118 -5
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +20 -0
- package/dist/agents/engine.js +177 -6
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -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/content-parts.d.ts +18 -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 +44 -2
- package/dist/core/events.js +31 -2
- package/dist/core/validation.js +19 -0
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/knowledge.d.ts +8 -0
- package/dist/entries/knowledge.js +8 -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 +3 -1
- package/dist/graphql/resolvers.js +14 -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 +24 -0
- package/dist/knowledge/index.js +36 -1
- package/dist/knowledge/retrieval.d.ts +26 -1
- package/dist/knowledge/retrieval.js +77 -1
- 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 +89 -0
- package/dist/models/streaming.js +179 -2
- 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 +46 -0
- 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/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +1 -0
- package/dist/telemetry/spans.js +5 -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/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +27 -1
- package/dist/toolkit/index.js +24 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -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/tools/credentials.d.ts +265 -6
- package/dist/tools/credentials.js +318 -3
- package/dist/tools/define.d.ts +14 -0
- package/dist/tools/define.js +3 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/index.d.ts +8 -0
- package/dist/tools/library/index.js +7 -0
- package/dist/tools/registry.d.ts +33 -0
- package/dist/tools/registry.js +72 -2
- 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 +29 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent seeding for the conformance suite.
|
|
3
|
+
*
|
|
4
|
+
* An adapter is entitled to enforce referential integrity, and the Postgres schema does: a checkpoint
|
|
5
|
+
* belongs to a run, a message and a binding belong to a conversation. The in-memory reference adapter
|
|
6
|
+
* holds no such constraint, so the shared harnesses never created those parent rows — and every
|
|
7
|
+
* foreign key added to the schema broke them.
|
|
8
|
+
*
|
|
9
|
+
* #95 solved that for `CheckpointStore` with a bespoke fixture type. #96 needed the same thing for
|
|
10
|
+
* `ConversationBindingStore`, which is the point at which a one-off becomes a pattern. This is the
|
|
11
|
+
* shared mechanism: a harness accepts either a bare store (adapters with no constraints, unchanged)
|
|
12
|
+
* or a fixture pairing the store with the seeders its constraints require.
|
|
13
|
+
*
|
|
14
|
+
* The alternative was to drop the foreign keys so the harness would pass, which would have traded a
|
|
15
|
+
* real guarantee — orphan rows are impossible — for a green suite. That is the wrong direction.
|
|
16
|
+
*/
|
|
17
|
+
const isFixture = (value) => typeof value === "object" && value !== null && "store" in value;
|
|
18
|
+
/**
|
|
19
|
+
* Normalise whichever shape the adapter supplied. Returns the store and the seeders, so a harness can
|
|
20
|
+
* seed unconditionally without caring which form it was given.
|
|
21
|
+
*/
|
|
22
|
+
export const openFixture = (made) => isFixture(made) ? made : { store: made };
|
|
23
|
+
/** Seed a run if the adapter needs one, then hand back the store. */
|
|
24
|
+
export const withRun = async (made, runs) => {
|
|
25
|
+
const fixture = openFixture(made);
|
|
26
|
+
if (fixture.seedRun)
|
|
27
|
+
for (const r of runs)
|
|
28
|
+
await fixture.seedRun(r);
|
|
29
|
+
return fixture.store;
|
|
30
|
+
};
|
|
31
|
+
/** Seed a conversation if the adapter needs one, then hand back the store. */
|
|
32
|
+
export const withConversation = async (made, conversations) => {
|
|
33
|
+
const fixture = openFixture(made);
|
|
34
|
+
if (fixture.seedConversation)
|
|
35
|
+
for (const c of conversations)
|
|
36
|
+
await fixture.seedConversation(c);
|
|
37
|
+
return fixture.store;
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=parents.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RateLimitStore` conformance — task #248, AC-6.
|
|
3
|
+
*
|
|
4
|
+
* The contract is small and every clause is a defect somebody would otherwise ship:
|
|
5
|
+
*
|
|
6
|
+
* - **Counting is per key.** A store that ignored the window start would never reset; one that ignored the
|
|
7
|
+
* tenant would let one customer exhaust another's allowance.
|
|
8
|
+
* - **`consume` returns the count *after* incrementing**, starting at 1. Off by one here means a limit of `max`
|
|
9
|
+
* admits `max + 1`, which no test of the guard alone would catch because the guard would be consistent with
|
|
10
|
+
* its own store.
|
|
11
|
+
* - **It is atomic under concurrency.** Twenty concurrent consumes must yield the twenty distinct counts 1..20,
|
|
12
|
+
* with no value repeated. A read-then-write implementation loses increments and quietly raises the effective
|
|
13
|
+
* limit — the failure this whole port exists to prevent, arriving from inside.
|
|
14
|
+
*
|
|
15
|
+
* Run against the in-memory store and against Redis. The in-memory one is single-process by construction, so it
|
|
16
|
+
* satisfies the contract while being unusable in a fleet; that is why the Redis suite exists separately and why
|
|
17
|
+
* `createMemoryRateLimitStore`'s header says so.
|
|
18
|
+
*/
|
|
19
|
+
import type { RateLimitStore } from "../../usage/index.js";
|
|
20
|
+
export declare function rateLimitStoreConformance(
|
|
21
|
+
/** The adapter under test, for the inner describe block. */
|
|
22
|
+
name: string, makeStore: () => RateLimitStore | Promise<RateLimitStore>,
|
|
23
|
+
/** A suffix making keys unique per run, so a rerun inside the same window does not inherit a count. */
|
|
24
|
+
unique: () => string): void;
|
|
25
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RateLimitStore` conformance — task #248, AC-6.
|
|
3
|
+
*
|
|
4
|
+
* The contract is small and every clause is a defect somebody would otherwise ship:
|
|
5
|
+
*
|
|
6
|
+
* - **Counting is per key.** A store that ignored the window start would never reset; one that ignored the
|
|
7
|
+
* tenant would let one customer exhaust another's allowance.
|
|
8
|
+
* - **`consume` returns the count *after* incrementing**, starting at 1. Off by one here means a limit of `max`
|
|
9
|
+
* admits `max + 1`, which no test of the guard alone would catch because the guard would be consistent with
|
|
10
|
+
* its own store.
|
|
11
|
+
* - **It is atomic under concurrency.** Twenty concurrent consumes must yield the twenty distinct counts 1..20,
|
|
12
|
+
* with no value repeated. A read-then-write implementation loses increments and quietly raises the effective
|
|
13
|
+
* limit — the failure this whole port exists to prevent, arriving from inside.
|
|
14
|
+
*
|
|
15
|
+
* Run against the in-memory store and against Redis. The in-memory one is single-process by construction, so it
|
|
16
|
+
* satisfies the contract while being unusable in a fleet; that is why the Redis suite exists separately and why
|
|
17
|
+
* `createMemoryRateLimitStore`'s header says so.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
import { asId } from "../../core/ids.js";
|
|
21
|
+
// `export function`, not `export const`: the coverage guard splits harness sources on
|
|
22
|
+
// `\nexport function (\w+Conformance)` to locate each body, so an arrow-function export is invisible to it and
|
|
23
|
+
// reads as a missing harness.
|
|
24
|
+
export function rateLimitStoreConformance(
|
|
25
|
+
/** The adapter under test, for the inner describe block. */
|
|
26
|
+
name, makeStore,
|
|
27
|
+
/** A suffix making keys unique per run, so a rerun inside the same window does not inherit a count. */
|
|
28
|
+
unique) {
|
|
29
|
+
// Exactly `<Port> conformance`: `scripts/conformance-matrix.mjs` maps that title back to the port. The
|
|
30
|
+
// adapter's name goes on the inner block instead.
|
|
31
|
+
describe("RateLimitStore conformance", () => {
|
|
32
|
+
const T1 = asId(`rlc-t1-${unique()}`);
|
|
33
|
+
const T2 = asId(`rlc-t2-${unique()}`);
|
|
34
|
+
const T = (s) => asId(`rlc-${s}-${unique()}`);
|
|
35
|
+
it(`[${name}] returns the count after incrementing, starting at 1`, async () => {
|
|
36
|
+
const store = await makeStore();
|
|
37
|
+
const tenantId = T("count");
|
|
38
|
+
const input = { tenantId, windowStartMs: 60_000, windowSeconds: 60 };
|
|
39
|
+
expect(await store.consume(input)).toBe(1);
|
|
40
|
+
expect(await store.consume(input)).toBe(2);
|
|
41
|
+
expect(await store.consume(input)).toBe(3);
|
|
42
|
+
});
|
|
43
|
+
it("counts each window separately", async () => {
|
|
44
|
+
const store = await makeStore();
|
|
45
|
+
const tenantId = T("window");
|
|
46
|
+
expect(await store.consume({ tenantId, windowStartMs: 60_000, windowSeconds: 60 })).toBe(1);
|
|
47
|
+
expect(await store.consume({ tenantId, windowStartMs: 60_000, windowSeconds: 60 })).toBe(2);
|
|
48
|
+
// A new window is a new count, which is what makes a limit recover rather than latch.
|
|
49
|
+
expect(await store.consume({ tenantId, windowStartMs: 120_000, windowSeconds: 60 })).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
it("counts each tenant separately — T2 is unaffected by T1's consumption", async () => {
|
|
52
|
+
// The isolation clause. There is no cross-tenant *read* to leak here, but there is a cross-tenant
|
|
53
|
+
// *count*: a store keyed on the window alone would let one customer exhaust every other customer's
|
|
54
|
+
// allowance, which is the same class of defect as the AgentStore leak #91 found.
|
|
55
|
+
const store = await makeStore();
|
|
56
|
+
expect(await store.consume({ tenantId: T1, windowStartMs: 60_000, windowSeconds: 60 })).toBe(1);
|
|
57
|
+
expect(await store.consume({ tenantId: T1, windowStartMs: 60_000, windowSeconds: 60 })).toBe(2);
|
|
58
|
+
expect(await store.consume({ tenantId: T2, windowStartMs: 60_000, windowSeconds: 60 })).toBe(1);
|
|
59
|
+
});
|
|
60
|
+
it("loses no increment under concurrency", async () => {
|
|
61
|
+
// Twenty concurrent consumes must produce the twenty distinct counts 1..20. A read-then-write store
|
|
62
|
+
// returns duplicates, and a duplicate is a lost increment — a limit that admits more than it says.
|
|
63
|
+
const store = await makeStore();
|
|
64
|
+
const tenantId = T("concurrent");
|
|
65
|
+
const input = { tenantId, windowStartMs: 60_000, windowSeconds: 60 };
|
|
66
|
+
const counts = await Promise.all(Array.from({ length: 20 }, () => store.consume(input)));
|
|
67
|
+
expect([...counts].sort((x, y) => x - y)).toEqual(Array.from({ length: 20 }, (_, i) => i + 1));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record-store conformance: `MessageStore`, `AgentStore`, `SkillStore`, `BlobStore`,
|
|
3
|
+
* `IdempotencyStore`, `PrincipalMemoryStore`, `McpConnectionStore`.
|
|
4
|
+
*
|
|
5
|
+
* Two of these carry a safety property rather than merely a data contract, and both are asserted
|
|
6
|
+
* here rather than assumed:
|
|
7
|
+
* - `BlobStore` — "tenant-scoped so a ref from one tenant can never resolve another's bytes".
|
|
8
|
+
* - `PrincipalMemoryStore` — every method takes `{ tenantId, principalId }`, so a query "can never
|
|
9
|
+
* reach another principal's or tenant's memory". Tenant isolation alone is not enough; the
|
|
10
|
+
* cross-*principal* case inside one tenant is the one that would leak between colleagues.
|
|
11
|
+
*/
|
|
12
|
+
import { type FixtureOrStore } from "./parents.js";
|
|
13
|
+
import type { TenantId } from "../../core/ids.js";
|
|
14
|
+
import type { AgentStore, BlobStore, MessageStore, SkillStore } from "../../persistence/index.js";
|
|
15
|
+
import { type IdempotencyStore } from "../../idempotency/index.js";
|
|
16
|
+
import type { PrincipalMemoryStore } from "../../principal-memory/index.js";
|
|
17
|
+
import type { McpConnectionStore } from "../../mcp/provider.js";
|
|
18
|
+
export declare function messageStoreConformance(makeFixture: () => FixtureOrStore<MessageStore>): void;
|
|
19
|
+
export declare function agentStoreConformance(makeStore: () => AgentStore, seed: (store: AgentStore, input: {
|
|
20
|
+
tenantId: TenantId;
|
|
21
|
+
agentId: string;
|
|
22
|
+
version: number;
|
|
23
|
+
}) => Promise<void>): void;
|
|
24
|
+
export declare function skillStoreConformance(makeStore: () => SkillStore, seed: (store: SkillStore, input: {
|
|
25
|
+
tenantId: TenantId;
|
|
26
|
+
name: string;
|
|
27
|
+
version: number;
|
|
28
|
+
}) => Promise<void>): void;
|
|
29
|
+
export declare function blobStoreConformance(makeStore: () => BlobStore): void;
|
|
30
|
+
export declare function idempotencyStoreConformance(makeStore: () => IdempotencyStore): void;
|
|
31
|
+
export declare function principalMemoryStoreConformance(makeStore: () => PrincipalMemoryStore): void;
|
|
32
|
+
export declare function mcpConnectionStoreConformance(makeStore: () => McpConnectionStore, seed: (store: McpConnectionStore, input: {
|
|
33
|
+
tenantId: TenantId;
|
|
34
|
+
id: string;
|
|
35
|
+
}) => Promise<void>): void;
|
|
36
|
+
//# sourceMappingURL=records.d.ts.map
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record-store conformance: `MessageStore`, `AgentStore`, `SkillStore`, `BlobStore`,
|
|
3
|
+
* `IdempotencyStore`, `PrincipalMemoryStore`, `McpConnectionStore`.
|
|
4
|
+
*
|
|
5
|
+
* Two of these carry a safety property rather than merely a data contract, and both are asserted
|
|
6
|
+
* here rather than assumed:
|
|
7
|
+
* - `BlobStore` — "tenant-scoped so a ref from one tenant can never resolve another's bytes".
|
|
8
|
+
* - `PrincipalMemoryStore` — every method takes `{ tenantId, principalId }`, so a query "can never
|
|
9
|
+
* reach another principal's or tenant's memory". Tenant isolation alone is not enough; the
|
|
10
|
+
* cross-*principal* case inside one tenant is the one that would leak between colleagues.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { withConversation } from "./parents.js";
|
|
14
|
+
import { asId } from "../../core/ids.js";
|
|
15
|
+
import { deriveIdempotencyKey } from "../../idempotency/index.js";
|
|
16
|
+
const T1 = asId("conf-tenant-1");
|
|
17
|
+
const T2 = asId("conf-tenant-2");
|
|
18
|
+
const P1 = asId("conf-principal-1");
|
|
19
|
+
const P2 = asId("conf-principal-2");
|
|
20
|
+
const RUN = asId("conf-run-1");
|
|
21
|
+
const C1 = asId("conf-convo-1");
|
|
22
|
+
export function messageStoreConformance(makeFixture) {
|
|
23
|
+
// A message references a conversation, and Postgres enforces that with a foreign key (#96) — the
|
|
24
|
+
// fixture's `seedConversation` creates the parent through the adapter's own executor. See ./parents.ts.
|
|
25
|
+
const open = (conversationId = C1) => withConversation(makeFixture(), [{ tenantId: T1, conversationId }]);
|
|
26
|
+
/**
|
|
27
|
+
* Seeding goes through the port as of #157. It used to be a callback each adapter test supplied, because
|
|
28
|
+
* `append` was a "test-only affordance" off the port with a positional signature — so all three adapter tests
|
|
29
|
+
* carried their own cast and their own copy of this loop, free to diverge on the very shapes the suite is
|
|
30
|
+
* supposed to be pinning down. The Postgres copy seeded `role: "user"` with a text part; the memory copy
|
|
31
|
+
* seeded `role: "assistant"` with none. The parts round-trip test below therefore proved nothing about the
|
|
32
|
+
* memory adapter: it asserted `Array.isArray(parts)`, and `[]` is an array.
|
|
33
|
+
*/
|
|
34
|
+
const seed = async (store, { tenantId, conversationId, count }) => {
|
|
35
|
+
for (let n = 0; n < count; n += 1) {
|
|
36
|
+
await store.append({ tenantId, message: message(conversationId, n) });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const message = (conversationId, n, id = `m${n}`) => ({
|
|
40
|
+
id: asId(id),
|
|
41
|
+
conversationId,
|
|
42
|
+
runId: RUN,
|
|
43
|
+
role: n % 2 === 0 ? "user" : "assistant",
|
|
44
|
+
parts: [
|
|
45
|
+
{
|
|
46
|
+
id: asId(`p${n}`),
|
|
47
|
+
type: "text",
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
createdAt: `2020-01-01T00:00:${String(n).padStart(2, "0")}.000Z`,
|
|
50
|
+
text: `message ${n}`,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
createdAt: `2020-01-01T00:00:${String(n).padStart(2, "0")}.000Z`,
|
|
54
|
+
});
|
|
55
|
+
describe("MessageStore conformance", () => {
|
|
56
|
+
it("returns null for an unknown id", async () => {
|
|
57
|
+
const store = await open();
|
|
58
|
+
expect(await store.findById({ tenantId: T1, id: asId("nope") })).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
it("reads back an appended message by id", async () => {
|
|
61
|
+
const store = await open();
|
|
62
|
+
await store.append({ tenantId: T1, message: message(C1, 0) });
|
|
63
|
+
const found = await store.findById({ tenantId: T1, id: asId("m0") });
|
|
64
|
+
expect(found?.id).toBe("m0");
|
|
65
|
+
expect(found?.role).toBe("user");
|
|
66
|
+
});
|
|
67
|
+
it("lists a conversation's messages in order", async () => {
|
|
68
|
+
const store = await open();
|
|
69
|
+
await seed(store, { tenantId: T1, conversationId: C1, count: 3 });
|
|
70
|
+
const page = await store.listByConversation({ tenantId: T1, conversationId: C1, limit: 10 });
|
|
71
|
+
expect(page.items).toHaveLength(3);
|
|
72
|
+
expect(page.items.map((m) => m.id)).toEqual(["m0", "m1", "m2"]);
|
|
73
|
+
});
|
|
74
|
+
it("pages by stable cursor with no overlap between pages", async () => {
|
|
75
|
+
const store = await open();
|
|
76
|
+
await seed(store, { tenantId: T1, conversationId: C1, count: 5 });
|
|
77
|
+
const first = await store.listByConversation({ tenantId: T1, conversationId: C1, limit: 2 });
|
|
78
|
+
expect(first.items).toHaveLength(2);
|
|
79
|
+
expect(first.nextCursor).toBeDefined();
|
|
80
|
+
const second = await store.listByConversation({
|
|
81
|
+
tenantId: T1,
|
|
82
|
+
conversationId: C1,
|
|
83
|
+
limit: 2,
|
|
84
|
+
cursor: first.nextCursor,
|
|
85
|
+
});
|
|
86
|
+
const firstIds = new Set(first.items.map((m) => m.id));
|
|
87
|
+
expect(second.items.some((m) => firstIds.has(m.id))).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
it("preserves typed content parts through a round-trip", async () => {
|
|
90
|
+
const store = await open();
|
|
91
|
+
await seed(store, { tenantId: T1, conversationId: C1, count: 1 });
|
|
92
|
+
const page = await store.listByConversation({ tenantId: T1, conversationId: C1, limit: 1 });
|
|
93
|
+
const part = page.items[0]?.parts[0];
|
|
94
|
+
expect(part?.type).toBe("text");
|
|
95
|
+
expect(part).toMatchObject({ id: "p0", schemaVersion: 1, text: "message 0" });
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* The most recent messages, which is the query a chat application actually makes — #167.
|
|
99
|
+
*
|
|
100
|
+
* The port could only answer "the oldest N", so a host wanting the tail of a long conversation either paged
|
|
101
|
+
* through the whole thing or, much more likely, asked for 100 and silently got turns 1–100. In a
|
|
102
|
+
* 2000-message conversation that hands a model the beginning and none of the present.
|
|
103
|
+
*/
|
|
104
|
+
it("reads the newest messages first when asked", async () => {
|
|
105
|
+
const store = await open();
|
|
106
|
+
await seed(store, { tenantId: T1, conversationId: C1, count: 5 });
|
|
107
|
+
const page = await store.listByConversation({ tenantId: T1, conversationId: C1, limit: 2, newestFirst: true });
|
|
108
|
+
expect(page.items.map((m) => m.id)).toEqual(["m4", "m3"]);
|
|
109
|
+
});
|
|
110
|
+
it("pages backwards without overlap or gaps", async () => {
|
|
111
|
+
const store = await open();
|
|
112
|
+
await seed(store, { tenantId: T1, conversationId: C1, count: 5 });
|
|
113
|
+
const first = await store.listByConversation({ tenantId: T1, conversationId: C1, limit: 2, newestFirst: true });
|
|
114
|
+
const second = await store.listByConversation({
|
|
115
|
+
tenantId: T1,
|
|
116
|
+
conversationId: C1,
|
|
117
|
+
limit: 2,
|
|
118
|
+
newestFirst: true,
|
|
119
|
+
cursor: first.nextCursor,
|
|
120
|
+
});
|
|
121
|
+
// The cursor has to flip with the sort. Split apart they produce a page that scans one way and pages the
|
|
122
|
+
// other, which returns rows already seen and looks like duplication.
|
|
123
|
+
expect(second.items.map((m) => m.id)).toEqual(["m2", "m1"]);
|
|
124
|
+
});
|
|
125
|
+
/**
|
|
126
|
+
* Ordered by timestamp, not by insertion.
|
|
127
|
+
*
|
|
128
|
+
* The two agree for anything appended in order, which is why this went unnoticed: every fixture seeded in
|
|
129
|
+
* timestamp order, so the in-memory adapter's array order happened to match the SQL adapter's `ORDER BY`.
|
|
130
|
+
* A backfilled or replayed message is where they diverge.
|
|
131
|
+
*/
|
|
132
|
+
it("orders by created_at even when messages arrive out of order", async () => {
|
|
133
|
+
const store = await open();
|
|
134
|
+
await store.append({ tenantId: T1, message: message(C1, 5, "later") });
|
|
135
|
+
await store.append({ tenantId: T1, message: message(C1, 1, "earlier") });
|
|
136
|
+
const page = await store.listByConversation({ tenantId: T1, conversationId: C1, limit: 10 });
|
|
137
|
+
expect(page.items.map((m) => m.id)).toEqual(["earlier", "later"]);
|
|
138
|
+
});
|
|
139
|
+
/**
|
|
140
|
+
* A retried request must neither fail nor duplicate. #157 put `append` on the port, and a caller that
|
|
141
|
+
* retries — the same user turn re-submitted after a dropped connection — carries the same message id;
|
|
142
|
+
* a second row would show the user their own message twice and feed the model a doubled turn.
|
|
143
|
+
*/
|
|
144
|
+
it("is idempotent on the message id", async () => {
|
|
145
|
+
const store = await open();
|
|
146
|
+
await store.append({ tenantId: T1, message: message(C1, 0) });
|
|
147
|
+
await store.append({ tenantId: T1, message: message(C1, 0) });
|
|
148
|
+
const page = await store.listByConversation({ tenantId: T1, conversationId: C1, limit: 10 });
|
|
149
|
+
expect(page.items).toHaveLength(1);
|
|
150
|
+
});
|
|
151
|
+
/** The same id under another tenant is a different message, not a conflict to swallow. */
|
|
152
|
+
it("scopes appended messages to the writing tenant", async () => {
|
|
153
|
+
const fixture = await withConversation(makeFixture(), [
|
|
154
|
+
{ tenantId: T1, conversationId: C1 },
|
|
155
|
+
{ tenantId: T2, conversationId: C1 },
|
|
156
|
+
]);
|
|
157
|
+
await fixture.append({ tenantId: T1, message: message(C1, 0) });
|
|
158
|
+
await fixture.append({ tenantId: T2, message: { ...message(C1, 0), role: "assistant" } });
|
|
159
|
+
expect((await fixture.findById({ tenantId: T1, id: asId("m0") }))?.role).toBe("user");
|
|
160
|
+
expect((await fixture.findById({ tenantId: T2, id: asId("m0") }))?.role).toBe("assistant");
|
|
161
|
+
});
|
|
162
|
+
it("enforces tenant isolation", async () => {
|
|
163
|
+
const store = await open();
|
|
164
|
+
await seed(store, { tenantId: T1, conversationId: C1, count: 2 });
|
|
165
|
+
const other = await store.listByConversation({ tenantId: T2, conversationId: C1, limit: 10 });
|
|
166
|
+
expect(other.items).toHaveLength(0);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
export function agentStoreConformance(makeStore, seed) {
|
|
171
|
+
describe("AgentStore conformance", () => {
|
|
172
|
+
it("returns null for an unknown agent version", async () => {
|
|
173
|
+
expect(await makeStore().findByVersion({ tenantId: T1, agentId: "nope", version: 1 })).toBeNull();
|
|
174
|
+
});
|
|
175
|
+
it("resolves the exact version requested", async () => {
|
|
176
|
+
const store = makeStore();
|
|
177
|
+
await seed(store, { tenantId: T1, agentId: "a", version: 1 });
|
|
178
|
+
await seed(store, { tenantId: T1, agentId: "a", version: 2 });
|
|
179
|
+
expect(await store.findByVersion({ tenantId: T1, agentId: "a", version: 1 })).not.toBeNull();
|
|
180
|
+
expect(await store.findByVersion({ tenantId: T1, agentId: "a", version: 2 })).not.toBeNull();
|
|
181
|
+
});
|
|
182
|
+
it("does not fall back to another version when the requested one is absent", async () => {
|
|
183
|
+
const store = makeStore();
|
|
184
|
+
await seed(store, { tenantId: T1, agentId: "a", version: 1 });
|
|
185
|
+
expect(await store.findByVersion({ tenantId: T1, agentId: "a", version: 7 })).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
/**
|
|
188
|
+
* Surfaced by #91 as a real leak and fixed: `createMemoryAgentStore` keyed its map by
|
|
189
|
+
* `id@version` alone and destructured only `{ agentId, version }`, so one tenant could resolve
|
|
190
|
+
* another tenant's manifest. It now partitions by tenant like every sibling store.
|
|
191
|
+
*
|
|
192
|
+
* Kept deliberately as a regression test rather than deleted — an agent manifest carries no
|
|
193
|
+
* `tenantId` of its own, so nothing in the type system stops this from being reintroduced.
|
|
194
|
+
*/
|
|
195
|
+
it("enforces tenant isolation", async () => {
|
|
196
|
+
const store = makeStore();
|
|
197
|
+
await seed(store, { tenantId: T1, agentId: "a", version: 1 });
|
|
198
|
+
expect(await store.findByVersion({ tenantId: T2, agentId: "a", version: 1 })).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
it("keeps same-id manifests separate per tenant", async () => {
|
|
201
|
+
const store = makeStore();
|
|
202
|
+
await seed(store, { tenantId: T1, agentId: "shared", version: 1 });
|
|
203
|
+
await seed(store, { tenantId: T2, agentId: "shared", version: 1 });
|
|
204
|
+
// Both tenants may legitimately own an agent with the same id; neither may see the other's.
|
|
205
|
+
expect(await store.findByVersion({ tenantId: T1, agentId: "shared", version: 1 })).not.toBeNull();
|
|
206
|
+
expect(await store.findByVersion({ tenantId: T2, agentId: "shared", version: 1 })).not.toBeNull();
|
|
207
|
+
});
|
|
208
|
+
it("does not leak a version one tenant owns and the other does not", async () => {
|
|
209
|
+
const store = makeStore();
|
|
210
|
+
await seed(store, { tenantId: T1, agentId: "a", version: 1 });
|
|
211
|
+
await seed(store, { tenantId: T2, agentId: "a", version: 2 });
|
|
212
|
+
expect(await store.findByVersion({ tenantId: T1, agentId: "a", version: 2 })).toBeNull();
|
|
213
|
+
expect(await store.findByVersion({ tenantId: T2, agentId: "a", version: 1 })).toBeNull();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
export function skillStoreConformance(makeStore, seed) {
|
|
218
|
+
describe("SkillStore conformance", () => {
|
|
219
|
+
it("returns an empty catalog for a tenant with no skills", async () => {
|
|
220
|
+
expect(await makeStore().listCatalog({ tenantId: T1 })).toHaveLength(0);
|
|
221
|
+
});
|
|
222
|
+
it("lists the catalog after seeding", async () => {
|
|
223
|
+
const store = makeStore();
|
|
224
|
+
await seed(store, { tenantId: T1, name: "post-composition", version: 1 });
|
|
225
|
+
expect((await store.listCatalog({ tenantId: T1 })).length).toBeGreaterThan(0);
|
|
226
|
+
});
|
|
227
|
+
it("resolves a specific version, so a run records what it actually used", async () => {
|
|
228
|
+
const store = makeStore();
|
|
229
|
+
await seed(store, { tenantId: T1, name: "post-composition", version: 1 });
|
|
230
|
+
await seed(store, { tenantId: T1, name: "post-composition", version: 2 });
|
|
231
|
+
const v1 = await store.findVersion({ tenantId: T1, name: "post-composition", version: 1 });
|
|
232
|
+
expect(v1?.version).toBe(1);
|
|
233
|
+
});
|
|
234
|
+
it("returns null for an absent version rather than the nearest one", async () => {
|
|
235
|
+
const store = makeStore();
|
|
236
|
+
await seed(store, { tenantId: T1, name: "post-composition", version: 1 });
|
|
237
|
+
expect(await store.findVersion({ tenantId: T1, name: "post-composition", version: 9 })).toBeNull();
|
|
238
|
+
});
|
|
239
|
+
it("enforces tenant isolation", async () => {
|
|
240
|
+
const store = makeStore();
|
|
241
|
+
await seed(store, { tenantId: T1, name: "post-composition", version: 1 });
|
|
242
|
+
expect(await store.listCatalog({ tenantId: T2 })).toHaveLength(0);
|
|
243
|
+
expect(await store.findVersion({ tenantId: T2, name: "post-composition", version: 1 })).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
export function blobStoreConformance(makeStore) {
|
|
248
|
+
describe("BlobStore conformance", () => {
|
|
249
|
+
it("round-trips a stored value by ref", async () => {
|
|
250
|
+
const store = makeStore();
|
|
251
|
+
const ref = await store.put({ tenantId: T1, value: { large: "payload" } });
|
|
252
|
+
expect(await store.get({ tenantId: T1, ref })).toEqual({ large: "payload" });
|
|
253
|
+
});
|
|
254
|
+
it("returns null for a ref that was never issued", async () => {
|
|
255
|
+
const store = makeStore();
|
|
256
|
+
// Previously this asked a *second store instance* for a ref the first had issued, which for an
|
|
257
|
+
// in-memory adapter means "unknown" and for a durable one means the opposite — a second store
|
|
258
|
+
// over the same database should find it. It passed only because the Postgres wiring hands each
|
|
259
|
+
// factory call a fresh database, so it was testing instance identity rather than absence.
|
|
260
|
+
// Fabricating a ref tests what the name says, and is correct for both (#102).
|
|
261
|
+
await store.put({ tenantId: T1, value: 1 });
|
|
262
|
+
expect(await store.get({ tenantId: T1, ref: asId("blob:never-issued:0") })).toBeNull();
|
|
263
|
+
});
|
|
264
|
+
it("a ref from one tenant never resolves another tenant's bytes", async () => {
|
|
265
|
+
const store = makeStore();
|
|
266
|
+
const ref = await store.put({ tenantId: T1, value: { secret: true } });
|
|
267
|
+
expect(await store.get({ tenantId: T2, ref })).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
it("keeps distinct values distinct", async () => {
|
|
270
|
+
const store = makeStore();
|
|
271
|
+
const a = await store.put({ tenantId: T1, value: { v: "a" } });
|
|
272
|
+
const b = await store.put({ tenantId: T1, value: { v: "b" } });
|
|
273
|
+
expect(await store.get({ tenantId: T1, ref: a })).toEqual({ v: "a" });
|
|
274
|
+
expect(await store.get({ tenantId: T1, ref: b })).toEqual({ v: "b" });
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
export function idempotencyStoreConformance(makeStore) {
|
|
279
|
+
const key = deriveIdempotencyKey({ tenantId: T1, runId: RUN, toolCallId: asId("tc1") });
|
|
280
|
+
describe("IdempotencyStore conformance", () => {
|
|
281
|
+
it("returns null for an unseen key", async () => {
|
|
282
|
+
expect(await makeStore().get({ tenantId: T1, key })).toBeNull();
|
|
283
|
+
});
|
|
284
|
+
it("returns the stored result for a repeated key, so the operation is not re-executed", async () => {
|
|
285
|
+
const store = makeStore();
|
|
286
|
+
await store.put({ tenantId: T1, key, result: { published: true } });
|
|
287
|
+
const seen = await store.get({ tenantId: T1, key });
|
|
288
|
+
expect(seen?.result).toEqual({ published: true });
|
|
289
|
+
});
|
|
290
|
+
it("reports firstSeen honestly on the replay path", async () => {
|
|
291
|
+
const store = makeStore();
|
|
292
|
+
await store.put({ tenantId: T1, key, result: 1 });
|
|
293
|
+
expect((await store.get({ tenantId: T1, key }))?.firstSeen).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
it("enforces tenant isolation — a key is never shared across tenants", async () => {
|
|
296
|
+
const store = makeStore();
|
|
297
|
+
await store.put({ tenantId: T1, key, result: { published: true } });
|
|
298
|
+
expect(await store.get({ tenantId: T2, key })).toBeNull();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
export function principalMemoryStoreConformance(makeStore) {
|
|
303
|
+
describe("PrincipalMemoryStore conformance", () => {
|
|
304
|
+
it("stores and reads back an entry for its principal", async () => {
|
|
305
|
+
const store = makeStore();
|
|
306
|
+
const entry = await store.put({ tenantId: T1, principalId: P1, text: "prefers concise answers" });
|
|
307
|
+
expect(await store.get({ tenantId: T1, principalId: P1, id: entry.id })).toMatchObject({
|
|
308
|
+
text: "prefers concise answers",
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
it("never reaches another principal's memory inside the same tenant", async () => {
|
|
312
|
+
const store = makeStore();
|
|
313
|
+
const entry = await store.put({ tenantId: T1, principalId: P1, text: "private to P1" });
|
|
314
|
+
expect(await store.get({ tenantId: T1, principalId: P2, id: entry.id })).toBeNull();
|
|
315
|
+
expect((await store.list({ tenantId: T1, principalId: P2, limit: 10 })).items).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
it("never reaches another tenant's memory", async () => {
|
|
318
|
+
const store = makeStore();
|
|
319
|
+
const entry = await store.put({ tenantId: T1, principalId: P1, text: "private to T1" });
|
|
320
|
+
expect(await store.get({ tenantId: T2, principalId: P1, id: entry.id })).toBeNull();
|
|
321
|
+
});
|
|
322
|
+
it("rejects a stale expectedVersion on update", async () => {
|
|
323
|
+
const store = makeStore();
|
|
324
|
+
const entry = await store.put({ tenantId: T1, principalId: P1, text: "v1" });
|
|
325
|
+
await store.update({
|
|
326
|
+
tenantId: T1,
|
|
327
|
+
principalId: P1,
|
|
328
|
+
id: entry.id,
|
|
329
|
+
expectedVersion: entry.version,
|
|
330
|
+
patch: { text: "v2" },
|
|
331
|
+
});
|
|
332
|
+
await expect(store.update({
|
|
333
|
+
tenantId: T1,
|
|
334
|
+
principalId: P1,
|
|
335
|
+
id: entry.id,
|
|
336
|
+
expectedVersion: entry.version,
|
|
337
|
+
patch: { text: "stale" },
|
|
338
|
+
})).rejects.toThrow();
|
|
339
|
+
});
|
|
340
|
+
it("hard-deletes, so a deleted entry cannot resurface in a later prompt", async () => {
|
|
341
|
+
const store = makeStore();
|
|
342
|
+
const entry = await store.put({ tenantId: T1, principalId: P1, text: "forget me" });
|
|
343
|
+
await store.delete({ tenantId: T1, principalId: P1, id: entry.id });
|
|
344
|
+
expect(await store.get({ tenantId: T1, principalId: P1, id: entry.id })).toBeNull();
|
|
345
|
+
expect(await store.retrieve({ tenantId: T1, principalId: P1, limit: 10 })).toHaveLength(0);
|
|
346
|
+
});
|
|
347
|
+
it("excludes disabled entries from retrieval but keeps them readable", async () => {
|
|
348
|
+
const store = makeStore();
|
|
349
|
+
const entry = await store.put({ tenantId: T1, principalId: P1, text: "temporarily off" });
|
|
350
|
+
await store.update({
|
|
351
|
+
tenantId: T1,
|
|
352
|
+
principalId: P1,
|
|
353
|
+
id: entry.id,
|
|
354
|
+
expectedVersion: entry.version,
|
|
355
|
+
patch: { disabled: true },
|
|
356
|
+
});
|
|
357
|
+
expect(await store.retrieve({ tenantId: T1, principalId: P1, limit: 10 })).toHaveLength(0);
|
|
358
|
+
expect(await store.get({ tenantId: T1, principalId: P1, id: entry.id })).not.toBeNull();
|
|
359
|
+
});
|
|
360
|
+
it("retrieve returns most salient first and honors the limit", async () => {
|
|
361
|
+
const store = makeStore();
|
|
362
|
+
await store.put({ tenantId: T1, principalId: P1, text: "low", salience: 1 });
|
|
363
|
+
await store.put({ tenantId: T1, principalId: P1, text: "high", salience: 9 });
|
|
364
|
+
const top = await store.retrieve({ tenantId: T1, principalId: P1, limit: 1 });
|
|
365
|
+
expect(top).toHaveLength(1);
|
|
366
|
+
expect(top[0]?.text).toBe("high");
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
export function mcpConnectionStoreConformance(makeStore, seed) {
|
|
371
|
+
describe("McpConnectionStore conformance", () => {
|
|
372
|
+
it("returns null for an unregistered connection", async () => {
|
|
373
|
+
expect(await makeStore().get({ tenantId: T1, id: "nope" })).toBeNull();
|
|
374
|
+
});
|
|
375
|
+
it("registers and reads back a connection", async () => {
|
|
376
|
+
const store = makeStore();
|
|
377
|
+
await seed(store, { tenantId: T1, id: "srv1" });
|
|
378
|
+
expect(await store.get({ tenantId: T1, id: "srv1" })).not.toBeNull();
|
|
379
|
+
});
|
|
380
|
+
it("lists a tenant's connections", async () => {
|
|
381
|
+
const store = makeStore();
|
|
382
|
+
await seed(store, { tenantId: T1, id: "srv1" });
|
|
383
|
+
await seed(store, { tenantId: T1, id: "srv2" });
|
|
384
|
+
expect(await store.list({ tenantId: T1 })).toHaveLength(2);
|
|
385
|
+
});
|
|
386
|
+
it("toggles enabled without losing the record", async () => {
|
|
387
|
+
const store = makeStore();
|
|
388
|
+
await seed(store, { tenantId: T1, id: "srv1" });
|
|
389
|
+
await store.setEnabled({ tenantId: T1, id: "srv1", enabled: false });
|
|
390
|
+
expect(await store.get({ tenantId: T1, id: "srv1" })).not.toBeNull();
|
|
391
|
+
});
|
|
392
|
+
it("enforces tenant isolation", async () => {
|
|
393
|
+
const store = makeStore();
|
|
394
|
+
await seed(store, { tenantId: T1, id: "srv1" });
|
|
395
|
+
expect(await store.get({ tenantId: T2, id: "srv1" })).toBeNull();
|
|
396
|
+
expect(await store.list({ tenantId: T2 })).toHaveLength(0);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
//# sourceMappingURL=records.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `UsageRollupStore` conformance (#139).
|
|
3
|
+
*
|
|
4
|
+
* Almost every case is about a property that only shows up under repetition or concurrency: idempotency, no
|
|
5
|
+
* double counting, no lost records, no cross-tenant aggregate. Those are the ones a spend figure has to get
|
|
6
|
+
* right, and the ones a single-write test cannot see.
|
|
7
|
+
*
|
|
8
|
+
* The fixture pairs the rollup store with the ledger it derives from, because a rollup computed from a
|
|
9
|
+
* *different* set of events than the ledger holds is the exact bug this port exists to prevent — and a suite
|
|
10
|
+
* that opened them separately could not notice.
|
|
11
|
+
*/
|
|
12
|
+
import type { RunId, TenantId } from "../../core/ids.js";
|
|
13
|
+
import type { UsageRollupStore, UsageStore } from "../../persistence/index.js";
|
|
14
|
+
/**
|
|
15
|
+
* Both ports over one set of events, which is how a real deployment provides them.
|
|
16
|
+
*
|
|
17
|
+
* `seedRun` because `usage_records` has a foreign key to `runs` — an event belongs to a run, and Postgres
|
|
18
|
+
* enforces it. The in-memory adapter has no such constraint, which is exactly the situation `parents.ts`
|
|
19
|
+
* exists for.
|
|
20
|
+
*/
|
|
21
|
+
export type RollupFixture = {
|
|
22
|
+
readonly usage: UsageStore;
|
|
23
|
+
readonly rollups: UsageRollupStore;
|
|
24
|
+
readonly seedRun?: (input: {
|
|
25
|
+
readonly tenantId: TenantId;
|
|
26
|
+
readonly runId: RunId;
|
|
27
|
+
}) => Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
export declare function usageRollupStoreConformance(make: () => RollupFixture | Promise<RollupFixture>): void;
|
|
30
|
+
//# sourceMappingURL=rollups.d.ts.map
|