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,250 @@
|
|
|
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
|
+
/** The spec the current async context belongs to, if any. */
|
|
19
|
+
function currentSpecId() {
|
|
20
|
+
return runtime()?.als.getStore()?.specId;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Opens `specId`'s bucket and runs `fn` inside its context. Every entry point —
|
|
24
|
+
* HTTP, Temporal activity, manual — funnels through here.
|
|
25
|
+
*
|
|
26
|
+
* The bucket outlives `fn`: work a request schedules and does not await still
|
|
27
|
+
* belongs to the spec that caused it.
|
|
28
|
+
*/
|
|
29
|
+
function runInSpec(specId, fn) {
|
|
30
|
+
const rt = runtime();
|
|
31
|
+
if (rt === void 0) return fn();
|
|
32
|
+
return rt.als.run({
|
|
33
|
+
specId,
|
|
34
|
+
files: openBucket(rt, specId)
|
|
35
|
+
}, fn);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Records that `tag` caused this work at `at`, without deciding whose it is.
|
|
39
|
+
*
|
|
40
|
+
* Carrier wins: a request that already named a spec needs no identity, and
|
|
41
|
+
* overwriting it would replace a fact with something the run still has to
|
|
42
|
+
* interpret. Everything else is recorded whoever it came from — the application
|
|
43
|
+
* is never told which identities are being measured, so it cannot filter, and
|
|
44
|
+
* the run discards the ones it did not ask for.
|
|
45
|
+
*/
|
|
46
|
+
function runAsActor(tag, at, fn) {
|
|
47
|
+
const rt = runtime();
|
|
48
|
+
if (rt === void 0) return fn();
|
|
49
|
+
if (rt.als.getStore()?.specId !== void 0) return fn();
|
|
50
|
+
return rt.als.run({
|
|
51
|
+
actor: {
|
|
52
|
+
tag,
|
|
53
|
+
at
|
|
54
|
+
},
|
|
55
|
+
files: openActorBucket(rt, tag, at)
|
|
56
|
+
}, fn);
|
|
57
|
+
}
|
|
58
|
+
/** The identity mark on the current async context, if it has one. */
|
|
59
|
+
function currentActor() {
|
|
60
|
+
return runtime()?.als.getStore()?.actor;
|
|
61
|
+
}
|
|
62
|
+
/** Returns `specId`'s file set, creating it — and arming the gate — if new. */
|
|
63
|
+
function openBucket(runtime, specId) {
|
|
64
|
+
let files = runtime.buckets.get(specId);
|
|
65
|
+
if (files === void 0) {
|
|
66
|
+
files = /* @__PURE__ */ new Set();
|
|
67
|
+
runtime.buckets.set(specId, files);
|
|
68
|
+
armGate(runtime);
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The key both halves of the collector agree on. A space separates them safely:
|
|
74
|
+
* a spec id can never contain one, so identity keys and spec ids stay disjoint
|
|
75
|
+
* where the collector tracks both in one map.
|
|
76
|
+
*/
|
|
77
|
+
function actorBucketKey(tag, at) {
|
|
78
|
+
return `${tag} ${at}`;
|
|
79
|
+
}
|
|
80
|
+
/** Returns the identity's file set for that instant, creating it if new. */
|
|
81
|
+
function openActorBucket(runtime, tag, at) {
|
|
82
|
+
const key = actorBucketKey(tag, at);
|
|
83
|
+
let bucket = runtime.actors.get(key);
|
|
84
|
+
if (bucket === void 0) {
|
|
85
|
+
bucket = {
|
|
86
|
+
tag,
|
|
87
|
+
at,
|
|
88
|
+
files: /* @__PURE__ */ new Set()
|
|
89
|
+
};
|
|
90
|
+
runtime.actors.set(key, bucket);
|
|
91
|
+
armGate(runtime);
|
|
92
|
+
}
|
|
93
|
+
return bucket.files;
|
|
94
|
+
}
|
|
95
|
+
function armGate(runtime) {
|
|
96
|
+
runtime.active = runtime.buckets.size + runtime.actors.size;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/coverage/wire.ts
|
|
100
|
+
/** Temporal header key, for the hop from client to workflow to activity. */
|
|
101
|
+
const TEMPORAL_HEADER = "ccqa-tools";
|
|
102
|
+
/**
|
|
103
|
+
* `<provider>:<identity>` — who caused a request, for the requests that cannot
|
|
104
|
+
* say which spec they belong to.
|
|
105
|
+
*
|
|
106
|
+
* The application stamps this and nothing else: which spec an identity was
|
|
107
|
+
* acting for at a given instant is decided by the run, which is the only side
|
|
108
|
+
* that knows. Nothing about the mapping is ever sent back here.
|
|
109
|
+
*/
|
|
110
|
+
const ACTOR_TAG = /^[A-Za-z0-9][A-Za-z0-9._-]*:\S{1,200}$/;
|
|
111
|
+
function parseActorTag(raw) {
|
|
112
|
+
if (!raw) return void 0;
|
|
113
|
+
const value = raw.trim();
|
|
114
|
+
return ACTOR_TAG.test(value) ? value : void 0;
|
|
115
|
+
}
|
|
116
|
+
/** Reads a mark off the wire, refusing anything that is not one of the two shapes. */
|
|
117
|
+
function parseMark(value) {
|
|
118
|
+
if (typeof value === "string") {
|
|
119
|
+
const spec = parseSpecId(value);
|
|
120
|
+
return spec === void 0 ? void 0 : { spec };
|
|
121
|
+
}
|
|
122
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
123
|
+
const { tag, at } = value;
|
|
124
|
+
if (typeof at !== "number" || !Number.isFinite(at)) return void 0;
|
|
125
|
+
const parsed = parseActorTag(typeof tag === "string" ? tag : void 0);
|
|
126
|
+
return parsed === void 0 ? void 0 : {
|
|
127
|
+
tag: parsed,
|
|
128
|
+
at
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const SPEC_ID = /^[A-Za-z0-9._\-/]{1,200}$/;
|
|
132
|
+
/**
|
|
133
|
+
* Accepts a carrier value only if it looks like an id we wrote.
|
|
134
|
+
*
|
|
135
|
+
* The cookie is client-controlled, so this is the first of two gates: the
|
|
136
|
+
* second is the hub refusing runs it never started.
|
|
137
|
+
*/
|
|
138
|
+
function parseSpecId(raw) {
|
|
139
|
+
if (!raw) return void 0;
|
|
140
|
+
const value = raw.trim();
|
|
141
|
+
if (!SPEC_ID.test(value)) return void 0;
|
|
142
|
+
if (value === "1" || value === "true") return void 0;
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
//#region src/coverage/temporal/header.ts
|
|
147
|
+
/**
|
|
148
|
+
* Reads and writes the one Temporal header this package uses.
|
|
149
|
+
*
|
|
150
|
+
* The payload is built by hand rather than through `defaultPayloadConverter` so
|
|
151
|
+
* that the workflow half of the integration stays free of Temporal imports —
|
|
152
|
+
* the workflow sandbox is picky, and a plain `json/plain` string is exactly
|
|
153
|
+
* what the default converter would have produced anyway.
|
|
154
|
+
*/
|
|
155
|
+
const ENCODING = "json/plain";
|
|
156
|
+
const textEncoder = new TextEncoder();
|
|
157
|
+
const textDecoder = new TextDecoder();
|
|
158
|
+
/**
|
|
159
|
+
* A spec travels as a bare string and an identity as an object, so the two are
|
|
160
|
+
* told apart by shape rather than by a discriminator nobody else would read.
|
|
161
|
+
*/
|
|
162
|
+
function toHeader(mark) {
|
|
163
|
+
const value = "spec" in mark ? mark.spec : {
|
|
164
|
+
tag: mark.tag,
|
|
165
|
+
at: mark.at
|
|
166
|
+
};
|
|
167
|
+
return { [TEMPORAL_HEADER]: {
|
|
168
|
+
metadata: { encoding: encode(ENCODING) },
|
|
169
|
+
data: encode(JSON.stringify(value))
|
|
170
|
+
} };
|
|
171
|
+
}
|
|
172
|
+
/** The raw value off the header. Callers validate it with `parseMark`. */
|
|
173
|
+
function fromHeader(headers) {
|
|
174
|
+
const payload = headers?.[TEMPORAL_HEADER];
|
|
175
|
+
if (!payload?.data) return void 0;
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(decode(payload.data));
|
|
178
|
+
} catch {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function encode(value) {
|
|
183
|
+
return textEncoder.encode(value);
|
|
184
|
+
}
|
|
185
|
+
function decode(value) {
|
|
186
|
+
return textDecoder.decode(value);
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/coverage/temporal/index.ts
|
|
190
|
+
/**
|
|
191
|
+
* Carries the spec id across Temporal, whose activities are the one async-job
|
|
192
|
+
* boundary that can be attributed at all: the SDK hands interceptors the same
|
|
193
|
+
* header map on the way out of a client and on the way into an activity.
|
|
194
|
+
*
|
|
195
|
+
* Three hops, three interceptors. The middle one — the workflow — lives in
|
|
196
|
+
* `ccqa-tools/coverage/temporal/workflow` because it is evaluated inside a
|
|
197
|
+
* deterministic sandbox that has no Node built-ins.
|
|
198
|
+
*/
|
|
199
|
+
/**
|
|
200
|
+
* Client side: stamps the spec of whatever request is starting the workflow.
|
|
201
|
+
*
|
|
202
|
+
* Both `start` and `signalWithStart` begin executions; covering only one leaves
|
|
203
|
+
* a hole that shows up as an unattributed activity much later.
|
|
204
|
+
*/
|
|
205
|
+
function createClientInterceptor() {
|
|
206
|
+
const stamp = (input, next) => {
|
|
207
|
+
const mark = currentMark();
|
|
208
|
+
if (mark === void 0) return next(input);
|
|
209
|
+
return next({
|
|
210
|
+
...input,
|
|
211
|
+
headers: {
|
|
212
|
+
...input.headers,
|
|
213
|
+
...toHeader(mark)
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
start: stamp,
|
|
219
|
+
signalWithStart: stamp
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Activity side: reopens the context the work came from. Activities run in an
|
|
224
|
+
* ordinary Node context, so everything the activity touches is attributed from
|
|
225
|
+
* here on.
|
|
226
|
+
*
|
|
227
|
+
* An identity mark keeps the instant it was stamped with rather than taking
|
|
228
|
+
* the current time — an activity may run long after the request that scheduled
|
|
229
|
+
* it, and the run matches on when the work was asked for.
|
|
230
|
+
*/
|
|
231
|
+
function createActivityInterceptor() {
|
|
232
|
+
return { execute: (input, next) => {
|
|
233
|
+
const mark = parseMark(fromHeader(input.headers));
|
|
234
|
+
if (mark === void 0) return next(input);
|
|
235
|
+
if ("spec" in mark) return runInSpec(mark.spec, () => next(input));
|
|
236
|
+
return runAsActor(mark.tag, mark.at, () => next(input));
|
|
237
|
+
} };
|
|
238
|
+
}
|
|
239
|
+
/** What the current context has to hand on: its spec, or who caused it. */
|
|
240
|
+
function currentMark() {
|
|
241
|
+
const specId = currentSpecId();
|
|
242
|
+
if (specId !== void 0) return { spec: specId };
|
|
243
|
+
const actor = currentActor();
|
|
244
|
+
return actor === void 0 ? void 0 : {
|
|
245
|
+
tag: actor.tag,
|
|
246
|
+
at: actor.at
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
export { TEMPORAL_HEADER, createActivityInterceptor, createClientInterceptor };
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ccqa-tools",
|
|
3
|
+
"version": "1.37.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Instrumentation the application under test installs, for ccqa. Currently: per-spec reach coverage.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/shibukazu/ccqa.git",
|
|
9
|
+
"directory": "packages/tools"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"exports": {
|
|
13
|
+
"./coverage": {
|
|
14
|
+
"types": "./dist/coverage/core.d.ts",
|
|
15
|
+
"import": "./dist/coverage/core.js",
|
|
16
|
+
"require": "./dist/coverage/core.cjs"
|
|
17
|
+
},
|
|
18
|
+
"./coverage/register": {
|
|
19
|
+
"types": "./dist/coverage/register.d.ts",
|
|
20
|
+
"import": "./dist/coverage/register.js",
|
|
21
|
+
"require": "./dist/coverage/register.cjs"
|
|
22
|
+
},
|
|
23
|
+
"./coverage/middleware": {
|
|
24
|
+
"types": "./dist/coverage/middleware.d.ts",
|
|
25
|
+
"import": "./dist/coverage/middleware.js",
|
|
26
|
+
"require": "./dist/coverage/middleware.cjs"
|
|
27
|
+
},
|
|
28
|
+
"./coverage/slack": {
|
|
29
|
+
"types": "./dist/coverage/slack.d.ts",
|
|
30
|
+
"import": "./dist/coverage/slack.js",
|
|
31
|
+
"require": "./dist/coverage/slack.cjs"
|
|
32
|
+
},
|
|
33
|
+
"./coverage/collector": {
|
|
34
|
+
"types": "./dist/coverage/collector.d.ts",
|
|
35
|
+
"import": "./dist/coverage/collector.js",
|
|
36
|
+
"require": "./dist/coverage/collector.cjs"
|
|
37
|
+
},
|
|
38
|
+
"./coverage/next": {
|
|
39
|
+
"types": "./dist/coverage/next.d.ts",
|
|
40
|
+
"import": "./dist/coverage/next.js",
|
|
41
|
+
"require": "./dist/coverage/next.cjs"
|
|
42
|
+
},
|
|
43
|
+
"./coverage/next/loader": {
|
|
44
|
+
"default": "./dist/coverage/next-loader.cjs"
|
|
45
|
+
},
|
|
46
|
+
"./coverage/temporal": {
|
|
47
|
+
"types": "./dist/coverage/temporal.d.ts",
|
|
48
|
+
"import": "./dist/coverage/temporal.js",
|
|
49
|
+
"require": "./dist/coverage/temporal.cjs"
|
|
50
|
+
},
|
|
51
|
+
"./coverage/temporal/workflow": {
|
|
52
|
+
"types": "./dist/coverage/temporal-workflow.d.ts",
|
|
53
|
+
"import": "./dist/coverage/temporal-workflow.js",
|
|
54
|
+
"require": "./dist/coverage/temporal-workflow.cjs"
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"files": [
|
|
58
|
+
"dist/"
|
|
59
|
+
],
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"acorn": "^8.15.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/node": "^22",
|
|
65
|
+
"tsdown": "^0.21.9",
|
|
66
|
+
"typescript": "^5"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "tsdown",
|
|
70
|
+
"typecheck": "tsc --noEmit"
|
|
71
|
+
},
|
|
72
|
+
"engines": {
|
|
73
|
+
"node": ">=20"
|
|
74
|
+
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"@temporalio/activity": ">=1.9.0",
|
|
77
|
+
"@temporalio/client": ">=1.9.0",
|
|
78
|
+
"@temporalio/worker": ">=1.9.0",
|
|
79
|
+
"@temporalio/workflow": ">=1.9.0"
|
|
80
|
+
},
|
|
81
|
+
"peerDependenciesMeta": {
|
|
82
|
+
"@temporalio/activity": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"@temporalio/client": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@temporalio/worker": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"@temporalio/workflow": {
|
|
92
|
+
"optional": true
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|