@gigzen/populace 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 +661 -0
- package/README.md +258 -0
- package/adapters/buzzbuzz.mjs +247 -0
- package/adapters/contract.md +164 -0
- package/adapters/template-rest.mjs +192 -0
- package/adapters/template.mjs +80 -0
- package/examples/buzzbuzz/populace-report.html +245 -0
- package/examples/buzzbuzz/populace-report.json +280 -0
- package/examples/buzzbuzz/populace.config.mjs +51 -0
- package/examples/buzzbuzz/run-test.ps1 +61 -0
- package/examples/demo/adapters/demo.mjs +90 -0
- package/examples/demo/populace-report.html +230 -0
- package/examples/demo/populace-report.json +219 -0
- package/examples/demo/populace.config.mjs +22 -0
- package/examples/rest-api/README.md +85 -0
- package/examples/rest-api/adapter.mjs +166 -0
- package/examples/rest-api/populace.config.mjs +40 -0
- package/examples/rest-api/server.mjs +247 -0
- package/examples/token-expiry/expiry-demo.mjs +119 -0
- package/package.json +56 -0
- package/populace.config.example.mjs +65 -0
- package/src/cli.mjs +591 -0
- package/src/config.mjs +186 -0
- package/src/contract.mjs +130 -0
- package/src/diagnose.mjs +40 -0
- package/src/engine/agent.mjs +264 -0
- package/src/engine/geo.mjs +59 -0
- package/src/engine/index.mjs +4 -0
- package/src/engine/personas.mjs +115 -0
- package/src/engine/world.mjs +120 -0
- package/src/html-report.mjs +218 -0
- package/src/index.mjs +38 -0
- package/src/instrument.mjs +299 -0
- package/src/net.mjs +175 -0
- package/src/report.mjs +251 -0
- package/src/selftest.mjs +1369 -0
- package/src/smoke.mjs +274 -0
- package/src/version.mjs +24 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter template — REST API with a bearer token.
|
|
3
|
+
*
|
|
4
|
+
* This is not a sketch. It is the adapter from examples/rest-api/, which has
|
|
5
|
+
* been run against a live server: 430 API calls, 0 failures, 13/13 methods.
|
|
6
|
+
* Every URL and field name below is marked EDIT — change those and the wiring
|
|
7
|
+
* around them already works.
|
|
8
|
+
*
|
|
9
|
+
* Start it up and see for yourself before you change anything:
|
|
10
|
+
*
|
|
11
|
+
* node examples/rest-api/server.mjs
|
|
12
|
+
* populace smoke --config examples/rest-api/populace.config.mjs
|
|
13
|
+
*
|
|
14
|
+
* Two rules, both in adapters/contract.md:
|
|
15
|
+
* 1. Never point this at production.
|
|
16
|
+
* 2. Go through your real API. No admin keys, no direct database writes — a
|
|
17
|
+
* simulation that bypasses your permission rules proves nothing about
|
|
18
|
+
* whether they work.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export function createAdapter(target, config) {
|
|
22
|
+
const base = target.url.replace(/\/$/, "");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The contract hands you a name, a phone and a persona — never a password.
|
|
26
|
+
* Choosing one is your job, because only you know what your API accepts.
|
|
27
|
+
*
|
|
28
|
+
* It must be STABLE. Identities are deterministic so that a re-run signs
|
|
29
|
+
* back into the same accounts instead of piling up new ones, and that only
|
|
30
|
+
* works if the credential is stable too.
|
|
31
|
+
*/
|
|
32
|
+
const PASSWORD = "PopulaceSim!2026"; // EDIT if your rules differ
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One place where HTTP becomes either a value or a thrown Error.
|
|
36
|
+
*
|
|
37
|
+
* Two things here matter more than they look:
|
|
38
|
+
*
|
|
39
|
+
* NEVER RETURN QUIETLY ON FAILURE. An adapter that swallows an error makes
|
|
40
|
+
* the report blame a later method for an earlier fault. Populace's own first
|
|
41
|
+
* run found exactly that bug in its own reference adapter: the report said
|
|
42
|
+
* "post failed 14 times" when the truth was "the profile row was never
|
|
43
|
+
* created".
|
|
44
|
+
*
|
|
45
|
+
* DESCRIBE THE FAULT, NOT THE REQUEST. Failures are grouped by shape, so
|
|
46
|
+
* "409 on POST /likes" collapses into one line. Put the post id in the
|
|
47
|
+
* message and one bug becomes fifty.
|
|
48
|
+
*/
|
|
49
|
+
async function call(method, path, { token, body } = {}) {
|
|
50
|
+
const res = await fetch(`${base}${path}`, {
|
|
51
|
+
method,
|
|
52
|
+
headers: {
|
|
53
|
+
...(body ? { "content-type": "application/json" } : {}),
|
|
54
|
+
...(token ? { authorization: `Bearer ${token}` } : {}), // EDIT: header scheme
|
|
55
|
+
},
|
|
56
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
57
|
+
signal: AbortSignal.timeout(15000),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (res.status === 204) return undefined;
|
|
61
|
+
|
|
62
|
+
const text = await res.text();
|
|
63
|
+
let payload;
|
|
64
|
+
try { payload = text ? JSON.parse(text) : undefined; } catch { payload = undefined; }
|
|
65
|
+
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const detail = payload?.error || text.slice(0, 120) || "no body";
|
|
68
|
+
throw new Error(`${res.status} on ${method} ${path}: ${detail}`);
|
|
69
|
+
}
|
|
70
|
+
return payload;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
name: "my-app", // EDIT
|
|
75
|
+
|
|
76
|
+
// doctor calls this, so an unreachable target is caught before you spend
|
|
77
|
+
// a run finding out.
|
|
78
|
+
async healthCheck() {
|
|
79
|
+
const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(10000) }); // EDIT
|
|
80
|
+
if (!res.ok) throw new Error(`health check returned ${res.status}`);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// ── identity ────────────────────────────────────────────────────────
|
|
84
|
+
// REQUIRED. The argument is exactly this shape; see adapters/contract.md.
|
|
85
|
+
// Return a HANDLE — you get it back as `user` on every later call, so put
|
|
86
|
+
// the token, session or client on it. The engine only ever reads `.id`.
|
|
87
|
+
async createUser({ name, phone, persona, index }) {
|
|
88
|
+
const out = await call("POST", "/auth/signup", { // EDIT
|
|
89
|
+
body: { phone, password: PASSWORD, name }, // EDIT: field names
|
|
90
|
+
});
|
|
91
|
+
return { id: out.id, token: out.token }; // EDIT: response shape
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
// Optional but worth it: lets `populace clean` check for leftover accounts
|
|
95
|
+
// without CREATING them to find out whether they exist. Return null when
|
|
96
|
+
// the account genuinely is not there, and throw for anything else — the
|
|
97
|
+
// difference is what stops "I could not look" being reported as "nothing
|
|
98
|
+
// was there".
|
|
99
|
+
async signIn({ phone }) {
|
|
100
|
+
try {
|
|
101
|
+
const out = await call("POST", "/auth/signin", { // EDIT
|
|
102
|
+
body: { phone, password: PASSWORD },
|
|
103
|
+
});
|
|
104
|
+
return { id: out.id, token: out.token };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (/^401/.test(error.message)) return null;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// IMPLEMENT THIS IF YOUR TOKENS EXPIRE. Populace calls it periodically.
|
|
112
|
+
// Without it, any run longer than your token lifetime collapses all at
|
|
113
|
+
// once, the report blames your API for what were really expired tokens,
|
|
114
|
+
// and cleanup cannot delete its own accounts — leaving simulated people
|
|
115
|
+
// in your environment.
|
|
116
|
+
async refreshSession(user) {
|
|
117
|
+
const out = await call("POST", "/auth/refresh", { token: user.token }); // EDIT
|
|
118
|
+
user.token = out.token;
|
|
119
|
+
return user;
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async setProfile(user, persona) {
|
|
123
|
+
await call("PATCH", "/me", { // EDIT
|
|
124
|
+
token: user.token,
|
|
125
|
+
body: { name: persona.name, city: persona.city?.name },
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// REQUIRED. The path almost nobody exercises, and the one regulators ask
|
|
130
|
+
// about.
|
|
131
|
+
async deleteUser(user) {
|
|
132
|
+
await call("DELETE", "/me", { token: user.token }); // EDIT
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// ── movement ────────────────────────────────────────────────────────
|
|
136
|
+
// Usually the heaviest sustained write load an app takes. Delete if your
|
|
137
|
+
// product has no location.
|
|
138
|
+
async reportLocation(user, { lat, lng, distanceKm, earnings, platform }) {
|
|
139
|
+
await call("POST", "/locations", { // EDIT
|
|
140
|
+
token: user.token,
|
|
141
|
+
body: { lat, lng, km: distanceKm },
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
// ── social ──────────────────────────────────────────────────────────
|
|
146
|
+
// Must return the new post's id, or `like` and `comment` have nothing to
|
|
147
|
+
// act on.
|
|
148
|
+
async post(user, text) {
|
|
149
|
+
const out = await call("POST", "/posts", { token: user.token, body: { body: text } }); // EDIT
|
|
150
|
+
return out.id;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Return an array. Objects with an `id`, or bare ids — both are accepted.
|
|
154
|
+
// This is the call that catches a feed leaking other people's rows.
|
|
155
|
+
async recentPostsByOthers(user, limit) {
|
|
156
|
+
return await call("GET", "/posts/others", { token: user.token }); // EDIT
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
async like(user, postId) {
|
|
160
|
+
await call("POST", "/likes", { token: user.token, body: { postId } }); // EDIT
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async comment(user, postId, text) {
|
|
164
|
+
await call("POST", "/comments", { token: user.token, body: { postId, body: text } }); // EDIT
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
// ── messaging ───────────────────────────────────────────────────────
|
|
168
|
+
async openConversation(user, otherUserId) {
|
|
169
|
+
const out = await call("POST", "/threads", { // EDIT
|
|
170
|
+
token: user.token,
|
|
171
|
+
body: { otherUserId },
|
|
172
|
+
});
|
|
173
|
+
return out.id; // conversation id
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
async sendMessage(user, conversationId, text) {
|
|
177
|
+
await call("POST", "/messages", { // EDIT
|
|
178
|
+
token: user.token,
|
|
179
|
+
body: { threadId: conversationId, body: text },
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
// ── groups ──────────────────────────────────────────────────────────
|
|
184
|
+
async listGroups(user) {
|
|
185
|
+
return await call("GET", "/groups", { token: user.token }); // EDIT
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
async joinGroup(user, groupId) {
|
|
189
|
+
await call("POST", "/groups/join", { token: user.token, body: { groupId } }); // EDIT
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Your adapter.
|
|
2
|
+
//
|
|
3
|
+
// Each method answers one question: "when a simulated person does this, what
|
|
4
|
+
// happens in MY app?" Delete the ones that don't apply — anything you leave out
|
|
5
|
+
// is skipped, and `populace doctor` will tell you exactly what that costs you
|
|
6
|
+
// in coverage.
|
|
7
|
+
//
|
|
8
|
+
// Two rules, both in adapters/contract.md:
|
|
9
|
+
// 1. Never point this at production.
|
|
10
|
+
// 2. Go through your real API. No admin keys, no direct database writes.
|
|
11
|
+
// A simulation that bypasses your permission rules proves nothing about
|
|
12
|
+
// whether they work.
|
|
13
|
+
|
|
14
|
+
export function createAdapter(target, config) {
|
|
15
|
+
return {
|
|
16
|
+
name: "my-app",
|
|
17
|
+
|
|
18
|
+
// Optional, but `populace doctor` uses it to check the target is up before
|
|
19
|
+
// you spend a run finding out that it isn't.
|
|
20
|
+
async healthCheck() {
|
|
21
|
+
const res = await fetch(`${target.url}/health`, { signal: AbortSignal.timeout(10000) });
|
|
22
|
+
if (!res.ok) throw new Error(`health check returned ${res.status}`);
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// --- identity (createUser is REQUIRED) --------------------------------
|
|
26
|
+
// Create or re-use an account. Return a handle — you'll get it back on
|
|
27
|
+
// every later call, so put the session/token/client on it.
|
|
28
|
+
// Re-runs should REUSE accounts, not pile up new ones.
|
|
29
|
+
async createUser({ name, phone, persona, index }) {
|
|
30
|
+
// const session = await signUpOrSignIn(...)
|
|
31
|
+
// return { id: session.userId, session };
|
|
32
|
+
throw new Error("createUser not implemented");
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async setProfile(user, persona) {},
|
|
36
|
+
|
|
37
|
+
// If your API uses expiring access tokens, IMPLEMENT THIS. Populace calls
|
|
38
|
+
// it every 30 minutes (see session.refreshEveryMinutes). Without it, a run
|
|
39
|
+
// longer than your token lifetime fails everywhere at once and the report
|
|
40
|
+
// blames your API — and cleanup can't run either, leaving simulated
|
|
41
|
+
// accounts stranded in your environment.
|
|
42
|
+
async refreshSession(user) {},
|
|
43
|
+
|
|
44
|
+
// --- the world --------------------------------------------------------
|
|
45
|
+
// Called every tick with a new position and running totals. Skip it
|
|
46
|
+
// entirely if your app has no location.
|
|
47
|
+
async reportLocation(user, { lat, lng, distanceKm, earnings, platform }) {},
|
|
48
|
+
|
|
49
|
+
// --- social -----------------------------------------------------------
|
|
50
|
+
// Empty bodies below are treated as NOT implemented, and `populace doctor`
|
|
51
|
+
// will list them as untested rather than pretending they passed. Fill in
|
|
52
|
+
// the ones your app has; delete the ones it doesn't.
|
|
53
|
+
async post(user, text) {
|
|
54
|
+
// return postId
|
|
55
|
+
},
|
|
56
|
+
async recentPostsByOthers(user, limit) {
|
|
57
|
+
// return [{ id, userId }]
|
|
58
|
+
},
|
|
59
|
+
async like(user, postId) {},
|
|
60
|
+
async comment(user, postId, text) {},
|
|
61
|
+
|
|
62
|
+
async openConversation(user, otherUserId) {
|
|
63
|
+
// return conversationId
|
|
64
|
+
},
|
|
65
|
+
async sendMessage(user, conversationId, text) {},
|
|
66
|
+
|
|
67
|
+
async listGroups(user) {
|
|
68
|
+
// return [{ id }]
|
|
69
|
+
},
|
|
70
|
+
async joinGroup(user, groupId) {},
|
|
71
|
+
|
|
72
|
+
// --- cleanup (REQUIRED) -----------------------------------------------
|
|
73
|
+
// Must fully remove the account. Prefer your app's OWN delete-account path
|
|
74
|
+
// so the simulation exercises it too — it is the route almost nobody tests
|
|
75
|
+
// and the one regulators ask about.
|
|
76
|
+
async deleteUser(user) {
|
|
77
|
+
throw new Error("deleteUser not implemented");
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="utf-8">
|
|
3
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
4
|
+
<title>Populace report — Buzz Buzz</title>
|
|
5
|
+
<style>
|
|
6
|
+
:root{
|
|
7
|
+
--paper:#F4F5F7;--card:#fff;--ink:#12181D;--body:#3A454E;--muted:#6B7883;
|
|
8
|
+
--rule:#DDE2E7;--accent:#A85A0B;--ok:#2C6E4C;--bad:#A63127;--warn:#9A6A08;
|
|
9
|
+
--bar:#C9D4DC;--barbad:#E0A99F;
|
|
10
|
+
--mono:ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,monospace;
|
|
11
|
+
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;
|
|
12
|
+
}
|
|
13
|
+
@media (prefers-color-scheme:dark){:root{
|
|
14
|
+
--paper:#0E1418;--card:#161E24;--ink:#E8EEF3;--body:#B0BDC7;--muted:#7B8994;
|
|
15
|
+
--rule:#26323A;--accent:#E0A45C;--ok:#5FAB80;--bad:#DE7568;--warn:#D2A24E;
|
|
16
|
+
--bar:#2E3A43;--barbad:#5C332C;}}
|
|
17
|
+
:root[data-theme="dark"]{--paper:#0E1418;--card:#161E24;--ink:#E8EEF3;--body:#B0BDC7;
|
|
18
|
+
--muted:#7B8994;--rule:#26323A;--accent:#E0A45C;--ok:#5FAB80;--bad:#DE7568;
|
|
19
|
+
--warn:#D2A24E;--bar:#2E3A43;--barbad:#5C332C;}
|
|
20
|
+
:root[data-theme="light"]{--paper:#F4F5F7;--card:#fff;--ink:#12181D;--body:#3A454E;
|
|
21
|
+
--muted:#6B7883;--rule:#DDE2E7;--accent:#A85A0B;--ok:#2C6E4C;--bad:#A63127;
|
|
22
|
+
--warn:#9A6A08;--bar:#C9D4DC;--barbad:#E0A99F;}
|
|
23
|
+
*{box-sizing:border-box}
|
|
24
|
+
body{margin:0;background:var(--paper);color:var(--body);font-family:var(--sans);
|
|
25
|
+
font-size:15.5px;line-height:1.6;-webkit-font-smoothing:antialiased}
|
|
26
|
+
.wrap{max-width:900px;margin:0 auto;padding:0 22px 80px}
|
|
27
|
+
h1,h2,h3{color:var(--ink);margin:0;text-wrap:balance}
|
|
28
|
+
h1{font-family:var(--mono);font-size:clamp(22px,4vw,30px);letter-spacing:-.02em}
|
|
29
|
+
h2{font-size:17px;font-family:var(--mono);letter-spacing:-.01em}
|
|
30
|
+
.mono{font-family:var(--mono)}
|
|
31
|
+
header{padding:44px 0 24px;border-bottom:2px solid var(--ink);display:flex;
|
|
32
|
+
flex-direction:column;gap:10px}
|
|
33
|
+
.sub{font-family:var(--mono);font-size:12.5px;color:var(--muted)}
|
|
34
|
+
.verdict{margin-top:26px;padding:20px 22px;border-radius:6px;display:flex;
|
|
35
|
+
flex-direction:column;gap:8px;border-left:4px solid}
|
|
36
|
+
.verdict.ok{background:color-mix(in srgb,var(--ok) 8%,transparent);border-color:var(--ok)}
|
|
37
|
+
.verdict.bad{background:color-mix(in srgb,var(--bad) 8%,transparent);border-color:var(--bad)}
|
|
38
|
+
.verdict h2{color:var(--ink)}
|
|
39
|
+
.verdict ul{margin:4px 0 0;padding-left:20px}
|
|
40
|
+
.stats{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));margin-top:26px}
|
|
41
|
+
.stat{background:var(--card);border:1px solid var(--rule);border-radius:6px;padding:15px 16px}
|
|
42
|
+
.stat .v{font-family:var(--mono);font-size:23px;color:var(--ink);font-weight:600;
|
|
43
|
+
letter-spacing:-.02em;font-variant-numeric:tabular-nums;display:block}
|
|
44
|
+
.stat .l{font-family:var(--mono);font-size:10px;letter-spacing:.12em;
|
|
45
|
+
text-transform:uppercase;color:var(--muted)}
|
|
46
|
+
section{margin-top:42px;display:flex;flex-direction:column;gap:14px}
|
|
47
|
+
.panel{background:var(--card);border:1px solid var(--rule);border-radius:6px;padding:20px 22px}
|
|
48
|
+
.panel.danger{border-color:var(--bad);border-left-width:4px}
|
|
49
|
+
.panel.danger h2{color:var(--bad)}
|
|
50
|
+
.tablewrap{overflow-x:auto;border:1px solid var(--rule);border-radius:6px;background:var(--card)}
|
|
51
|
+
table{border-collapse:collapse;width:100%;min-width:600px}
|
|
52
|
+
th,td{padding:10px 14px;text-align:left;border-bottom:1px solid var(--rule)}
|
|
53
|
+
th{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;
|
|
54
|
+
color:var(--muted);font-weight:600}
|
|
55
|
+
td.num{text-align:right;font-family:var(--mono);font-variant-numeric:tabular-nums;font-size:13.5px}
|
|
56
|
+
td.name{font-size:13.5px;color:var(--ink);white-space:nowrap}
|
|
57
|
+
td.zero{color:var(--muted)}
|
|
58
|
+
td.fail{color:var(--bad);font-weight:600}
|
|
59
|
+
.x{color:var(--bad);margin-right:6px}
|
|
60
|
+
.barcell{width:120px}
|
|
61
|
+
.bar{height:7px;background:var(--bar);border-radius:4px}
|
|
62
|
+
.row-bad .bar{background:var(--barbad)}
|
|
63
|
+
tr.errrow td{padding-top:0;border-bottom:1px solid var(--rule)}
|
|
64
|
+
.err{font-family:var(--mono);font-size:12px;color:var(--bad);padding:3px 0 3px 26px}
|
|
65
|
+
.errn{color:var(--muted);margin-right:8px}
|
|
66
|
+
.trace{font-family:var(--mono);font-size:11.5px;white-space:pre-wrap;overflow-x:auto;
|
|
67
|
+
background:var(--paper);padding:14px;border-radius:5px;margin:0;color:var(--body)}
|
|
68
|
+
ul.cov{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:9px}
|
|
69
|
+
ul.cov li{display:flex;gap:14px;flex-wrap:wrap;align-items:baseline;font-size:14px}
|
|
70
|
+
ul.cov .mono{color:var(--ink);min-width:170px;font-size:13px}
|
|
71
|
+
.would{color:var(--muted);font-size:13.5px}
|
|
72
|
+
.chip{display:inline-block;font-family:var(--mono);font-size:12px;padding:7px 13px;
|
|
73
|
+
border-radius:5px;border:1px solid}
|
|
74
|
+
.chip.ok{color:var(--ok);border-color:var(--ok);background:color-mix(in srgb,var(--ok) 8%,transparent)}
|
|
75
|
+
.chip.bad{color:var(--bad);border-color:var(--bad);background:color-mix(in srgb,var(--bad) 8%,transparent)}
|
|
76
|
+
.chip.warn{color:var(--warn);border-color:var(--warn);background:color-mix(in srgb,var(--warn) 8%,transparent)}
|
|
77
|
+
footer{margin-top:52px;padding-top:20px;border-top:1px solid var(--rule);
|
|
78
|
+
font-size:13px;color:var(--muted);display:flex;flex-direction:column;gap:6px}
|
|
79
|
+
</style></head><body>
|
|
80
|
+
<div class="wrap">
|
|
81
|
+
|
|
82
|
+
<header>
|
|
83
|
+
<h1>Buzz Buzz</h1>
|
|
84
|
+
<span class="sub">test environment · 10 simulated people ·
|
|
85
|
+
14m 38s · Sat, 15 Aug 2026 11:18:42 GMT</span>
|
|
86
|
+
</header>
|
|
87
|
+
|
|
88
|
+
<div class="verdict ok">
|
|
89
|
+
<h2>No failures across 2030 API calls</h2>
|
|
90
|
+
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
<div class="stats">
|
|
96
|
+
<div class="stat"><span class="v">2030</span><span class="l">API calls</span></div>
|
|
97
|
+
<div class="stat"><span class="v" style="color:var(--ok)">0</span><span class="l">failed</span></div>
|
|
98
|
+
<div class="stat"><span class="v">0.0%</span><span class="l">failure rate</span></div>
|
|
99
|
+
<div class="stat"><span class="v">10</span><span class="l">concurrent users</span></div>
|
|
100
|
+
<div class="stat"><span class="v">13/13</span><span class="l">coverage</span></div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<section>
|
|
104
|
+
<h2>Your API under 10 concurrent users</h2>
|
|
105
|
+
<div class="tablewrap">
|
|
106
|
+
<table>
|
|
107
|
+
<thead><tr><th>Method</th><th style="text-align:right">Calls</th>
|
|
108
|
+
<th style="text-align:right">Fails</th><th style="text-align:right">p50</th>
|
|
109
|
+
<th style="text-align:right">p95</th><th>p95 relative</th></tr></thead>
|
|
110
|
+
<tbody>
|
|
111
|
+
<tr class="">
|
|
112
|
+
<td class="mono name">reportLocation</td>
|
|
113
|
+
<td class="num">892</td>
|
|
114
|
+
<td class="num zero">0</td>
|
|
115
|
+
<td class="num">711ms</td>
|
|
116
|
+
<td class="num">1.0s</td>
|
|
117
|
+
<td class="barcell"><div class="bar" style="width:100.0%"></div></td>
|
|
118
|
+
</tr>
|
|
119
|
+
|
|
120
|
+
<tr class="">
|
|
121
|
+
<td class="mono name">recentPostsByOthers</td>
|
|
122
|
+
<td class="num">298</td>
|
|
123
|
+
<td class="num zero">0</td>
|
|
124
|
+
<td class="num">241ms</td>
|
|
125
|
+
<td class="num">349ms</td>
|
|
126
|
+
<td class="barcell"><div class="bar" style="width:34.8%"></div></td>
|
|
127
|
+
</tr>
|
|
128
|
+
|
|
129
|
+
<tr class="">
|
|
130
|
+
<td class="mono name">like</td>
|
|
131
|
+
<td class="num">296</td>
|
|
132
|
+
<td class="num zero">0</td>
|
|
133
|
+
<td class="num">238ms</td>
|
|
134
|
+
<td class="num">319ms</td>
|
|
135
|
+
<td class="barcell"><div class="bar" style="width:31.8%"></div></td>
|
|
136
|
+
</tr>
|
|
137
|
+
|
|
138
|
+
<tr class="">
|
|
139
|
+
<td class="mono name">post</td>
|
|
140
|
+
<td class="num">139</td>
|
|
141
|
+
<td class="num zero">0</td>
|
|
142
|
+
<td class="num">240ms</td>
|
|
143
|
+
<td class="num">313ms</td>
|
|
144
|
+
<td class="barcell"><div class="bar" style="width:31.2%"></div></td>
|
|
145
|
+
</tr>
|
|
146
|
+
|
|
147
|
+
<tr class="">
|
|
148
|
+
<td class="mono name">comment</td>
|
|
149
|
+
<td class="num">123</td>
|
|
150
|
+
<td class="num zero">0</td>
|
|
151
|
+
<td class="num">243ms</td>
|
|
152
|
+
<td class="num">326ms</td>
|
|
153
|
+
<td class="barcell"><div class="bar" style="width:32.5%"></div></td>
|
|
154
|
+
</tr>
|
|
155
|
+
|
|
156
|
+
<tr class="">
|
|
157
|
+
<td class="mono name">openConversation</td>
|
|
158
|
+
<td class="num">110</td>
|
|
159
|
+
<td class="num zero">0</td>
|
|
160
|
+
<td class="num">245ms</td>
|
|
161
|
+
<td class="num">336ms</td>
|
|
162
|
+
<td class="barcell"><div class="bar" style="width:33.5%"></div></td>
|
|
163
|
+
</tr>
|
|
164
|
+
|
|
165
|
+
<tr class="">
|
|
166
|
+
<td class="mono name">sendMessage</td>
|
|
167
|
+
<td class="num">110</td>
|
|
168
|
+
<td class="num zero">0</td>
|
|
169
|
+
<td class="num">244ms</td>
|
|
170
|
+
<td class="num">351ms</td>
|
|
171
|
+
<td class="barcell"><div class="bar" style="width:35.0%"></div></td>
|
|
172
|
+
</tr>
|
|
173
|
+
|
|
174
|
+
<tr class="">
|
|
175
|
+
<td class="mono name">listGroups</td>
|
|
176
|
+
<td class="num">16</td>
|
|
177
|
+
<td class="num zero">0</td>
|
|
178
|
+
<td class="num">250ms</td>
|
|
179
|
+
<td class="num">312ms</td>
|
|
180
|
+
<td class="barcell"><div class="bar" style="width:31.1%"></div></td>
|
|
181
|
+
</tr>
|
|
182
|
+
|
|
183
|
+
<tr class="">
|
|
184
|
+
<td class="mono name">joinGroup</td>
|
|
185
|
+
<td class="num">16</td>
|
|
186
|
+
<td class="num zero">0</td>
|
|
187
|
+
<td class="num">235ms</td>
|
|
188
|
+
<td class="num">311ms</td>
|
|
189
|
+
<td class="barcell"><div class="bar" style="width:31.0%"></div></td>
|
|
190
|
+
</tr>
|
|
191
|
+
|
|
192
|
+
<tr class="">
|
|
193
|
+
<td class="mono name">createUser</td>
|
|
194
|
+
<td class="num">10</td>
|
|
195
|
+
<td class="num zero">0</td>
|
|
196
|
+
<td class="num">601ms</td>
|
|
197
|
+
<td class="num">834ms</td>
|
|
198
|
+
<td class="barcell"><div class="bar" style="width:83.2%"></div></td>
|
|
199
|
+
</tr>
|
|
200
|
+
|
|
201
|
+
<tr class="">
|
|
202
|
+
<td class="mono name">setProfile</td>
|
|
203
|
+
<td class="num">10</td>
|
|
204
|
+
<td class="num zero">0</td>
|
|
205
|
+
<td class="num">249ms</td>
|
|
206
|
+
<td class="num">392ms</td>
|
|
207
|
+
<td class="barcell"><div class="bar" style="width:39.1%"></div></td>
|
|
208
|
+
</tr>
|
|
209
|
+
|
|
210
|
+
<tr class="">
|
|
211
|
+
<td class="mono name">deleteUser</td>
|
|
212
|
+
<td class="num">10</td>
|
|
213
|
+
<td class="num zero">0</td>
|
|
214
|
+
<td class="num">320ms</td>
|
|
215
|
+
<td class="num">508ms</td>
|
|
216
|
+
<td class="barcell"><div class="bar" style="width:50.6%"></div></td>
|
|
217
|
+
</tr>
|
|
218
|
+
</tbody>
|
|
219
|
+
</table>
|
|
220
|
+
</div>
|
|
221
|
+
</section>
|
|
222
|
+
|
|
223
|
+
<section>
|
|
224
|
+
<h2>What the population did</h2>
|
|
225
|
+
<div class="panel">
|
|
226
|
+
36 km travelled · 139 posts ·
|
|
227
|
+
296 likes · 123 comments ·
|
|
228
|
+
110 messages · 16 group joins
|
|
229
|
+
</div>
|
|
230
|
+
</section>
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
<section>
|
|
235
|
+
<h2>Cleanup</h2>
|
|
236
|
+
<div><div class="chip ok">Cleanup complete — 10 accounts removed</div></div>
|
|
237
|
+
</section>
|
|
238
|
+
|
|
239
|
+
<footer>
|
|
240
|
+
<span>Generated by Populace 0.1.0 · adapter <span class="mono">../../adapters/buzzbuzz.mjs</span></span>
|
|
241
|
+
<span>Simulated people are generated from patterns. This report shows whether your app
|
|
242
|
+
<strong>works</strong> — not whether anyone wants it.</span>
|
|
243
|
+
</footer>
|
|
244
|
+
|
|
245
|
+
</div></body></html>
|