@affiant/core 0.1.0-alpha.0 → 0.1.0-alpha.2

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.
@@ -0,0 +1,214 @@
1
+ /**
2
+ * The store contract — every assertion a Docket store has to pass, written once and
3
+ * parametrised over the store under test.
4
+ *
5
+ * `@affiant/core` ships an in-memory reference store, and a host running on a
6
+ * database ships its own. The two are only interchangeable if they are measured by
7
+ * the same assertions, so the assertions live here rather than in the reference
8
+ * store's own suite: {@link runDocketStoreContract} and
9
+ * {@link runSessionStoreContract} register them against whatever factory they are
10
+ * handed, and the reference store is simply the first caller.
11
+ *
12
+ * ```ts
13
+ * import { describe, it, expect, beforeAll, afterAll } from "vitest";
14
+ * import { InMemoryDocketStore } from "@affiant/core/store-memory";
15
+ * import { runDocketStoreContract } from "@affiant/core/testing";
16
+ *
17
+ * runDocketStoreContract((clock) => new InMemoryDocketStore({ clock }), {
18
+ * api: { describe, it, expect, beforeAll, afterAll },
19
+ * });
20
+ * ```
21
+ *
22
+ * **The test runner comes in as a parameter.** Nothing here imports `vitest`, and
23
+ * `@affiant/core` gains no dependency — runtime, peer or optional — from carrying
24
+ * the contract. The caller passes the four functions it already has in scope, which
25
+ * is also what lets a store built on another runner run the same assertions.
26
+ *
27
+ * **A store is built once per block, and every case owns a tenant.** The factory is
28
+ * called from the block's `beforeAll`, because a store backed by a database is
29
+ * expensive to build and a fresh instance would not give a fresh database anyway.
30
+ * Isolation comes from tenancy instead: each case files under a tenant id of its
31
+ * own, which is a property the contract requires regardless (AZ-2). A case that
32
+ * needs a second tenant is handed one.
33
+ *
34
+ * **The clock is the case's.** A store is built with a {@link Clock} rather than
35
+ * handed an instant per call, so the harness owns a settable one, hands it to the
36
+ * factory, and resets it before every case.
37
+ *
38
+ * Rules the cases serve: DK-1 (idempotent filing, the guarded compare-and-set,
39
+ * expiry as queryable state, preserved amendments, execution recorded once,
40
+ * lineage), DK-2 (an amendment's `null` clears a field and an absent key leaves it
41
+ * alone), DK-3 (a bounded, paged, host-scheduled sweep and an opaque cursor on
42
+ * every list), DK-4 (retention, purge and export as hooks, and a row that reads
43
+ * forward), DK-5 (rehydration order), AZ-2 (a wrong-tenant lookup is a miss),
44
+ * AZ-5 (an approved write nobody has reported on is never aged out), GT-4 (a
45
+ * re-file never refreshes the deadline).
46
+ *
47
+ * @packageDocumentation
48
+ */
49
+ import type { DocketEntry, NewEntryInit } from "./docket/entry.js";
50
+ import type { DocketStore, Scope, SessionStore } from "./docket/store.js";
51
+ import type { Affidavit, AffidavitField, JsonValue } from "./model/affidavit.js";
52
+ import type { Clock } from "./ports.js";
53
+ /**
54
+ * The matchers the contract uses.
55
+ *
56
+ * Declared here rather than imported so that this module names no test runner. The
57
+ * shape is a subset of what `vitest`, `jest` and `bun:test` all expose, so passing
58
+ * any of their `expect` functions satisfies it.
59
+ */
60
+ export interface ContractMatchers {
61
+ toBe(expected: unknown): void;
62
+ toEqual(expected: unknown): void;
63
+ toBeNull(): void;
64
+ toHaveLength(length: number): void;
65
+ toContain(item: unknown): void;
66
+ }
67
+ /** One assertion, with its negation and its rejection form. */
68
+ export interface ContractAssertion extends ContractMatchers {
69
+ /** The same matchers, inverted. */
70
+ readonly not: ContractMatchers;
71
+ /** Assertions about a rejected promise. */
72
+ readonly rejects: {
73
+ toThrow(expected?: ErrorConstructor): Promise<unknown>;
74
+ };
75
+ }
76
+ /** The `expect` function the contract is written against. */
77
+ export interface ContractExpect {
78
+ (actual: unknown, message?: string): ContractAssertion;
79
+ }
80
+ /**
81
+ * The test runner's own API, handed in by the caller.
82
+ *
83
+ * Every member is declared as a method so that a runner's richer signature — a
84
+ * `describe` that also carries `.skip`, an `it` that takes a timeout — is accepted
85
+ * as it stands.
86
+ */
87
+ export interface ContractRunnerApi {
88
+ describe(name: string, fn: () => void): void;
89
+ it(name: string, fn: () => Promise<void> | void): void;
90
+ beforeAll(fn: () => Promise<void> | void): void;
91
+ afterAll(fn: () => Promise<void> | void): void;
92
+ expect: ContractExpect;
93
+ }
94
+ /** How a contract run is configured. */
95
+ export interface StoreContractOptions<TStore, TSection extends string> {
96
+ /** The test runner's `describe`, `it`, `beforeAll`, `afterAll` and `expect`. */
97
+ readonly api: ContractRunnerApi;
98
+ /** A label put in front of every block, when one store runs the contract twice. */
99
+ readonly name?: string;
100
+ /**
101
+ * Case ids not to register, each of which must be one this contract defines — an
102
+ * id nothing matches is a {@link RangeError}, because a skip that silently matches
103
+ * nothing is a case a store stopped running and nobody noticed.
104
+ */
105
+ readonly skip?: readonly string[];
106
+ /**
107
+ * The sections to register, defaulting to all of them. Same rule as `skip`: a
108
+ * section name this contract does not define is a {@link RangeError}.
109
+ */
110
+ readonly sections?: readonly TSection[];
111
+ /**
112
+ * Released in the block's `afterAll`, once per store the factory produced — where
113
+ * a store holds a database handle its owner has to close.
114
+ */
115
+ readonly dispose?: (store: TStore) => void | Promise<void>;
116
+ }
117
+ /** Builds the store under test on the harness's own clock. */
118
+ export type DocketStoreFactory = (clock: Clock) => DocketStore | Promise<DocketStore>;
119
+ /** Builds a store that is both a Docket and the rehydration surface over it. */
120
+ export type SessionStoreFactory = (clock: Clock) => (DocketStore & SessionStore) | Promise<DocketStore & SessionStore>;
121
+ /** A {@link Clock} a test drives by hand, so a deadline can be pinned to the millisecond. */
122
+ export interface StubClock extends Clock {
123
+ /** Move the clock to `instant`. */
124
+ set(instant: string): void;
125
+ }
126
+ /** A {@link Clock} that reads `start` until a test moves it. */
127
+ export declare function stubClock(start: string): StubClock;
128
+ /** One sworn field, filled in enough to be a real Affidavit field. */
129
+ export declare function sampleField(name: string, value: JsonValue): AffidavitField;
130
+ /**
131
+ * An Affidavit over `names`, shaped like something a pipeline would actually file.
132
+ *
133
+ * Built through `withConfidence` rather than as an object literal so the three
134
+ * numbers on it are the ones AF-2 computes, never numbers a test author typed.
135
+ */
136
+ export declare function sampleAffidavit(names?: readonly string[]): Affidavit;
137
+ /** A filed entry with `entryId`, overridable field by field. */
138
+ export declare function sampleEntry(entryId: string, overrides?: Partial<Omit<NewEntryInit, "entryId">>): DocketEntry;
139
+ /** The ids of `entries`, in the order they were returned. */
140
+ export declare function entryIds(entries: readonly DocketEntry[]): string[];
141
+ /**
142
+ * One object that is both a Docket and the rehydration surface over it.
143
+ *
144
+ * A store built on a database implements both interfaces itself; the reference
145
+ * pair is two objects, and this is what hands them to
146
+ * {@link runSessionStoreContract} as one.
147
+ */
148
+ export declare function withSessionStore(docket: DocketStore, sessions: SessionStore): DocketStore & SessionStore;
149
+ /** What one contract case is handed. */
150
+ export interface ContractCaseContext<TStore> {
151
+ /** The store under test, shared with the other cases of the same block. */
152
+ readonly store: TStore;
153
+ /** The harness's clock, reset to a fixed instant before this case. */
154
+ readonly clock: StubClock;
155
+ /** The runner's `expect`. */
156
+ readonly expect: ContractExpect;
157
+ /** This case's own tenant: nothing another case filed is visible in it. */
158
+ readonly scope: Scope;
159
+ /** A second tenant, for the cases that need one. */
160
+ readonly otherScope: Scope;
161
+ /** This case's tenant narrowed to one conversation. */
162
+ conversation(conversationId: string): Scope;
163
+ /** An entry in this case's tenant, overridable field by field. */
164
+ entry(entryId: string, overrides?: Partial<Omit<NewEntryInit, "entryId">>): DocketEntry;
165
+ }
166
+ /** The blocks {@link runDocketStoreContract} registers. */
167
+ export type DocketContractSection = "filing" | "transition" | "deadline" | "execution" | "lineage" | "sweep" | "paging" | "retention" | "purge" | "export" | "tenancy";
168
+ /** The blocks {@link runSessionStoreContract} registers. */
169
+ export type SessionContractSection = "rehydration";
170
+ /** The rehydration sequence DK-5 fixes, for the store the harness was handed. */
171
+ type SessionStoreUnderTest = DocketStore & SessionStore;
172
+ /** Every block {@link runDocketStoreContract} registers, in registration order. */
173
+ export declare const DOCKET_CONTRACT_SECTIONS: readonly DocketContractSection[];
174
+ /** Every block {@link runSessionStoreContract} registers, in registration order. */
175
+ export declare const SESSION_CONTRACT_SECTIONS: readonly SessionContractSection[];
176
+ /**
177
+ * One case of a contract, as a caller sees it from outside.
178
+ *
179
+ * `id` is what `skip` names; `block` and `title` are the two names the case is
180
+ * registered under, which is what lets a caller check that a suite registered the
181
+ * cases it was supposed to rather than that it meant to.
182
+ */
183
+ export interface ContractCaseSummary {
184
+ /** The id `skip` names. Stable across releases. */
185
+ readonly id: string;
186
+ /** The block the case belongs to, which `sections` names. */
187
+ readonly section: string;
188
+ /** The name of the `describe` the case is registered under, without any label. */
189
+ readonly block: string;
190
+ /** The name the case is registered as. */
191
+ readonly title: string;
192
+ }
193
+ /** Every case {@link runDocketStoreContract} registers, in registration order. */
194
+ export declare const DOCKET_CONTRACT_CASES: readonly ContractCaseSummary[];
195
+ /** Every case {@link runSessionStoreContract} registers, in registration order. */
196
+ export declare const SESSION_CONTRACT_CASES: readonly ContractCaseSummary[];
197
+ /**
198
+ * Register the Docket store contract against `factory`.
199
+ *
200
+ * The factory is called once per block with the harness's clock, and may be async —
201
+ * a store backed by a database is built from a connection the caller opened. Pass
202
+ * `dispose` to release it when the block ends.
203
+ */
204
+ export declare function runDocketStoreContract(factory: DocketStoreFactory, options: StoreContractOptions<DocketStore, DocketContractSection>): void;
205
+ /**
206
+ * Register the rehydration contract (DK-5) against `factory`.
207
+ *
208
+ * The factory returns one object that is both the Docket and the rehydration
209
+ * surface over it, because the cases file the rows they then rehydrate. A pair of
210
+ * separate reference objects is joined by {@link withSessionStore}.
211
+ */
212
+ export declare function runSessionStoreContract(factory: SessionStoreFactory, options: StoreContractOptions<SessionStoreUnderTest, SessionContractSection>): void;
213
+ export {};
214
+ //# sourceMappingURL=testing-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing-store.d.ts","sourceRoot":"","sources":["../src/testing-store.ts"],"names":[],"mappings":"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"}