@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
package/dist/skills/index.d.ts
CHANGED
package/dist/skills/index.js
CHANGED
|
@@ -27,5 +27,7 @@ export const SKILL_LIMITS = {
|
|
|
27
27
|
/** Per run, to bound what `load_skill` can pull into context. */
|
|
28
28
|
maxLoadedPerRun: 5,
|
|
29
29
|
};
|
|
30
|
+
export * from "./catalogue.js";
|
|
31
|
+
export * from "./context.js";
|
|
30
32
|
export * from "./resolver.js";
|
|
31
33
|
//# sourceMappingURL=index.js.map
|
package/dist/telemetry/spans.js
CHANGED
|
@@ -36,8 +36,25 @@ export const SPAN_FOR_RUN_EVENT = {
|
|
|
36
36
|
"question.answered": "hitl.question",
|
|
37
37
|
"approval.requested": "hitl.approval",
|
|
38
38
|
"approval.decided": "hitl.approval",
|
|
39
|
+
// Its own span rather than `hitl.approval`: both pause a run for a person, and the questions they answer are
|
|
40
|
+
// different — "may I do this" versus "I have no access, grant me some" — so a trace that merged them would
|
|
41
|
+
// make "how often do runs stall on a missing connection" unanswerable.
|
|
42
|
+
"connection.requested": "hitl.connection",
|
|
43
|
+
"connection.completed": "hitl.connection",
|
|
39
44
|
"usage.updated": "run.step",
|
|
40
45
|
"context.compacted": "context.compact",
|
|
46
|
+
// Its own span, not `run.step`: a guardrail verdict is the boundary of a decision somebody will need to find
|
|
47
|
+
// later — "what stopped this turn" is the question a trace gets opened to answer.
|
|
48
|
+
"guardrail.verdict": "guardrail.inspect",
|
|
49
|
+
/**
|
|
50
|
+
* Its own span, and the reason is a rule this repository enforces: a span shares its first word with its
|
|
51
|
+
* event unless it is deliberately folded into `run.step`.
|
|
52
|
+
*
|
|
53
|
+
* `context.compact` was the first choice — dropping tools to fit a budget is the same *kind* of act as
|
|
54
|
+
* dropping history to fit one — and `telemetry.test.ts` rejected it. Correctly: an operator searching traces
|
|
55
|
+
* for why a turn was short of tools would not find it filed under context compaction.
|
|
56
|
+
*/
|
|
57
|
+
"catalog.truncated": "catalog.truncate",
|
|
41
58
|
};
|
|
42
59
|
/**
|
|
43
60
|
* The spans that are *not* run events.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ArtifactExportStore` conformance (#134).
|
|
3
|
+
*
|
|
4
|
+
* Almost entirely about `claim`, because that is the method whose contract two adapters can plausibly get
|
|
5
|
+
* differently and where differing is expensive: a claim that is not exclusive renders the same PDF twice and
|
|
6
|
+
* leaves two rows pointing at two identical files, and a caller has no way to tell which is canonical.
|
|
7
|
+
*/
|
|
8
|
+
import type { ArtifactId, ConversationId, TenantId } from "../../core/ids.js";
|
|
9
|
+
import type { ArtifactExportStore } from "../../persistence/index.js";
|
|
10
|
+
import { type Fixture, type FixtureOrStore } from "./parents.js";
|
|
11
|
+
/** The parent an adapter with a foreign key needs: an artifact, which itself needs a conversation. */
|
|
12
|
+
export type ExportFixture = Fixture<ArtifactExportStore> & {
|
|
13
|
+
readonly seedArtifact?: (input: {
|
|
14
|
+
readonly tenantId: TenantId;
|
|
15
|
+
readonly artifactId: ArtifactId;
|
|
16
|
+
readonly conversationId: ConversationId;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
export declare function artifactExportStoreConformance(make: () => FixtureOrStore<ArtifactExportStore> | Promise<FixtureOrStore<ArtifactExportStore>>): void;
|
|
20
|
+
//# sourceMappingURL=artifact-exports.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ArtifactExportStore` conformance (#134).
|
|
3
|
+
*
|
|
4
|
+
* Almost entirely about `claim`, because that is the method whose contract two adapters can plausibly get
|
|
5
|
+
* differently and where differing is expensive: a claim that is not exclusive renders the same PDF twice and
|
|
6
|
+
* leaves two rows pointing at two identical files, and a caller has no way to tell which is canonical.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { asId } from "../../core/ids.js";
|
|
10
|
+
import { openFixture } from "./parents.js";
|
|
11
|
+
const T1 = asId("conf-export-tenant-1");
|
|
12
|
+
const T2 = asId("conf-export-tenant-2");
|
|
13
|
+
const A1 = asId("conf-export-artifact-1");
|
|
14
|
+
const AT = "2026-08-23T12:00:00.000Z";
|
|
15
|
+
const USER = asId("conf-export-user");
|
|
16
|
+
const requested = (overrides = {}) => ({
|
|
17
|
+
id: overrides.id ?? "e1",
|
|
18
|
+
artifactId: A1,
|
|
19
|
+
version: overrides.version ?? 1,
|
|
20
|
+
format: overrides.format ?? "pdf",
|
|
21
|
+
requestedBy: USER,
|
|
22
|
+
createdAt: AT,
|
|
23
|
+
});
|
|
24
|
+
export function artifactExportStoreConformance(make) {
|
|
25
|
+
describe("ArtifactExportStore conformance", () => {
|
|
26
|
+
const open = async () => {
|
|
27
|
+
const fixture = openFixture(await make());
|
|
28
|
+
// Both tenants, because every isolation case below needs the parent to exist on both sides — otherwise
|
|
29
|
+
// a cross-tenant write would fail on the foreign key and look like isolation working.
|
|
30
|
+
for (const tenantId of [T1, T2]) {
|
|
31
|
+
await fixture.seedArtifact?.({
|
|
32
|
+
tenantId,
|
|
33
|
+
artifactId: A1,
|
|
34
|
+
conversationId: asId("conf-export-convo-1"),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return fixture.store;
|
|
38
|
+
};
|
|
39
|
+
it("claims a slot and records it pending", async () => {
|
|
40
|
+
const store = await open();
|
|
41
|
+
const result = await store.claim({ tenantId: T1, export: requested() });
|
|
42
|
+
expect(result.claimed).toBe(true);
|
|
43
|
+
expect(result.export).toMatchObject({ id: "e1", version: 1, format: "pdf", state: "pending" });
|
|
44
|
+
});
|
|
45
|
+
it("lets exactly one of two racing claims win, and hands the loser the winner's row", async () => {
|
|
46
|
+
// The property the whole port exists for. Both callers then read the same row, which is why the loser
|
|
47
|
+
// gets it rather than an error: its next move is identical either way.
|
|
48
|
+
const store = await open();
|
|
49
|
+
const results = await Promise.all([
|
|
50
|
+
store.claim({ tenantId: T1, export: requested({ id: "race-a" }) }),
|
|
51
|
+
store.claim({ tenantId: T1, export: requested({ id: "race-b" }) }),
|
|
52
|
+
]);
|
|
53
|
+
expect(results.filter((r) => r.claimed)).toHaveLength(1);
|
|
54
|
+
const winner = results.find((r) => r.claimed).export;
|
|
55
|
+
const loser = results.find((r) => !r.claimed).export;
|
|
56
|
+
expect(loser.id).toBe(winner.id);
|
|
57
|
+
});
|
|
58
|
+
it("treats a different format as a different slot", async () => {
|
|
59
|
+
const store = await open();
|
|
60
|
+
await store.claim({ tenantId: T1, export: requested({ id: "e1", format: "pdf" }) });
|
|
61
|
+
const second = await store.claim({ tenantId: T1, export: requested({ id: "e2", format: "markdown" }) });
|
|
62
|
+
expect(second.claimed).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it("treats a different version as a different slot", async () => {
|
|
65
|
+
// An export is of a *version*. Sharing one across versions would hand someone last week's document.
|
|
66
|
+
const store = await open();
|
|
67
|
+
await store.claim({ tenantId: T1, export: requested({ id: "e1", version: 1 }) });
|
|
68
|
+
const second = await store.claim({ tenantId: T1, export: requested({ id: "e2", version: 2 }) });
|
|
69
|
+
expect(second.claimed).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
it("does not share a slot across tenants", async () => {
|
|
72
|
+
const store = await open();
|
|
73
|
+
await store.claim({ tenantId: T1, export: requested({ id: "e1" }) });
|
|
74
|
+
const other = await store.claim({ tenantId: T2, export: requested({ id: "e2" }) });
|
|
75
|
+
expect(other.claimed).toBe(true);
|
|
76
|
+
expect(await store.get({ tenantId: T2, id: "e1" })).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
it("records a rendered outcome with its file and checksum", async () => {
|
|
79
|
+
const store = await open();
|
|
80
|
+
await store.claim({ tenantId: T1, export: requested() });
|
|
81
|
+
expect(await store.complete({
|
|
82
|
+
tenantId: T1,
|
|
83
|
+
id: "e1",
|
|
84
|
+
state: "rendered",
|
|
85
|
+
fileId: asId("file-1"),
|
|
86
|
+
byteSize: 2048,
|
|
87
|
+
checksum: "abc",
|
|
88
|
+
at: AT,
|
|
89
|
+
})).toEqual({ recorded: true });
|
|
90
|
+
expect(await store.get({ tenantId: T1, id: "e1" })).toMatchObject({
|
|
91
|
+
state: "rendered",
|
|
92
|
+
fileId: "file-1",
|
|
93
|
+
byteSize: 2048,
|
|
94
|
+
checksum: "abc",
|
|
95
|
+
renderedAt: AT,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
it("records a failure with its reason and message", async () => {
|
|
99
|
+
// AC-4 depends on both surviving storage: the reason drives behaviour and the message is what the user
|
|
100
|
+
// reads, so a store keeping one and dropping the other would half-work.
|
|
101
|
+
const store = await open();
|
|
102
|
+
await store.claim({ tenantId: T1, export: requested() });
|
|
103
|
+
await store.complete({
|
|
104
|
+
tenantId: T1,
|
|
105
|
+
id: "e1",
|
|
106
|
+
state: "failed",
|
|
107
|
+
failureReason: "render-failed",
|
|
108
|
+
failureMessage: "That artifact could not be rendered.",
|
|
109
|
+
at: AT,
|
|
110
|
+
});
|
|
111
|
+
expect(await store.get({ tenantId: T1, id: "e1" })).toMatchObject({
|
|
112
|
+
state: "failed",
|
|
113
|
+
failureReason: "render-failed",
|
|
114
|
+
failureMessage: "That artifact could not be rendered.",
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
it("does not complete another tenant's export", async () => {
|
|
118
|
+
const store = await open();
|
|
119
|
+
await store.claim({ tenantId: T1, export: requested() });
|
|
120
|
+
// Reported rather than thrown: it is the same answer a deleted row gives, and a worker must be able to
|
|
121
|
+
// tell "gone" from "broken".
|
|
122
|
+
expect(await store.complete({ tenantId: T2, id: "e1", state: "rendered", fileId: asId("f"), at: AT })).toEqual({ recorded: false });
|
|
123
|
+
expect(await store.get({ tenantId: T1, id: "e1" })).toMatchObject({ state: "pending" });
|
|
124
|
+
});
|
|
125
|
+
it("finds an export by version and format, which is the cache lookup", async () => {
|
|
126
|
+
const store = await open();
|
|
127
|
+
await store.claim({ tenantId: T1, export: requested({ id: "e1" }) });
|
|
128
|
+
expect(await store.find({ tenantId: T1, artifactId: A1, version: 1, format: "pdf" })).toMatchObject({
|
|
129
|
+
id: "e1",
|
|
130
|
+
});
|
|
131
|
+
expect(await store.find({ tenantId: T1, artifactId: A1, version: 1, format: "markdown" })).toBeNull();
|
|
132
|
+
expect(await store.find({ tenantId: T2, artifactId: A1, version: 1, format: "pdf" })).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
it("lists an artifact's exports, paging on a keyset cursor", async () => {
|
|
135
|
+
// Two formats requested together share a timestamp, so a cursor on the timestamp alone would skip or
|
|
136
|
+
// repeat -- which is the normal case here rather than a rare one.
|
|
137
|
+
const store = await open();
|
|
138
|
+
for (const n of [1, 2, 3, 4, 5]) {
|
|
139
|
+
await store.claim({
|
|
140
|
+
tenantId: T1,
|
|
141
|
+
export: { ...requested({ id: `e${n}`, version: n }), createdAt: AT },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const seen = [];
|
|
145
|
+
let cursor;
|
|
146
|
+
do {
|
|
147
|
+
const page = await store.listByArtifact({
|
|
148
|
+
tenantId: T1,
|
|
149
|
+
artifactId: A1,
|
|
150
|
+
limit: 2,
|
|
151
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
152
|
+
});
|
|
153
|
+
seen.push(...page.items.map((e) => e.id));
|
|
154
|
+
cursor = page.nextCursor;
|
|
155
|
+
} while (cursor !== undefined);
|
|
156
|
+
expect(seen.sort()).toEqual(["e1", "e2", "e3", "e4", "e5"]);
|
|
157
|
+
});
|
|
158
|
+
it("lists nothing for another tenant", async () => {
|
|
159
|
+
const store = await open();
|
|
160
|
+
await store.claim({ tenantId: T1, export: requested() });
|
|
161
|
+
expect((await store.listByArtifact({ tenantId: T2, artifactId: A1, limit: 10 })).items).toEqual([]);
|
|
162
|
+
});
|
|
163
|
+
it("rejects a non-timestamp `at` rather than storing it", async () => {
|
|
164
|
+
// The lesson from #129: a reference adapter laxer than the real one turns a production write failure
|
|
165
|
+
// into a passing test.
|
|
166
|
+
const store = await open();
|
|
167
|
+
await store.claim({ tenantId: T1, export: requested() });
|
|
168
|
+
await expect(store.complete({ tenantId: T1, id: "e1", state: "rendered", fileId: asId("f"), at: "t" })).rejects.toThrow();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=artifact-exports.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ArtifactStore` conformance (#133) — AC-6.
|
|
3
|
+
*
|
|
4
|
+
* The cases are mostly about **versioning**, because that is the part where two adapters can plausibly
|
|
5
|
+
* disagree and where disagreeing is expensive: an earlier version that stops resolving is a shared link that
|
|
6
|
+
* 404s, and a race that silently collapses two regenerations into one is data loss that looks like success.
|
|
7
|
+
*/
|
|
8
|
+
import type { ArtifactStore } from "../../persistence/index.js";
|
|
9
|
+
import { type FixtureOrStore } from "./parents.js";
|
|
10
|
+
export declare function artifactStoreConformance(make: () => FixtureOrStore<ArtifactStore> | Promise<FixtureOrStore<ArtifactStore>>): void;
|
|
11
|
+
//# sourceMappingURL=artifacts.d.ts.map
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ArtifactStore` conformance (#133) — AC-6.
|
|
3
|
+
*
|
|
4
|
+
* The cases are mostly about **versioning**, because that is the part where two adapters can plausibly
|
|
5
|
+
* disagree and where disagreeing is expensive: an earlier version that stops resolving is a shared link that
|
|
6
|
+
* 404s, and a race that silently collapses two regenerations into one is data loss that looks like success.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { asId } from "../../core/ids.js";
|
|
10
|
+
import { withConversation } from "./parents.js";
|
|
11
|
+
const T1 = asId("conf-artifact-tenant-1");
|
|
12
|
+
const T2 = asId("conf-artifact-tenant-2");
|
|
13
|
+
const C1 = asId("conf-artifact-convo-1");
|
|
14
|
+
const C2 = asId("conf-artifact-convo-2");
|
|
15
|
+
const AT = "2026-08-23T12:00:00.000Z";
|
|
16
|
+
const USER = asId("conf-artifact-user");
|
|
17
|
+
const version = (overrides = {}) => ({
|
|
18
|
+
id: asId(`v-${Math.abs(hash(JSON.stringify(overrides)))}`),
|
|
19
|
+
contentRef: asId("blob-1"),
|
|
20
|
+
byteSize: 128,
|
|
21
|
+
provenance: { producedBy: "create_artifact", inputs: { topic: "q3" } },
|
|
22
|
+
createdBy: USER,
|
|
23
|
+
createdAt: AT,
|
|
24
|
+
...overrides,
|
|
25
|
+
});
|
|
26
|
+
/** Deterministic, because `Math.random` in a fixture makes a failure unreproducible. */
|
|
27
|
+
const hash = (s) => {
|
|
28
|
+
let h = 0;
|
|
29
|
+
for (const ch of s)
|
|
30
|
+
h = (h * 31 + ch.charCodeAt(0)) | 0;
|
|
31
|
+
return h;
|
|
32
|
+
};
|
|
33
|
+
const artifact = (overrides = {}) => ({
|
|
34
|
+
id: asId(overrides.id ?? "a1"),
|
|
35
|
+
conversationId: overrides.conversationId ?? C1,
|
|
36
|
+
kind: "markdown",
|
|
37
|
+
name: overrides.name ?? "Q3 summary",
|
|
38
|
+
createdAt: AT,
|
|
39
|
+
});
|
|
40
|
+
export function artifactStoreConformance(make) {
|
|
41
|
+
describe("ArtifactStore conformance", () => {
|
|
42
|
+
const open = async (conversations = [C1]) => withConversation(await make(), conversations.flatMap((conversationId) => [
|
|
43
|
+
{ tenantId: T1, conversationId },
|
|
44
|
+
{ tenantId: T2, conversationId },
|
|
45
|
+
]));
|
|
46
|
+
it("creates an artifact at version 1 with its content by reference", async () => {
|
|
47
|
+
// AC-1 and AC-5 together: the row carries a ref, and there is no field content could live in.
|
|
48
|
+
const store = await open();
|
|
49
|
+
const created = await store.create({
|
|
50
|
+
tenantId: T1,
|
|
51
|
+
artifact: artifact(),
|
|
52
|
+
version: version({ id: asId("v1"), contentRef: asId("blob-a") }),
|
|
53
|
+
});
|
|
54
|
+
expect(created).toMatchObject({ id: "a1", name: "Q3 summary", kind: "markdown", latestVersion: 1 });
|
|
55
|
+
const v1 = await store.getVersion({ tenantId: T1, id: asId("a1") });
|
|
56
|
+
expect(v1).toMatchObject({ version: 1, contentRef: "blob-a", byteSize: 128 });
|
|
57
|
+
// Nothing resembling content on the version row itself.
|
|
58
|
+
expect(Object.keys(v1 ?? {})).not.toContain("content");
|
|
59
|
+
});
|
|
60
|
+
it("refuses to create the same id twice", async () => {
|
|
61
|
+
// Overwriting would repoint a name at different content while leaving the old versions attached to it.
|
|
62
|
+
const store = await open();
|
|
63
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
64
|
+
await expect(store.create({ tenantId: T1, artifact: artifact(), version: version() })).rejects.toMatchObject({ code: "conflict" });
|
|
65
|
+
});
|
|
66
|
+
it("returns null for another tenant's artifact", async () => {
|
|
67
|
+
// Indistinguishable from absent, so the endpoint cannot be used to probe which ids exist.
|
|
68
|
+
const store = await open();
|
|
69
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
70
|
+
expect(await store.get({ tenantId: T2, id: asId("a1") })).toBeNull();
|
|
71
|
+
expect(await store.getVersion({ tenantId: T2, id: asId("a1") })).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
it("makes a new version the default and keeps the earlier one resolvable", async () => {
|
|
74
|
+
// AC-2, both halves. The second half is the one that breaks quietly: an implementation that overwrote
|
|
75
|
+
// would pass every assertion about the *new* version.
|
|
76
|
+
const store = await open();
|
|
77
|
+
await store.create({
|
|
78
|
+
tenantId: T1,
|
|
79
|
+
artifact: artifact(),
|
|
80
|
+
version: version({ id: asId("v1"), contentRef: asId("blob-1") }),
|
|
81
|
+
});
|
|
82
|
+
const added = await store.addVersion({
|
|
83
|
+
tenantId: T1,
|
|
84
|
+
id: asId("a1"),
|
|
85
|
+
expectedLatestVersion: 1,
|
|
86
|
+
version: version({
|
|
87
|
+
id: asId("v2"),
|
|
88
|
+
contentRef: asId("blob-2"),
|
|
89
|
+
createdAt: "2026-08-23T13:00:00.000Z",
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
expect(added).toEqual({ added: true, version: 2 });
|
|
93
|
+
expect(await store.get({ tenantId: T1, id: asId("a1") })).toMatchObject({ latestVersion: 2 });
|
|
94
|
+
// No version asked for: the current one.
|
|
95
|
+
expect(await store.getVersion({ tenantId: T1, id: asId("a1") })).toMatchObject({
|
|
96
|
+
version: 2,
|
|
97
|
+
contentRef: "blob-2",
|
|
98
|
+
});
|
|
99
|
+
// Asked for by number: still there, still pointing at its own content.
|
|
100
|
+
expect(await store.getVersion({ tenantId: T1, id: asId("a1"), version: 1 })).toMatchObject({
|
|
101
|
+
version: 1,
|
|
102
|
+
contentRef: "blob-1",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it("lets exactly one of two racing regenerations win", async () => {
|
|
106
|
+
// Both hold `expectedLatestVersion: 1`. Without the compare both become version 2 and one silently
|
|
107
|
+
// replaces the other -- which is AC-2 failing in the way nobody notices.
|
|
108
|
+
const store = await open();
|
|
109
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
110
|
+
const results = await Promise.all([
|
|
111
|
+
store.addVersion({
|
|
112
|
+
tenantId: T1,
|
|
113
|
+
id: asId("a1"),
|
|
114
|
+
expectedLatestVersion: 1,
|
|
115
|
+
version: version({ id: asId("race-a") }),
|
|
116
|
+
}),
|
|
117
|
+
store.addVersion({
|
|
118
|
+
tenantId: T1,
|
|
119
|
+
id: asId("a1"),
|
|
120
|
+
expectedLatestVersion: 1,
|
|
121
|
+
version: version({ id: asId("race-b") }),
|
|
122
|
+
}),
|
|
123
|
+
]);
|
|
124
|
+
expect(results.filter((r) => r.added)).toHaveLength(1);
|
|
125
|
+
expect(await store.get({ tenantId: T1, id: asId("a1") })).toMatchObject({ latestVersion: 2 });
|
|
126
|
+
});
|
|
127
|
+
it("refuses a version whose expected latest is stale", async () => {
|
|
128
|
+
const store = await open();
|
|
129
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
130
|
+
const result = await store.addVersion({
|
|
131
|
+
tenantId: T1,
|
|
132
|
+
id: asId("a1"),
|
|
133
|
+
// Wrong on purpose: a caller working from a read that has since moved on.
|
|
134
|
+
expectedLatestVersion: 7,
|
|
135
|
+
version: version({ id: asId("stale") }),
|
|
136
|
+
});
|
|
137
|
+
expect(result).toEqual({ added: false });
|
|
138
|
+
expect(await store.get({ tenantId: T1, id: asId("a1") })).toMatchObject({ latestVersion: 1 });
|
|
139
|
+
});
|
|
140
|
+
it("does not add a version to another tenant's artifact", async () => {
|
|
141
|
+
const store = await open();
|
|
142
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
143
|
+
const result = await store.addVersion({
|
|
144
|
+
tenantId: T2,
|
|
145
|
+
id: asId("a1"),
|
|
146
|
+
expectedLatestVersion: 1,
|
|
147
|
+
version: version({ id: asId("cross") }),
|
|
148
|
+
});
|
|
149
|
+
expect(result).toEqual({ added: false });
|
|
150
|
+
expect(await store.get({ tenantId: T1, id: asId("a1") })).toMatchObject({ latestVersion: 1 });
|
|
151
|
+
});
|
|
152
|
+
it("records the conversation, run and inputs that produced a version", async () => {
|
|
153
|
+
// AC-3. Every field, because provenance is stored as one JSON value and a mapper that dropped part of
|
|
154
|
+
// it would still return a plausible record.
|
|
155
|
+
const store = await open();
|
|
156
|
+
await store.create({
|
|
157
|
+
tenantId: T1,
|
|
158
|
+
artifact: artifact(),
|
|
159
|
+
version: version({
|
|
160
|
+
id: asId("v1"),
|
|
161
|
+
provenance: {
|
|
162
|
+
runId: asId("run-7"),
|
|
163
|
+
producedBy: "summarize_document",
|
|
164
|
+
inputs: { fileId: "file-3", sections: ["revenue"] },
|
|
165
|
+
sourceFileIds: [asId("file-3")],
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
const v1 = await store.getVersion({ tenantId: T1, id: asId("a1") });
|
|
170
|
+
// The conversation comes from the artifact -- it owns the artifact, so duplicating it per version would
|
|
171
|
+
// be a second place for it to disagree.
|
|
172
|
+
expect(await store.get({ tenantId: T1, id: asId("a1") })).toMatchObject({
|
|
173
|
+
conversationId: C1,
|
|
174
|
+
});
|
|
175
|
+
expect(v1?.provenance).toEqual({
|
|
176
|
+
runId: "run-7",
|
|
177
|
+
producedBy: "summarize_document",
|
|
178
|
+
inputs: { fileId: "file-3", sections: ["revenue"] },
|
|
179
|
+
sourceFileIds: ["file-3"],
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
it("lists a conversation's artifacts, newest cursor last", async () => {
|
|
183
|
+
const store = await open([C1, C2]);
|
|
184
|
+
await store.create({
|
|
185
|
+
tenantId: T1,
|
|
186
|
+
artifact: artifact({ id: "a1", name: "first" }),
|
|
187
|
+
version: version({ id: asId("v-a1") }),
|
|
188
|
+
});
|
|
189
|
+
await store.create({
|
|
190
|
+
tenantId: T1,
|
|
191
|
+
artifact: artifact({ id: "a2", name: "second" }),
|
|
192
|
+
version: version({ id: asId("v-a2"), createdAt: "2026-08-23T13:00:00.000Z" }),
|
|
193
|
+
});
|
|
194
|
+
await store.create({
|
|
195
|
+
tenantId: T1,
|
|
196
|
+
artifact: artifact({ id: "a3", conversationId: C2, name: "elsewhere" }),
|
|
197
|
+
version: version({ id: asId("v-a3") }),
|
|
198
|
+
});
|
|
199
|
+
const page = await store.listByConversation({ tenantId: T1, conversationId: C1, limit: 10 });
|
|
200
|
+
expect(page.items.map((a) => a.id).sort()).toEqual(["a1", "a2"]);
|
|
201
|
+
});
|
|
202
|
+
it("pages a listing on a keyset cursor rather than an offset", async () => {
|
|
203
|
+
// An offset cursor shifts when a row is inserted, so a caller paging while an artifact is created
|
|
204
|
+
// either sees one twice or misses one.
|
|
205
|
+
const store = await open();
|
|
206
|
+
for (const n of [1, 2, 3, 4, 5]) {
|
|
207
|
+
await store.create({
|
|
208
|
+
tenantId: T1,
|
|
209
|
+
artifact: { ...artifact({ id: `a${n}` }), createdAt: `2026-08-23T1${n}:00:00.000Z` },
|
|
210
|
+
version: version({ id: asId(`v-a${n}`), createdAt: `2026-08-23T1${n}:00:00.000Z` }),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const seen = [];
|
|
214
|
+
let cursor;
|
|
215
|
+
do {
|
|
216
|
+
const page = await store.listByConversation({
|
|
217
|
+
tenantId: T1,
|
|
218
|
+
conversationId: C1,
|
|
219
|
+
limit: 2,
|
|
220
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
221
|
+
});
|
|
222
|
+
seen.push(...page.items.map((a) => a.id));
|
|
223
|
+
cursor = page.nextCursor;
|
|
224
|
+
} while (cursor !== undefined);
|
|
225
|
+
expect(seen).toEqual(["a1", "a2", "a3", "a4", "a5"]);
|
|
226
|
+
});
|
|
227
|
+
it("lists every version in order, oldest first", async () => {
|
|
228
|
+
// The history a restore reads. Order matters more here than anywhere else in this port.
|
|
229
|
+
const store = await open();
|
|
230
|
+
await store.create({
|
|
231
|
+
tenantId: T1,
|
|
232
|
+
artifact: artifact(),
|
|
233
|
+
version: version({ id: asId("v1") }),
|
|
234
|
+
});
|
|
235
|
+
for (const n of [2, 3, 4]) {
|
|
236
|
+
await store.addVersion({
|
|
237
|
+
tenantId: T1,
|
|
238
|
+
id: asId("a1"),
|
|
239
|
+
expectedLatestVersion: n - 1,
|
|
240
|
+
// Every version created in the same millisecond: a timestamp keyset would tie, and the version
|
|
241
|
+
// number cannot.
|
|
242
|
+
version: version({ id: asId(`v${n}`), createdAt: AT }),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const seen = [];
|
|
246
|
+
let cursor;
|
|
247
|
+
do {
|
|
248
|
+
const page = await store.listVersions({
|
|
249
|
+
tenantId: T1,
|
|
250
|
+
id: asId("a1"),
|
|
251
|
+
limit: 2,
|
|
252
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
253
|
+
});
|
|
254
|
+
seen.push(...page.items.map((v) => v.version));
|
|
255
|
+
cursor = page.nextCursor;
|
|
256
|
+
} while (cursor !== undefined);
|
|
257
|
+
expect(seen).toEqual([1, 2, 3, 4]);
|
|
258
|
+
});
|
|
259
|
+
it("returns no versions for another tenant's artifact", async () => {
|
|
260
|
+
const store = await open();
|
|
261
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
262
|
+
expect((await store.listVersions({ tenantId: T2, id: asId("a1"), limit: 10 })).items).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
it("hides a soft-deleted artifact from the conversation listing but keeps it resolvable", async () => {
|
|
265
|
+
// Both halves. The row is kept precisely so a shared link resolves to "deleted" rather than to nothing,
|
|
266
|
+
// and a listing that still showed it would be a deleted document coming back.
|
|
267
|
+
const store = await open();
|
|
268
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
269
|
+
expect(await store.softDelete({ tenantId: T1, id: asId("a1"), at: AT })).toEqual({
|
|
270
|
+
deleted: true,
|
|
271
|
+
});
|
|
272
|
+
expect((await store.listByConversation({ tenantId: T1, conversationId: C1, limit: 10 })).items).toEqual([]);
|
|
273
|
+
expect(await store.get({ tenantId: T1, id: asId("a1") })).toMatchObject({ deletedAt: AT });
|
|
274
|
+
});
|
|
275
|
+
it("keeps the first deletion timestamp when deleted again", async () => {
|
|
276
|
+
// "When was this deleted" must not move every time someone clicks again.
|
|
277
|
+
const store = await open();
|
|
278
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
279
|
+
await store.softDelete({ tenantId: T1, id: asId("a1"), at: AT });
|
|
280
|
+
await store.softDelete({
|
|
281
|
+
tenantId: T1,
|
|
282
|
+
id: asId("a1"),
|
|
283
|
+
at: "2026-08-24T12:00:00.000Z",
|
|
284
|
+
});
|
|
285
|
+
expect(await store.get({ tenantId: T1, id: asId("a1") })).toMatchObject({ deletedAt: AT });
|
|
286
|
+
});
|
|
287
|
+
it("does not delete another tenant's artifact", async () => {
|
|
288
|
+
const store = await open();
|
|
289
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
290
|
+
expect(await store.softDelete({ tenantId: T2, id: asId("a1"), at: AT })).toEqual({
|
|
291
|
+
deleted: false,
|
|
292
|
+
});
|
|
293
|
+
expect((await store.get({ tenantId: T1, id: asId("a1") }))?.deletedAt).toBeUndefined();
|
|
294
|
+
});
|
|
295
|
+
it("adds no version to a deleted artifact", async () => {
|
|
296
|
+
// A new version would resurrect it in every listing that filters through the artifact row.
|
|
297
|
+
const store = await open();
|
|
298
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
299
|
+
await store.softDelete({ tenantId: T1, id: asId("a1"), at: AT });
|
|
300
|
+
expect(await store.addVersion({
|
|
301
|
+
tenantId: T1,
|
|
302
|
+
id: asId("a1"),
|
|
303
|
+
expectedLatestVersion: 1,
|
|
304
|
+
version: version({ id: asId("after-delete") }),
|
|
305
|
+
})).toEqual({ added: false });
|
|
306
|
+
});
|
|
307
|
+
it("rejects a non-timestamp `at` rather than storing it", async () => {
|
|
308
|
+
// The lesson from #129: a reference adapter laxer than the real one turns a production write failure
|
|
309
|
+
// into a passing test.
|
|
310
|
+
const store = await open();
|
|
311
|
+
await store.create({ tenantId: T1, artifact: artifact(), version: version() });
|
|
312
|
+
await expect(store.softDelete({ tenantId: T1, id: asId("a1"), at: "t" })).rejects.toThrow();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
//# sourceMappingURL=artifacts.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
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 type { SpeechProvider, TranscriptionProvider } from "../../audio/index.js";
|
|
30
|
+
/** Every key a transcription result must carry. A provider cannot quietly return less. */
|
|
31
|
+
export declare const TRANSCRIPTION_CONTRACT_KEYS: readonly ["text", "truncated"];
|
|
32
|
+
/** And a speech result. `durationSeconds` is optional on both — absent is a legitimate answer. */
|
|
33
|
+
export declare const SPEECH_CONTRACT_KEYS: readonly ["audio", "mediaType"];
|
|
34
|
+
export declare function transcriptionProviderConformance(make: () => TranscriptionProvider): void;
|
|
35
|
+
export declare function speechProviderConformance(make: () => SpeechProvider): void;
|
|
36
|
+
/** The transcript bounding, on its own — shared by both suites and worth asserting once. */
|
|
37
|
+
export declare function transcriptBoundingConformance(): void;
|
|
38
|
+
//# sourceMappingURL=audio.d.ts.map
|