@pithy-sh/testers 0.1.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/LICENSE +21 -0
- package/README.md +17 -0
- package/docs/store-apis.md +107 -0
- package/package.json +62 -0
- package/pithy.manifest.json +52 -0
- package/src/activity/resolve.ts +273 -0
- package/src/audit/actions.ts +56 -0
- package/src/capability.ts +128 -0
- package/src/clock/days.ts +70 -0
- package/src/clock/replay.ts +190 -0
- package/src/cloudflare-test.d.ts +13 -0
- package/src/config/config.ts +518 -0
- package/src/crypto/token.ts +60 -0
- package/src/data/cohort.ts +83 -0
- package/src/data/enums.ts +134 -0
- package/src/data/event.ts +81 -0
- package/src/data/member.ts +79 -0
- package/src/data/snapshot.ts +280 -0
- package/src/data/tables.ts +49 -0
- package/src/error/errors.ts +229 -0
- package/src/health/score.ts +225 -0
- package/src/http/guards.ts +37 -0
- package/src/http/pages.ts +66 -0
- package/src/http/responses.ts +634 -0
- package/src/http/routes.ts +933 -0
- package/src/http/schemas.ts +210 -0
- package/src/http/scopes.ts +79 -0
- package/src/http/view.ts +304 -0
- package/src/index.ts +80 -0
- package/src/migrations/0001_cohorts.ts +202 -0
- package/src/nudge/cooldown.ts +104 -0
- package/src/nudge/copy.ts +179 -0
- package/src/nudge/enqueueSeam.ts +95 -0
- package/src/nudge/send.ts +89 -0
- package/src/projection/build.ts +285 -0
- package/src/projection/forecast.ts +348 -0
- package/src/projection/inputs.ts +63 -0
- package/src/projection/poissonBinomial.ts +91 -0
- package/src/projection/trend.ts +185 -0
- package/src/provision/provisionTesters.ts +109 -0
- package/src/provision/resolveTestersConfig.ts +155 -0
- package/src/roster/read.ts +227 -0
- package/src/roster/write.ts +511 -0
- package/src/seeds/example.ts +219 -0
- package/src/version.generated.ts +16 -0
- package/src/workflows/daily.ts +513 -0
- package/src/workflows/pass.ts +100 -0
- package/src/workflows/report.ts +52 -0
- package/src/workflows/retryPolicy.ts +48 -0
- package/src/workflows/specs.ts +73 -0
- package/src/workflows/worker.ts +132 -0
- package/src/workflows/wrangler.jsonc +66 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { EXAMPLE_ADA, EXAMPLE_ALAN, EXAMPLE_GRACE } from "@pithy-sh/core/src/seed/exampleIdentities";
|
|
5
|
+
import { d1SeedGroup, defineSeed, type SeedSet } from "@pithy-sh/core/src/seed/seed";
|
|
6
|
+
import { TestersCohort } from "../data/cohort";
|
|
7
|
+
import { TestersEvent } from "../data/event";
|
|
8
|
+
import { TestersMember } from "../data/member";
|
|
9
|
+
import { TESTERS_COHORTS_TABLE, TESTERS_EVENTS_TABLE, TESTERS_MEMBERS_TABLE } from "../data/tables";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A worked example: one cohort, three testers, and a roster that is deliberately *not* healthy.
|
|
13
|
+
*
|
|
14
|
+
* The interesting demo is a cohort in trouble, because that is the state this capability exists to make
|
|
15
|
+
* visible. Ada confirmed and is testing. Grace confirmed and has gone quiet. Alan was invited a week ago
|
|
16
|
+
* and has never responded. Seeding three happy testers would show a green dashboard and teach nobody
|
|
17
|
+
* what the columns mean.
|
|
18
|
+
*
|
|
19
|
+
* Fixed ids and a fixed clock throughout: seeding is `INSERT OR IGNORE`, so a generated id would insert
|
|
20
|
+
* a second copy on every run, and a generated timestamp would make the streak a different number every
|
|
21
|
+
* time the fixture loaded.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Where this set sorts in the project's seed registry. After auth (100), which owns these users. */
|
|
25
|
+
const TESTERS_EXAMPLE_SEED_ORDER = 260;
|
|
26
|
+
|
|
27
|
+
/** The cohort every fixture row hangs off. */
|
|
28
|
+
const COHORT_ID = "3f0c2b18-9a4d-4e77-b5c1-2d8e6f0a1b93";
|
|
29
|
+
|
|
30
|
+
/** Ada: confirmed, and actually testing. */
|
|
31
|
+
const ADA_MEMBER_ID = "8c1d5e20-7b39-4a62-9f04-6e2a1c7d3b85";
|
|
32
|
+
/** Grace: confirmed, then went quiet. The early-warning case. */
|
|
33
|
+
const GRACE_MEMBER_ID = "b7e94a13-2c68-4d05-8a71-9f3b0e6c4d27";
|
|
34
|
+
/** Alan: invited a week ago, never responded. The conversion case. */
|
|
35
|
+
const ALAN_MEMBER_ID = "d2a86f47-5e10-4b93-a6c8-31f7b9d0e582";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fixed confirmation tokens.
|
|
39
|
+
*
|
|
40
|
+
* Real ones are 32 bytes of CSPRNG; these are fixed for the same reason the ids are, because seeding is
|
|
41
|
+
* `INSERT OR IGNORE` and a generated value would insert a second copy on every run. They are obviously
|
|
42
|
+
* not random, which is the point — a fixture token should never be mistaken for a live credential.
|
|
43
|
+
*
|
|
44
|
+
* They are exactly 43 base64url characters, because that is what `OPT_IN_TOKEN_PATTERN` accepts and the
|
|
45
|
+
* route validator refuses anything else. A fixture that fails the shape check makes every seeded
|
|
46
|
+
* tester's link 400 — a demo that looks fine until somebody clicks it.
|
|
47
|
+
*/
|
|
48
|
+
const ADA_OPT_IN_TOKEN = "seed-ada-opt-in-token----------------------";
|
|
49
|
+
const GRACE_OPT_IN_TOKEN = "seed-grace-opt-in-token--------------------";
|
|
50
|
+
const ALAN_OPT_IN_TOKEN = "seed-alan-opt-in-token---------------------";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The fixture's timeline, anchored to the day it is seeded rather than to a fixed calendar date.
|
|
54
|
+
*
|
|
55
|
+
* The offsets are fixed, so the demo state is deterministic — nine days into a fourteen-day window,
|
|
56
|
+
* two confirmed, one silent — and that is what determinism has to mean here. Fixed *absolute* dates
|
|
57
|
+
* looked deterministic and were quietly wrong: `memberFor` bounds a link by the age of the invitation
|
|
58
|
+
* that carried it, so from thirty days after 2026-01-01 every seeded tester's link answered 400. Alan
|
|
59
|
+
* is seeded `invited` with no nudges recorded, so the first `pithy testers run` against a seeded
|
|
60
|
+
* database mails a real email carrying a dead link. A demo that breaks the moment anyone clicks it is
|
|
61
|
+
* the exact failure the token comment above says these fixtures exist to avoid.
|
|
62
|
+
*
|
|
63
|
+
* Read once at module load, so every row in one seed run shares one anchor. Re-seeding cannot shift
|
|
64
|
+
* them either: seeding is `INSERT OR IGNORE`, so the rows keep the timeline they were first written
|
|
65
|
+
* with.
|
|
66
|
+
*/
|
|
67
|
+
export const SEED_ANCHOR = new Date();
|
|
68
|
+
|
|
69
|
+
/** Days before the anchor, as a date. */
|
|
70
|
+
function daysAgo(days: number): Date {
|
|
71
|
+
return new Date(SEED_ANCHOR.getTime() - days * 86_400_000);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** When the cohort was created — nine days in, so the window is live and the demo has history. */
|
|
75
|
+
const CREATED_AT = daysAgo(9);
|
|
76
|
+
/** When the two who confirmed did so. */
|
|
77
|
+
const OPTED_IN_AT = daysAgo(8);
|
|
78
|
+
/** When Alan was invited and has been ignoring it since. Inside the link TTL, so his link works. */
|
|
79
|
+
const INVITED_AT = daysAgo(9);
|
|
80
|
+
|
|
81
|
+
export const testersExampleSeed: SeedSet = defineSeed({
|
|
82
|
+
name: "example",
|
|
83
|
+
order: TESTERS_EXAMPLE_SEED_ORDER,
|
|
84
|
+
environments: ["dev", "staging"],
|
|
85
|
+
example: true,
|
|
86
|
+
d1: [
|
|
87
|
+
d1SeedGroup("app", TESTERS_COHORTS_TABLE, TestersCohort, [
|
|
88
|
+
{
|
|
89
|
+
id: COHORT_ID,
|
|
90
|
+
name: "closed-test",
|
|
91
|
+
targetPlatform: "android",
|
|
92
|
+
// Google Play's own numbers, so the fixture demonstrates the requirement rather than a toy.
|
|
93
|
+
targetSize: 12,
|
|
94
|
+
windowDays: 14,
|
|
95
|
+
maxRosterSize: 100,
|
|
96
|
+
// The store's own opt-in page, which is where a tester actually enrolls. Fixed, and obviously a
|
|
97
|
+
// placeholder package name, so nobody mistakes the fixture for a real app.
|
|
98
|
+
storeOptInUrl: "https://play.google.com/apps/testing/com.example.app",
|
|
99
|
+
resetPolicy: "reset",
|
|
100
|
+
closedAt: null,
|
|
101
|
+
createdAt: CREATED_AT,
|
|
102
|
+
updatedAt: CREATED_AT,
|
|
103
|
+
},
|
|
104
|
+
]),
|
|
105
|
+
d1SeedGroup("app", TESTERS_MEMBERS_TABLE, TestersMember, [
|
|
106
|
+
{
|
|
107
|
+
id: ADA_MEMBER_ID,
|
|
108
|
+
cohortId: COHORT_ID,
|
|
109
|
+
optInToken: ADA_OPT_IN_TOKEN,
|
|
110
|
+
email: EXAMPLE_ADA.email,
|
|
111
|
+
name: EXAMPLE_ADA.name,
|
|
112
|
+
state: "opted_in",
|
|
113
|
+
invitedAt: INVITED_AT,
|
|
114
|
+
acceptedAt: OPTED_IN_AT,
|
|
115
|
+
optedInAt: OPTED_IN_AT,
|
|
116
|
+
lapsedAt: null,
|
|
117
|
+
lastInvitedAt: INVITED_AT,
|
|
118
|
+
lastNudgedAt: null,
|
|
119
|
+
nudgeCount: 0,
|
|
120
|
+
unreachable: false,
|
|
121
|
+
createdAt: INVITED_AT,
|
|
122
|
+
updatedAt: OPTED_IN_AT,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: GRACE_MEMBER_ID,
|
|
126
|
+
cohortId: COHORT_ID,
|
|
127
|
+
optInToken: GRACE_OPT_IN_TOKEN,
|
|
128
|
+
email: EXAMPLE_GRACE.email,
|
|
129
|
+
name: EXAMPLE_GRACE.name,
|
|
130
|
+
// Still `opted_in`, and that is the point: going quiet never moves anyone off the count, because
|
|
131
|
+
// Google counts opt-ins rather than engagement. Grace shows up as a health problem, not as a
|
|
132
|
+
// missing tester.
|
|
133
|
+
state: "opted_in",
|
|
134
|
+
invitedAt: INVITED_AT,
|
|
135
|
+
acceptedAt: OPTED_IN_AT,
|
|
136
|
+
optedInAt: OPTED_IN_AT,
|
|
137
|
+
lapsedAt: null,
|
|
138
|
+
lastInvitedAt: INVITED_AT,
|
|
139
|
+
lastNudgedAt: null,
|
|
140
|
+
nudgeCount: 1,
|
|
141
|
+
unreachable: false,
|
|
142
|
+
createdAt: INVITED_AT,
|
|
143
|
+
updatedAt: OPTED_IN_AT,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: ALAN_MEMBER_ID,
|
|
147
|
+
cohortId: COHORT_ID,
|
|
148
|
+
optInToken: ALAN_OPT_IN_TOKEN,
|
|
149
|
+
email: EXAMPLE_ALAN.email,
|
|
150
|
+
name: EXAMPLE_ALAN.name,
|
|
151
|
+
state: "invited",
|
|
152
|
+
invitedAt: INVITED_AT,
|
|
153
|
+
acceptedAt: null,
|
|
154
|
+
optedInAt: null,
|
|
155
|
+
lapsedAt: null,
|
|
156
|
+
lastInvitedAt: INVITED_AT,
|
|
157
|
+
lastNudgedAt: null,
|
|
158
|
+
nudgeCount: 0,
|
|
159
|
+
unreachable: false,
|
|
160
|
+
createdAt: INVITED_AT,
|
|
161
|
+
updatedAt: INVITED_AT,
|
|
162
|
+
},
|
|
163
|
+
]),
|
|
164
|
+
d1SeedGroup("app", TESTERS_EVENTS_TABLE, TestersEvent, [
|
|
165
|
+
// The events are the source of truth, so the fixture seeds them too: a roster with member rows
|
|
166
|
+
// and no history would replay to an empty clock, and the demo's whole point is the clock.
|
|
167
|
+
{
|
|
168
|
+
id: 1,
|
|
169
|
+
cohortId: COHORT_ID,
|
|
170
|
+
memberId: ADA_MEMBER_ID,
|
|
171
|
+
kind: "invited",
|
|
172
|
+
actor: "developer",
|
|
173
|
+
occurredAt: INVITED_AT,
|
|
174
|
+
metadata: {},
|
|
175
|
+
createdAt: INVITED_AT,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 2,
|
|
179
|
+
cohortId: COHORT_ID,
|
|
180
|
+
memberId: ADA_MEMBER_ID,
|
|
181
|
+
kind: "opted_in",
|
|
182
|
+
actor: "tester",
|
|
183
|
+
occurredAt: OPTED_IN_AT,
|
|
184
|
+
metadata: {},
|
|
185
|
+
createdAt: OPTED_IN_AT,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 3,
|
|
189
|
+
cohortId: COHORT_ID,
|
|
190
|
+
memberId: GRACE_MEMBER_ID,
|
|
191
|
+
kind: "invited",
|
|
192
|
+
actor: "developer",
|
|
193
|
+
occurredAt: INVITED_AT,
|
|
194
|
+
metadata: {},
|
|
195
|
+
createdAt: INVITED_AT,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 4,
|
|
199
|
+
cohortId: COHORT_ID,
|
|
200
|
+
memberId: GRACE_MEMBER_ID,
|
|
201
|
+
kind: "opted_in",
|
|
202
|
+
actor: "tester",
|
|
203
|
+
occurredAt: OPTED_IN_AT,
|
|
204
|
+
metadata: {},
|
|
205
|
+
createdAt: OPTED_IN_AT,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 5,
|
|
209
|
+
cohortId: COHORT_ID,
|
|
210
|
+
memberId: ALAN_MEMBER_ID,
|
|
211
|
+
kind: "invited",
|
|
212
|
+
actor: "developer",
|
|
213
|
+
occurredAt: INVITED_AT,
|
|
214
|
+
metadata: {},
|
|
215
|
+
createdAt: INVITED_AT,
|
|
216
|
+
},
|
|
217
|
+
]),
|
|
218
|
+
],
|
|
219
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
// GENERATED by scripts/stampVersions.ts — do not edit by hand. Regenerate with `bun run stamp-versions`.
|
|
5
|
+
//
|
|
6
|
+
// A Worker cannot read its own package.json, so this is how @pithy-sh/testers knows its own version at
|
|
7
|
+
// runtime. The capability attaches it, and `GET /control-plane/manifest` reports it per capability —
|
|
8
|
+
// which is what answers "should this project upgrade" and "is this customer exposed to what we just
|
|
9
|
+
// fixed". Those questions are only answerable per module, because a project composes some capabilities
|
|
10
|
+
// and not others.
|
|
11
|
+
|
|
12
|
+
/** This package's npm name — the join key against a release feed. */
|
|
13
|
+
export const PACKAGE_NAME = "@pithy-sh/testers";
|
|
14
|
+
|
|
15
|
+
/** This package's version, stamped from its own package.json at generation time. */
|
|
16
|
+
export const PACKAGE_VERSION = "0.1.0";
|