ccqa 1.40.2 → 1.40.4
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/dist/bin/ccqa.mjs +758 -231
- package/dist/package.json +1 -1
- package/dist/runtime/vitest.config.d.mts +2 -2
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -18,10 +18,11 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
18
18
|
import { promisify } from "node:util";
|
|
19
19
|
import { createInterface } from "node:readline/promises";
|
|
20
20
|
import { createServer } from "node:http";
|
|
21
|
+
import { connect, createServer as createServer$1 } from "node:net";
|
|
22
|
+
import { connect as connect$1 } from "node:tls";
|
|
21
23
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
22
24
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
23
25
|
import { createInterface as createInterface$1 } from "node:readline";
|
|
24
|
-
import { createServer as createServer$1 } from "node:net";
|
|
25
26
|
//#region src/run/report-constants.ts
|
|
26
27
|
/**
|
|
27
28
|
* Pure report/run constants with no runtime dependencies. Kept separate from
|
|
@@ -6559,16 +6560,370 @@ function message$2(error) {
|
|
|
6559
6560
|
return error instanceof Error ? error.message : String(error);
|
|
6560
6561
|
}
|
|
6561
6562
|
//#endregion
|
|
6563
|
+
//#region src/coverage/browser/ws.ts
|
|
6564
|
+
/**
|
|
6565
|
+
* Minimal RFC 6455 client for the CDP transport, dependency-free on purpose.
|
|
6566
|
+
*
|
|
6567
|
+
* Not a general WebSocket: it exists because the runtime's global `WebSocket`
|
|
6568
|
+
* (undici) unconditionally offers `permessage-deflate`, Chromium's DevTools
|
|
6569
|
+
* server accepts it with context takeover, and a single hiccup in that
|
|
6570
|
+
* stateful inflate stream kills the connection from the client side — the
|
|
6571
|
+
* browser and the spec keep running while the measurement silently dies.
|
|
6572
|
+
* Every CDP client that ships (Playwright, puppeteer) disables the extension;
|
|
6573
|
+
* this transport never offers it, so there is no compressed state to corrupt.
|
|
6574
|
+
*
|
|
6575
|
+
* Deliberately lenient where the payload is concerned — text arrives through
|
|
6576
|
+
* `Buffer.toString("utf8")`, so an invalid byte becomes U+FFFD instead of a
|
|
6577
|
+
* dead transport — and strict only about frame structure, where a violation
|
|
6578
|
+
* means the stream can no longer be trusted at all.
|
|
6579
|
+
*/
|
|
6580
|
+
const HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
6581
|
+
const HANDSHAKE_HEADER_CAP = 64 * 1024;
|
|
6582
|
+
/** One assembled message; CDP takes and `IO.read` chunks reach megabytes, not this. */
|
|
6583
|
+
const MESSAGE_CAP = 256 * 1024 * 1024;
|
|
6584
|
+
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
6585
|
+
const OP_CONTINUATION = 0;
|
|
6586
|
+
const OP_TEXT = 1;
|
|
6587
|
+
const OP_BINARY = 2;
|
|
6588
|
+
const OP_CLOSE = 8;
|
|
6589
|
+
const OP_PING = 9;
|
|
6590
|
+
const OP_PONG = 10;
|
|
6591
|
+
var WsError = class extends Error {};
|
|
6592
|
+
/** Why a 101 response head is unacceptable, or `undefined` when it is fine. */
|
|
6593
|
+
function handshakeFailure(head, expectedAccept, host) {
|
|
6594
|
+
const [statusLine = "", ...headerLines] = head.split("\r\n");
|
|
6595
|
+
if (!/^HTTP\/1\.1 101 /i.test(statusLine)) return `handshake with ${host} answered "${statusLine.slice(0, 80)}"`;
|
|
6596
|
+
const headers = /* @__PURE__ */ new Map();
|
|
6597
|
+
for (const line of headerLines) {
|
|
6598
|
+
const colon = line.indexOf(":");
|
|
6599
|
+
if (colon === -1) continue;
|
|
6600
|
+
headers.set(line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim());
|
|
6601
|
+
}
|
|
6602
|
+
if (headers.get("sec-websocket-accept") !== expectedAccept) return `handshake with ${host} returned a wrong Sec-WebSocket-Accept`;
|
|
6603
|
+
const extensions = headers.get("sec-websocket-extensions");
|
|
6604
|
+
if (extensions !== void 0 && extensions !== "") return `server negotiated unrequested extension "${extensions}"`;
|
|
6605
|
+
}
|
|
6606
|
+
var RawWebSocket = class RawWebSocket {
|
|
6607
|
+
socket;
|
|
6608
|
+
handlers;
|
|
6609
|
+
buffer = Buffer.alloc(0);
|
|
6610
|
+
/**
|
|
6611
|
+
* Arrived-but-unparsed chunks. Left as a list until a parse attempt can
|
|
6612
|
+
* make progress: a multi-megabyte take answered in one frame would
|
|
6613
|
+
* otherwise re-copy everything buffered so far on every TCP chunk.
|
|
6614
|
+
*/
|
|
6615
|
+
pendingChunks = [];
|
|
6616
|
+
pendingBytes = 0;
|
|
6617
|
+
/** Bytes `buffer` must reach before another parse attempt can complete a frame. */
|
|
6618
|
+
needed = 0;
|
|
6619
|
+
fragments = [];
|
|
6620
|
+
fragmentedBytes = 0;
|
|
6621
|
+
fragmentedOpcode;
|
|
6622
|
+
closeSent = false;
|
|
6623
|
+
finished = false;
|
|
6624
|
+
finishDetail;
|
|
6625
|
+
peerCloseDetail;
|
|
6626
|
+
/** Bytes that arrived with the 101, held until attach() wires the handlers. */
|
|
6627
|
+
pendingLeftover;
|
|
6628
|
+
isOpen = true;
|
|
6629
|
+
constructor(socket) {
|
|
6630
|
+
this.socket = socket;
|
|
6631
|
+
}
|
|
6632
|
+
get open() {
|
|
6633
|
+
return this.isOpen;
|
|
6634
|
+
}
|
|
6635
|
+
/**
|
|
6636
|
+
* Opens the socket and completes the upgrade. No `Sec-WebSocket-Extensions`
|
|
6637
|
+
* is offered — that omission is this class's reason to exist — and a server
|
|
6638
|
+
* that answers with one anyway is refused: it would speak a framing this
|
|
6639
|
+
* side does not.
|
|
6640
|
+
*/
|
|
6641
|
+
static async connect(wsUrl) {
|
|
6642
|
+
let url;
|
|
6643
|
+
try {
|
|
6644
|
+
url = new URL(wsUrl);
|
|
6645
|
+
} catch {
|
|
6646
|
+
throw new WsError(`not a ws URL: "${wsUrl}"`);
|
|
6647
|
+
}
|
|
6648
|
+
if (url.protocol !== "ws:" && url.protocol !== "wss:") throw new WsError(`not a ws URL: "${wsUrl}"`);
|
|
6649
|
+
const secure = url.protocol === "wss:";
|
|
6650
|
+
const port = url.port !== "" ? Number(url.port) : secure ? 443 : 80;
|
|
6651
|
+
const host = url.hostname;
|
|
6652
|
+
const socket = secure ? connect$1({
|
|
6653
|
+
host,
|
|
6654
|
+
port,
|
|
6655
|
+
servername: host
|
|
6656
|
+
}) : connect({
|
|
6657
|
+
host,
|
|
6658
|
+
port
|
|
6659
|
+
});
|
|
6660
|
+
socket.setNoDelay(true);
|
|
6661
|
+
const key = randomBytes(16).toString("base64");
|
|
6662
|
+
const expectedAccept = createHash("sha1").update(`${key}${WS_GUID}`).digest("base64");
|
|
6663
|
+
const request = `GET ${`${url.pathname || "/"}${url.search}`} HTTP/1.1\r\nHost: ${url.host}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ${key}\r\nSec-WebSocket-Version: 13\r\n\r\n`;
|
|
6664
|
+
const leftover = await new Promise((resolve, reject) => {
|
|
6665
|
+
let response = Buffer.alloc(0);
|
|
6666
|
+
const timer = setTimeout(() => {
|
|
6667
|
+
fail(new WsError(`handshake with ${url.host} timed out`));
|
|
6668
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
6669
|
+
timer.unref?.();
|
|
6670
|
+
const fail = (error) => {
|
|
6671
|
+
cleanup();
|
|
6672
|
+
socket.destroy();
|
|
6673
|
+
reject(error);
|
|
6674
|
+
};
|
|
6675
|
+
const onData = (chunk) => {
|
|
6676
|
+
response = Buffer.concat([response, chunk]);
|
|
6677
|
+
const headerEnd = response.indexOf("\r\n\r\n");
|
|
6678
|
+
if (headerEnd === -1) {
|
|
6679
|
+
if (response.length > HANDSHAKE_HEADER_CAP) fail(new WsError(`handshake response from ${url.host} exceeded ${HANDSHAKE_HEADER_CAP} bytes`));
|
|
6680
|
+
return;
|
|
6681
|
+
}
|
|
6682
|
+
const failure = handshakeFailure(response.subarray(0, headerEnd).toString("latin1"), expectedAccept, url.host);
|
|
6683
|
+
if (failure !== void 0) {
|
|
6684
|
+
fail(new WsError(failure));
|
|
6685
|
+
return;
|
|
6686
|
+
}
|
|
6687
|
+
cleanup();
|
|
6688
|
+
resolve(response.subarray(headerEnd + 4));
|
|
6689
|
+
};
|
|
6690
|
+
const onError = (error) => {
|
|
6691
|
+
fail(new WsError(`could not connect to ${url.host} (${error.message})`));
|
|
6692
|
+
};
|
|
6693
|
+
const onEnd = () => {
|
|
6694
|
+
fail(new WsError(`${url.host} closed the connection during the handshake`));
|
|
6695
|
+
};
|
|
6696
|
+
const cleanup = () => {
|
|
6697
|
+
clearTimeout(timer);
|
|
6698
|
+
socket.off("data", onData);
|
|
6699
|
+
socket.off("error", onError);
|
|
6700
|
+
socket.off("close", onEnd);
|
|
6701
|
+
};
|
|
6702
|
+
socket.on("data", onData);
|
|
6703
|
+
socket.on("error", onError);
|
|
6704
|
+
socket.on("close", onEnd);
|
|
6705
|
+
socket.on("connect", () => socket.write(request));
|
|
6706
|
+
});
|
|
6707
|
+
const ws = new RawWebSocket(socket);
|
|
6708
|
+
socket.on("data", (chunk) => ws.ingest(chunk));
|
|
6709
|
+
socket.on("error", (error) => ws.finish(`socket error: ${error.message}`));
|
|
6710
|
+
socket.on("close", () => ws.finish(ws.peerCloseDetail ?? "connection closed abruptly"));
|
|
6711
|
+
socket.pause();
|
|
6712
|
+
if (leftover.length > 0) ws.pendingLeftover = leftover;
|
|
6713
|
+
return ws;
|
|
6714
|
+
}
|
|
6715
|
+
attach(handlers) {
|
|
6716
|
+
this.handlers = handlers;
|
|
6717
|
+
if (this.finished) {
|
|
6718
|
+
handlers.onClose(this.finishDetail ?? "connection closed");
|
|
6719
|
+
return;
|
|
6720
|
+
}
|
|
6721
|
+
const leftover = this.pendingLeftover;
|
|
6722
|
+
this.pendingLeftover = void 0;
|
|
6723
|
+
if (leftover !== void 0) this.ingest(leftover);
|
|
6724
|
+
if (!this.finished) this.socket.resume();
|
|
6725
|
+
}
|
|
6726
|
+
/** Sends one text message. Throws `WsError` when the connection is gone. */
|
|
6727
|
+
send(text) {
|
|
6728
|
+
if (!this.isOpen) throw new WsError("connection closed");
|
|
6729
|
+
this.sendFrame(OP_TEXT, Buffer.from(text, "utf8"));
|
|
6730
|
+
}
|
|
6731
|
+
/** Starts an orderly close; `onClose` fires when the socket is down. */
|
|
6732
|
+
close() {
|
|
6733
|
+
if (!this.isOpen) return;
|
|
6734
|
+
this.isOpen = false;
|
|
6735
|
+
this.peerCloseDetail ??= "connection closed";
|
|
6736
|
+
try {
|
|
6737
|
+
if (!this.closeSent) {
|
|
6738
|
+
this.closeSent = true;
|
|
6739
|
+
this.sendFrameRaw(OP_CLOSE, Buffer.from([3, 232]));
|
|
6740
|
+
}
|
|
6741
|
+
this.socket.end();
|
|
6742
|
+
} catch {
|
|
6743
|
+
this.socket.destroy();
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
ingest(chunk) {
|
|
6747
|
+
if (this.finished) return;
|
|
6748
|
+
this.pendingChunks.push(chunk);
|
|
6749
|
+
this.pendingBytes += chunk.length;
|
|
6750
|
+
if (this.buffer.length + this.pendingBytes < this.needed) return;
|
|
6751
|
+
this.buffer = Buffer.concat([this.buffer, ...this.pendingChunks]);
|
|
6752
|
+
this.pendingChunks = [];
|
|
6753
|
+
this.pendingBytes = 0;
|
|
6754
|
+
this.needed = 0;
|
|
6755
|
+
for (;;) {
|
|
6756
|
+
const frame = this.parseFrame();
|
|
6757
|
+
if (frame === void 0 || this.finished) return;
|
|
6758
|
+
this.handleFrame(frame.fin, frame.opcode, frame.payload);
|
|
6759
|
+
}
|
|
6760
|
+
}
|
|
6761
|
+
parseFrame() {
|
|
6762
|
+
const buf = this.buffer;
|
|
6763
|
+
if (buf.length < 2) return void 0;
|
|
6764
|
+
const first = buf[0] ?? 0;
|
|
6765
|
+
const second = buf[1] ?? 0;
|
|
6766
|
+
if ((first & 112) !== 0) {
|
|
6767
|
+
this.fail(`frame with RSV bits set (0x${first.toString(16)}) though no extension was negotiated`);
|
|
6768
|
+
return;
|
|
6769
|
+
}
|
|
6770
|
+
const fin = (first & 128) !== 0;
|
|
6771
|
+
const opcode = first & 15;
|
|
6772
|
+
const masked = (second & 128) !== 0;
|
|
6773
|
+
let length = second & 127;
|
|
6774
|
+
let offset = 2;
|
|
6775
|
+
if (length === 126) {
|
|
6776
|
+
if (buf.length < offset + 2) return void 0;
|
|
6777
|
+
length = buf.readUInt16BE(offset);
|
|
6778
|
+
offset += 2;
|
|
6779
|
+
} else if (length === 127) {
|
|
6780
|
+
if (buf.length < offset + 8) return void 0;
|
|
6781
|
+
const big = buf.readBigUInt64BE(offset);
|
|
6782
|
+
if (big > BigInt(MESSAGE_CAP)) {
|
|
6783
|
+
this.fail(`frame of ${big} bytes exceeds the ${MESSAGE_CAP}-byte cap`);
|
|
6784
|
+
return;
|
|
6785
|
+
}
|
|
6786
|
+
length = Number(big);
|
|
6787
|
+
offset += 8;
|
|
6788
|
+
}
|
|
6789
|
+
let mask;
|
|
6790
|
+
if (masked) {
|
|
6791
|
+
if (buf.length < offset + 4) return void 0;
|
|
6792
|
+
mask = buf.subarray(offset, offset + 4);
|
|
6793
|
+
offset += 4;
|
|
6794
|
+
}
|
|
6795
|
+
if (buf.length < offset + length) {
|
|
6796
|
+
this.needed = offset + length;
|
|
6797
|
+
return;
|
|
6798
|
+
}
|
|
6799
|
+
const payload = Buffer.from(buf.subarray(offset, offset + length));
|
|
6800
|
+
if (mask !== void 0) for (let i = 0; i < payload.length; i++) payload[i] = (payload[i] ?? 0) ^ (mask[i % 4] ?? 0);
|
|
6801
|
+
this.buffer = buf.subarray(offset + length);
|
|
6802
|
+
return {
|
|
6803
|
+
fin,
|
|
6804
|
+
opcode,
|
|
6805
|
+
payload
|
|
6806
|
+
};
|
|
6807
|
+
}
|
|
6808
|
+
handleFrame(fin, opcode, payload) {
|
|
6809
|
+
switch (opcode) {
|
|
6810
|
+
case OP_TEXT:
|
|
6811
|
+
case OP_BINARY:
|
|
6812
|
+
if (this.fragmentedOpcode !== void 0) {
|
|
6813
|
+
this.fail("new data frame started inside a fragmented message");
|
|
6814
|
+
return;
|
|
6815
|
+
}
|
|
6816
|
+
if (fin) {
|
|
6817
|
+
this.deliver(payload);
|
|
6818
|
+
return;
|
|
6819
|
+
}
|
|
6820
|
+
this.fragmentedOpcode = opcode;
|
|
6821
|
+
this.fragments = [payload];
|
|
6822
|
+
this.fragmentedBytes = payload.length;
|
|
6823
|
+
return;
|
|
6824
|
+
case OP_CONTINUATION:
|
|
6825
|
+
if (this.fragmentedOpcode === void 0) {
|
|
6826
|
+
this.fail("continuation frame outside a fragmented message");
|
|
6827
|
+
return;
|
|
6828
|
+
}
|
|
6829
|
+
this.fragments.push(payload);
|
|
6830
|
+
this.fragmentedBytes += payload.length;
|
|
6831
|
+
if (this.fragmentedBytes > MESSAGE_CAP) {
|
|
6832
|
+
this.fail(`fragmented message exceeds the ${MESSAGE_CAP}-byte cap`);
|
|
6833
|
+
return;
|
|
6834
|
+
}
|
|
6835
|
+
if (fin) {
|
|
6836
|
+
const whole = Buffer.concat(this.fragments);
|
|
6837
|
+
this.fragments = [];
|
|
6838
|
+
this.fragmentedBytes = 0;
|
|
6839
|
+
this.fragmentedOpcode = void 0;
|
|
6840
|
+
this.deliver(whole);
|
|
6841
|
+
}
|
|
6842
|
+
return;
|
|
6843
|
+
case OP_PING:
|
|
6844
|
+
try {
|
|
6845
|
+
this.sendFrameRaw(OP_PONG, payload);
|
|
6846
|
+
} catch {}
|
|
6847
|
+
return;
|
|
6848
|
+
case OP_PONG: return;
|
|
6849
|
+
case OP_CLOSE: {
|
|
6850
|
+
const code = payload.length >= 2 ? payload.readUInt16BE(0) : void 0;
|
|
6851
|
+
const reason = payload.length > 2 ? payload.subarray(2).toString("utf8") : "";
|
|
6852
|
+
this.peerCloseDetail = code === void 0 ? "connection closed" : `connection closed (code ${code}${reason ? `: ${reason}` : ""})`;
|
|
6853
|
+
if (!this.closeSent) {
|
|
6854
|
+
this.closeSent = true;
|
|
6855
|
+
try {
|
|
6856
|
+
this.sendFrameRaw(OP_CLOSE, payload.subarray(0, 2));
|
|
6857
|
+
} catch {}
|
|
6858
|
+
}
|
|
6859
|
+
this.socket.end();
|
|
6860
|
+
return;
|
|
6861
|
+
}
|
|
6862
|
+
default: this.fail(`unknown frame opcode 0x${opcode.toString(16)}`);
|
|
6863
|
+
}
|
|
6864
|
+
}
|
|
6865
|
+
deliver(payload) {
|
|
6866
|
+
this.handlers?.onMessage(payload.toString("utf8"));
|
|
6867
|
+
}
|
|
6868
|
+
sendFrame(opcode, payload) {
|
|
6869
|
+
try {
|
|
6870
|
+
this.sendFrameRaw(opcode, payload);
|
|
6871
|
+
} catch (error) {
|
|
6872
|
+
throw new WsError(`send failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
sendFrameRaw(opcode, payload) {
|
|
6876
|
+
const mask = randomBytes(4);
|
|
6877
|
+
let header;
|
|
6878
|
+
if (payload.length < 126) header = Buffer.from([128 | opcode, 128 | payload.length]);
|
|
6879
|
+
else if (payload.length < 65536) {
|
|
6880
|
+
header = Buffer.alloc(4);
|
|
6881
|
+
header[0] = 128 | opcode;
|
|
6882
|
+
header[1] = 254;
|
|
6883
|
+
header.writeUInt16BE(payload.length, 2);
|
|
6884
|
+
} else {
|
|
6885
|
+
header = Buffer.alloc(10);
|
|
6886
|
+
header[0] = 128 | opcode;
|
|
6887
|
+
header[1] = 255;
|
|
6888
|
+
header.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
6889
|
+
}
|
|
6890
|
+
const masked = Buffer.from(payload);
|
|
6891
|
+
for (let i = 0; i < masked.length; i++) masked[i] = (masked[i] ?? 0) ^ (mask[i % 4] ?? 0);
|
|
6892
|
+
this.socket.write(Buffer.concat([
|
|
6893
|
+
header,
|
|
6894
|
+
mask,
|
|
6895
|
+
masked
|
|
6896
|
+
]));
|
|
6897
|
+
}
|
|
6898
|
+
fail(reason) {
|
|
6899
|
+
this.socket.destroy();
|
|
6900
|
+
this.finish(reason);
|
|
6901
|
+
}
|
|
6902
|
+
finish(detail) {
|
|
6903
|
+
if (this.finished) return;
|
|
6904
|
+
this.finished = true;
|
|
6905
|
+
this.finishDetail = detail;
|
|
6906
|
+
this.isOpen = false;
|
|
6907
|
+
this.buffer = Buffer.alloc(0);
|
|
6908
|
+
this.pendingChunks = [];
|
|
6909
|
+
this.pendingBytes = 0;
|
|
6910
|
+
this.fragments = [];
|
|
6911
|
+
this.fragmentedBytes = 0;
|
|
6912
|
+
this.handlers?.onClose(detail);
|
|
6913
|
+
}
|
|
6914
|
+
};
|
|
6915
|
+
//#endregion
|
|
6562
6916
|
//#region src/coverage/browser/cdp.ts
|
|
6563
6917
|
/**
|
|
6564
6918
|
* Minimal Chrome DevTools Protocol client, dependency-free on purpose.
|
|
6565
6919
|
*
|
|
6566
6920
|
* Coverage acquisition speaks a handful of domains over one transport, which
|
|
6567
6921
|
* is not enough to justify a protocol library in a published CLI. The
|
|
6568
|
-
* transport is
|
|
6569
|
-
*
|
|
6570
|
-
*
|
|
6571
|
-
*
|
|
6922
|
+
* transport is `./ws.ts`'s own RFC 6455 client, not the `WebSocket` global:
|
|
6923
|
+
* the global (undici) unconditionally negotiates `permessage-deflate` with
|
|
6924
|
+
* Chromium's DevTools server, and one hiccup in that stateful inflate stream
|
|
6925
|
+
* kills the connection from the client side while the browser and the spec
|
|
6926
|
+
* keep running (observed in CI as every measurement dying mid-spec).
|
|
6572
6927
|
*/
|
|
6573
6928
|
var CdpError = class extends Error {};
|
|
6574
6929
|
/**
|
|
@@ -6590,10 +6945,6 @@ function trace(direction, text) {
|
|
|
6590
6945
|
appendFileSync(TRACE_FILE, line);
|
|
6591
6946
|
} catch {}
|
|
6592
6947
|
}
|
|
6593
|
-
/** Throws with the actual requirement when the runtime cannot open the socket. */
|
|
6594
|
-
function requireWebSocket() {
|
|
6595
|
-
if (typeof WebSocket === "undefined") throw new CdpError(`browser coverage needs the WebSocket global (node 22+); this is node ${process.version}`);
|
|
6596
|
-
}
|
|
6597
6948
|
/**
|
|
6598
6949
|
* Resolves whatever a target hands us — `host:port`, an `http://` endpoint, or
|
|
6599
6950
|
* a ws URL — to the **browser-level** ws endpoint. A page-level ws URL is not
|
|
@@ -6626,21 +6977,18 @@ var CdpClient = class CdpClient {
|
|
|
6626
6977
|
closeHandlers = /* @__PURE__ */ new Set();
|
|
6627
6978
|
constructor(ws) {
|
|
6628
6979
|
this.ws = ws;
|
|
6629
|
-
ws.
|
|
6630
|
-
|
|
6631
|
-
|
|
6980
|
+
ws.attach({
|
|
6981
|
+
onMessage: (text) => this.receive(text),
|
|
6982
|
+
onClose: (detail) => this.drop(detail)
|
|
6983
|
+
});
|
|
6632
6984
|
}
|
|
6633
6985
|
static async connect(wsUrl) {
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
ws.addEventListener("open", () => resolve(), { once: true });
|
|
6638
|
-
ws.addEventListener("error", () => reject(new CdpError(`could not connect to ${wsUrl}`)), { once: true });
|
|
6639
|
-
});
|
|
6640
|
-
return new CdpClient(ws);
|
|
6986
|
+
return new CdpClient(await RawWebSocket.connect(wsUrl).catch((error) => {
|
|
6987
|
+
throw new CdpError(`could not connect to ${wsUrl} (${message$1(error)})`);
|
|
6988
|
+
}));
|
|
6641
6989
|
}
|
|
6642
6990
|
send(method, params, sessionId) {
|
|
6643
|
-
if (this.ws.
|
|
6991
|
+
if (!this.ws.open) return Promise.reject(new CdpError(`${method}: connection closed`));
|
|
6644
6992
|
const id = this.nextId++;
|
|
6645
6993
|
const promise = new Promise((resolve, reject) => {
|
|
6646
6994
|
this.pending.set(id, {
|
|
@@ -6650,12 +6998,17 @@ var CdpClient = class CdpClient {
|
|
|
6650
6998
|
});
|
|
6651
6999
|
});
|
|
6652
7000
|
trace("->", `#${id} ${method} sid:${shortId(sessionId)}`);
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
7001
|
+
try {
|
|
7002
|
+
this.ws.send(JSON.stringify({
|
|
7003
|
+
id,
|
|
7004
|
+
method,
|
|
7005
|
+
params: params ?? {},
|
|
7006
|
+
sessionId
|
|
7007
|
+
}));
|
|
7008
|
+
} catch (error) {
|
|
7009
|
+
this.pending.delete(id);
|
|
7010
|
+
return Promise.reject(new CdpError(`${method}: ${message$1(error)}`));
|
|
7011
|
+
}
|
|
6659
7012
|
return promise;
|
|
6660
7013
|
}
|
|
6661
7014
|
on(method, handler) {
|
|
@@ -6705,10 +7058,11 @@ var CdpClient = class CdpClient {
|
|
|
6705
7058
|
}
|
|
6706
7059
|
}
|
|
6707
7060
|
drop(reason) {
|
|
7061
|
+
trace("!!", `dropped: ${reason}`);
|
|
6708
7062
|
for (const waiting of this.pending.values()) waiting.reject(new CdpError(`${waiting.method}: ${reason}`));
|
|
6709
7063
|
this.pending.clear();
|
|
6710
7064
|
for (const handler of this.closeHandlers) try {
|
|
6711
|
-
handler();
|
|
7065
|
+
handler(reason);
|
|
6712
7066
|
} catch {}
|
|
6713
7067
|
this.closeHandlers.clear();
|
|
6714
7068
|
}
|
|
@@ -6733,14 +7087,25 @@ const TAKE_INTERVAL_MS = 400;
|
|
|
6733
7087
|
const NAVIGATION_GUARD_MS = 5e3;
|
|
6734
7088
|
/** How long `stop()` waits for the final take before declaring the tail lost. */
|
|
6735
7089
|
const STOP_TAKE_TIMEOUT_MS = 2e3;
|
|
7090
|
+
/**
|
|
7091
|
+
* Dropped-transport reconnects per engine (= per spec). Bounded because a
|
|
7092
|
+
* deterministic failure — a proxy mangling frames, a peer that keeps
|
|
7093
|
+
* violating the framing — would otherwise loop for the whole spec; four
|
|
7094
|
+
* doubling delays cover a browser that was merely busy.
|
|
7095
|
+
*/
|
|
7096
|
+
const MAX_RECONNECTS = 4;
|
|
7097
|
+
const RECONNECT_BASE_MS = 200;
|
|
6736
7098
|
/** The browser's own chrome. Nothing there is the application under test. */
|
|
6737
7099
|
const INTERNAL_URL = /^(chrome|chrome-untrusted|chrome-extension|devtools):/;
|
|
6738
7100
|
async function startBrowserCoverage(opts) {
|
|
6739
|
-
const
|
|
6740
|
-
const
|
|
7101
|
+
const connect = opts.connect ?? ((wsUrl) => CdpClient.connect(wsUrl));
|
|
7102
|
+
const wsUrl = await browserWebSocketUrl(opts.cdpUrl);
|
|
7103
|
+
const client = await connect(wsUrl);
|
|
7104
|
+
const engine = new Engine(client, opts, connect, wsUrl);
|
|
6741
7105
|
try {
|
|
6742
7106
|
await engine.arm();
|
|
6743
7107
|
} catch (error) {
|
|
7108
|
+
engine.abandon();
|
|
6744
7109
|
client.close();
|
|
6745
7110
|
throw error;
|
|
6746
7111
|
}
|
|
@@ -6748,6 +7113,10 @@ async function startBrowserCoverage(opts) {
|
|
|
6748
7113
|
}
|
|
6749
7114
|
var Engine = class {
|
|
6750
7115
|
client;
|
|
7116
|
+
connectFn;
|
|
7117
|
+
wsUrl;
|
|
7118
|
+
reconnects = 0;
|
|
7119
|
+
reconnectInFlight = false;
|
|
6751
7120
|
opts;
|
|
6752
7121
|
resolution;
|
|
6753
7122
|
pages = /* @__PURE__ */ new Map();
|
|
@@ -6760,11 +7129,15 @@ var Engine = class {
|
|
|
6760
7129
|
armedTargets = /* @__PURE__ */ new Set();
|
|
6761
7130
|
/** Sessions whose take failure was already said; see enqueueTake. */
|
|
6762
7131
|
warnedTakeSessions = /* @__PURE__ */ new Set();
|
|
7132
|
+
/** Same, for the cookie warning: one per session, not one per 400ms tick. */
|
|
7133
|
+
warnedCookieSessions = /* @__PURE__ */ new Set();
|
|
6763
7134
|
cookies;
|
|
6764
7135
|
timer;
|
|
6765
7136
|
stopped = false;
|
|
6766
|
-
constructor(client, opts) {
|
|
7137
|
+
constructor(client, opts, connectFn, wsUrl) {
|
|
6767
7138
|
this.client = client;
|
|
7139
|
+
this.connectFn = connectFn;
|
|
7140
|
+
this.wsUrl = wsUrl;
|
|
6768
7141
|
this.opts = opts;
|
|
6769
7142
|
this.cookies = opts.origins.map((url) => ({
|
|
6770
7143
|
name: COVERAGE_COOKIE,
|
|
@@ -6812,10 +7185,14 @@ var Engine = class {
|
|
|
6812
7185
|
if (page === void 0 || !this.isMainFrame(page, params.frameId)) return;
|
|
6813
7186
|
page.navigatingSince = void 0;
|
|
6814
7187
|
});
|
|
6815
|
-
this.client.onClose(() => {
|
|
7188
|
+
this.client.onClose((reason) => {
|
|
6816
7189
|
if (!this.stopped && this.pages.size > 0) this.resolution.markStopped();
|
|
6817
7190
|
this.pages.clear();
|
|
6818
|
-
|
|
7191
|
+
this.armedTargets.clear();
|
|
7192
|
+
this.warnedTakeSessions.clear();
|
|
7193
|
+
this.warnedCookieSessions.clear();
|
|
7194
|
+
this.clearTimer();
|
|
7195
|
+
if (!this.stopped) this.reconnect(reason);
|
|
6819
7196
|
});
|
|
6820
7197
|
await this.client.send("Target.setAutoAttach", {
|
|
6821
7198
|
autoAttach: true,
|
|
@@ -6824,13 +7201,10 @@ var Engine = class {
|
|
|
6824
7201
|
filter: [{ type: "tab" }, { exclude: true }]
|
|
6825
7202
|
});
|
|
6826
7203
|
const existing = await this.client.send("Target.getTargets", { filter: [{ type: "tab" }] });
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
flatten: true
|
|
6832
|
-
}).catch(() => void 0);
|
|
6833
|
-
}
|
|
7204
|
+
await Promise.all(existing.targetInfos.filter((info) => !this.armedTargets.has(info.targetId)).map((info) => this.client.send("Target.attachToTarget", {
|
|
7205
|
+
targetId: info.targetId,
|
|
7206
|
+
flatten: true
|
|
7207
|
+
}).catch(() => void 0)));
|
|
6834
7208
|
this.timer = setInterval(() => {
|
|
6835
7209
|
for (const page of this.pages.values()) {
|
|
6836
7210
|
this.enqueueTake(page.sessionId);
|
|
@@ -6839,9 +7213,71 @@ var Engine = class {
|
|
|
6839
7213
|
}, TAKE_INTERVAL_MS);
|
|
6840
7214
|
this.timer.unref?.();
|
|
6841
7215
|
}
|
|
7216
|
+
/**
|
|
7217
|
+
* A dropped transport is re-opened and re-armed, bounded by
|
|
7218
|
+
* `MAX_RECONNECTS`. Sound to retry: `startPreciseCoverage` restarts the
|
|
7219
|
+
* counters from zero and `absorbEntries` takes the union of covered
|
|
7220
|
+
* ranges, so a re-measured page adds to the result instead of doubling it.
|
|
7221
|
+
* What ran while disconnected is gone either way — `markStopped` was
|
|
7222
|
+
* already recorded when the transport dropped, so the hole stays visible
|
|
7223
|
+
* in the result even after a successful reconnect.
|
|
7224
|
+
*/
|
|
7225
|
+
clearTimer() {
|
|
7226
|
+
if (this.timer !== void 0) clearInterval(this.timer);
|
|
7227
|
+
this.timer = void 0;
|
|
7228
|
+
}
|
|
7229
|
+
/** Stops a never-armed engine: no takes ran, so there is nothing to flush. */
|
|
7230
|
+
abandon() {
|
|
7231
|
+
this.stopped = true;
|
|
7232
|
+
this.clearTimer();
|
|
7233
|
+
}
|
|
7234
|
+
async reconnect(initialReason) {
|
|
7235
|
+
if (this.reconnectInFlight) return;
|
|
7236
|
+
this.reconnectInFlight = true;
|
|
7237
|
+
try {
|
|
7238
|
+
let reason = initialReason;
|
|
7239
|
+
while (!this.stopped) {
|
|
7240
|
+
if (this.reconnects >= MAX_RECONNECTS) {
|
|
7241
|
+
this.opts.warn(`browser coverage transport dropped (${reason}); giving up after ${MAX_RECONNECTS} reconnects — the rest of the spec goes unmeasured`);
|
|
7242
|
+
return;
|
|
7243
|
+
}
|
|
7244
|
+
this.reconnects += 1;
|
|
7245
|
+
const delay = RECONNECT_BASE_MS * 2 ** (this.reconnects - 1);
|
|
7246
|
+
this.opts.warn(`browser coverage transport dropped (${reason}); reconnecting in ${delay}ms (${this.reconnects}/${MAX_RECONNECTS})`);
|
|
7247
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
7248
|
+
if (this.stopped) return;
|
|
7249
|
+
let fresh;
|
|
7250
|
+
try {
|
|
7251
|
+
fresh = await this.connectFn(this.wsUrl);
|
|
7252
|
+
} catch (error) {
|
|
7253
|
+
reason = `reconnect failed: ${message(error)}`;
|
|
7254
|
+
continue;
|
|
7255
|
+
}
|
|
7256
|
+
if (this.stopped) {
|
|
7257
|
+
fresh.close();
|
|
7258
|
+
return;
|
|
7259
|
+
}
|
|
7260
|
+
this.client = fresh;
|
|
7261
|
+
try {
|
|
7262
|
+
await this.arm();
|
|
7263
|
+
} catch (error) {
|
|
7264
|
+
fresh.close();
|
|
7265
|
+
reason = `reconnect failed: ${message(error)}`;
|
|
7266
|
+
continue;
|
|
7267
|
+
}
|
|
7268
|
+
if (this.stopped) {
|
|
7269
|
+
this.clearTimer();
|
|
7270
|
+
fresh.close();
|
|
7271
|
+
}
|
|
7272
|
+
return;
|
|
7273
|
+
}
|
|
7274
|
+
} finally {
|
|
7275
|
+
this.reconnectInFlight = false;
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
6842
7278
|
async stop() {
|
|
6843
7279
|
this.stopped = true;
|
|
6844
|
-
|
|
7280
|
+
this.clearTimer();
|
|
6845
7281
|
let sawEverything = ![...this.pages.values()].some((page) => !page.armed);
|
|
6846
7282
|
for (const page of this.pages.values()) page.navigatingSince = void 0;
|
|
6847
7283
|
const takes = Promise.all([...this.pages.keys()].map((sessionId) => this.enqueueTake(sessionId)));
|
|
@@ -6906,6 +7342,9 @@ var Engine = class {
|
|
|
6906
7342
|
setCookies(page) {
|
|
6907
7343
|
if (this.cookies.length === 0) return Promise.resolve();
|
|
6908
7344
|
return this.client.send("Network.setCookies", { cookies: this.cookies }, page.sessionId).catch((error) => {
|
|
7345
|
+
if (this.stopped || !this.pages.has(page.sessionId)) return;
|
|
7346
|
+
if (this.warnedCookieSessions.has(page.sessionId)) return;
|
|
7347
|
+
this.warnedCookieSessions.add(page.sessionId);
|
|
6909
7348
|
this.opts.warn(`could not attach the spec cookie (${message(error)})`);
|
|
6910
7349
|
});
|
|
6911
7350
|
}
|
|
@@ -7140,6 +7579,10 @@ var CoverageSession = class CoverageSession {
|
|
|
7140
7579
|
get sinkUrl() {
|
|
7141
7580
|
return this.sink?.url ?? "";
|
|
7142
7581
|
}
|
|
7582
|
+
/** The id this run's events carry in the stream — what a hub resolve is asked for. */
|
|
7583
|
+
get streamRunId() {
|
|
7584
|
+
return this.runId;
|
|
7585
|
+
}
|
|
7143
7586
|
/**
|
|
7144
7587
|
* True in hub mode: the facts leave as a stream, rows carry no coverage,
|
|
7145
7588
|
* and the run-side health read-outs below answer empty. The one mode flag
|
|
@@ -11443,6 +11886,189 @@ function foldTouchIndex(current, entry, selection) {
|
|
|
11443
11886
|
}
|
|
11444
11887
|
return out;
|
|
11445
11888
|
}
|
|
11889
|
+
z.object({
|
|
11890
|
+
runId: z.string(),
|
|
11891
|
+
hubRunId: z.string().optional(),
|
|
11892
|
+
asOf: z.number(),
|
|
11893
|
+
lastSeq: z.number(),
|
|
11894
|
+
universe: z.object({
|
|
11895
|
+
include: z.array(z.string()),
|
|
11896
|
+
files: z.array(z.string())
|
|
11897
|
+
}).optional(),
|
|
11898
|
+
specs: z.array(z.object({
|
|
11899
|
+
specId: z.string(),
|
|
11900
|
+
files: z.array(z.string()),
|
|
11901
|
+
actorEvents: z.record(z.string(), z.number())
|
|
11902
|
+
})),
|
|
11903
|
+
boot: z.array(z.string()),
|
|
11904
|
+
health: z.object({
|
|
11905
|
+
heardFromApplication: z.boolean(),
|
|
11906
|
+
pushesDuringRun: z.number(),
|
|
11907
|
+
attributedSpecs: z.number(),
|
|
11908
|
+
rejectedPushes: z.number(),
|
|
11909
|
+
uninstrumentedFiles: z.number(),
|
|
11910
|
+
uninstrumentedProcesses: z.number(),
|
|
11911
|
+
droppedPushes: z.number(),
|
|
11912
|
+
unmappedActorEvents: z.number(),
|
|
11913
|
+
outsideWindowEvents: z.record(z.string(), z.number()),
|
|
11914
|
+
specsMeasured: z.number()
|
|
11915
|
+
})
|
|
11916
|
+
});
|
|
11917
|
+
/**
|
|
11918
|
+
* One read-out line for a resolved spec — shared by the run-end summary and
|
|
11919
|
+
* `ccqa hub coverage`, so the two never drift on how a measurement reads.
|
|
11920
|
+
*/
|
|
11921
|
+
function formatResolvedSpec(spec) {
|
|
11922
|
+
const actors = Object.entries(spec.actorEvents).map(([key, count]) => `${key}: ${count} event(s)`).join(", ");
|
|
11923
|
+
return `${spec.specId}: ${spec.files.length} file(s)${actors ? ` (${actors})` : ""}`;
|
|
11924
|
+
}
|
|
11925
|
+
/**
|
|
11926
|
+
* Interprets `runId`'s view of the stream.
|
|
11927
|
+
*
|
|
11928
|
+
* Two passes, because the resolver needs its context up front: the first
|
|
11929
|
+
* collects what the run's own markers establish — which ids it issued, which
|
|
11930
|
+
* identity tags were its to hand out, its universe, and when its first and
|
|
11931
|
+
* last marker arrived. The second replays the stream through the shared
|
|
11932
|
+
* resolver: this run's window markers as they came, and every application
|
|
11933
|
+
* push — as-is when its stamp falls inside the span the run's sink would
|
|
11934
|
+
* have been listening (first marker to last marker plus `GRACE_MS`),
|
|
11935
|
+
* stripped of its spec and actor attribution when it does not. A push
|
|
11936
|
+
* outside the span was another run's audience, so its attribution is not
|
|
11937
|
+
* this run's to claim — but the collector never re-sends what an earlier
|
|
11938
|
+
* run acked, so on an always-on hub the boot set and each process's health
|
|
11939
|
+
* figures arrived long before this run began, and only survive here.
|
|
11940
|
+
*/
|
|
11941
|
+
function resolveStream(events, runId) {
|
|
11942
|
+
const issued = /* @__PURE__ */ new Set();
|
|
11943
|
+
const specOrder = [];
|
|
11944
|
+
const tagToKey = /* @__PURE__ */ new Map();
|
|
11945
|
+
const browserFiles = /* @__PURE__ */ new Map();
|
|
11946
|
+
let universe;
|
|
11947
|
+
let hubRunId;
|
|
11948
|
+
let firstMarkerAt;
|
|
11949
|
+
let lastMarkerAt = 0;
|
|
11950
|
+
for (const event of events) {
|
|
11951
|
+
const body = event.body;
|
|
11952
|
+
if (!("kind" in body) || body.runId !== runId) continue;
|
|
11953
|
+
if (firstMarkerAt === void 0) firstMarkerAt = event.at;
|
|
11954
|
+
lastMarkerAt = event.at;
|
|
11955
|
+
switch (body.kind) {
|
|
11956
|
+
case "spec-open":
|
|
11957
|
+
if (!issued.has(body.specId)) {
|
|
11958
|
+
issued.add(body.specId);
|
|
11959
|
+
specOrder.push(body.specId);
|
|
11960
|
+
}
|
|
11961
|
+
break;
|
|
11962
|
+
case "window-open":
|
|
11963
|
+
tagToKey.set(body.tag, body.key);
|
|
11964
|
+
break;
|
|
11965
|
+
case "universe":
|
|
11966
|
+
universe = {
|
|
11967
|
+
include: body.include,
|
|
11968
|
+
files: body.files
|
|
11969
|
+
};
|
|
11970
|
+
break;
|
|
11971
|
+
case "run-link":
|
|
11972
|
+
hubRunId = body.hubRunId;
|
|
11973
|
+
break;
|
|
11974
|
+
case "browser": {
|
|
11975
|
+
const files = browserFiles.get(body.specId) ?? /* @__PURE__ */ new Set();
|
|
11976
|
+
for (const file of body.files) files.add(file);
|
|
11977
|
+
browserFiles.set(body.specId, files);
|
|
11978
|
+
break;
|
|
11979
|
+
}
|
|
11980
|
+
}
|
|
11981
|
+
}
|
|
11982
|
+
const resolver = new CoverageResolver(issued, tagToKey);
|
|
11983
|
+
let asOf = 0;
|
|
11984
|
+
let lastSeq = 0;
|
|
11985
|
+
let pushesDuringRun = 0;
|
|
11986
|
+
for (const event of events) {
|
|
11987
|
+
if (event.seq > lastSeq) lastSeq = event.seq;
|
|
11988
|
+
const body = event.body;
|
|
11989
|
+
if ("kind" in body) {
|
|
11990
|
+
if (body.runId !== runId) continue;
|
|
11991
|
+
asOf = event.at;
|
|
11992
|
+
if (body.kind === "window-open") resolver.apply({
|
|
11993
|
+
kind: "window-open",
|
|
11994
|
+
at: event.at,
|
|
11995
|
+
tag: body.tag,
|
|
11996
|
+
key: body.key,
|
|
11997
|
+
specId: body.specId
|
|
11998
|
+
});
|
|
11999
|
+
else if (body.kind === "window-close") resolver.apply({
|
|
12000
|
+
kind: "window-close",
|
|
12001
|
+
at: event.at,
|
|
12002
|
+
tag: body.tag
|
|
12003
|
+
});
|
|
12004
|
+
continue;
|
|
12005
|
+
}
|
|
12006
|
+
if (firstMarkerAt === void 0 || event.at < firstMarkerAt || event.at > lastMarkerAt + 3e4) {
|
|
12007
|
+
resolver.apply({
|
|
12008
|
+
kind: "push",
|
|
12009
|
+
at: event.at,
|
|
12010
|
+
push: {
|
|
12011
|
+
...body,
|
|
12012
|
+
specs: {},
|
|
12013
|
+
actors: []
|
|
12014
|
+
}
|
|
12015
|
+
});
|
|
12016
|
+
continue;
|
|
12017
|
+
}
|
|
12018
|
+
asOf = event.at;
|
|
12019
|
+
pushesDuringRun++;
|
|
12020
|
+
resolver.apply({
|
|
12021
|
+
kind: "push",
|
|
12022
|
+
at: event.at,
|
|
12023
|
+
push: body
|
|
12024
|
+
});
|
|
12025
|
+
}
|
|
12026
|
+
const specs = specOrder.map((specId) => {
|
|
12027
|
+
const actorEvents = {};
|
|
12028
|
+
for (const [key, count] of resolver.actorEventsFor(specId)) actorEvents[key] = count;
|
|
12029
|
+
return {
|
|
12030
|
+
specId,
|
|
12031
|
+
files: [...new Set([...resolver.filesFor(specId) ?? [], ...browserFiles.get(specId) ?? []])].sort(),
|
|
12032
|
+
actorEvents
|
|
12033
|
+
};
|
|
12034
|
+
});
|
|
12035
|
+
const outsideWindowEvents = {};
|
|
12036
|
+
for (const [key, count] of resolver.outsideWindowEvents()) outsideWindowEvents[key] = count;
|
|
12037
|
+
return {
|
|
12038
|
+
runId,
|
|
12039
|
+
...hubRunId !== void 0 ? { hubRunId } : {},
|
|
12040
|
+
asOf,
|
|
12041
|
+
lastSeq,
|
|
12042
|
+
...universe !== void 0 ? { universe } : {},
|
|
12043
|
+
specs,
|
|
12044
|
+
boot: [...resolver.boot()].sort(),
|
|
12045
|
+
health: {
|
|
12046
|
+
heardFromApplication: resolver.heardFromApplication(),
|
|
12047
|
+
pushesDuringRun,
|
|
12048
|
+
attributedSpecs: resolver.attributedSpecs(),
|
|
12049
|
+
rejectedPushes: resolver.rejectedPushes(),
|
|
12050
|
+
uninstrumentedFiles: resolver.uninstrumentedFiles(),
|
|
12051
|
+
uninstrumentedProcesses: resolver.uninstrumentedProcesses(),
|
|
12052
|
+
droppedPushes: resolver.droppedPushes(),
|
|
12053
|
+
unmappedActorEvents: resolver.unmappedActorEvents(),
|
|
12054
|
+
outsideWindowEvents,
|
|
12055
|
+
specsMeasured: specs.length
|
|
12056
|
+
}
|
|
12057
|
+
};
|
|
12058
|
+
}
|
|
12059
|
+
/**
|
|
12060
|
+
* Every run that opened a spec in this stream, most recently heard-from
|
|
12061
|
+
* first — recency by the arrival position of each run's latest spec-open,
|
|
12062
|
+
* the one order the hub's stamps establish.
|
|
12063
|
+
*/
|
|
12064
|
+
function listRunIds(events) {
|
|
12065
|
+
const lastOpenIndex = /* @__PURE__ */ new Map();
|
|
12066
|
+
events.forEach((event, index) => {
|
|
12067
|
+
const body = event.body;
|
|
12068
|
+
if ("kind" in body && body.kind === "spec-open") lastOpenIndex.set(body.runId, index);
|
|
12069
|
+
});
|
|
12070
|
+
return [...lastOpenIndex.entries()].sort((a, b) => b[1] - a[1]).map(([id]) => id);
|
|
12071
|
+
}
|
|
11446
12072
|
//#endregion
|
|
11447
12073
|
//#region src/cli/session.ts
|
|
11448
12074
|
const AB = resolveAgentBrowserBin$1();
|
|
@@ -11566,7 +12192,7 @@ function resolveBaseUrl(opts) {
|
|
|
11566
12192
|
return baseUrl.replace(/\/+$/, "");
|
|
11567
12193
|
}
|
|
11568
12194
|
/** Resolve the hub client from flags / env, or exit 2 with a clear message. */
|
|
11569
|
-
function connect(opts) {
|
|
12195
|
+
function connect$2(opts) {
|
|
11570
12196
|
const client = resolveHubClient(opts);
|
|
11571
12197
|
if (client) return client;
|
|
11572
12198
|
resolveBaseUrl(opts);
|
|
@@ -11615,7 +12241,7 @@ const sessionPush = new Command("push").description("Upload a locally-saved brow
|
|
|
11615
12241
|
hint(`create it first with: ccqa hub session capture ${name}${opts.profile ? ` --profile ${opts.profile}` : ""}`);
|
|
11616
12242
|
process.exit(2);
|
|
11617
12243
|
}
|
|
11618
|
-
await connect(opts).putSession(project, profile, name, state);
|
|
12244
|
+
await connect$2(opts).putSession(project, profile, name, state);
|
|
11619
12245
|
header("hub session push", name);
|
|
11620
12246
|
meta("project", project);
|
|
11621
12247
|
meta("profile", profile);
|
|
@@ -11624,7 +12250,7 @@ const sessionPush = new Command("push").description("Upload a locally-saved brow
|
|
|
11624
12250
|
const sessionLs = new Command("ls").description("List sessions stored on the hub for a project/profile (names + last-updated times). `ls` shows metadata only.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...profileOption).option(...cwdOption).action(withHubErrors(async (opts) => {
|
|
11625
12251
|
const project = resolveProject(opts);
|
|
11626
12252
|
const profile = opts.profile ?? "default";
|
|
11627
|
-
const sessions = await connect(opts).listSessions(project, profile);
|
|
12253
|
+
const sessions = await connect$2(opts).listSessions(project, profile);
|
|
11628
12254
|
header("hub sessions", `${project}/${profile}`);
|
|
11629
12255
|
if (sessions.length === 0) {
|
|
11630
12256
|
info("no sessions stored on the hub for this project/profile");
|
|
@@ -11636,7 +12262,7 @@ const sessionRm = new Command("rm").description("Delete a session from the hub."
|
|
|
11636
12262
|
const name = validateSessionName(rawName);
|
|
11637
12263
|
const project = resolveProject(opts);
|
|
11638
12264
|
const profile = opts.profile ?? "default";
|
|
11639
|
-
await connect(opts).deleteSession(project, profile, name);
|
|
12265
|
+
await connect$2(opts).deleteSession(project, profile, name);
|
|
11640
12266
|
header("hub session rm", name);
|
|
11641
12267
|
info(`deleted session "${name}" from the hub`);
|
|
11642
12268
|
}));
|
|
@@ -11649,7 +12275,7 @@ const varSet = new Command("set").description("Store an environment variable on
|
|
|
11649
12275
|
error("no value provided (pass --value <value> or pipe it on stdin)");
|
|
11650
12276
|
process.exit(2);
|
|
11651
12277
|
}
|
|
11652
|
-
await connect(opts).putVariable(project, profile, name, {
|
|
12278
|
+
await connect$2(opts).putVariable(project, profile, name, {
|
|
11653
12279
|
value,
|
|
11654
12280
|
sensitive: opts.sensitive ?? false
|
|
11655
12281
|
});
|
|
@@ -11662,7 +12288,7 @@ const varSet = new Command("set").description("Store an environment variable on
|
|
|
11662
12288
|
const varLs = new Command("ls").description("List variables stored on the hub for a project/profile. Non-sensitive values are shown inline; sensitive ones are hidden here but still fetched at run time by `ccqa run` / `ccqa record`.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...profileOption).option(...cwdOption).action(withHubErrors(async (opts) => {
|
|
11663
12289
|
const project = resolveProject(opts);
|
|
11664
12290
|
const profile = opts.profile ?? "default";
|
|
11665
|
-
const variables = await connect(opts).listVariables(project, profile);
|
|
12291
|
+
const variables = await connect$2(opts).listVariables(project, profile);
|
|
11666
12292
|
header("hub variables", `${project}/${profile}`);
|
|
11667
12293
|
if (variables.length === 0) {
|
|
11668
12294
|
info("no variables stored on the hub for this project/profile");
|
|
@@ -11676,7 +12302,7 @@ const varLs = new Command("ls").description("List variables stored on the hub fo
|
|
|
11676
12302
|
const varRm = new Command("rm").description("Delete a variable from the hub.").argument("<name>", "Variable name to delete").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...profileOption).option(...cwdOption).action(withHubErrors(async (name, opts) => {
|
|
11677
12303
|
const project = resolveProject(opts);
|
|
11678
12304
|
const profile = opts.profile ?? "default";
|
|
11679
|
-
await connect(opts).deleteVariable(project, profile, name);
|
|
12305
|
+
await connect$2(opts).deleteVariable(project, profile, name);
|
|
11680
12306
|
header("hub var rm", name);
|
|
11681
12307
|
info(`deleted variable "${name}" from the hub`);
|
|
11682
12308
|
}));
|
|
@@ -11707,14 +12333,14 @@ const promptPush = new Command("push").description("Upload a locally-generated p
|
|
|
11707
12333
|
hint("nothing to push; generate it first (e.g. ccqa run --learn-hub-live-prompt)");
|
|
11708
12334
|
process.exit(2);
|
|
11709
12335
|
}
|
|
11710
|
-
await connect(opts).putPrompt(project, name, body);
|
|
12336
|
+
await connect$2(opts).putPrompt(project, name, body);
|
|
11711
12337
|
header("hub prompt push", name);
|
|
11712
12338
|
meta("project", project);
|
|
11713
12339
|
info(`uploaded prompt "${name}" to the hub`);
|
|
11714
12340
|
}));
|
|
11715
12341
|
const promptLs = new Command("ls").description("List prompts stored on the hub for a project (name, kind, last-updated). Prompts are project-wide (not per-profile). `ls` shows metadata only.").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...cwdOption).action(withHubErrors(async (opts) => {
|
|
11716
12342
|
const project = resolveProject(opts);
|
|
11717
|
-
const prompts = await connect(opts).listPrompts(project);
|
|
12343
|
+
const prompts = await connect$2(opts).listPrompts(project);
|
|
11718
12344
|
header("hub prompts", project);
|
|
11719
12345
|
if (prompts.length === 0) {
|
|
11720
12346
|
info("no prompts stored on the hub for this project");
|
|
@@ -11725,7 +12351,7 @@ const promptLs = new Command("ls").description("List prompts stored on the hub f
|
|
|
11725
12351
|
const promptRm = new Command("rm").description("Delete a prompt from the hub.").argument("<name>", "Prompt name to delete").option(...hubUrlOption).option(...hubTokenOption).option(...projectOption).option(...cwdOption).action(withHubErrors(async (rawName, opts) => {
|
|
11726
12352
|
const name = validatePromptName(rawName);
|
|
11727
12353
|
const project = resolveProject(opts);
|
|
11728
|
-
await connect(opts).deletePrompt(project, name);
|
|
12354
|
+
await connect$2(opts).deletePrompt(project, name);
|
|
11729
12355
|
header("hub prompt rm", name);
|
|
11730
12356
|
info(`deleted prompt "${name}" from the hub`);
|
|
11731
12357
|
}));
|
|
@@ -11736,7 +12362,7 @@ const deployRecord = new Command("record").description("Tell the hub what a depl
|
|
|
11736
12362
|
async function runDeployRecord(opts) {
|
|
11737
12363
|
const cwd = resolveCwd(opts.cwd);
|
|
11738
12364
|
const project = resolveProject(opts);
|
|
11739
|
-
const hub = connect(opts);
|
|
12365
|
+
const hub = connect$2(opts);
|
|
11740
12366
|
const previous = opts.previous ?? await deployHeadSha(hub, project, opts.profile);
|
|
11741
12367
|
const runUrl = githubRunUrl();
|
|
11742
12368
|
const diff = previous === null ? null : await diffOrNull(previous, opts.sha, cwd);
|
|
@@ -11830,7 +12456,7 @@ async function runCostPush(opts) {
|
|
|
11830
12456
|
process.exit(2);
|
|
11831
12457
|
}
|
|
11832
12458
|
const project = resolveProject(opts);
|
|
11833
|
-
const hub = connect(opts);
|
|
12459
|
+
const hub = connect$2(opts);
|
|
11834
12460
|
header("hub cost push", opts.label);
|
|
11835
12461
|
meta("project", project);
|
|
11836
12462
|
const total = await readCostFileTotal(path);
|
|
@@ -11872,7 +12498,7 @@ const pushCommand = new Command("push").description("Upload the report directory
|
|
|
11872
12498
|
const deployedSha = parsed.data.deployedSha;
|
|
11873
12499
|
const branch = opts.branch ?? await detectBranch(cwd);
|
|
11874
12500
|
const archive = await packDirToTarGz(reportDir);
|
|
11875
|
-
const run = await connect(opts).pushRun(archive, {
|
|
12501
|
+
const run = await connect$2(opts).pushRun(archive, {
|
|
11876
12502
|
project,
|
|
11877
12503
|
...branch ? { branch } : {},
|
|
11878
12504
|
...opts.profile ? { profile: opts.profile } : {},
|
|
@@ -11888,7 +12514,7 @@ const pushCommand = new Command("push").description("Upload the report directory
|
|
|
11888
12514
|
}));
|
|
11889
12515
|
const attestCommand = new Command("attest").argument("<feature/spec>", "Spec id, e.g. checkout/happy-path").description("Record that a person checked a spec's behaviour by hand against the deployed environment. The verdict answers manuallyVerified instead of asking a person for what a person already did — the drift ledger is untouched, so the repair loop keeps its reason to fix the test. The attestation lapses on its own when a deploy reaches the spec or the spec is edited.").requiredOption("--profile <name>", "Environment that was checked (e.g. 'stg'). The attestation is anchored to its current deploy head.").option("--by <name>", "Who checked. Required unless --revoke.").option("--note <text>", "What was checked and how — the reader deciding whether to trust it sees this.").option("--revoke", "Withdraw the spec's attestation instead of recording one.").option(...hubUrlOption).option(...hubTokenOption).option("--project <name>", "Hub project. Defaults to the current directory's name.").option("--cwd <path>", "Directory the default --project name is resolved against.").action(withHubErrors(async (rawSpecId, opts) => {
|
|
11890
12516
|
const project = resolveProject(opts);
|
|
11891
|
-
const hub = connect(opts);
|
|
12517
|
+
const hub = connect$2(opts);
|
|
11892
12518
|
const specId = requireSpecId(rawSpecId);
|
|
11893
12519
|
if (opts.revoke) {
|
|
11894
12520
|
await hub.deleteAttestation(project, { profile: opts.profile }, specId);
|
|
@@ -11911,7 +12537,7 @@ const attestCommand = new Command("attest").argument("<feature/spec>", "Spec id,
|
|
|
11911
12537
|
}));
|
|
11912
12538
|
const dismissCommand = new Command("dismiss").argument("<feature/spec>", "Spec id, e.g. checkout/happy-path").description("Record that a person judged the spec's current audit finding wrong: the spec describes the code fine. This settles the audit axis rather than the verdict — the spec goes back to being run like any other, and the next run says whether the person was right. The dismissal is pinned to the audit run that raised the finding, so a later audit can raise it again. No --profile: a finding is about the repository, not an environment.").option("--by <name>", "Who judged it wrong. Required unless --revoke.").option("--reason <text>", "Why the finding is wrong. Required unless --revoke — this is what a mis-firing audit learns from.").option("--revoke", "Withdraw the dismissal, putting the finding back in force.").option(...hubUrlOption).option(...hubTokenOption).option("--project <name>", "Hub project. Defaults to the current directory's name.").option("--cwd <path>", "Directory the default --project name is resolved against.").action(withHubErrors(async (rawSpecId, opts) => {
|
|
11913
12539
|
const project = resolveProject(opts);
|
|
11914
|
-
const hub = connect(opts);
|
|
12540
|
+
const hub = connect$2(opts);
|
|
11915
12541
|
const specId = requireSpecId(rawSpecId);
|
|
11916
12542
|
if (opts.revoke) {
|
|
11917
12543
|
await hub.deleteAuditDismissal(project, specId);
|
|
@@ -11932,7 +12558,44 @@ const dismissCommand = new Command("dismiss").argument("<feature/spec>", "Spec i
|
|
|
11932
12558
|
meta("dismissed", `${res.dismissal.label} — ${res.dismissal.headline || "(no headline)"}`);
|
|
11933
12559
|
info("this finding no longer holds the spec back; a later audit can raise one of its own");
|
|
11934
12560
|
}));
|
|
11935
|
-
const
|
|
12561
|
+
const coverageCommand = new Command("coverage").description("Print what the hub's coverage stream resolved for a run: per-spec measured file counts and the stream's health counters. This is the read-out measured spec selection consumes — use it to see why a spec's reach is empty before blaming the selection.").option("--run-id <id>", "Stream run id to resolve. Defaults to the most recently measured run.").option("--files", "List each spec's measured files, not just their count.").option("--json", "Print the raw resolved answer as JSON (everything, including file lists).").option("--project <name>", "Project whose stream is read. Defaults to the current directory's name.").option(...cwdOption).option(...hubUrlOption).option(...hubTokenOption).action(withHubErrors(runCoverageInspect));
|
|
12562
|
+
async function runCoverageInspect(opts) {
|
|
12563
|
+
const project = resolveProject(opts);
|
|
12564
|
+
const answer = await connect$2(opts).getCoverage(project, opts.runId ? { runId: opts.runId } : {});
|
|
12565
|
+
if (opts.json === true) {
|
|
12566
|
+
process.stdout.write(`${JSON.stringify(answer, null, 2)}\n`);
|
|
12567
|
+
return;
|
|
12568
|
+
}
|
|
12569
|
+
header("hub coverage", project);
|
|
12570
|
+
if (answer.runIds.length === 0) {
|
|
12571
|
+
warn("the stream holds no measured runs for this project");
|
|
12572
|
+
return;
|
|
12573
|
+
}
|
|
12574
|
+
meta("measured runs", `${answer.runIds.length} (newest first): ${answer.runIds.join(", ")}`);
|
|
12575
|
+
const resolved = answer.resolved;
|
|
12576
|
+
if (resolved == null) {
|
|
12577
|
+
warn("nothing resolved — pass --run-id with one of the runs above");
|
|
12578
|
+
return;
|
|
12579
|
+
}
|
|
12580
|
+
if (resolved.asOf === 0 && resolved.specs.length === 0) {
|
|
12581
|
+
warn(`the stream holds no events for run ${resolved.runId} — check it against the runs above`);
|
|
12582
|
+
return;
|
|
12583
|
+
}
|
|
12584
|
+
meta("run", resolved.runId + (resolved.hubRunId ? ` (hub run ${resolved.hubRunId})` : ""));
|
|
12585
|
+
meta("as of", new Date(resolved.asOf).toISOString());
|
|
12586
|
+
if (resolved.universe) meta("universe", `${resolved.universe.files.length} file(s) (include: ${resolved.universe.include.join(", ")})`);
|
|
12587
|
+
meta("boot", `${resolved.boot.length} file(s) reached only at module load`);
|
|
12588
|
+
meta("specs", `${resolved.specs.filter((spec) => spec.files.length > 0).length}/${resolved.specs.length} measured files`);
|
|
12589
|
+
for (const spec of resolved.specs) {
|
|
12590
|
+
info(` ${formatResolvedSpec(spec)}`);
|
|
12591
|
+
if (opts.files === true) for (const file of spec.files) info(` ${file}`);
|
|
12592
|
+
}
|
|
12593
|
+
const h = resolved.health;
|
|
12594
|
+
meta("health", `heard-from-application=${h.heardFromApplication} pushes-during-run=${h.pushesDuringRun} attributed-specs=${h.attributedSpecs} specs-measured=${h.specsMeasured} rejected=${h.rejectedPushes} dropped=${h.droppedPushes} uninstrumented-files=${h.uninstrumentedFiles} uninstrumented-processes=${h.uninstrumentedProcesses} unmapped-actor-events=${h.unmappedActorEvents}`);
|
|
12595
|
+
const outside = Object.entries(h.outsideWindowEvents);
|
|
12596
|
+
if (outside.length > 0) warn(`outside-window events (identity was driven while unclaimed): ${outside.map(([key, count]) => `${key}: ${count}`).join(", ")}`);
|
|
12597
|
+
}
|
|
12598
|
+
const hubCommand = new Command("hub").description("Client for a ccqa hub: push run results and manage sessions/variables/prompts used by `ccqa run`. See docs/hub.md.").addCommand(pushCommand).addCommand(deployCommand).addCommand(costCommand).addCommand(coverageCommand).addCommand(sessionCommand).addCommand(varCommand).addCommand(promptCommand).addCommand(attestCommand).addCommand(dismissCommand);
|
|
11936
12599
|
/** Loose check that a fetched session is agent-browser storage-state, mirroring loadStorageState. */
|
|
11937
12600
|
function isStorageStateShape(state) {
|
|
11938
12601
|
return typeof state === "object" && state !== null && Array.isArray(state.cookies) && Array.isArray(state.origins);
|
|
@@ -16113,6 +16776,7 @@ async function executeRun(targets, opts) {
|
|
|
16113
16776
|
};
|
|
16114
16777
|
const live = await runLiveSpecs(liveSpecs, liveOpts);
|
|
16115
16778
|
if (coverage && !coverage.streamsToHub) reportCoverageHealth(coverage, [...externalRows, ...live.reportResults]);
|
|
16779
|
+
else if (coverage && hubCtx != null) await reportStreamedCoverageHealth(coverage, hubCtx);
|
|
16116
16780
|
let overallExitCode = det.exitCode !== 0 ? 1 : 0;
|
|
16117
16781
|
if (live.failedCount > 0) overallExitCode = 1;
|
|
16118
16782
|
if (externalRows.some((r) => r.status === "failed")) overallExitCode = 1;
|
|
@@ -16242,6 +16906,44 @@ function explainMissingCoverage(row) {
|
|
|
16242
16906
|
coverageUnavailable: row.status === "skipped" ? "the spec did not execute" : "this target is not measured by --coverage yet"
|
|
16243
16907
|
};
|
|
16244
16908
|
}
|
|
16909
|
+
/**
|
|
16910
|
+
* The hub-inbox counterpart of `reportCoverageHealth`: ask the hub to resolve
|
|
16911
|
+
* this run's slice of the stream and read the answer out into the run log.
|
|
16912
|
+
* Best-effort — the measurement already left as events, so a failed read-out
|
|
16913
|
+
* loses visibility, never data. Application pushes may still land for
|
|
16914
|
+
* `GRACE_MS` after the last window closed, so the counts here are a floor.
|
|
16915
|
+
*/
|
|
16916
|
+
async function reportStreamedCoverageHealth(coverage, hubCtx) {
|
|
16917
|
+
await new Promise((resolve) => setTimeout(resolve, 3e3));
|
|
16918
|
+
let resolved;
|
|
16919
|
+
try {
|
|
16920
|
+
resolved = (await hubCtx.hub.getCoverage(hubCtx.project, { runId: coverage.streamRunId })).resolved;
|
|
16921
|
+
} catch (error) {
|
|
16922
|
+
warn(`coverage: could not read this run's resolve from the hub (${errMessage(error)})`);
|
|
16923
|
+
return;
|
|
16924
|
+
}
|
|
16925
|
+
if (resolved == null) {
|
|
16926
|
+
warn("coverage: the hub resolved nothing for this run — its events never reached the stream, so every spec's measured reach is absent");
|
|
16927
|
+
return;
|
|
16928
|
+
}
|
|
16929
|
+
const measured = resolved.specs.filter((spec) => spec.files.length > 0);
|
|
16930
|
+
const empty = resolved.specs.filter((spec) => spec.files.length === 0);
|
|
16931
|
+
meta("coverage", `stream resolve: ${measured.length}/${resolved.specs.length} spec(s) measured files` + (resolved.boot.length > 0 ? `; ${resolved.boot.length} file(s) reached only at boot` : ""));
|
|
16932
|
+
for (const spec of measured) meta("coverage", ` ${formatResolvedSpec(spec)}`);
|
|
16933
|
+
if (empty.length > 0) warn(`coverage: ${empty.length} spec(s) had measured no files at read-out time — unless a late push still lands (\`ccqa hub coverage\` shows the settled answer), their reach stays unknown to selection: ${empty.map((spec) => spec.specId).join(", ")}`);
|
|
16934
|
+
const h = resolved.health;
|
|
16935
|
+
if (!h.heardFromApplication) warn("coverage: no instrumented application process pushed to the stream — only the browser half was measured. The server needs ccqa-tools and CCQA_COVERAGE_ENDPOINT pointed at the hub's inbox");
|
|
16936
|
+
else if (h.pushesDuringRun === 0) warn("coverage: the stream holds application pushes, but none arrived while this run was measuring — check CCQA_COVERAGE_ENDPOINT on the application");
|
|
16937
|
+
else if (h.attributedSpecs === 0) warn(`coverage: ${h.pushesDuringRun} application push(es) arrived during the run, but none was attributed to a spec — the spec cookie is not reaching the application, so every spec's server-side reach is zero for that reason rather than because the server ran nothing`);
|
|
16938
|
+
const trouble = [];
|
|
16939
|
+
if (h.rejectedPushes > 0) trouble.push(`rejected=${h.rejectedPushes}`);
|
|
16940
|
+
if (h.droppedPushes > 0) trouble.push(`dropped=${h.droppedPushes}`);
|
|
16941
|
+
if (h.unmappedActorEvents > 0) trouble.push(`unmapped-actor-events=${h.unmappedActorEvents}`);
|
|
16942
|
+
if (h.uninstrumentedProcesses > 0) trouble.push(`uninstrumented-processes=${h.uninstrumentedProcesses}`);
|
|
16943
|
+
const outside = Object.entries(h.outsideWindowEvents);
|
|
16944
|
+
if (outside.length > 0) trouble.push(`outside-window=${outside.map(([key, count]) => `${key}:${count}`).join(",")}`);
|
|
16945
|
+
if (trouble.length > 0) warn(`coverage: stream health flags — ${trouble.join(" ")}`);
|
|
16946
|
+
}
|
|
16245
16947
|
/** Everything the measurement could not place; silence here reads as "never reached". */
|
|
16246
16948
|
function reportCoverageHealth(coverage, rows) {
|
|
16247
16949
|
if (!coverage.heardFromApplication()) warn("no instrumented application process reported — only the browser half was measured. The server needs ccqa-tools and CCQA_COVERAGE_ENDPOINT pointed at this run's sink");
|
|
@@ -22140,181 +22842,6 @@ z.object({
|
|
|
22140
22842
|
at: z.number(),
|
|
22141
22843
|
body: InboxBodySchema
|
|
22142
22844
|
});
|
|
22143
|
-
z.object({
|
|
22144
|
-
runId: z.string(),
|
|
22145
|
-
hubRunId: z.string().optional(),
|
|
22146
|
-
asOf: z.number(),
|
|
22147
|
-
lastSeq: z.number(),
|
|
22148
|
-
universe: z.object({
|
|
22149
|
-
include: z.array(z.string()),
|
|
22150
|
-
files: z.array(z.string())
|
|
22151
|
-
}).optional(),
|
|
22152
|
-
specs: z.array(z.object({
|
|
22153
|
-
specId: z.string(),
|
|
22154
|
-
files: z.array(z.string()),
|
|
22155
|
-
actorEvents: z.record(z.string(), z.number())
|
|
22156
|
-
})),
|
|
22157
|
-
boot: z.array(z.string()),
|
|
22158
|
-
health: z.object({
|
|
22159
|
-
heardFromApplication: z.boolean(),
|
|
22160
|
-
pushesDuringRun: z.number(),
|
|
22161
|
-
attributedSpecs: z.number(),
|
|
22162
|
-
rejectedPushes: z.number(),
|
|
22163
|
-
uninstrumentedFiles: z.number(),
|
|
22164
|
-
uninstrumentedProcesses: z.number(),
|
|
22165
|
-
droppedPushes: z.number(),
|
|
22166
|
-
unmappedActorEvents: z.number(),
|
|
22167
|
-
outsideWindowEvents: z.record(z.string(), z.number()),
|
|
22168
|
-
specsMeasured: z.number()
|
|
22169
|
-
})
|
|
22170
|
-
});
|
|
22171
|
-
/**
|
|
22172
|
-
* Interprets `runId`'s view of the stream.
|
|
22173
|
-
*
|
|
22174
|
-
* Two passes, because the resolver needs its context up front: the first
|
|
22175
|
-
* collects what the run's own markers establish — which ids it issued, which
|
|
22176
|
-
* identity tags were its to hand out, its universe, and when its first and
|
|
22177
|
-
* last marker arrived. The second replays the stream through the shared
|
|
22178
|
-
* resolver: this run's window markers as they came, and every application
|
|
22179
|
-
* push — as-is when its stamp falls inside the span the run's sink would
|
|
22180
|
-
* have been listening (first marker to last marker plus `GRACE_MS`),
|
|
22181
|
-
* stripped of its spec and actor attribution when it does not. A push
|
|
22182
|
-
* outside the span was another run's audience, so its attribution is not
|
|
22183
|
-
* this run's to claim — but the collector never re-sends what an earlier
|
|
22184
|
-
* run acked, so on an always-on hub the boot set and each process's health
|
|
22185
|
-
* figures arrived long before this run began, and only survive here.
|
|
22186
|
-
*/
|
|
22187
|
-
function resolveStream(events, runId) {
|
|
22188
|
-
const issued = /* @__PURE__ */ new Set();
|
|
22189
|
-
const specOrder = [];
|
|
22190
|
-
const tagToKey = /* @__PURE__ */ new Map();
|
|
22191
|
-
const browserFiles = /* @__PURE__ */ new Map();
|
|
22192
|
-
let universe;
|
|
22193
|
-
let hubRunId;
|
|
22194
|
-
let firstMarkerAt;
|
|
22195
|
-
let lastMarkerAt = 0;
|
|
22196
|
-
for (const event of events) {
|
|
22197
|
-
const body = event.body;
|
|
22198
|
-
if (!("kind" in body) || body.runId !== runId) continue;
|
|
22199
|
-
if (firstMarkerAt === void 0) firstMarkerAt = event.at;
|
|
22200
|
-
lastMarkerAt = event.at;
|
|
22201
|
-
switch (body.kind) {
|
|
22202
|
-
case "spec-open":
|
|
22203
|
-
if (!issued.has(body.specId)) {
|
|
22204
|
-
issued.add(body.specId);
|
|
22205
|
-
specOrder.push(body.specId);
|
|
22206
|
-
}
|
|
22207
|
-
break;
|
|
22208
|
-
case "window-open":
|
|
22209
|
-
tagToKey.set(body.tag, body.key);
|
|
22210
|
-
break;
|
|
22211
|
-
case "universe":
|
|
22212
|
-
universe = {
|
|
22213
|
-
include: body.include,
|
|
22214
|
-
files: body.files
|
|
22215
|
-
};
|
|
22216
|
-
break;
|
|
22217
|
-
case "run-link":
|
|
22218
|
-
hubRunId = body.hubRunId;
|
|
22219
|
-
break;
|
|
22220
|
-
case "browser": {
|
|
22221
|
-
const files = browserFiles.get(body.specId) ?? /* @__PURE__ */ new Set();
|
|
22222
|
-
for (const file of body.files) files.add(file);
|
|
22223
|
-
browserFiles.set(body.specId, files);
|
|
22224
|
-
break;
|
|
22225
|
-
}
|
|
22226
|
-
}
|
|
22227
|
-
}
|
|
22228
|
-
const resolver = new CoverageResolver(issued, tagToKey);
|
|
22229
|
-
let asOf = 0;
|
|
22230
|
-
let lastSeq = 0;
|
|
22231
|
-
let pushesDuringRun = 0;
|
|
22232
|
-
for (const event of events) {
|
|
22233
|
-
if (event.seq > lastSeq) lastSeq = event.seq;
|
|
22234
|
-
const body = event.body;
|
|
22235
|
-
if ("kind" in body) {
|
|
22236
|
-
if (body.runId !== runId) continue;
|
|
22237
|
-
asOf = event.at;
|
|
22238
|
-
if (body.kind === "window-open") resolver.apply({
|
|
22239
|
-
kind: "window-open",
|
|
22240
|
-
at: event.at,
|
|
22241
|
-
tag: body.tag,
|
|
22242
|
-
key: body.key,
|
|
22243
|
-
specId: body.specId
|
|
22244
|
-
});
|
|
22245
|
-
else if (body.kind === "window-close") resolver.apply({
|
|
22246
|
-
kind: "window-close",
|
|
22247
|
-
at: event.at,
|
|
22248
|
-
tag: body.tag
|
|
22249
|
-
});
|
|
22250
|
-
continue;
|
|
22251
|
-
}
|
|
22252
|
-
if (firstMarkerAt === void 0 || event.at < firstMarkerAt || event.at > lastMarkerAt + 3e4) {
|
|
22253
|
-
resolver.apply({
|
|
22254
|
-
kind: "push",
|
|
22255
|
-
at: event.at,
|
|
22256
|
-
push: {
|
|
22257
|
-
...body,
|
|
22258
|
-
specs: {},
|
|
22259
|
-
actors: []
|
|
22260
|
-
}
|
|
22261
|
-
});
|
|
22262
|
-
continue;
|
|
22263
|
-
}
|
|
22264
|
-
asOf = event.at;
|
|
22265
|
-
pushesDuringRun++;
|
|
22266
|
-
resolver.apply({
|
|
22267
|
-
kind: "push",
|
|
22268
|
-
at: event.at,
|
|
22269
|
-
push: body
|
|
22270
|
-
});
|
|
22271
|
-
}
|
|
22272
|
-
const specs = specOrder.map((specId) => {
|
|
22273
|
-
const actorEvents = {};
|
|
22274
|
-
for (const [key, count] of resolver.actorEventsFor(specId)) actorEvents[key] = count;
|
|
22275
|
-
return {
|
|
22276
|
-
specId,
|
|
22277
|
-
files: [...new Set([...resolver.filesFor(specId) ?? [], ...browserFiles.get(specId) ?? []])].sort(),
|
|
22278
|
-
actorEvents
|
|
22279
|
-
};
|
|
22280
|
-
});
|
|
22281
|
-
const outsideWindowEvents = {};
|
|
22282
|
-
for (const [key, count] of resolver.outsideWindowEvents()) outsideWindowEvents[key] = count;
|
|
22283
|
-
return {
|
|
22284
|
-
runId,
|
|
22285
|
-
...hubRunId !== void 0 ? { hubRunId } : {},
|
|
22286
|
-
asOf,
|
|
22287
|
-
lastSeq,
|
|
22288
|
-
...universe !== void 0 ? { universe } : {},
|
|
22289
|
-
specs,
|
|
22290
|
-
boot: [...resolver.boot()].sort(),
|
|
22291
|
-
health: {
|
|
22292
|
-
heardFromApplication: resolver.heardFromApplication(),
|
|
22293
|
-
pushesDuringRun,
|
|
22294
|
-
attributedSpecs: resolver.attributedSpecs(),
|
|
22295
|
-
rejectedPushes: resolver.rejectedPushes(),
|
|
22296
|
-
uninstrumentedFiles: resolver.uninstrumentedFiles(),
|
|
22297
|
-
uninstrumentedProcesses: resolver.uninstrumentedProcesses(),
|
|
22298
|
-
droppedPushes: resolver.droppedPushes(),
|
|
22299
|
-
unmappedActorEvents: resolver.unmappedActorEvents(),
|
|
22300
|
-
outsideWindowEvents,
|
|
22301
|
-
specsMeasured: specs.length
|
|
22302
|
-
}
|
|
22303
|
-
};
|
|
22304
|
-
}
|
|
22305
|
-
/**
|
|
22306
|
-
* Every run that opened a spec in this stream, most recently heard-from
|
|
22307
|
-
* first — recency by the arrival position of each run's latest spec-open,
|
|
22308
|
-
* the one order the hub's stamps establish.
|
|
22309
|
-
*/
|
|
22310
|
-
function listRunIds(events) {
|
|
22311
|
-
const lastOpenIndex = /* @__PURE__ */ new Map();
|
|
22312
|
-
events.forEach((event, index) => {
|
|
22313
|
-
const body = event.body;
|
|
22314
|
-
if ("kind" in body && body.kind === "spec-open") lastOpenIndex.set(body.runId, index);
|
|
22315
|
-
});
|
|
22316
|
-
return [...lastOpenIndex.entries()].sort((a, b) => b[1] - a[1]).map(([id]) => id);
|
|
22317
|
-
}
|
|
22318
22845
|
//#endregion
|
|
22319
22846
|
//#region src/hub/api/auth.ts
|
|
22320
22847
|
/**
|
package/dist/package.json
CHANGED
|
@@ -3,14 +3,14 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import { URL as URL$1 } from "node:url";
|
|
4
4
|
import * as http from "node:http";
|
|
5
5
|
import { Agent, ClientRequest, ClientRequestArgs, OutgoingHttpHeaders } from "node:http";
|
|
6
|
-
import { ZlibOptions } from "node:zlib";
|
|
7
6
|
import * as net from "node:net";
|
|
7
|
+
import { SecureContextOptions } from "node:tls";
|
|
8
|
+
import { ZlibOptions } from "node:zlib";
|
|
8
9
|
import { Http2SecureServer } from "node:http2";
|
|
9
10
|
import { EventEmitter } from "node:events";
|
|
10
11
|
import { Server as Server$1, ServerOptions as ServerOptions$1 } from "node:https";
|
|
11
12
|
import { Duplex, DuplexOptions, Stream } from "node:stream";
|
|
12
13
|
import esbuild from "esbuild";
|
|
13
|
-
import { SecureContextOptions } from "node:tls";
|
|
14
14
|
import DartSass from "sass";
|
|
15
15
|
import SassEmbedded from "sass-embedded";
|
|
16
16
|
import Less from "less";
|