routstrd 0.4.8 → 0.4.10
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 +124 -5
- package/SKILL.md +341 -0
- package/dist/daemon/index.js +7995 -14602
- package/dist/index.js +155916 -14225
- package/package.json +9 -4
- package/src/cli.ts +279 -110
- package/src/daemon/args.ts +4 -5
- package/src/daemon/http/index.ts +37 -2
- package/src/daemon/index.ts +94 -33
- package/src/daemon/models.ts +13 -10
- package/src/daemon/wallet/auto-refill.ts +1 -1
- package/src/daemon/wallet/coco-client.ts +205 -3
- package/src/daemon/wallet/cocod-client.ts +19 -3
- package/src/daemon/wallet/index.ts +1 -1
- package/src/index.ts +9 -1
- package/src/runtime.ts +55 -0
- package/src/start-daemon.ts +4 -8
- package/src/tui/usage/app.ts +16 -2
- package/src/tui/usage/data.ts +110 -0
- package/src/tui/usage/render.ts +45 -20
- package/src/utils/clients.ts +74 -1
- package/src/utils/config.ts +14 -0
- package/src/utils/daemon-stop.ts +125 -0
- package/src/utils/standalone-update.ts +214 -0
- package/src/utils/update-checker.ts +81 -18
- package/src/version.ts +3 -0
- package/src/cli.test.ts +0 -192
- package/src/daemon/fatal-error.test.ts +0 -104
- package/src/daemon/http/usage-summary.test.ts +0 -272
- package/src/daemon/models.test.ts +0 -81
- package/src/daemon/wallet/cleanup.test.ts +0 -168
- package/src/daemon/wallet/coco-client.npc.test.ts +0 -252
- package/src/daemon/wallet/coco-client.test.ts +0 -669
- package/src/daemon/wallet/diagnostics.test.ts +0 -378
- package/src/daemon/wallet/fixtures/cocod-0.0.24-wallet.db.gz +0 -0
- package/src/daemon/wallet/migration.test.ts +0 -171
- package/src/daemon/wallet/receive-dedup.test.ts +0 -238
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
-
import { createMemoryUsageTrackingDriver } from "@routstr/sdk/storage";
|
|
3
|
-
import type { UsageTrackingEntry } from "@routstr/sdk/storage";
|
|
4
|
-
import type { ClientEntry } from "../../utils/clients";
|
|
5
|
-
import { getUsageSummary, __resetUsageSummaryCacheForTest } from "./usage-summary";
|
|
6
|
-
|
|
7
|
-
// ─── Test fixtures ────────────────────────────────────────────────────────────
|
|
8
|
-
//
|
|
9
|
-
// All timestamps are computed relative to "now" so the fixtures never age
|
|
10
|
-
// out of getUsageSummary's 30-day rolling window. We anchor to a recent UTC
|
|
11
|
-
// midnight ~10 days ago and keep the same hour-of-day structure the
|
|
12
|
-
// tz-bucketing assertions depend on.
|
|
13
|
-
//
|
|
14
|
-
// tz = 300 (UTC-5 / EST). After shifting by -300min, UTC dates become:
|
|
15
|
-
// d1: <anchor> 10:00Z → local <anchor> 05:00 → local day "<anchor>"
|
|
16
|
-
// d2: <anchor+1d> 03:00Z → local <anchor> 22:00 → local day "<anchor>"
|
|
17
|
-
// d3: <anchor+1d> 06:00Z → local <anchor+1d> 01:00 → local day "<anchor+1d>"
|
|
18
|
-
|
|
19
|
-
const _BASE_UTC_MIDNIGHT = Math.floor(Date.now() / 86_400_000) * 86_400_000;
|
|
20
|
-
const _ANCHOR_MS = _BASE_UTC_MIDNIGHT - 10 * 86_400_000; // 10 days ago UTC
|
|
21
|
-
const D1 = _ANCHOR_MS + 10 * 3_600_000; // 10:00Z → local day <anchor>
|
|
22
|
-
const D2 = _ANCHOR_MS + 1 * 86_400_000 + 3 * 3_600_000; // 03:00Z next day → local day <anchor>
|
|
23
|
-
const D3 = _ANCHOR_MS + 1 * 86_400_000 + 6 * 3_600_000; // 06:00Z next day → local day <anchor+1d>
|
|
24
|
-
|
|
25
|
-
/** Compute the local-day date string the same way the SDK's day grouping does. */
|
|
26
|
-
function localDayDateStr(ts: number, tzOffsetMinutes: number): string {
|
|
27
|
-
const d = new Date(ts - tzOffsetMinutes * 60_000);
|
|
28
|
-
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const BASE_ENTRY: Omit<UsageTrackingEntry, "id" | "timestamp" | "modelId" | "client" | "totalTokens"> = {
|
|
32
|
-
baseUrl: "https://api.openai.com/",
|
|
33
|
-
requestId: "req-1",
|
|
34
|
-
cost: 0.01,
|
|
35
|
-
satsCost: 10,
|
|
36
|
-
promptTokens: 100,
|
|
37
|
-
completionTokens: 50,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
function makeEntry(
|
|
41
|
-
id: string,
|
|
42
|
-
overrides: Partial<UsageTrackingEntry> & Pick<UsageTrackingEntry, "id" | "timestamp" | "modelId" | "totalTokens">,
|
|
43
|
-
): UsageTrackingEntry {
|
|
44
|
-
return {
|
|
45
|
-
...BASE_ENTRY,
|
|
46
|
-
...overrides,
|
|
47
|
-
id,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Seed entries:
|
|
52
|
-
// - 2 entries for model-a, client-1 (owned by npub1)
|
|
53
|
-
// - 1 entry for model-b, client-2 (no npub)
|
|
54
|
-
// - 1 entry for model-a, no client (null client)
|
|
55
|
-
// - totalTokens spread across tiny (< 1000) and small (1000-10000) buckets
|
|
56
|
-
|
|
57
|
-
const ENTRIES: UsageTrackingEntry[] = [
|
|
58
|
-
// model-a, client-1, day 2026-05-20 (D1), totalTokens=500 (tiny)
|
|
59
|
-
makeEntry("e1", {
|
|
60
|
-
id: "e1",
|
|
61
|
-
timestamp: D1,
|
|
62
|
-
modelId: "model-a",
|
|
63
|
-
client: "client-1",
|
|
64
|
-
totalTokens: 500,
|
|
65
|
-
promptTokens: 350,
|
|
66
|
-
completionTokens: 150,
|
|
67
|
-
satsCost: 10,
|
|
68
|
-
cost: 0.01,
|
|
69
|
-
}),
|
|
70
|
-
// model-a, client-1, day 2026-05-20 (D2), totalTokens=2000 (small)
|
|
71
|
-
makeEntry("e2", {
|
|
72
|
-
id: "e2",
|
|
73
|
-
timestamp: D2,
|
|
74
|
-
modelId: "model-a",
|
|
75
|
-
client: "client-1",
|
|
76
|
-
totalTokens: 2000,
|
|
77
|
-
promptTokens: 1400,
|
|
78
|
-
completionTokens: 600,
|
|
79
|
-
satsCost: 20,
|
|
80
|
-
cost: 0.02,
|
|
81
|
-
}),
|
|
82
|
-
// model-b, client-2, day 2026-05-21 (D3), totalTokens=800 (tiny)
|
|
83
|
-
makeEntry("e3", {
|
|
84
|
-
id: "e3",
|
|
85
|
-
timestamp: D3,
|
|
86
|
-
modelId: "model-b",
|
|
87
|
-
client: "client-2",
|
|
88
|
-
totalTokens: 800,
|
|
89
|
-
promptTokens: 600,
|
|
90
|
-
completionTokens: 200,
|
|
91
|
-
satsCost: 8,
|
|
92
|
-
cost: 0.008,
|
|
93
|
-
}),
|
|
94
|
-
// model-a, no client, day 2026-05-21 (D3), totalTokens=5000 (small)
|
|
95
|
-
makeEntry("e4", {
|
|
96
|
-
id: "e4",
|
|
97
|
-
timestamp: D3 + 1000,
|
|
98
|
-
modelId: "model-a",
|
|
99
|
-
client: undefined,
|
|
100
|
-
totalTokens: 5000,
|
|
101
|
-
promptTokens: 3500,
|
|
102
|
-
completionTokens: 1500,
|
|
103
|
-
satsCost: 50,
|
|
104
|
-
cost: 0.05,
|
|
105
|
-
}),
|
|
106
|
-
];
|
|
107
|
-
|
|
108
|
-
const CLIENTS: ClientEntry[] = [
|
|
109
|
-
{
|
|
110
|
-
clientId: "client-1",
|
|
111
|
-
name: "Client One",
|
|
112
|
-
apiKey: "sk-client1",
|
|
113
|
-
createdAt: D1 - 86400000,
|
|
114
|
-
ownerNpub: "npub1abc",
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
clientId: "client-2",
|
|
118
|
-
name: "Client Two",
|
|
119
|
-
apiKey: "sk-client2",
|
|
120
|
-
createdAt: D1 - 86400000,
|
|
121
|
-
// no ownerNpub
|
|
122
|
-
},
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
const TZ = 300; // UTC-5 / EST
|
|
126
|
-
|
|
127
|
-
describe("getUsageSummary", () => {
|
|
128
|
-
let driver: ReturnType<typeof createMemoryUsageTrackingDriver>;
|
|
129
|
-
|
|
130
|
-
beforeEach(async () => {
|
|
131
|
-
// Reset the module-level memo cache explicitly so each test starts cold.
|
|
132
|
-
// Without this, tests 2-10 would share the cached object from test 1
|
|
133
|
-
// (all use the same fixtures → identical cache key → same TTL window).
|
|
134
|
-
__resetUsageSummaryCacheForTest();
|
|
135
|
-
driver = createMemoryUsageTrackingDriver(ENTRIES);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("returns correct totals", async () => {
|
|
139
|
-
const summary = await getUsageSummary(driver, CLIENTS, TZ);
|
|
140
|
-
|
|
141
|
-
expect(summary.totals.requests).toBe(4);
|
|
142
|
-
expect(summary.totals.totalTokens).toBe(500 + 2000 + 800 + 5000);
|
|
143
|
-
expect(summary.totals.promptTokens).toBe(350 + 1400 + 600 + 3500);
|
|
144
|
-
expect(summary.totals.completionTokens).toBe(150 + 600 + 200 + 1500);
|
|
145
|
-
expect(summary.totals.satsCost).toBe(10 + 20 + 8 + 50);
|
|
146
|
-
expect(summary.totals.cost).toBeCloseTo(0.01 + 0.02 + 0.008 + 0.05, 5);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("returns models sorted desc by satsCost", async () => {
|
|
150
|
-
const summary = await getUsageSummary(driver, CLIENTS, TZ);
|
|
151
|
-
const { models } = summary;
|
|
152
|
-
|
|
153
|
-
// model-a: 3 entries (e1, e2, e4) total satsCost = 10+20+50 = 80
|
|
154
|
-
// model-b: 1 entry (e3) satsCost = 8
|
|
155
|
-
expect(models).toHaveLength(2);
|
|
156
|
-
expect(models[0]!.modelId).toBe("model-a");
|
|
157
|
-
expect(models[0]!.satsCost).toBe(80);
|
|
158
|
-
expect(models[0]!.requests).toBe(3);
|
|
159
|
-
expect(models[1]!.modelId).toBe("model-b");
|
|
160
|
-
expect(models[1]!.satsCost).toBe(8);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("returns days most-recent-first with correct tz-bucketing", async () => {
|
|
164
|
-
const summary = await getUsageSummary(driver, CLIENTS, TZ);
|
|
165
|
-
const { days } = summary;
|
|
166
|
-
|
|
167
|
-
// D1 (10:00Z) and D2 (next day 03:00Z) both bucket to local day <anchor>;
|
|
168
|
-
// D3 (next day 06:00Z) buckets to local day <anchor+1d>.
|
|
169
|
-
// So: day <anchor> has e1+e2, day <anchor+1d> has e3+e4
|
|
170
|
-
expect(days.length).toBeGreaterThanOrEqual(2);
|
|
171
|
-
|
|
172
|
-
// Most-recent-first: <anchor+1d> first
|
|
173
|
-
expect(days[0]!.date).toBe(localDayDateStr(D3, TZ));
|
|
174
|
-
expect(days[0]!.requests).toBe(2); // e3, e4
|
|
175
|
-
expect(days[0]!.satsCost).toBe(8 + 50);
|
|
176
|
-
|
|
177
|
-
expect(days[1]!.date).toBe(localDayDateStr(D1, TZ));
|
|
178
|
-
expect(days[1]!.requests).toBe(2); // e1, e2
|
|
179
|
-
expect(days[1]!.satsCost).toBe(10 + 20);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("returns empty hoursToday (test entries are not today)", async () => {
|
|
183
|
-
const summary = await getUsageSummary(driver, CLIENTS, TZ);
|
|
184
|
-
// Test entries are ~10 days ago, not today
|
|
185
|
-
expect(summary.hoursToday).toHaveLength(0);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("returns correct sizeBuckets", async () => {
|
|
189
|
-
const summary = await getUsageSummary(driver, CLIENTS, TZ);
|
|
190
|
-
const { sizeBuckets } = summary;
|
|
191
|
-
|
|
192
|
-
// tiny [0, 1000): e1 (500) + e3 (800) = 2 entries
|
|
193
|
-
expect(sizeBuckets.tiny.count).toBe(2);
|
|
194
|
-
expect(sizeBuckets.tiny.cost).toBe(10 + 8);
|
|
195
|
-
|
|
196
|
-
// small [1000, 10000): e2 (2000) + e4 (5000) = 2 entries
|
|
197
|
-
expect(sizeBuckets.small.count).toBe(2);
|
|
198
|
-
expect(sizeBuckets.small.cost).toBe(20 + 50);
|
|
199
|
-
|
|
200
|
-
// medium, large, huge should be 0
|
|
201
|
-
expect(sizeBuckets.medium.count).toBe(0);
|
|
202
|
-
expect(sizeBuckets.large.count).toBe(0);
|
|
203
|
-
expect(sizeBuckets.huge.count).toBe(0);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("returns per-client topModels for top non-null clients", async () => {
|
|
207
|
-
const summary = await getUsageSummary(driver, CLIENTS, TZ);
|
|
208
|
-
const { clients } = summary;
|
|
209
|
-
|
|
210
|
-
// client-1: e1 + e2 (model-a), satsCost = 30
|
|
211
|
-
// client-2: e3 (model-b), satsCost = 8
|
|
212
|
-
// unknown (null client): e4 (model-a), satsCost = 50
|
|
213
|
-
// Sorted desc by satsCost: unknown (50), client-1 (30), client-2 (8)
|
|
214
|
-
|
|
215
|
-
const c1 = clients.find((c) => c.client === "client-1");
|
|
216
|
-
expect(c1).toBeDefined();
|
|
217
|
-
expect(c1!.requests).toBe(2);
|
|
218
|
-
expect(c1!.satsCost).toBe(30);
|
|
219
|
-
// client-1 is in top 3 non-null clients, should have topModels
|
|
220
|
-
expect(c1!.topModels.length).toBeGreaterThan(0);
|
|
221
|
-
expect(c1!.topModels[0]!.modelId).toBe("model-a");
|
|
222
|
-
|
|
223
|
-
const c2 = clients.find((c) => c.client === "client-2");
|
|
224
|
-
expect(c2).toBeDefined();
|
|
225
|
-
expect(c2!.requests).toBe(1);
|
|
226
|
-
expect(c2!.satsCost).toBe(8);
|
|
227
|
-
|
|
228
|
-
// unknown client (null group) should have empty topModels
|
|
229
|
-
const unknown = clients.find((c) => c.client === "unknown");
|
|
230
|
-
expect(unknown).toBeDefined();
|
|
231
|
-
expect(unknown!.topModels).toHaveLength(0);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("folds client rows into npubs correctly", async () => {
|
|
235
|
-
const summary = await getUsageSummary(driver, CLIENTS, TZ);
|
|
236
|
-
const { npubs } = summary;
|
|
237
|
-
|
|
238
|
-
// Only client-1 has ownerNpub = "npub1abc"
|
|
239
|
-
// npub1abc: e1 + e2 = requests 2, satsCost 30
|
|
240
|
-
expect(npubs).toHaveLength(1);
|
|
241
|
-
expect(npubs[0]!.npub).toBe("npub1abc");
|
|
242
|
-
expect(npubs[0]!.requests).toBe(2);
|
|
243
|
-
expect(npubs[0]!.satsCost).toBe(30);
|
|
244
|
-
// Top models for npub1abc (clients: ["client-1"]): only model-a
|
|
245
|
-
expect(npubs[0]!.topModels.length).toBeGreaterThan(0);
|
|
246
|
-
expect(npubs[0]!.topModels[0]!.modelId).toBe("model-a");
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("returns recent entries (up to 50)", async () => {
|
|
250
|
-
const summary = await getUsageSummary(driver, CLIENTS, TZ);
|
|
251
|
-
expect(summary.recent).toHaveLength(4); // only 4 entries in driver
|
|
252
|
-
// Should be sorted desc by timestamp
|
|
253
|
-
expect(summary.recent[0]!.timestamp).toBeGreaterThanOrEqual(summary.recent[1]!.timestamp);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it("returns providers correctly", async () => {
|
|
257
|
-
const summary = await getUsageSummary(driver, CLIENTS, TZ);
|
|
258
|
-
const { providers } = summary;
|
|
259
|
-
|
|
260
|
-
// All entries use the same baseUrl
|
|
261
|
-
expect(providers).toHaveLength(1);
|
|
262
|
-
expect(providers[0]!.baseUrl).toBe("https://api.openai.com/");
|
|
263
|
-
expect(providers[0]!.requests).toBe(4);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it("caches results for same key within TTL", async () => {
|
|
267
|
-
const summary1 = await getUsageSummary(driver, CLIENTS, TZ);
|
|
268
|
-
const summary2 = await getUsageSummary(driver, CLIENTS, TZ);
|
|
269
|
-
// Same object reference means it was served from cache
|
|
270
|
-
expect(summary1).toBe(summary2);
|
|
271
|
-
});
|
|
272
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "bun:test";
|
|
2
|
-
import type { SdkStore } from "@routstr/sdk";
|
|
3
|
-
import { createModelService } from "./models";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* `ensureProvidersBootstrapped` must mirror the review-disabled provider set
|
|
7
|
-
* (kind 38425) into the SdkStore. The SDK applies review disables to the
|
|
8
|
-
* discovery adapter during bootstrap, but `providers list` and per-model
|
|
9
|
-
* provider views read the store — if the store is not kept in sync, a fresh
|
|
10
|
-
* install reports "0 disabled" while routing silently excludes them.
|
|
11
|
-
*/
|
|
12
|
-
describe("createModelService.ensureProvidersBootstrapped", () => {
|
|
13
|
-
function makeStore(initialBaseUrls: string[] = []) {
|
|
14
|
-
const disabledCalls: string[][] = [];
|
|
15
|
-
const state: Record<string, unknown> = {
|
|
16
|
-
baseUrlsList: initialBaseUrls,
|
|
17
|
-
setBaseUrlsList: (urls: string[]) => {
|
|
18
|
-
state.baseUrlsList = urls;
|
|
19
|
-
},
|
|
20
|
-
disabledProviders: [],
|
|
21
|
-
setDisabledProviders: (urls: string[]) => {
|
|
22
|
-
state.disabledProviders = urls;
|
|
23
|
-
disabledCalls.push(urls);
|
|
24
|
-
},
|
|
25
|
-
manuallyDisabledProviders: [],
|
|
26
|
-
manuallyEnabledProviders: [],
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
store: { getState: () => state } as unknown as SdkStore,
|
|
31
|
-
state,
|
|
32
|
-
disabledCalls,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function makeModelManager(
|
|
37
|
-
providers: string[],
|
|
38
|
-
reviewedDisabled: string[] | null,
|
|
39
|
-
) {
|
|
40
|
-
return {
|
|
41
|
-
bootstrapProviders: async () => providers,
|
|
42
|
-
syncReviewedProvidersFromNostr: async () => reviewedDisabled,
|
|
43
|
-
} as never;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
it("mirrors review-disabled providers into the store after bootstrap", async () => {
|
|
47
|
-
const { store, state, disabledCalls } = makeStore();
|
|
48
|
-
const modelManager = makeModelManager(
|
|
49
|
-
["https://ok.example/", "https://bad.example/"],
|
|
50
|
-
["https://bad.example/"],
|
|
51
|
-
);
|
|
52
|
-
const service = createModelService(
|
|
53
|
-
modelManager as never,
|
|
54
|
-
{} as never,
|
|
55
|
-
store,
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
await service.ensureProvidersBootstrapped();
|
|
59
|
-
|
|
60
|
-
expect(disabledCalls).toEqual([["https://bad.example/"]]);
|
|
61
|
-
expect(state.disabledProviders).toEqual(["https://bad.example/"]);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("leaves the store unchanged when review sync reports no change (null)", async () => {
|
|
65
|
-
const { store, state, disabledCalls } = makeStore(["https://ok.example/"]);
|
|
66
|
-
const modelManager = makeModelManager(
|
|
67
|
-
["https://ok.example/", "https://new.example/"],
|
|
68
|
-
null,
|
|
69
|
-
);
|
|
70
|
-
const service = createModelService(
|
|
71
|
-
modelManager as never,
|
|
72
|
-
{} as never,
|
|
73
|
-
store,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
await service.ensureProvidersBootstrapped();
|
|
77
|
-
|
|
78
|
-
expect(disabledCalls).toEqual([]);
|
|
79
|
-
expect(state.disabledProviders).toEqual([]);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { selectCleanupOperations } from "./cleanup";
|
|
3
|
-
|
|
4
|
-
const NOW_MS = 1_800_000_000_000;
|
|
5
|
-
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
6
|
-
|
|
7
|
-
function mint(overrides: Record<string, unknown>) {
|
|
8
|
-
return {
|
|
9
|
-
id: "mint-1",
|
|
10
|
-
state: "pending",
|
|
11
|
-
expiry: NOW_MS / 1000 - 1000, // expired
|
|
12
|
-
updatedAt: NOW_MS - 2 * DAY_MS,
|
|
13
|
-
lastObservedRemoteState: undefined,
|
|
14
|
-
...overrides,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function send(overrides: Record<string, unknown>) {
|
|
19
|
-
return {
|
|
20
|
-
id: "send-1",
|
|
21
|
-
state: "pending",
|
|
22
|
-
updatedAt: NOW_MS - 2 * DAY_MS,
|
|
23
|
-
...overrides,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function melt(overrides: Record<string, unknown>) {
|
|
28
|
-
return {
|
|
29
|
-
id: "melt-1",
|
|
30
|
-
state: "prepared",
|
|
31
|
-
updatedAt: NOW_MS - 2 * DAY_MS,
|
|
32
|
-
...overrides,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe("selectCleanupOperations", () => {
|
|
37
|
-
it("selects expired pending mint quotes that are old enough", () => {
|
|
38
|
-
const result = selectCleanupOperations({
|
|
39
|
-
mints: [mint({ id: "a" })],
|
|
40
|
-
sends: [],
|
|
41
|
-
melts: [],
|
|
42
|
-
nowMs: NOW_MS,
|
|
43
|
-
minAgeMs: DAY_MS,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
expect(result.mintsToFail.map((op) => op.id)).toEqual(["a"]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("ignores mint quotes that have not expired", () => {
|
|
50
|
-
const result = selectCleanupOperations({
|
|
51
|
-
mints: [mint({ id: "a", expiry: NOW_MS / 1000 + 1000 })],
|
|
52
|
-
sends: [],
|
|
53
|
-
melts: [],
|
|
54
|
-
nowMs: NOW_MS,
|
|
55
|
-
minAgeMs: DAY_MS,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
expect(result.mintsToFail).toEqual([]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("ignores expired mint quotes already observed as PAID", () => {
|
|
62
|
-
const result = selectCleanupOperations({
|
|
63
|
-
mints: [mint({ id: "a", lastObservedRemoteState: "PAID" })],
|
|
64
|
-
sends: [],
|
|
65
|
-
melts: [],
|
|
66
|
-
nowMs: NOW_MS,
|
|
67
|
-
minAgeMs: DAY_MS,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(result.mintsToFail).toEqual([]);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("ignores expired mint quotes already observed as ISSUED", () => {
|
|
74
|
-
const result = selectCleanupOperations({
|
|
75
|
-
mints: [mint({ id: "a", lastObservedRemoteState: "ISSUED" })],
|
|
76
|
-
sends: [],
|
|
77
|
-
melts: [],
|
|
78
|
-
nowMs: NOW_MS,
|
|
79
|
-
minAgeMs: DAY_MS,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
expect(result.mintsToFail).toEqual([]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("ignores mint quotes without an expiry", () => {
|
|
86
|
-
const result = selectCleanupOperations({
|
|
87
|
-
mints: [mint({ id: "a", expiry: 0 })],
|
|
88
|
-
sends: [],
|
|
89
|
-
melts: [],
|
|
90
|
-
nowMs: NOW_MS,
|
|
91
|
-
minAgeMs: DAY_MS,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
expect(result.mintsToFail).toEqual([]);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("selects expired mint quotes even when the watcher recently touched them", () => {
|
|
98
|
-
const result = selectCleanupOperations({
|
|
99
|
-
mints: [mint({ id: "a", updatedAt: NOW_MS - 60_000 })],
|
|
100
|
-
sends: [],
|
|
101
|
-
melts: [],
|
|
102
|
-
nowMs: NOW_MS,
|
|
103
|
-
minAgeMs: DAY_MS,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
expect(result.mintsToFail.map((op) => op.id)).toEqual(["a"]);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("selects stale pending sends for reclaim", () => {
|
|
110
|
-
const result = selectCleanupOperations({
|
|
111
|
-
mints: [],
|
|
112
|
-
sends: [send({ id: "s" })],
|
|
113
|
-
melts: [],
|
|
114
|
-
nowMs: NOW_MS,
|
|
115
|
-
minAgeMs: DAY_MS,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
expect(result.sendsToReclaim.map((op) => op.id)).toEqual(["s"]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("ignores recent pending sends", () => {
|
|
122
|
-
const result = selectCleanupOperations({
|
|
123
|
-
mints: [],
|
|
124
|
-
sends: [send({ id: "s", updatedAt: NOW_MS - 60_000 })],
|
|
125
|
-
melts: [],
|
|
126
|
-
nowMs: NOW_MS,
|
|
127
|
-
minAgeMs: DAY_MS,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
expect(result.sendsToReclaim).toEqual([]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("ignores non-pending sends even when old", () => {
|
|
134
|
-
const result = selectCleanupOperations({
|
|
135
|
-
mints: [],
|
|
136
|
-
sends: [send({ id: "s", state: "executing" })],
|
|
137
|
-
melts: [],
|
|
138
|
-
nowMs: NOW_MS,
|
|
139
|
-
minAgeMs: DAY_MS,
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
expect(result.sendsToReclaim).toEqual([]);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("selects stale prepared melts for cancellation", () => {
|
|
146
|
-
const result = selectCleanupOperations({
|
|
147
|
-
mints: [],
|
|
148
|
-
sends: [],
|
|
149
|
-
melts: [melt({ id: "m" })],
|
|
150
|
-
nowMs: NOW_MS,
|
|
151
|
-
minAgeMs: DAY_MS,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
expect(result.meltsToCancel.map((op) => op.id)).toEqual(["m"]);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("ignores non-prepared melts", () => {
|
|
158
|
-
const result = selectCleanupOperations({
|
|
159
|
-
mints: [],
|
|
160
|
-
sends: [],
|
|
161
|
-
melts: [melt({ id: "m", state: "pending" })],
|
|
162
|
-
nowMs: NOW_MS,
|
|
163
|
-
minAgeMs: DAY_MS,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
expect(result.meltsToCancel).toEqual([]);
|
|
167
|
-
});
|
|
168
|
-
});
|