@usagekit/store 0.1.0 → 0.4.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/dist/conformance/admission.js +99 -5
- package/dist/conformance/alerts.d.ts +2 -0
- package/dist/conformance/alerts.js +146 -0
- package/dist/conformance/counters.d.ts +2 -0
- package/dist/conformance/counters.js +159 -0
- package/dist/conformance/operations.d.ts +2 -0
- package/dist/conformance/operations.js +103 -0
- package/dist/conformance/properties.js +12 -5
- package/dist/conformance/reads.js +65 -1
- package/dist/conformance/reserve.js +2 -2
- package/dist/conformance/suite.js +8 -0
- package/dist/conformance/tags.d.ts +2 -0
- package/dist/conformance/tags.js +101 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +2 -0
- package/dist/memory/admission.d.ts +29 -3
- package/dist/memory/admission.js +141 -40
- package/dist/memory/commands.d.ts +1 -1
- package/dist/memory/commands.js +20 -7
- package/dist/memory/counters.d.ts +33 -0
- package/dist/memory/counters.js +113 -0
- package/dist/memory/memory-store.js +7 -1
- package/dist/memory/reads.d.ts +3 -1
- package/dist/memory/reads.js +87 -33
- package/dist/memory/state.d.ts +21 -1
- package/dist/memory/state.js +14 -1
- package/dist/reference.d.ts +2 -2
- package/dist/reference.js +2 -2
- package/package.json +2 -2
|
@@ -17,6 +17,7 @@ export function admissionTests(factory) {
|
|
|
17
17
|
accessCredential: { kind: "oauth_client", id: "k1" },
|
|
18
18
|
},
|
|
19
19
|
{ kind: "platform_pool", namespace: "test", poolId: "p1" },
|
|
20
|
+
{ kind: "tag", namespace: "test", tag: "t1" },
|
|
20
21
|
];
|
|
21
22
|
const candidate = () => input({
|
|
22
23
|
scope: {
|
|
@@ -25,6 +26,7 @@ export function admissionTests(factory) {
|
|
|
25
26
|
group: "g1",
|
|
26
27
|
connection: "c1",
|
|
27
28
|
accessCredential: { kind: "oauth_client", id: "k1" },
|
|
29
|
+
tags: ["t1"],
|
|
28
30
|
},
|
|
29
31
|
fundingSource: "platform",
|
|
30
32
|
platformPools: ["p1"],
|
|
@@ -51,11 +53,61 @@ export function admissionTests(factory) {
|
|
|
51
53
|
test("surface exact and any", async () => {
|
|
52
54
|
f.budgets.push(budget({ surface: "programmatic", limit: quantity(0n) }));
|
|
53
55
|
expect(await f.store.reserve(candidate())).toMatchObject({ outcome: "reserved" });
|
|
54
|
-
expect(await f.store.reserve(
|
|
56
|
+
expect(await f.store.reserve(programmatic("api"))).toMatchObject({ outcome: "exceeded" });
|
|
57
|
+
f.budgets[0].surface = "any";
|
|
58
|
+
expect(await f.store.reserve(candidate())).toMatchObject({ outcome: "exceeded" });
|
|
59
|
+
});
|
|
60
|
+
const programmatic = (source) => ({
|
|
61
|
+
...candidate(),
|
|
62
|
+
surface: "programmatic",
|
|
63
|
+
source,
|
|
64
|
+
});
|
|
65
|
+
test("source budgets on one connection admit independently", async () => {
|
|
66
|
+
const connection = { kind: "connection", namespace: "test", connection: "c1" };
|
|
67
|
+
const cli = budget({ scope: connection, surface: "cli", limit: quantity(1n) }), api = budget({ scope: connection, surface: "api", limit: quantity(1n) });
|
|
68
|
+
f.budgets.push(cli, api);
|
|
69
|
+
expect(await f.store.reserve(programmatic("cli"))).toMatchObject({ outcome: "reserved" });
|
|
70
|
+
expect(await f.store.reserve(programmatic("cli"))).toMatchObject({
|
|
55
71
|
outcome: "exceeded",
|
|
72
|
+
exceeded: { budget: { id: cli.id } },
|
|
56
73
|
});
|
|
57
|
-
f.
|
|
74
|
+
expect(await f.store.reserve(programmatic("api"))).toMatchObject({ outcome: "reserved" });
|
|
75
|
+
expect(await f.store.reserve(programmatic("api"))).toMatchObject({
|
|
76
|
+
outcome: "exceeded",
|
|
77
|
+
exceeded: { budget: { id: api.id } },
|
|
78
|
+
});
|
|
79
|
+
expect(await f.store.reserve(programmatic("mcp"))).toMatchObject({ outcome: "reserved" });
|
|
80
|
+
});
|
|
81
|
+
test("surface budget covers its source group and ignores other sources", async () => {
|
|
82
|
+
f.budgets.push(budget({ surface: "app", limit: quantity(0n) }));
|
|
83
|
+
expect(await f.store.reserve(programmatic("api"))).toMatchObject({ outcome: "reserved" });
|
|
58
84
|
expect(await f.store.reserve(candidate())).toMatchObject({ outcome: "exceeded" });
|
|
85
|
+
expect(await f.store.reserve({ ...candidate(), source: "worker" })).toMatchObject({
|
|
86
|
+
outcome: "exceeded",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
test("source and surface budgets stack; the first exceeded in default order is reported", async () => {
|
|
90
|
+
const principal = budget({ surface: "programmatic", limit: quantity(1n) }), connection = budget({
|
|
91
|
+
scope: { kind: "connection", namespace: "test", connection: "c1" },
|
|
92
|
+
surface: "cli",
|
|
93
|
+
limit: quantity(1n),
|
|
94
|
+
});
|
|
95
|
+
f.budgets.push(connection, principal);
|
|
96
|
+
const first = await f.store.reserve(programmatic("cli"));
|
|
97
|
+
expect(first).toMatchObject({ outcome: "reserved" });
|
|
98
|
+
if (first.outcome !== "reserved")
|
|
99
|
+
throw new Error("fixture");
|
|
100
|
+
expect(first.operation.budgetEpochs.map((e) => e.budgetId).sort()).toEqual([principal.id, connection.id].sort());
|
|
101
|
+
expect(await f.store.reserve(programmatic("cli"))).toMatchObject({
|
|
102
|
+
outcome: "exceeded",
|
|
103
|
+
exceeded: { budget: { id: principal.id } },
|
|
104
|
+
});
|
|
105
|
+
principal.limit = quantity(5n);
|
|
106
|
+
expect(await f.store.reserve(programmatic("cli"))).toMatchObject({
|
|
107
|
+
outcome: "exceeded",
|
|
108
|
+
exceeded: { budget: { id: connection.id } },
|
|
109
|
+
});
|
|
110
|
+
expect(await f.store.reserve(programmatic("sdk"))).toMatchObject({ outcome: "reserved" });
|
|
59
111
|
});
|
|
60
112
|
for (const scope of scopes)
|
|
61
113
|
for (const n of [2, 5, 20])
|
|
@@ -74,14 +126,56 @@ export function admissionTests(factory) {
|
|
|
74
126
|
exceeded: { budget: { id: pool.id } },
|
|
75
127
|
});
|
|
76
128
|
});
|
|
77
|
-
test("
|
|
78
|
-
const b = budget({ onExceed: "
|
|
129
|
+
test("allow admits and reports warnings at the soft limit", async () => {
|
|
130
|
+
const b = budget({ onExceed: "allow", limit: quantity(0n) });
|
|
79
131
|
f.budgets.push(b);
|
|
80
132
|
expect(await f.store.reserve(candidate())).toMatchObject({
|
|
81
133
|
outcome: "reserved",
|
|
82
|
-
warnings: [{ budget: { id: b.id } }],
|
|
134
|
+
warnings: [{ budget: { id: b.id }, boundary: "limit", used: quantity(0n) }],
|
|
83
135
|
});
|
|
84
136
|
});
|
|
137
|
+
test("soft budget warns past limit and blocks past hardLimit", async () => {
|
|
138
|
+
const b = budget({ onExceed: "allow", limit: quantity(1n), hardLimit: quantity(2n) });
|
|
139
|
+
f.budgets.push(b);
|
|
140
|
+
expect(await f.store.reserve(candidate())).toMatchObject({
|
|
141
|
+
outcome: "reserved",
|
|
142
|
+
warnings: [],
|
|
143
|
+
});
|
|
144
|
+
expect(await f.store.reserve(candidate())).toMatchObject({
|
|
145
|
+
outcome: "reserved",
|
|
146
|
+
warnings: [{ budget: { id: b.id }, boundary: "limit", reserved: quantity(1n) }],
|
|
147
|
+
});
|
|
148
|
+
const blocked = await f.store.reserve(candidate());
|
|
149
|
+
expect(blocked).toMatchObject({
|
|
150
|
+
outcome: "exceeded",
|
|
151
|
+
exceeded: { budget: { id: b.id }, boundary: "hardLimit", reserved: quantity(2n) },
|
|
152
|
+
});
|
|
153
|
+
expect(await f.store.reserve({ ...candidate(), estimate: [quantity(0n)] })).toMatchObject({
|
|
154
|
+
outcome: "reserved",
|
|
155
|
+
warnings: [{ boundary: "limit" }],
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
test.each([
|
|
159
|
+
{ name: "hardLimit below limit", onExceed: "allow", limit: 2n, hardLimit: quantity(1n) },
|
|
160
|
+
{ name: "hardLimit equal to limit", onExceed: "allow", limit: 2n, hardLimit: quantity(2n) },
|
|
161
|
+
{ name: "block with hardLimit", onExceed: "block", limit: 1n, hardLimit: quantity(2n) },
|
|
162
|
+
{
|
|
163
|
+
name: "hardLimit in another unit",
|
|
164
|
+
onExceed: "allow",
|
|
165
|
+
limit: 1n,
|
|
166
|
+
hardLimit: quantity(2n, "tokens"),
|
|
167
|
+
},
|
|
168
|
+
{ name: "hardLimit without limit", onExceed: "allow", limit: null, hardLimit: quantity(2n) },
|
|
169
|
+
])("invalid budget definition is a validation failure: $name", async (c) => {
|
|
170
|
+
f.budgets.push(budget({
|
|
171
|
+
onExceed: c.onExceed,
|
|
172
|
+
limit: c.limit === null ? null : quantity(c.limit),
|
|
173
|
+
hardLimit: c.hardLimit,
|
|
174
|
+
}));
|
|
175
|
+
const i = candidate();
|
|
176
|
+
await expect(f.store.reserve(i)).rejects.toThrow("InvalidInput");
|
|
177
|
+
expect(await f.store.getOperation(ref(i))).toBeNull();
|
|
178
|
+
});
|
|
85
179
|
test("current version only; epochs immutable; unbounded still snapshotted", async () => {
|
|
86
180
|
const b = budget({ limit: null });
|
|
87
181
|
f.budgets.push({ ...b, version: 0, limit: quantity(0n) }, b);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { input, budget, quantity, command, receipt, ref } from "./helpers.js";
|
|
3
|
+
export function alertTests(factory) {
|
|
4
|
+
describe("alert thresholds recorded once per epoch", () => {
|
|
5
|
+
let f;
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
f = await factory();
|
|
8
|
+
});
|
|
9
|
+
afterEach(async () => f.close());
|
|
10
|
+
const percent = (n) => ({ at: { percent: n } });
|
|
11
|
+
const reserved = async (estimate) => {
|
|
12
|
+
const r = await f.store.reserve(input({ estimate: [quantity(estimate)] }));
|
|
13
|
+
if (r.outcome !== "reserved")
|
|
14
|
+
throw new Error("fixture");
|
|
15
|
+
return r;
|
|
16
|
+
};
|
|
17
|
+
const settle = async (op, measured) => {
|
|
18
|
+
const g = await f.store.markDispatchIntent({ ...command(op), holder: "h", leaseTtlMs: 1000 });
|
|
19
|
+
if (!("granted" in g) || !g.granted)
|
|
20
|
+
throw new Error("fixture");
|
|
21
|
+
const c = {
|
|
22
|
+
...command(g.operation),
|
|
23
|
+
authority: { kind: "lease", leaseId: g.lease.leaseId },
|
|
24
|
+
receipt: receipt({
|
|
25
|
+
measurements: [{ unit: "requests", certainty: "measured", quantity: quantity(measured) }],
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
const s = await f.store.settle(c);
|
|
29
|
+
if (s.outcome !== "settled")
|
|
30
|
+
throw new Error("fixture");
|
|
31
|
+
return { result: s, replay: () => f.store.settle(c) };
|
|
32
|
+
};
|
|
33
|
+
test("a reserve crossing a threshold reports it once with the projected figures", async () => {
|
|
34
|
+
const b = budget({ limit: quantity(10n), alerts: [percent(80)] });
|
|
35
|
+
f.budgets.push(b);
|
|
36
|
+
const first = await reserved(8n);
|
|
37
|
+
expect(first.alerts).toEqual([
|
|
38
|
+
{
|
|
39
|
+
budgetId: b.id,
|
|
40
|
+
budgetVersion: 1,
|
|
41
|
+
epoch: "2026-09",
|
|
42
|
+
at: { percent: 80 },
|
|
43
|
+
used: quantity(0n),
|
|
44
|
+
reserved: quantity(8n),
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
expect((await reserved(1n)).alerts).toEqual([]);
|
|
48
|
+
expect(await f.store.reserve(input({ estimate: [quantity(9n)] }))).toMatchObject({
|
|
49
|
+
outcome: "exceeded",
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
test("percent and quantity thresholds report in ascending order on one reserve", async () => {
|
|
53
|
+
const b = budget({
|
|
54
|
+
limit: quantity(10n),
|
|
55
|
+
alerts: [percent(50), { at: quantity(7n) }, percent(90)],
|
|
56
|
+
});
|
|
57
|
+
f.budgets.push(b);
|
|
58
|
+
expect((await reserved(8n)).alerts.map((a) => a.at)).toEqual([{ percent: 50 }, quantity(7n)]);
|
|
59
|
+
expect((await reserved(1n)).alerts.map((a) => a.at)).toEqual([{ percent: 90 }]);
|
|
60
|
+
});
|
|
61
|
+
test("a settlement whose measured amount crosses a threshold reports it on settle", async () => {
|
|
62
|
+
const b = budget({ limit: quantity(10n), alerts: [percent(80)] });
|
|
63
|
+
f.budgets.push(b);
|
|
64
|
+
const r = await reserved(5n);
|
|
65
|
+
expect(r.alerts).toEqual([]);
|
|
66
|
+
const { result, replay } = await settle(r.operation, 9n);
|
|
67
|
+
expect(result.alerts).toEqual([
|
|
68
|
+
{
|
|
69
|
+
budgetId: b.id,
|
|
70
|
+
budgetVersion: 1,
|
|
71
|
+
epoch: "2026-09",
|
|
72
|
+
at: { percent: 80 },
|
|
73
|
+
used: quantity(9n),
|
|
74
|
+
reserved: quantity(0n),
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
expect(await replay()).toEqual({ ...result, replayed: true });
|
|
78
|
+
expect((await reserved(1n)).alerts).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
test("a settlement below the estimate reports nothing and later reserves may still cross", async () => {
|
|
81
|
+
const b = budget({ limit: quantity(10n), alerts: [percent(50)] });
|
|
82
|
+
f.budgets.push(b);
|
|
83
|
+
const r = await reserved(4n);
|
|
84
|
+
const { result } = await settle(r.operation, 1n);
|
|
85
|
+
expect(result.alerts).toEqual([]);
|
|
86
|
+
expect((await reserved(4n)).alerts).toHaveLength(1);
|
|
87
|
+
});
|
|
88
|
+
test("concurrent reserves crossing one threshold report it exactly once in total", async () => {
|
|
89
|
+
f.budgets.push(budget({ limit: quantity(100n), alerts: [percent(5)] }));
|
|
90
|
+
const results = await Promise.all(Array.from({ length: 6 }, () => f.store.reserve(input({ estimate: [quantity(6n)] }))));
|
|
91
|
+
const crossings = results.flatMap((r) => (r.outcome === "reserved" ? r.alerts : []));
|
|
92
|
+
expect(results.every((r) => r.outcome === "reserved")).toBe(true);
|
|
93
|
+
expect(crossings).toHaveLength(1);
|
|
94
|
+
});
|
|
95
|
+
test("a new epoch reports the same threshold again", async () => {
|
|
96
|
+
const b = budget({
|
|
97
|
+
limit: quantity(2n),
|
|
98
|
+
alerts: [percent(50)],
|
|
99
|
+
window: { kind: "since_reset", epoch: "e1", startsAt: "2026-09-01T00:00:00.000Z" },
|
|
100
|
+
});
|
|
101
|
+
f.budgets.push(b);
|
|
102
|
+
expect((await reserved(1n)).alerts).toMatchObject([{ epoch: "e1" }]);
|
|
103
|
+
expect((await reserved(1n)).alerts).toEqual([]);
|
|
104
|
+
b.window = { kind: "since_reset", epoch: "e2", startsAt: f.clock.now().toISOString() };
|
|
105
|
+
expect((await reserved(1n)).alerts).toMatchObject([{ epoch: "e2", at: { percent: 50 } }]);
|
|
106
|
+
});
|
|
107
|
+
test("reserve replay returns the original crossings and records nothing new", async () => {
|
|
108
|
+
const b = budget({ limit: quantity(10n), alerts: [percent(50), percent(80)] });
|
|
109
|
+
f.budgets.push(b);
|
|
110
|
+
const i = input({ estimate: [quantity(6n)] });
|
|
111
|
+
const first = await f.store.reserve(i);
|
|
112
|
+
if (first.outcome !== "reserved")
|
|
113
|
+
throw new Error("fixture");
|
|
114
|
+
expect(first.alerts.map((a) => a.at)).toEqual([{ percent: 50 }]);
|
|
115
|
+
expect(await f.store.reserve(i)).toEqual({ ...first, replayed: true });
|
|
116
|
+
expect((await reserved(2n)).alerts.map((a) => a.at)).toEqual([{ percent: 80 }]);
|
|
117
|
+
expect(await f.store.reserve(i)).toEqual({ ...first, replayed: true });
|
|
118
|
+
});
|
|
119
|
+
test("a denied reserve records no crossing", async () => {
|
|
120
|
+
const b = budget({ limit: quantity(10n), alerts: [percent(50)] }), blocker = budget({ limit: quantity(0n) });
|
|
121
|
+
f.budgets.push(b, blocker);
|
|
122
|
+
expect(await f.store.reserve(input({ estimate: [quantity(6n)] }))).toMatchObject({
|
|
123
|
+
outcome: "exceeded",
|
|
124
|
+
exceeded: { budget: { id: blocker.id } },
|
|
125
|
+
});
|
|
126
|
+
blocker.limit = quantity(100n);
|
|
127
|
+
expect((await reserved(6n)).alerts).toHaveLength(1);
|
|
128
|
+
});
|
|
129
|
+
const invalid = [
|
|
130
|
+
{ name: "more than eight", alerts: Array.from({ length: 9 }, (_, n) => percent(n + 10)) },
|
|
131
|
+
{ name: "not ascending", alerts: [percent(80), percent(50)] },
|
|
132
|
+
{ name: "equal thresholds", alerts: [percent(50), { at: quantity(5n) }] },
|
|
133
|
+
{ name: "percent below one", alerts: [percent(0)] },
|
|
134
|
+
{ name: "percent above hundred", alerts: [percent(101)] },
|
|
135
|
+
{ name: "fractional percent", alerts: [percent(12.5)] },
|
|
136
|
+
{ name: "quantity in another unit", alerts: [{ at: quantity(1n, "tokens") }] },
|
|
137
|
+
{ name: "percent without a limit", alerts: [percent(50)], limit: null },
|
|
138
|
+
];
|
|
139
|
+
test.each(invalid)("invalid alert definition is a validation failure: $name", async (c) => {
|
|
140
|
+
f.budgets.push(budget({ limit: c.limit === null ? null : quantity(10n), alerts: c.alerts }));
|
|
141
|
+
const i = input();
|
|
142
|
+
await expect(f.store.reserve(i)).rejects.toThrow("InvalidInput");
|
|
143
|
+
expect(await f.store.getOperation(ref(i))).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { beforeEach, afterEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { budget, input, quantity } from "./helpers.js";
|
|
3
|
+
export function counterTests(factory) {
|
|
4
|
+
describe("request counter", () => {
|
|
5
|
+
let f;
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
f = await factory();
|
|
8
|
+
});
|
|
9
|
+
afterEach(async () => f.close());
|
|
10
|
+
const count = (overrides = {}) => ({
|
|
11
|
+
commandId: crypto.randomUUID(),
|
|
12
|
+
scope: { namespace: "test", principal: "u1", connection: "c1" },
|
|
13
|
+
surface: "programmatic",
|
|
14
|
+
source: "proxy",
|
|
15
|
+
provider: "search",
|
|
16
|
+
operation: "search",
|
|
17
|
+
state: "passthrough",
|
|
18
|
+
...overrides,
|
|
19
|
+
});
|
|
20
|
+
const month = { from: "2026-09-01T00:00:00.000Z", to: "2026-10-01T00:00:00.000Z" };
|
|
21
|
+
const principal = { kind: "principal", namespace: "test", principal: "u1" };
|
|
22
|
+
const query = (overrides = {}) => ({
|
|
23
|
+
scope: principal,
|
|
24
|
+
...month,
|
|
25
|
+
groupBy: [],
|
|
26
|
+
...overrides,
|
|
27
|
+
});
|
|
28
|
+
test("counted request ids cannot become reservations after a policy change", async () => {
|
|
29
|
+
const i = input();
|
|
30
|
+
await f.store.countRequest(count({ commandId: i.operationId, scope: i.scope }));
|
|
31
|
+
await expect(f.store.reserve(i)).rejects.toThrow("operationId");
|
|
32
|
+
const next = input();
|
|
33
|
+
expect(await f.store.reserve(next)).toMatchObject({ outcome: "reserved" });
|
|
34
|
+
expect(await f.store.countRequest(count({ commandId: next.operationId, scope: next.scope }))).toMatchObject({ outcome: "conflict" });
|
|
35
|
+
});
|
|
36
|
+
test("counts each state separately and reads them back", async () => {
|
|
37
|
+
for (const state of [
|
|
38
|
+
"passthrough",
|
|
39
|
+
"passthrough",
|
|
40
|
+
"unpriced",
|
|
41
|
+
"cached",
|
|
42
|
+
"rate_limited",
|
|
43
|
+
])
|
|
44
|
+
expect(await f.store.countRequest(count({ state }))).toEqual({
|
|
45
|
+
outcome: "counted",
|
|
46
|
+
replayed: false,
|
|
47
|
+
});
|
|
48
|
+
const page = await f.store.requestCounts(query());
|
|
49
|
+
expect(page.rows).toEqual([
|
|
50
|
+
{ dimensions: {}, state: "cached", count: 1n },
|
|
51
|
+
{ dimensions: {}, state: "passthrough", count: 2n },
|
|
52
|
+
{ dimensions: {}, state: "rate_limited", count: 1n },
|
|
53
|
+
{ dimensions: {}, state: "unpriced", count: 1n },
|
|
54
|
+
]);
|
|
55
|
+
expect(page.truncated).toBe(false);
|
|
56
|
+
expect(page.asOf).toBe(f.clock.now().toISOString());
|
|
57
|
+
});
|
|
58
|
+
test("an identical replay counts once; a changed payload under the same command conflicts", async () => {
|
|
59
|
+
const c = count();
|
|
60
|
+
await f.store.countRequest(c);
|
|
61
|
+
expect(await f.store.countRequest(c)).toEqual({ outcome: "counted", replayed: true });
|
|
62
|
+
expect(await f.store.countRequest({ ...c, state: "cached" })).toEqual({
|
|
63
|
+
outcome: "conflict",
|
|
64
|
+
reason: "command_mismatch",
|
|
65
|
+
});
|
|
66
|
+
expect((await f.store.requestCounts(query())).rows).toEqual([
|
|
67
|
+
{ dimensions: {}, state: "passthrough", count: 1n },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
test("the window is [from, to) at store time", async () => {
|
|
71
|
+
f.clock.set("2026-09-30T23:59:59.999Z");
|
|
72
|
+
await f.store.countRequest(count());
|
|
73
|
+
f.clock.set("2026-10-01T00:00:00.000Z");
|
|
74
|
+
await f.store.countRequest(count());
|
|
75
|
+
expect((await f.store.requestCounts(query())).rows).toEqual([
|
|
76
|
+
{ dimensions: {}, state: "passthrough", count: 1n },
|
|
77
|
+
]);
|
|
78
|
+
expect((await f.store.requestCounts(query({ from: "2026-10-01T00:00:00.000Z", to: "2026-10-02T00:00:00.000Z" }))).rows).toEqual([{ dimensions: {}, state: "passthrough", count: 1n }]);
|
|
79
|
+
});
|
|
80
|
+
test("scopes select principal, group, namespace and pool; connection filters", async () => {
|
|
81
|
+
await f.store.countRequest(count());
|
|
82
|
+
await f.store.countRequest(count({
|
|
83
|
+
scope: { namespace: "test", principal: "u2", group: "g1", connection: "c2" },
|
|
84
|
+
platformPools: ["pool_a"],
|
|
85
|
+
}));
|
|
86
|
+
await f.store.countRequest(count({ scope: { namespace: "other", principal: "u1", connection: "c1" } }));
|
|
87
|
+
const total = async (q) => (await f.store.requestCounts(query(q))).rows.reduce((a, r) => a + r.count, 0n);
|
|
88
|
+
expect(await total({})).toBe(1n);
|
|
89
|
+
expect(await total({ scope: { kind: "group", namespace: "test", group: "g1" } })).toBe(1n);
|
|
90
|
+
expect(await total({ scope: { kind: "namespace", namespace: "test" } })).toBe(2n);
|
|
91
|
+
expect(await total({ scope: { kind: "namespace", namespace: "test" }, connection: "c2" })).toBe(1n);
|
|
92
|
+
expect(await total({ scope: { kind: "platform_pool", namespace: "test", poolId: "pool_a" } })).toBe(1n);
|
|
93
|
+
expect(await total({ scope: { kind: "platform_pool", namespace: "test", poolId: "pool_b" } })).toBe(0n);
|
|
94
|
+
});
|
|
95
|
+
test("groupBy provider, operation, connection, source and day", async () => {
|
|
96
|
+
await f.store.countRequest(count({ operation: "a" }));
|
|
97
|
+
await f.store.countRequest(count({ operation: "b", source: "cli" }));
|
|
98
|
+
f.clock.advance(86400000);
|
|
99
|
+
await f.store.countRequest(count({ operation: "a", state: "unpriced" }));
|
|
100
|
+
const rows = (await f.store.requestCounts(query({ groupBy: ["provider", "operation", "connection", "source", "day"] }))).rows;
|
|
101
|
+
expect(rows).toEqual([
|
|
102
|
+
{
|
|
103
|
+
dimensions: {
|
|
104
|
+
provider: "search",
|
|
105
|
+
operation: "a",
|
|
106
|
+
connection: "c1",
|
|
107
|
+
source: "proxy",
|
|
108
|
+
day: "2026-09-23",
|
|
109
|
+
},
|
|
110
|
+
state: "passthrough",
|
|
111
|
+
count: 1n,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
dimensions: {
|
|
115
|
+
provider: "search",
|
|
116
|
+
operation: "a",
|
|
117
|
+
connection: "c1",
|
|
118
|
+
source: "proxy",
|
|
119
|
+
day: "2026-09-24",
|
|
120
|
+
},
|
|
121
|
+
state: "unpriced",
|
|
122
|
+
count: 1n,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
dimensions: {
|
|
126
|
+
provider: "search",
|
|
127
|
+
operation: "b",
|
|
128
|
+
connection: "c1",
|
|
129
|
+
source: "cli",
|
|
130
|
+
day: "2026-09-23",
|
|
131
|
+
},
|
|
132
|
+
state: "passthrough",
|
|
133
|
+
count: 1n,
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
test("limit truncates the ordered rows", async () => {
|
|
138
|
+
for (const operation of ["c", "a", "b"])
|
|
139
|
+
await f.store.countRequest(count({ operation }));
|
|
140
|
+
const page = await f.store.requestCounts(query({ groupBy: ["operation"], limit: 2 }));
|
|
141
|
+
expect(page.rows.map((r) => r.dimensions.operation)).toEqual(["a", "b"]);
|
|
142
|
+
expect(page.truncated).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
test("counting never reserves or touches budgets", async () => {
|
|
145
|
+
f.budgets.push(budget({ limit: quantity(0n) }));
|
|
146
|
+
expect(await f.store.countRequest(count())).toMatchObject({ outcome: "counted" });
|
|
147
|
+
expect(await f.store.reserve(input())).toMatchObject({ outcome: "exceeded" });
|
|
148
|
+
const statuses = await f.store.applicableBudgets({
|
|
149
|
+
scope: { namespace: "test", principal: "u1", connection: "c1" },
|
|
150
|
+
surface: "app",
|
|
151
|
+
units: ["requests"],
|
|
152
|
+
});
|
|
153
|
+
expect(statuses[0]).toMatchObject({ used: quantity(0n), reserved: quantity(0n) });
|
|
154
|
+
});
|
|
155
|
+
test("an unknown state is a validation failure", async () => {
|
|
156
|
+
await expect(f.store.countRequest(count({ state: "metered" }))).rejects.toThrow("InvalidInput");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { command, input, receipt, unknownReceipt } from "./helpers.js";
|
|
3
|
+
export function operationsTests(factory) {
|
|
4
|
+
describe("operation listing", () => {
|
|
5
|
+
let f;
|
|
6
|
+
beforeEach(async () => {
|
|
7
|
+
f = await factory();
|
|
8
|
+
});
|
|
9
|
+
afterEach(async () => f.close());
|
|
10
|
+
const query = (overrides = {}) => ({
|
|
11
|
+
scope: { kind: "namespace", namespace: "test" },
|
|
12
|
+
from: "2026-09-01T00:00:00.000Z",
|
|
13
|
+
to: "2026-10-01T00:00:00.000Z",
|
|
14
|
+
...overrides,
|
|
15
|
+
});
|
|
16
|
+
const reserved = async (overrides = {}) => {
|
|
17
|
+
const r = await f.store.reserve(input(overrides));
|
|
18
|
+
if (r.outcome !== "reserved")
|
|
19
|
+
throw new Error("fixture");
|
|
20
|
+
f.clock.advance(1000);
|
|
21
|
+
return r.operation;
|
|
22
|
+
};
|
|
23
|
+
const dispatched = async (overrides = {}) => {
|
|
24
|
+
const op = await reserved(overrides);
|
|
25
|
+
const g = await f.store.markDispatchIntent({
|
|
26
|
+
...command(op),
|
|
27
|
+
holder: "h",
|
|
28
|
+
leaseTtlMs: 60000,
|
|
29
|
+
});
|
|
30
|
+
if (!("granted" in g) || !g.granted)
|
|
31
|
+
throw new Error("fixture");
|
|
32
|
+
return g;
|
|
33
|
+
};
|
|
34
|
+
const settledWith = async (unknown, overrides = {}) => {
|
|
35
|
+
const g = await dispatched(overrides);
|
|
36
|
+
const s = await f.store.settle({
|
|
37
|
+
...command(g.operation),
|
|
38
|
+
authority: { kind: "lease", leaseId: g.lease.leaseId },
|
|
39
|
+
receipt: unknown ? unknownReceipt() : receipt(),
|
|
40
|
+
});
|
|
41
|
+
if (s.outcome !== "settled")
|
|
42
|
+
throw new Error("fixture");
|
|
43
|
+
return s.operation;
|
|
44
|
+
};
|
|
45
|
+
const ids = (ops) => ops.map((o) => o.operationId);
|
|
46
|
+
test("lists every state by default and filters by state in creation order", async () => {
|
|
47
|
+
const a = await reserved(), b = (await dispatched()).operation, c = await settledWith(true), d = await settledWith(false);
|
|
48
|
+
expect(c.state).toBe("pending");
|
|
49
|
+
expect(d.state).toBe("settled");
|
|
50
|
+
const all = await f.store.listOperations(query());
|
|
51
|
+
expect(ids(all.operations)).toEqual(ids([a, b, c, d]));
|
|
52
|
+
expect(all.nextCursor).toBeUndefined();
|
|
53
|
+
expect(all.asOf).toBe(f.clock.now().toISOString());
|
|
54
|
+
expect(typeof all.watermark).toBe("string");
|
|
55
|
+
const open = await f.store.listOperations(query({ states: ["reserved", "pending"] }));
|
|
56
|
+
expect(ids(open.operations)).toEqual([a.operationId, c.operationId]);
|
|
57
|
+
expect(open.operations[1].state).toBe("pending");
|
|
58
|
+
expect(open.operations[1].receipts).toHaveLength(1);
|
|
59
|
+
expect(ids((await f.store.listOperations(query({ states: ["dispatch_intended"] }))).operations)).toEqual([b.operationId]);
|
|
60
|
+
});
|
|
61
|
+
test("scope and connection narrow the listing without widening it", async () => {
|
|
62
|
+
const own = await reserved({
|
|
63
|
+
scope: { namespace: "test", principal: "u1", connection: "c1", group: "g1" },
|
|
64
|
+
platformPools: ["p1"],
|
|
65
|
+
}), other = await reserved({
|
|
66
|
+
scope: { namespace: "test", principal: "u2", connection: "c2" },
|
|
67
|
+
costOwner: "u2",
|
|
68
|
+
});
|
|
69
|
+
await f.store.reserve(input({ scope: { namespace: "other", principal: "u1", connection: "c1" } }));
|
|
70
|
+
const list = async (o) => ids((await f.store.listOperations(query(o))).operations);
|
|
71
|
+
expect(await list({})).toEqual([own.operationId, other.operationId]);
|
|
72
|
+
expect(await list({ scope: { kind: "principal", namespace: "test", principal: "u2" } })).toEqual([other.operationId]);
|
|
73
|
+
expect(await list({ scope: { kind: "group", namespace: "test", group: "g1" } })).toEqual([
|
|
74
|
+
own.operationId,
|
|
75
|
+
]);
|
|
76
|
+
expect(await list({ scope: { kind: "platform_pool", namespace: "test", poolId: "p1" } })).toEqual([own.operationId]);
|
|
77
|
+
expect(await list({ connection: "c2" })).toEqual([other.operationId]);
|
|
78
|
+
});
|
|
79
|
+
test("the window is createdAt from inclusive to exclusive", async () => {
|
|
80
|
+
const first = await reserved(), second = await reserved();
|
|
81
|
+
const list = async (from, to) => ids((await f.store.listOperations(query({ from, to }))).operations);
|
|
82
|
+
expect(await list(first.createdAt, second.createdAt)).toEqual([first.operationId]);
|
|
83
|
+
expect(await list(second.createdAt, "2026-10-01T00:00:00.000Z")).toEqual([
|
|
84
|
+
second.operationId,
|
|
85
|
+
]);
|
|
86
|
+
expect(await list("2026-09-01T00:00:00.000Z", first.createdAt)).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
test("pages keep first-page membership and reject a changed query", async () => {
|
|
89
|
+
const a = await reserved(), b = await reserved(), c = await reserved();
|
|
90
|
+
const first = await f.store.listOperations(query({ limit: 2 }));
|
|
91
|
+
expect(ids(first.operations)).toEqual([a.operationId, b.operationId]);
|
|
92
|
+
expect(first.nextCursor).toBeTruthy();
|
|
93
|
+
await reserved();
|
|
94
|
+
const second = await f.store.listOperations(query({ limit: 2, cursor: first.nextCursor }));
|
|
95
|
+
expect(ids(second.operations)).toEqual([c.operationId]);
|
|
96
|
+
expect(second.nextCursor).toBeUndefined();
|
|
97
|
+
expect(second.watermark).toBe(first.watermark);
|
|
98
|
+
expect(second.asOf).toBe(first.asOf);
|
|
99
|
+
await expect(f.store.listOperations(query({ limit: 1, cursor: first.nextCursor }))).rejects.toThrow("InvalidInput");
|
|
100
|
+
await expect(f.store.listOperations(query({ limit: 2, cursor: "forged" }))).rejects.toThrow("InvalidInput");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -125,11 +125,12 @@ export function propertyTests(factory) {
|
|
|
125
125
|
}
|
|
126
126
|
}), { numRuns, verbose: 2 });
|
|
127
127
|
});
|
|
128
|
-
test("many operations never over-admit any blocking budget", async () => {
|
|
128
|
+
test("many operations never over-admit any blocking budget or hard limit", async () => {
|
|
129
129
|
await fc.assert(fc.asyncProperty(fc.array(fc.record({
|
|
130
130
|
kind: fc.constantFrom("principal", "connection", "platform_pool"),
|
|
131
131
|
limit: fc.integer({ min: 0, max: 30 }),
|
|
132
|
-
|
|
132
|
+
allow: fc.boolean(),
|
|
133
|
+
headroom: fc.option(fc.integer({ min: 1, max: 10 }), { nil: undefined }),
|
|
133
134
|
}), { minLength: 1, maxLength: 4 }), fc.array(fc.record({
|
|
134
135
|
owner: fc.integer({ min: 1, max: 3 }),
|
|
135
136
|
amount: fc.integer({ min: 0, max: 5 }),
|
|
@@ -145,7 +146,10 @@ export function propertyTests(factory) {
|
|
|
145
146
|
? { kind: "connection", namespace: "test", connection: "c1" }
|
|
146
147
|
: { kind: "platform_pool", namespace: "test", poolId: "pool" },
|
|
147
148
|
limit: quantity(BigInt(spec.limit)),
|
|
148
|
-
onExceed: spec.
|
|
149
|
+
onExceed: spec.allow ? "allow" : "block",
|
|
150
|
+
...(spec.allow && spec.headroom !== undefined
|
|
151
|
+
? { hardLimit: quantity(BigInt(spec.limit + spec.headroom)) }
|
|
152
|
+
: {}),
|
|
149
153
|
}));
|
|
150
154
|
const accepted = [];
|
|
151
155
|
for (const spec of operations) {
|
|
@@ -180,11 +184,14 @@ export function propertyTests(factory) {
|
|
|
180
184
|
});
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
|
-
for (const b of f.budgets
|
|
187
|
+
for (const b of f.budgets) {
|
|
188
|
+
const ceiling = b.onExceed === "block" ? b.limit : (b.hardLimit ?? null);
|
|
189
|
+
if (!ceiling)
|
|
190
|
+
continue;
|
|
184
191
|
const total = accepted
|
|
185
192
|
.filter((op) => op.budgetEpochs.some((e) => e.budgetId === b.id))
|
|
186
193
|
.reduce((n, op) => n + op.estimate[0].value, 0n);
|
|
187
|
-
expect(total).toBeLessThanOrEqual(
|
|
194
|
+
expect(total).toBeLessThanOrEqual(ceiling.value);
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
197
|
}
|