ccqa-tools 1.37.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/README.md +177 -0
- package/dist/coverage/collector.cjs +229 -0
- package/dist/coverage/collector.d.cts +183 -0
- package/dist/coverage/collector.d.ts +183 -0
- package/dist/coverage/collector.js +225 -0
- package/dist/coverage/core.cjs +176 -0
- package/dist/coverage/core.d.cts +157 -0
- package/dist/coverage/core.d.ts +157 -0
- package/dist/coverage/core.js +163 -0
- package/dist/coverage/middleware.cjs +161 -0
- package/dist/coverage/middleware.d.cts +23 -0
- package/dist/coverage/middleware.d.ts +23 -0
- package/dist/coverage/middleware.js +158 -0
- package/dist/coverage/next-loader.cjs +158 -0
- package/dist/coverage/next-loader.d.cts +18 -0
- package/dist/coverage/next-loader.d.ts +19 -0
- package/dist/coverage/next-loader.js +158 -0
- package/dist/coverage/next.cjs +101 -0
- package/dist/coverage/next.d.cts +33 -0
- package/dist/coverage/next.d.ts +33 -0
- package/dist/coverage/next.js +100 -0
- package/dist/coverage/register.cjs +741 -0
- package/dist/coverage/register.d.cts +1 -0
- package/dist/coverage/register.d.ts +1 -0
- package/dist/coverage/register.js +716 -0
- package/dist/coverage/slack.cjs +228 -0
- package/dist/coverage/slack.d.cts +47 -0
- package/dist/coverage/slack.d.ts +47 -0
- package/dist/coverage/slack.js +225 -0
- package/dist/coverage/temporal-workflow.cjs +154 -0
- package/dist/coverage/temporal-workflow.d.cts +28 -0
- package/dist/coverage/temporal-workflow.d.ts +28 -0
- package/dist/coverage/temporal-workflow.js +153 -0
- package/dist/coverage/temporal.cjs +253 -0
- package/dist/coverage/temporal.d.cts +40 -0
- package/dist/coverage/temporal.d.ts +40 -0
- package/dist/coverage/temporal.js +250 -0
- package/package.json +95 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/coverage/core.ts
|
|
3
|
+
const RUNTIME_KEY = Symbol.for("ccqa.coverage.runtime");
|
|
4
|
+
function globals() {
|
|
5
|
+
return globalThis;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Memoized so the hot path skips the `globalThis` read. `installRuntime`
|
|
9
|
+
* primes it and never replaces an installed runtime, so once set it never
|
|
10
|
+
* goes stale — but it must stay undefined (and keep re-reading `globalThis`)
|
|
11
|
+
* until then, or a module that finished loading before `register` installs
|
|
12
|
+
* the runtime would be stuck uninstrumented for the rest of the process.
|
|
13
|
+
*/
|
|
14
|
+
let cachedRuntime;
|
|
15
|
+
function runtime() {
|
|
16
|
+
if (cachedRuntime === void 0) cachedRuntime = globals()[RUNTIME_KEY];
|
|
17
|
+
return cachedRuntime;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Records that `tag` caused this work at `at`, without deciding whose it is.
|
|
21
|
+
*
|
|
22
|
+
* Carrier wins: a request that already named a spec needs no identity, and
|
|
23
|
+
* overwriting it would replace a fact with something the run still has to
|
|
24
|
+
* interpret. Everything else is recorded whoever it came from — the application
|
|
25
|
+
* is never told which identities are being measured, so it cannot filter, and
|
|
26
|
+
* the run discards the ones it did not ask for.
|
|
27
|
+
*/
|
|
28
|
+
function runAsActor(tag, at, fn) {
|
|
29
|
+
const rt = runtime();
|
|
30
|
+
if (rt === void 0) return fn();
|
|
31
|
+
if (rt.als.getStore()?.specId !== void 0) return fn();
|
|
32
|
+
return rt.als.run({
|
|
33
|
+
actor: {
|
|
34
|
+
tag,
|
|
35
|
+
at
|
|
36
|
+
},
|
|
37
|
+
files: openActorBucket(rt, tag, at)
|
|
38
|
+
}, fn);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The key both halves of the collector agree on. A space separates them safely:
|
|
42
|
+
* a spec id can never contain one, so identity keys and spec ids stay disjoint
|
|
43
|
+
* where the collector tracks both in one map.
|
|
44
|
+
*/
|
|
45
|
+
function actorBucketKey(tag, at) {
|
|
46
|
+
return `${tag} ${at}`;
|
|
47
|
+
}
|
|
48
|
+
/** Returns the identity's file set for that instant, creating it if new. */
|
|
49
|
+
function openActorBucket(runtime, tag, at) {
|
|
50
|
+
const key = actorBucketKey(tag, at);
|
|
51
|
+
let bucket = runtime.actors.get(key);
|
|
52
|
+
if (bucket === void 0) {
|
|
53
|
+
bucket = {
|
|
54
|
+
tag,
|
|
55
|
+
at,
|
|
56
|
+
files: /* @__PURE__ */ new Set()
|
|
57
|
+
};
|
|
58
|
+
runtime.actors.set(key, bucket);
|
|
59
|
+
armGate(runtime);
|
|
60
|
+
}
|
|
61
|
+
return bucket.files;
|
|
62
|
+
}
|
|
63
|
+
function armGate(runtime) {
|
|
64
|
+
runtime.active = runtime.buckets.size + runtime.actors.size;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/coverage/wire.ts
|
|
68
|
+
/**
|
|
69
|
+
* Enables the instrumentation. Unset means the register hook is never loaded
|
|
70
|
+
* and the application pays nothing at all.
|
|
71
|
+
*
|
|
72
|
+
* `1` / `true` turns it on and leaves attribution to the incoming carrier.
|
|
73
|
+
* Any other value is itself a `<runId>.<specId>` and becomes the ambient spec
|
|
74
|
+
* for the process — the only way to attribute an entry point that has no
|
|
75
|
+
* inbound request to read, such as a worker started per spec.
|
|
76
|
+
*/
|
|
77
|
+
const ENV_NAME = "CCQA_COVERAGE";
|
|
78
|
+
const SPEC_ID = /^[A-Za-z0-9._\-/]{1,200}$/;
|
|
79
|
+
/**
|
|
80
|
+
* Accepts a carrier value only if it looks like an id we wrote.
|
|
81
|
+
*
|
|
82
|
+
* The cookie is client-controlled, so this is the first of two gates: the
|
|
83
|
+
* second is the hub refusing runs it never started.
|
|
84
|
+
*/
|
|
85
|
+
function parseSpecId(raw) {
|
|
86
|
+
if (!raw) return void 0;
|
|
87
|
+
const value = raw.trim();
|
|
88
|
+
if (!SPEC_ID.test(value)) return void 0;
|
|
89
|
+
if (value === "1" || value === "true") return void 0;
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/coverage/runtime-env.ts
|
|
94
|
+
function readConfig(env = process.env) {
|
|
95
|
+
const raw = env[ENV_NAME];
|
|
96
|
+
const include = (env["CCQA_COVERAGE_INCLUDE"] ?? "src").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
97
|
+
return {
|
|
98
|
+
enabled: raw !== void 0 && raw !== "" && raw !== "0" && raw !== "false",
|
|
99
|
+
ambientSpecId: parseSpecId(raw),
|
|
100
|
+
root: env["CCQA_COVERAGE_ROOT"] ?? process.cwd(),
|
|
101
|
+
include,
|
|
102
|
+
debug: env["CCQA_COVERAGE_DEBUG"] === "1" || env["CCQA_COVERAGE_DEBUG"] === "true"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Diagnostics go to stderr and nowhere else. A `--import` preload is inherited
|
|
107
|
+
* by every child node process, and writing to stdout corrupts whatever the host
|
|
108
|
+
* was parsing there — enough to make a framework's own toolchain fail to start.
|
|
109
|
+
*/
|
|
110
|
+
function debugLog(config, message) {
|
|
111
|
+
if (!config.debug) return;
|
|
112
|
+
process.stderr.write(`[ccqa-tools] ${message}\n`);
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/coverage/presets/slack.ts
|
|
116
|
+
/**
|
|
117
|
+
* Records who caused a Slack request, so the flows a chat platform drives can
|
|
118
|
+
* be attributed at all.
|
|
119
|
+
*
|
|
120
|
+
* Those requests are sent by Slack, not by the browser under test, so none of
|
|
121
|
+
* the carriers the rest of this package relies on is present — no cookie, no
|
|
122
|
+
* baggage header, nothing. What the payload does say is which user acted, and
|
|
123
|
+
* that is the whole of what this records:
|
|
124
|
+
*
|
|
125
|
+
* app.use(slackActor()) // after whatever parses the body
|
|
126
|
+
*
|
|
127
|
+
* No arguments, no configuration, no environment. It does not know which users
|
|
128
|
+
* are being measured, when a spec is running, or that ccqa exists — the run
|
|
129
|
+
* decides all of that from the other end. With `CCQA_COVERAGE` unset the
|
|
130
|
+
* runtime is absent and every call here returns immediately.
|
|
131
|
+
*
|
|
132
|
+
* Extraction is a fixed list of the places Slack puts a user id. A payload
|
|
133
|
+
* shape not on the list records nothing rather than guessing, because a wrong
|
|
134
|
+
* identity is a wrong answer while a missing one is a counted gap.
|
|
135
|
+
*/
|
|
136
|
+
/**
|
|
137
|
+
* Read once: the identity of every inbound request would otherwise re-read the
|
|
138
|
+
* environment, and this sits in front of every Slack request the app serves.
|
|
139
|
+
*/
|
|
140
|
+
const config = readConfig();
|
|
141
|
+
/** The tag prefix, and therefore the provider key a project writes in its config. */
|
|
142
|
+
const SLACK_PROVIDER = "slack";
|
|
143
|
+
/**
|
|
144
|
+
* Middleware for either shape a Slack app uses.
|
|
145
|
+
*
|
|
146
|
+
* Bolt hands its middleware one object holding the parsed body and its own
|
|
147
|
+
* `next`, and awaits what comes back. A connect-style stack hands
|
|
148
|
+
* `(request, response, next)`, and a context-style one `(ctx, next)`. All are
|
|
149
|
+
* accepted so the application's one line does not have to know which it is —
|
|
150
|
+
* and in a Bolt app `app.use()` is the only insertion point there is.
|
|
151
|
+
*
|
|
152
|
+
* Place it after whatever parses the body: the payload is what carries the
|
|
153
|
+
* identity, and before parsing there is none. A framework whose body is only
|
|
154
|
+
* available by awaiting it is out of reach here for the same reason — there is
|
|
155
|
+
* nothing to read at the moment this runs.
|
|
156
|
+
*/
|
|
157
|
+
function slackActor() {
|
|
158
|
+
return async function coverageSlackActor(...args) {
|
|
159
|
+
const first = args[0];
|
|
160
|
+
const bolt = args.length === 1 && isRecord(first) && typeof first.next === "function" ? first : void 0;
|
|
161
|
+
const body = bolt ? bolt.body : requestBody(first);
|
|
162
|
+
const next = bolt ? bolt.next : args.slice(1).find(isFunction);
|
|
163
|
+
if (next === void 0) return;
|
|
164
|
+
const user = slackUserId(body);
|
|
165
|
+
if (user === void 0) {
|
|
166
|
+
await next();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const tag = `${SLACK_PROVIDER}:${user}`;
|
|
170
|
+
debugLog(config, `slack actor ${tag}`);
|
|
171
|
+
await runAsActor(tag, Date.now(), next);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The user id in a Slack payload, or undefined if this shape carries none.
|
|
176
|
+
*
|
|
177
|
+
* Exported for its own tests: the list is the entire contract, and a shape
|
|
178
|
+
* silently dropping off it looks exactly like a flow that reached no code.
|
|
179
|
+
*/
|
|
180
|
+
function slackUserId(body) {
|
|
181
|
+
if (!isRecord(body)) return void 0;
|
|
182
|
+
const payload = parsePayload(body.payload);
|
|
183
|
+
if (payload !== void 0) return idOf(payload.user);
|
|
184
|
+
const event = body.event;
|
|
185
|
+
if (isRecord(event)) return idOf(event.user);
|
|
186
|
+
if (body.user !== void 0) return idOf(body.user);
|
|
187
|
+
if (typeof body.user_id === "string") return nonEmpty(body.user_id);
|
|
188
|
+
}
|
|
189
|
+
function parsePayload(raw) {
|
|
190
|
+
if (isRecord(raw)) return raw;
|
|
191
|
+
if (typeof raw !== "string") return void 0;
|
|
192
|
+
try {
|
|
193
|
+
const parsed = JSON.parse(raw);
|
|
194
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
195
|
+
} catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Slack writes a user as a bare id in some payloads and as an object in others. */
|
|
200
|
+
function idOf(user) {
|
|
201
|
+
if (typeof user === "string") return nonEmpty(user);
|
|
202
|
+
if (isRecord(user) && typeof user.id === "string") return nonEmpty(user.id);
|
|
203
|
+
}
|
|
204
|
+
function nonEmpty(value) {
|
|
205
|
+
const trimmed = value.trim();
|
|
206
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
207
|
+
}
|
|
208
|
+
function isRecord(value) {
|
|
209
|
+
return typeof value === "object" && value !== null;
|
|
210
|
+
}
|
|
211
|
+
function isFunction(value) {
|
|
212
|
+
return typeof value === "function";
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The parsed request body, from the request itself or from a context wrapping
|
|
216
|
+
* it. A context's own `body` is the *response* it is building, so reading that
|
|
217
|
+
* would hand the extractor the wrong object and quietly find no identity.
|
|
218
|
+
*/
|
|
219
|
+
function requestBody(first) {
|
|
220
|
+
if (!isRecord(first)) return void 0;
|
|
221
|
+
const request = first.request;
|
|
222
|
+
if (isRecord(request) && isRecord(request.body)) return request.body;
|
|
223
|
+
return isRecord(first.body) ? first.body : void 0;
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
exports.SLACK_PROVIDER = SLACK_PROVIDER;
|
|
227
|
+
exports.slackActor = slackActor;
|
|
228
|
+
exports.slackUserId = slackUserId;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/coverage/presets/slack.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Records who caused a Slack request, so the flows a chat platform drives can
|
|
4
|
+
* be attributed at all.
|
|
5
|
+
*
|
|
6
|
+
* Those requests are sent by Slack, not by the browser under test, so none of
|
|
7
|
+
* the carriers the rest of this package relies on is present — no cookie, no
|
|
8
|
+
* baggage header, nothing. What the payload does say is which user acted, and
|
|
9
|
+
* that is the whole of what this records:
|
|
10
|
+
*
|
|
11
|
+
* app.use(slackActor()) // after whatever parses the body
|
|
12
|
+
*
|
|
13
|
+
* No arguments, no configuration, no environment. It does not know which users
|
|
14
|
+
* are being measured, when a spec is running, or that ccqa exists — the run
|
|
15
|
+
* decides all of that from the other end. With `CCQA_COVERAGE` unset the
|
|
16
|
+
* runtime is absent and every call here returns immediately.
|
|
17
|
+
*
|
|
18
|
+
* Extraction is a fixed list of the places Slack puts a user id. A payload
|
|
19
|
+
* shape not on the list records nothing rather than guessing, because a wrong
|
|
20
|
+
* identity is a wrong answer while a missing one is a counted gap.
|
|
21
|
+
*/
|
|
22
|
+
/** The tag prefix, and therefore the provider key a project writes in its config. */
|
|
23
|
+
declare const SLACK_PROVIDER = "slack";
|
|
24
|
+
/**
|
|
25
|
+
* Middleware for either shape a Slack app uses.
|
|
26
|
+
*
|
|
27
|
+
* Bolt hands its middleware one object holding the parsed body and its own
|
|
28
|
+
* `next`, and awaits what comes back. A connect-style stack hands
|
|
29
|
+
* `(request, response, next)`, and a context-style one `(ctx, next)`. All are
|
|
30
|
+
* accepted so the application's one line does not have to know which it is —
|
|
31
|
+
* and in a Bolt app `app.use()` is the only insertion point there is.
|
|
32
|
+
*
|
|
33
|
+
* Place it after whatever parses the body: the payload is what carries the
|
|
34
|
+
* identity, and before parsing there is none. A framework whose body is only
|
|
35
|
+
* available by awaiting it is out of reach here for the same reason — there is
|
|
36
|
+
* nothing to read at the moment this runs.
|
|
37
|
+
*/
|
|
38
|
+
declare function slackActor(): (...args: unknown[]) => Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* The user id in a Slack payload, or undefined if this shape carries none.
|
|
41
|
+
*
|
|
42
|
+
* Exported for its own tests: the list is the entire contract, and a shape
|
|
43
|
+
* silently dropping off it looks exactly like a flow that reached no code.
|
|
44
|
+
*/
|
|
45
|
+
declare function slackUserId(body: unknown): string | undefined;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { SLACK_PROVIDER, slackActor, slackUserId };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/coverage/presets/slack.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Records who caused a Slack request, so the flows a chat platform drives can
|
|
4
|
+
* be attributed at all.
|
|
5
|
+
*
|
|
6
|
+
* Those requests are sent by Slack, not by the browser under test, so none of
|
|
7
|
+
* the carriers the rest of this package relies on is present — no cookie, no
|
|
8
|
+
* baggage header, nothing. What the payload does say is which user acted, and
|
|
9
|
+
* that is the whole of what this records:
|
|
10
|
+
*
|
|
11
|
+
* app.use(slackActor()) // after whatever parses the body
|
|
12
|
+
*
|
|
13
|
+
* No arguments, no configuration, no environment. It does not know which users
|
|
14
|
+
* are being measured, when a spec is running, or that ccqa exists — the run
|
|
15
|
+
* decides all of that from the other end. With `CCQA_COVERAGE` unset the
|
|
16
|
+
* runtime is absent and every call here returns immediately.
|
|
17
|
+
*
|
|
18
|
+
* Extraction is a fixed list of the places Slack puts a user id. A payload
|
|
19
|
+
* shape not on the list records nothing rather than guessing, because a wrong
|
|
20
|
+
* identity is a wrong answer while a missing one is a counted gap.
|
|
21
|
+
*/
|
|
22
|
+
/** The tag prefix, and therefore the provider key a project writes in its config. */
|
|
23
|
+
declare const SLACK_PROVIDER = "slack";
|
|
24
|
+
/**
|
|
25
|
+
* Middleware for either shape a Slack app uses.
|
|
26
|
+
*
|
|
27
|
+
* Bolt hands its middleware one object holding the parsed body and its own
|
|
28
|
+
* `next`, and awaits what comes back. A connect-style stack hands
|
|
29
|
+
* `(request, response, next)`, and a context-style one `(ctx, next)`. All are
|
|
30
|
+
* accepted so the application's one line does not have to know which it is —
|
|
31
|
+
* and in a Bolt app `app.use()` is the only insertion point there is.
|
|
32
|
+
*
|
|
33
|
+
* Place it after whatever parses the body: the payload is what carries the
|
|
34
|
+
* identity, and before parsing there is none. A framework whose body is only
|
|
35
|
+
* available by awaiting it is out of reach here for the same reason — there is
|
|
36
|
+
* nothing to read at the moment this runs.
|
|
37
|
+
*/
|
|
38
|
+
declare function slackActor(): (...args: unknown[]) => Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* The user id in a Slack payload, or undefined if this shape carries none.
|
|
41
|
+
*
|
|
42
|
+
* Exported for its own tests: the list is the entire contract, and a shape
|
|
43
|
+
* silently dropping off it looks exactly like a flow that reached no code.
|
|
44
|
+
*/
|
|
45
|
+
declare function slackUserId(body: unknown): string | undefined;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { SLACK_PROVIDER, slackActor, slackUserId };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
//#region src/coverage/core.ts
|
|
2
|
+
const RUNTIME_KEY = Symbol.for("ccqa.coverage.runtime");
|
|
3
|
+
function globals() {
|
|
4
|
+
return globalThis;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Memoized so the hot path skips the `globalThis` read. `installRuntime`
|
|
8
|
+
* primes it and never replaces an installed runtime, so once set it never
|
|
9
|
+
* goes stale — but it must stay undefined (and keep re-reading `globalThis`)
|
|
10
|
+
* until then, or a module that finished loading before `register` installs
|
|
11
|
+
* the runtime would be stuck uninstrumented for the rest of the process.
|
|
12
|
+
*/
|
|
13
|
+
let cachedRuntime;
|
|
14
|
+
function runtime() {
|
|
15
|
+
if (cachedRuntime === void 0) cachedRuntime = globals()[RUNTIME_KEY];
|
|
16
|
+
return cachedRuntime;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Records that `tag` caused this work at `at`, without deciding whose it is.
|
|
20
|
+
*
|
|
21
|
+
* Carrier wins: a request that already named a spec needs no identity, and
|
|
22
|
+
* overwriting it would replace a fact with something the run still has to
|
|
23
|
+
* interpret. Everything else is recorded whoever it came from — the application
|
|
24
|
+
* is never told which identities are being measured, so it cannot filter, and
|
|
25
|
+
* the run discards the ones it did not ask for.
|
|
26
|
+
*/
|
|
27
|
+
function runAsActor(tag, at, fn) {
|
|
28
|
+
const rt = runtime();
|
|
29
|
+
if (rt === void 0) return fn();
|
|
30
|
+
if (rt.als.getStore()?.specId !== void 0) return fn();
|
|
31
|
+
return rt.als.run({
|
|
32
|
+
actor: {
|
|
33
|
+
tag,
|
|
34
|
+
at
|
|
35
|
+
},
|
|
36
|
+
files: openActorBucket(rt, tag, at)
|
|
37
|
+
}, fn);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The key both halves of the collector agree on. A space separates them safely:
|
|
41
|
+
* a spec id can never contain one, so identity keys and spec ids stay disjoint
|
|
42
|
+
* where the collector tracks both in one map.
|
|
43
|
+
*/
|
|
44
|
+
function actorBucketKey(tag, at) {
|
|
45
|
+
return `${tag} ${at}`;
|
|
46
|
+
}
|
|
47
|
+
/** Returns the identity's file set for that instant, creating it if new. */
|
|
48
|
+
function openActorBucket(runtime, tag, at) {
|
|
49
|
+
const key = actorBucketKey(tag, at);
|
|
50
|
+
let bucket = runtime.actors.get(key);
|
|
51
|
+
if (bucket === void 0) {
|
|
52
|
+
bucket = {
|
|
53
|
+
tag,
|
|
54
|
+
at,
|
|
55
|
+
files: /* @__PURE__ */ new Set()
|
|
56
|
+
};
|
|
57
|
+
runtime.actors.set(key, bucket);
|
|
58
|
+
armGate(runtime);
|
|
59
|
+
}
|
|
60
|
+
return bucket.files;
|
|
61
|
+
}
|
|
62
|
+
function armGate(runtime) {
|
|
63
|
+
runtime.active = runtime.buckets.size + runtime.actors.size;
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/coverage/wire.ts
|
|
67
|
+
/**
|
|
68
|
+
* Enables the instrumentation. Unset means the register hook is never loaded
|
|
69
|
+
* and the application pays nothing at all.
|
|
70
|
+
*
|
|
71
|
+
* `1` / `true` turns it on and leaves attribution to the incoming carrier.
|
|
72
|
+
* Any other value is itself a `<runId>.<specId>` and becomes the ambient spec
|
|
73
|
+
* for the process — the only way to attribute an entry point that has no
|
|
74
|
+
* inbound request to read, such as a worker started per spec.
|
|
75
|
+
*/
|
|
76
|
+
const ENV_NAME = "CCQA_COVERAGE";
|
|
77
|
+
const SPEC_ID = /^[A-Za-z0-9._\-/]{1,200}$/;
|
|
78
|
+
/**
|
|
79
|
+
* Accepts a carrier value only if it looks like an id we wrote.
|
|
80
|
+
*
|
|
81
|
+
* The cookie is client-controlled, so this is the first of two gates: the
|
|
82
|
+
* second is the hub refusing runs it never started.
|
|
83
|
+
*/
|
|
84
|
+
function parseSpecId(raw) {
|
|
85
|
+
if (!raw) return void 0;
|
|
86
|
+
const value = raw.trim();
|
|
87
|
+
if (!SPEC_ID.test(value)) return void 0;
|
|
88
|
+
if (value === "1" || value === "true") return void 0;
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
//#region src/coverage/runtime-env.ts
|
|
93
|
+
function readConfig(env = process.env) {
|
|
94
|
+
const raw = env[ENV_NAME];
|
|
95
|
+
const include = (env["CCQA_COVERAGE_INCLUDE"] ?? "src").split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
96
|
+
return {
|
|
97
|
+
enabled: raw !== void 0 && raw !== "" && raw !== "0" && raw !== "false",
|
|
98
|
+
ambientSpecId: parseSpecId(raw),
|
|
99
|
+
root: env["CCQA_COVERAGE_ROOT"] ?? process.cwd(),
|
|
100
|
+
include,
|
|
101
|
+
debug: env["CCQA_COVERAGE_DEBUG"] === "1" || env["CCQA_COVERAGE_DEBUG"] === "true"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Diagnostics go to stderr and nowhere else. A `--import` preload is inherited
|
|
106
|
+
* by every child node process, and writing to stdout corrupts whatever the host
|
|
107
|
+
* was parsing there — enough to make a framework's own toolchain fail to start.
|
|
108
|
+
*/
|
|
109
|
+
function debugLog(config, message) {
|
|
110
|
+
if (!config.debug) return;
|
|
111
|
+
process.stderr.write(`[ccqa-tools] ${message}\n`);
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/coverage/presets/slack.ts
|
|
115
|
+
/**
|
|
116
|
+
* Records who caused a Slack request, so the flows a chat platform drives can
|
|
117
|
+
* be attributed at all.
|
|
118
|
+
*
|
|
119
|
+
* Those requests are sent by Slack, not by the browser under test, so none of
|
|
120
|
+
* the carriers the rest of this package relies on is present — no cookie, no
|
|
121
|
+
* baggage header, nothing. What the payload does say is which user acted, and
|
|
122
|
+
* that is the whole of what this records:
|
|
123
|
+
*
|
|
124
|
+
* app.use(slackActor()) // after whatever parses the body
|
|
125
|
+
*
|
|
126
|
+
* No arguments, no configuration, no environment. It does not know which users
|
|
127
|
+
* are being measured, when a spec is running, or that ccqa exists — the run
|
|
128
|
+
* decides all of that from the other end. With `CCQA_COVERAGE` unset the
|
|
129
|
+
* runtime is absent and every call here returns immediately.
|
|
130
|
+
*
|
|
131
|
+
* Extraction is a fixed list of the places Slack puts a user id. A payload
|
|
132
|
+
* shape not on the list records nothing rather than guessing, because a wrong
|
|
133
|
+
* identity is a wrong answer while a missing one is a counted gap.
|
|
134
|
+
*/
|
|
135
|
+
/**
|
|
136
|
+
* Read once: the identity of every inbound request would otherwise re-read the
|
|
137
|
+
* environment, and this sits in front of every Slack request the app serves.
|
|
138
|
+
*/
|
|
139
|
+
const config = readConfig();
|
|
140
|
+
/** The tag prefix, and therefore the provider key a project writes in its config. */
|
|
141
|
+
const SLACK_PROVIDER = "slack";
|
|
142
|
+
/**
|
|
143
|
+
* Middleware for either shape a Slack app uses.
|
|
144
|
+
*
|
|
145
|
+
* Bolt hands its middleware one object holding the parsed body and its own
|
|
146
|
+
* `next`, and awaits what comes back. A connect-style stack hands
|
|
147
|
+
* `(request, response, next)`, and a context-style one `(ctx, next)`. All are
|
|
148
|
+
* accepted so the application's one line does not have to know which it is —
|
|
149
|
+
* and in a Bolt app `app.use()` is the only insertion point there is.
|
|
150
|
+
*
|
|
151
|
+
* Place it after whatever parses the body: the payload is what carries the
|
|
152
|
+
* identity, and before parsing there is none. A framework whose body is only
|
|
153
|
+
* available by awaiting it is out of reach here for the same reason — there is
|
|
154
|
+
* nothing to read at the moment this runs.
|
|
155
|
+
*/
|
|
156
|
+
function slackActor() {
|
|
157
|
+
return async function coverageSlackActor(...args) {
|
|
158
|
+
const first = args[0];
|
|
159
|
+
const bolt = args.length === 1 && isRecord(first) && typeof first.next === "function" ? first : void 0;
|
|
160
|
+
const body = bolt ? bolt.body : requestBody(first);
|
|
161
|
+
const next = bolt ? bolt.next : args.slice(1).find(isFunction);
|
|
162
|
+
if (next === void 0) return;
|
|
163
|
+
const user = slackUserId(body);
|
|
164
|
+
if (user === void 0) {
|
|
165
|
+
await next();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const tag = `${SLACK_PROVIDER}:${user}`;
|
|
169
|
+
debugLog(config, `slack actor ${tag}`);
|
|
170
|
+
await runAsActor(tag, Date.now(), next);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The user id in a Slack payload, or undefined if this shape carries none.
|
|
175
|
+
*
|
|
176
|
+
* Exported for its own tests: the list is the entire contract, and a shape
|
|
177
|
+
* silently dropping off it looks exactly like a flow that reached no code.
|
|
178
|
+
*/
|
|
179
|
+
function slackUserId(body) {
|
|
180
|
+
if (!isRecord(body)) return void 0;
|
|
181
|
+
const payload = parsePayload(body.payload);
|
|
182
|
+
if (payload !== void 0) return idOf(payload.user);
|
|
183
|
+
const event = body.event;
|
|
184
|
+
if (isRecord(event)) return idOf(event.user);
|
|
185
|
+
if (body.user !== void 0) return idOf(body.user);
|
|
186
|
+
if (typeof body.user_id === "string") return nonEmpty(body.user_id);
|
|
187
|
+
}
|
|
188
|
+
function parsePayload(raw) {
|
|
189
|
+
if (isRecord(raw)) return raw;
|
|
190
|
+
if (typeof raw !== "string") return void 0;
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(raw);
|
|
193
|
+
return isRecord(parsed) ? parsed : void 0;
|
|
194
|
+
} catch {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** Slack writes a user as a bare id in some payloads and as an object in others. */
|
|
199
|
+
function idOf(user) {
|
|
200
|
+
if (typeof user === "string") return nonEmpty(user);
|
|
201
|
+
if (isRecord(user) && typeof user.id === "string") return nonEmpty(user.id);
|
|
202
|
+
}
|
|
203
|
+
function nonEmpty(value) {
|
|
204
|
+
const trimmed = value.trim();
|
|
205
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
206
|
+
}
|
|
207
|
+
function isRecord(value) {
|
|
208
|
+
return typeof value === "object" && value !== null;
|
|
209
|
+
}
|
|
210
|
+
function isFunction(value) {
|
|
211
|
+
return typeof value === "function";
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The parsed request body, from the request itself or from a context wrapping
|
|
215
|
+
* it. A context's own `body` is the *response* it is building, so reading that
|
|
216
|
+
* would hand the extractor the wrong object and quietly find no identity.
|
|
217
|
+
*/
|
|
218
|
+
function requestBody(first) {
|
|
219
|
+
if (!isRecord(first)) return void 0;
|
|
220
|
+
const request = first.request;
|
|
221
|
+
if (isRecord(request) && isRecord(request.body)) return request.body;
|
|
222
|
+
return isRecord(first.body) ? first.body : void 0;
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
export { SLACK_PROVIDER, slackActor, slackUserId };
|