@specific.dev/spectest 0.82.0 → 0.85.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.
Files changed (46) hide show
  1. package/dist/components/index.d.ts +1 -0
  2. package/dist/components/index.js +1 -0
  3. package/dist/components/postgres-tls.d.ts +10 -0
  4. package/dist/components/postgres-tls.js +9 -0
  5. package/dist/components/postgres.d.ts +59 -1
  6. package/dist/components/postgres.js +5 -2
  7. package/dist/components/supabase-config.d.ts +5 -0
  8. package/dist/components/supabase-config.js +12 -4
  9. package/dist/components/supabase-webhooks.d.ts +1 -0
  10. package/dist/components/supabase-webhooks.js +262 -0
  11. package/dist/components/supabase.d.ts +36 -11
  12. package/dist/components/supabase.js +75 -18
  13. package/dist/components/temporal.d.ts +89 -0
  14. package/dist/components/temporal.js +185 -0
  15. package/dist/coverage.d.ts +78 -3
  16. package/dist/coverage.js +251 -11
  17. package/dist/daemon.js +71 -5
  18. package/dist/environment-ca.d.ts +8 -0
  19. package/dist/environment-ca.js +16 -0
  20. package/dist/harness/container-run.js +3 -0
  21. package/dist/harness/coverage.d.ts +19 -7
  22. package/dist/harness/coverage.js +52 -9
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/package.json +1 -1
  26. package/src/components/index.ts +3 -0
  27. package/src/components/postgres-tls.test.ts +23 -0
  28. package/src/components/postgres-tls.ts +11 -0
  29. package/src/components/postgres.ts +8 -2
  30. package/src/components/supabase-config.ts +11 -2
  31. package/src/components/supabase-db.test.ts +64 -0
  32. package/src/components/supabase-dir.test.ts +71 -0
  33. package/src/components/supabase-webhooks.ts +263 -0
  34. package/src/components/supabase.ts +108 -28
  35. package/src/components/temporal.test.ts +86 -0
  36. package/src/components/temporal.ts +188 -0
  37. package/src/coverage.test.ts +93 -13
  38. package/src/coverage.ts +295 -11
  39. package/src/daemon.ts +68 -3
  40. package/src/environment-ca.test.ts +44 -0
  41. package/src/environment-ca.ts +20 -0
  42. package/src/harness/container-run.test.ts +14 -2
  43. package/src/harness/container-run.ts +3 -0
  44. package/src/harness/coverage.test.ts +32 -1
  45. package/src/harness/coverage.ts +47 -9
  46. package/src/index.ts +1 -0
package/src/daemon.ts CHANGED
@@ -23,6 +23,7 @@ import { existsSync, promises as fs, readFileSync } from "node:fs";
23
23
  import net from "node:net";
24
24
  import path from "node:path";
25
25
  import { pathToFileURL } from "node:url";
26
+ import { ENVIRONMENT_CA_PATH as CA_PATH } from "./environment-ca.js";
26
27
 
27
28
  import {
28
29
  assert,
@@ -43,12 +44,14 @@ import {
43
44
  isEmptyReport,
44
45
  readCoverageDir,
45
46
  applyCoverageDelta,
47
+ walkProjectFiles,
46
48
  type CoverageBundleRef,
47
49
  type CoverageReport,
48
50
  type ServiceCoverageCapture,
49
51
  } from "./harness/coverage.js";
50
52
  import { configureBrowserCoverage } from "./browser-coverage.js";
51
53
  import {
54
+ INVENTORY_REPORT,
52
55
  NODE_COVERAGE_MARKERS_SUBDIR,
53
56
  NODE_COVERAGE_SCRIPTS_SUBDIR,
54
57
  applyCoverageAdapters,
@@ -57,6 +60,7 @@ import {
57
60
  validateCoverage,
58
61
  type CoverageAdapter,
59
62
  type CoverageCaptureContext,
63
+ type CoverageInventoryContext,
60
64
  } from "./coverage.js";
61
65
  import { serviceForHost as hostToService } from "./harness/browser-coverage.js";
62
66
  import { acquirePersistentBrowser, mobileKey } from "./browser.js";
@@ -300,7 +304,6 @@ const BUN_BIN = "/usr/local/bin/bun";
300
304
  // (see base.rs::BASE_SETUP_SH). Bind-mounted into every service
301
305
  // container so apps can verify HTTPS to the daemon's fakes, and
302
306
  // referenced when we layer it into each image's system trust store.
303
- const CA_PATH = process.env.SPECTEST_CA_PATH ?? "/etc/spectest/ca.crt";
304
307
  const CA_KEY_PATH = process.env.SPECTEST_CA_KEY_PATH ?? "/etc/spectest/ca.key";
305
308
  // Public roots ⧺ the spectest CA, materialized by `ensureCaBundle` and
306
309
  // bind-mounted into every container. `SSL_CERT_FILE`/`REQUESTS_CA_BUNDLE`/
@@ -4124,7 +4127,62 @@ async function coverageRoot(service: string): Promise<string> {
4124
4127
  return root;
4125
4128
  }
4126
4129
 
4127
- async function captureServiceCoverage(): Promise<ServiceCoverageCapture[]> {
4130
+ /** Budget for one service's inventory adapters, all of them. */
4131
+ const COVERAGE_INVENTORY_TIMEOUT_MS = 120_000;
4132
+
4133
+ /**
4134
+ * Run one service's `inventory` adapters and write the union as the
4135
+ * `inventory.units` report — for the bring-up capture only: the
4136
+ * inventory is a property of the environment, not of a case. The file
4137
+ * is removed after the read so no later capture ships it again. A
4138
+ * failure is the capture's error, like a failing `capture`.
4139
+ */
4140
+ async function writeCoverageInventory(svc: NamedService): Promise<string | undefined> {
4141
+ const adapters = coverageAdapters(svc.coverage).filter((a) => a.inventory);
4142
+ if (adapters.length === 0) return undefined;
4143
+ const ac = new AbortController();
4144
+ const timer = setTimeout(() => ac.abort(), COVERAGE_INVENTORY_TIMEOUT_MS);
4145
+ const reportDir = coverageHostDir(WORKSPACE, svc.name);
4146
+ const ctx: CoverageInventoryContext = {
4147
+ service: svc.name,
4148
+ signal: ac.signal,
4149
+ async exec(command: string) {
4150
+ const r = await docker(["exec", svc.name, "sh", "-c", command], COVERAGE_INVENTORY_TIMEOUT_MS);
4151
+ if (r.code !== 0) {
4152
+ const tail = capMiddle((r.stderr || r.stdout).trim(), 2000).value;
4153
+ throw new Error(`${JSON.stringify(command)} exited ${r.code}${tail ? `\n${tail}` : ""}`);
4154
+ }
4155
+ return { stdout: r.stdout, stderr: r.stderr };
4156
+ },
4157
+ projectFiles: () => walkProjectFiles(WORKSPACE),
4158
+ async readProjectFile(p: string) {
4159
+ try {
4160
+ return await fs.readFile(resolveProjectPath(p), "utf8");
4161
+ } catch {
4162
+ return null;
4163
+ }
4164
+ },
4165
+ };
4166
+ const units = new Set<string>();
4167
+ try {
4168
+ for (const a of adapters) {
4169
+ try {
4170
+ for (const u of await (a as Required<Pick<CoverageAdapter, "inventory">>).inventory(ctx)) units.add(u);
4171
+ } catch (err) {
4172
+ return `coverage adapter "${a.name}" on service "${svc.name}" inventory failed: ${err instanceof Error ? err.message : String(err)}`;
4173
+ }
4174
+ }
4175
+ } finally {
4176
+ clearTimeout(timer);
4177
+ }
4178
+ const file = path.join(reportDir, INVENTORY_REPORT);
4179
+ await fs.mkdir(reportDir, { recursive: true });
4180
+ await fs.writeFile(file, [...units].sort().join("\n") + (units.size ? "\n" : ""));
4181
+ console.log(`[coverage] ${svc.name}: inventory of ${units.size} unit(s)`);
4182
+ return undefined;
4183
+ }
4184
+
4185
+ async function captureServiceCoverage(baseline = false): Promise<ServiceCoverageCapture[]> {
4128
4186
  const l = loaded;
4129
4187
  if (!l) return [];
4130
4188
  const byName = new Map<string, NamedService>();
@@ -4135,7 +4193,14 @@ async function captureServiceCoverage(): Promise<ServiceCoverageCapture[]> {
4135
4193
  services.map(async (svc): Promise<ServiceCoverageCapture> => {
4136
4194
  const { error, deltaReports } = await runCoverageAdapters(svc);
4137
4195
  if (error) return { service: svc.name, reports: [], error };
4196
+ if (baseline) {
4197
+ const invError = await writeCoverageInventory(svc);
4198
+ if (invError) return { service: svc.name, reports: [], error: invError };
4199
+ }
4138
4200
  const read = await readCoverageDir(svc.name, coverageHostDir(WORKSPACE, svc.name));
4201
+ if (baseline) {
4202
+ await fs.rm(path.join(coverageHostDir(WORKSPACE, svc.name), INVENTORY_REPORT), { force: true }).catch(() => {});
4203
+ }
4139
4204
  const root = await coverageRoot(svc.name);
4140
4205
  if (root) read.root = root;
4141
4206
  // What ships is what ran during this capture, whatever the tool
@@ -6516,7 +6581,7 @@ export function harnessMethods(state: RouteState): MethodTable {
6516
6581
  // the control plane logs it.
6517
6582
  let coverage: CoverageBundleRef | undefined;
6518
6583
  try {
6519
- const captures = await captureServiceCoverage();
6584
+ const captures = await captureServiceCoverage(true);
6520
6585
  for (const c of captures) {
6521
6586
  // After bring-up the code that started the service has run, so a
6522
6587
  // report set that names no file at all is a misconfigured tool —
@@ -0,0 +1,44 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { runContainerArgs } from "./harness/container-run.js";
6
+
7
+ test("the CA is a plain PEM string when the environment definition is evaluated", () => {
8
+ const dir = mkdtempSync(join(tmpdir(), "spectest-ca-"));
9
+ const caPath = join(dir, "ca.crt");
10
+ const pem = "-----BEGIN CERTIFICATE-----\npublic certificate\n-----END CERTIFICATE-----\n";
11
+ try {
12
+ writeFileSync(caPath, pem);
13
+ const result = Bun.spawnSync([process.execPath, "-e", `
14
+ import { defineEnvironment, ca } from ${JSON.stringify(new URL("./index.ts", import.meta.url).href)};
15
+ const certificate = ca();
16
+ const env = defineEnvironment({ name: "test", services: { app: {
17
+ image: { type: "registry", reference: "test:latest" },
18
+ env: { CUSTOM_CA: certificate, LITERAL: "$(not a command)" },
19
+ files: [{ path: "/app/ca.pem", content: certificate }],
20
+ } } });
21
+ console.log(JSON.stringify(env.config.services.app));
22
+ `], { env: { ...process.env, SPECTEST_CA_PATH: caPath } });
23
+ expect(result.exitCode).toBe(0);
24
+ const svc = JSON.parse(result.stdout.toString());
25
+ expect(svc.env.CUSTOM_CA).toBe(pem);
26
+ expect(svc.files[0].content).toBe(pem);
27
+ const args = runContainerArgs({ svc: { name: "app", env: svc.env },
28
+ tag: "app", network: "test", hostCacheName: "spectest-host", caPath });
29
+ expect(args).toContain(`CUSTOM_CA=${pem}`);
30
+ expect(args).toContain("LITERAL=$(not a command)");
31
+ } finally { rmSync(dir, { recursive: true, force: true }); }
32
+ });
33
+
34
+ test("a missing environment CA fails at the call site with an actionable error", () => {
35
+ const dir = mkdtempSync(join(tmpdir(), "spectest-ca-"));
36
+ try {
37
+ const result = Bun.spawnSync([process.execPath, "-e", `
38
+ import { ca } from ${JSON.stringify(new URL("./index.ts", import.meta.url).href)};
39
+ ca();
40
+ `], { env: { ...process.env, SPECTEST_CA_PATH: join(dir, "missing.crt") } });
41
+ expect(result.exitCode).not.toBe(0);
42
+ expect(result.stderr.toString()).toContain("run this code inside a Spectest environment");
43
+ } finally { rmSync(dir, { recursive: true, force: true }); }
44
+ });
@@ -0,0 +1,20 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ /** @internal Shared by certificate issuance, trust mounts and the public reader. */
4
+ export const ENVIRONMENT_CA_PATH = process.env.SPECTEST_CA_PATH ?? "/etc/spectest/ca.crt";
5
+
6
+ /** Read the environment's public CA certificate as a PEM string.
7
+ * The base VM creates it before importing the environment definition, so it
8
+ * can be used directly in service env, file contents or runtime client options.
9
+ * Normal TLS trust is automatic; use this only when explicit CA data is needed.
10
+ */
11
+ export function ca(): string {
12
+ try {
13
+ return readFileSync(ENVIRONMENT_CA_PATH, "utf8");
14
+ } catch (cause) {
15
+ throw new Error(
16
+ `ca(): cannot read the environment CA at ${ENVIRONMENT_CA_PATH}; run this code inside a Spectest environment`,
17
+ { cause },
18
+ );
19
+ }
20
+ }
@@ -91,13 +91,13 @@ describe("certificate trust", () => {
91
91
  expect(envOf(args, "NODE_EXTRA_CA_CERTS")).toBe("/etc/spectest/ca.crt");
92
92
  });
93
93
 
94
- /** The load-bearing one. These three variables REPLACE the trust store,
94
+ /** The load-bearing one. These variables REPLACE the trust store,
95
95
  * so handing them the bare CA leaves the container trusting spectest and
96
96
  * no public root — Python requests and every AWS SDK then fail to verify
97
97
  * real HTTPS, in any image. They must get the combined bundle. */
98
98
  test("the replacing variables get the bundle, never the bare CA", () => {
99
99
  const args = runContainerArgs(base({ caBundle: "/etc/spectest/ca-bundle.crt" }));
100
- for (const name of ["SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "AWS_CA_BUNDLE"]) {
100
+ for (const name of ["SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "AWS_CA_BUNDLE", "PGSSLROOTCERT"]) {
101
101
  expect(envOf(args, name)).toBe("/etc/spectest/ca-bundle.crt");
102
102
  expect(envOf(args, name)).not.toBe("/etc/spectest/ca.crt");
103
103
  }
@@ -118,10 +118,22 @@ describe("certificate trust", () => {
118
118
  const args = runContainerArgs(base({ caBundle: null }));
119
119
  expect(envOf(args, "SSL_CERT_FILE")).toBeUndefined();
120
120
  expect(envOf(args, "AWS_CA_BUNDLE")).toBeUndefined();
121
+ expect(envOf(args, "PGSSLROOTCERT")).toBeUndefined();
121
122
  // The appending one is still safe to set on its own.
122
123
  expect(envOf(args, "NODE_EXTRA_CA_CERTS")).toBe("/etc/spectest/ca.crt");
123
124
  });
124
125
 
126
+ test("libpq gets trusted roots without forcing TLS, and may override them", () => {
127
+ const args = runContainerArgs(base({
128
+ caBundle: "/etc/spectest/ca-bundle.crt",
129
+ svc: { name: "web", env: { PGSSLROOTCERT: "/custom.pem" } },
130
+ }));
131
+ expect(envOf(args, "PGSSLMODE")).toBeUndefined();
132
+ expect(args.indexOf("PGSSLROOTCERT=/custom.pem")).toBeGreaterThan(
133
+ args.indexOf("PGSSLROOTCERT=/etc/spectest/ca-bundle.crt"),
134
+ );
135
+ });
136
+
125
137
  test("a bundle equal to the CA path is not mounted twice", () => {
126
138
  const args = runContainerArgs(base({ caBundle: "/etc/spectest/ca.crt" }));
127
139
  const mounts = args.filter((a) => a === "--volume=/etc/spectest/ca.crt:/etc/spectest/ca.crt:ro");
@@ -181,6 +181,9 @@ export function runContainerArgs(input: ContainerRunInput): string[] {
181
181
  args.push("-e", `SSL_CERT_FILE=${bundle}`);
182
182
  args.push("-e", `REQUESTS_CA_BUNDLE=${bundle}`);
183
183
  args.push("-e", `AWS_CA_BUNDLE=${bundle}`);
184
+ // libpq does not use the system trust store by default. Supply the
185
+ // same combined roots to psql/psycopg without changing their SSL mode.
186
+ args.push("-e", `PGSSLROOTCERT=${bundle}`);
184
187
  // The system store itself, for clients that read no variable at all.
185
188
  // Only a real bundle: the caller falls back to the bare CA when the
186
189
  // guest's roots are unreadable, and the bare CA over the system store
@@ -101,7 +101,7 @@ describe("readCoverageDir", () => {
101
101
  await fs.writeFile(path.join(dir, "coverage.json"), "{}");
102
102
  const r = await readCoverageDir("web", dir);
103
103
  expect(r.reports.length).toBe(1);
104
- expect(r.error).toContain("none is an lcov report or a V8 coverage JSON document");
104
+ expect(r.error).toContain("none is an lcov report, a V8 coverage JSON document, or a `.units` list");
105
105
  });
106
106
 
107
107
  test("listReportFiles skips dotfiles", async () => {
@@ -138,6 +138,8 @@ import {
138
138
  renderLcovCounts,
139
139
  resetCoverageDeltaMemory,
140
140
  subtractLcovCounts,
141
+ isUnitsReport,
142
+ walkProjectFiles,
141
143
  } from "./coverage.js";
142
144
 
143
145
  describe("coverage delta", () => {
@@ -216,3 +218,32 @@ describe("coverage delta", () => {
216
218
  expect(err.error).toBe("x");
217
219
  });
218
220
  });
221
+
222
+ describe("units reports", () => {
223
+ test("isUnitsReport: one scheme-prefixed id per line, blanks allowed, an empty file is a report", () => {
224
+ expect(isUnitsReport("npm:picocolors@1.1.1\nmanifest:packages/cli/package.json#3f9a1b2c\n")).toBe(true);
225
+ expect(isUnitsReport("")).toBe(true);
226
+ expect(isUnitsReport("\n\n")).toBe(true);
227
+ expect(isUnitsReport("picocolors\n")).toBe(false);
228
+ expect(isUnitsReport("npm:a b\n")).toBe(false);
229
+ });
230
+ test("readCoverageDir accepts a .units file beside lcov and rejects a malformed one", async () => {
231
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-units-"));
232
+ await fs.writeFile(path.join(dir, "node.lcov"), "TN:\nSF:/app/a.js\nDA:1,1\nend_of_record\n");
233
+ await fs.writeFile(path.join(dir, "inventory.units"), "npm:a@1.0.0\n");
234
+ const ok = await readCoverageDir("api", dir);
235
+ expect(ok.error).toBeUndefined();
236
+ expect(ok.reports.map((r) => r.name)).toEqual(["inventory.units", "node.lcov"]);
237
+ await fs.writeFile(path.join(dir, "bad.units"), "no scheme here\n");
238
+ const bad = await readCoverageDir("api", dir);
239
+ expect(bad.error).toContain("bad.units");
240
+ });
241
+ test("walkProjectFiles lists project files and skips node_modules, .git and build output", async () => {
242
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "spectest-walk-"));
243
+ for (const f of ["package.json", "packages/cli/package.json", "packages/cli/src/a.ts", "node_modules/x/package.json", ".git/HEAD", "packages/cli/dist/cli.js"]) {
244
+ await fs.mkdir(path.join(root, path.dirname(f)), { recursive: true });
245
+ await fs.writeFile(path.join(root, f), "x");
246
+ }
247
+ expect(await walkProjectFiles(root)).toEqual(["package.json", "packages/cli/package.json", "packages/cli/src/a.ts"]);
248
+ });
249
+ });
@@ -108,9 +108,10 @@ export function isLcovShaped(text: string): boolean {
108
108
 
109
109
  /**
110
110
  * True when `text` looks like a V8 coverage JSON document — what
111
- * `NODE_V8_COVERAGE` / `v8.takeCoverage()` write: an object with a
112
- * `result` array of scripts. A cheap shape check, not a parse (a report
113
- * can be megabytes); the control plane parses for real.
111
+ * `NODE_V8_COVERAGE` / `v8.takeCoverage()` write, and the shape the
112
+ * `node()` hook's dumps keep: an object with a `result` array of
113
+ * scripts. A cheap shape check, not a parse (a report can be
114
+ * megabytes); the control plane parses for real.
114
115
  */
115
116
  export function isV8CoverageJson(text: string): boolean {
116
117
  return /^\s*\{/.test(text) && /"result"\s*:\s*\[/.test(text);
@@ -123,19 +124,56 @@ export function isSpectestScriptRecord(text: string): boolean {
123
124
  return /^\s*\{\s*"spectestScript"\s*:/.test(text);
124
125
  }
125
126
 
126
- /** True when `text` is a report spectest stores: lcov, V8 JSON, or a
127
- * node() script record. */
128
- export function isWellFormedReport(text: string): boolean {
127
+ /** True when `text` is a `.units` report: one unit id per non-blank
128
+ * line, `<scheme>:<rest>` (`npm:picocolors@1.1.1`,
129
+ * `manifest:packages/cli/package.json#3f9a…`). An empty file is a
130
+ * report that names no unit. What a unit means is the adapter's; here
131
+ * and on the control plane an id is an opaque string. */
132
+ export function isUnitsReport(text: string): boolean {
133
+ return text.split(/\r?\n/).every((l) => l.trim().length === 0 || /^[a-z][a-z0-9+.-]*:\S+$/.test(l.trim()));
134
+ }
135
+
136
+ /** True when `text` is a report spectest stores: lcov, V8 JSON, a node()
137
+ * script record, or (by name) a `.units` list. */
138
+ export function isWellFormedReport(text: string, name = ""): boolean {
139
+ if (name.endsWith(".units")) return isUnitsReport(text);
129
140
  return isLcovShaped(text) || isV8CoverageJson(text) || isSpectestScriptRecord(text);
130
141
  }
131
142
 
132
143
  /** True when a well-formed report names no source file at all (an lcov
133
- * with no `SF:`, a V8 document with an empty `result`). */
144
+ * with no `SF:`, a V8 document with an empty `result`, a units list
145
+ * with no unit). */
134
146
  export function isEmptyReport(text: string): boolean {
135
147
  if (isV8CoverageJson(text)) return /"result"\s*:\s*\[\s*\]/.test(text);
148
+ if (isSpectestScriptRecord(text)) return false;
149
+ if (isUnitsReport(text) && !isLcovShaped(text)) return text.trim().length === 0;
136
150
  return !hasLcovSourceFile(text);
137
151
  }
138
152
 
153
+ /** Every regular file under `root`, recursively, as `/`-separated paths
154
+ * relative to it, with the directories no project file lives in
155
+ * skipped. The harness's project file list for an adapter's inventory. */
156
+ export async function walkProjectFiles(root: string): Promise<string[]> {
157
+ const skip = new Set(["node_modules", ".git", ".spectest", "target", "__pycache__", ".venv", "dist", "build", ".next"]);
158
+ const out: string[] = [];
159
+ async function walk(d: string, rel: string): Promise<void> {
160
+ let entries;
161
+ try {
162
+ entries = await fs.readdir(d, { withFileTypes: true });
163
+ } catch {
164
+ return;
165
+ }
166
+ for (const e of entries) {
167
+ if (skip.has(e.name)) continue;
168
+ const r = rel ? `${rel}/${e.name}` : e.name;
169
+ if (e.isDirectory()) await walk(path.join(d, e.name), r);
170
+ else if (e.isFile()) out.push(r);
171
+ }
172
+ }
173
+ await walk(root, "");
174
+ return out.sort();
175
+ }
176
+
139
177
  /** Every regular file under `dir`, recursively, as paths relative to it.
140
178
  * Hidden files are skipped (a tool's own bookkeeping — `.seq`, `.lock`,
141
179
  * a tmp file mid-write — is not a report). */
@@ -197,7 +235,7 @@ export async function readCoverageDir(
197
235
  error: `service "${service}" has more than ${COVERAGE_MAX_BYTES_PER_SERVICE >> 20} MiB of reports in ${COVERAGE_CONTAINER_DIR}/ — that is a runaway, not coverage`,
198
236
  };
199
237
  }
200
- if (isWellFormedReport(content)) anyReport = true;
238
+ if (isWellFormedReport(content, name)) anyReport = true;
201
239
  else malformed.push(name);
202
240
  reports.push({ name, content });
203
241
  }
@@ -207,7 +245,7 @@ export async function readCoverageDir(
207
245
  reports,
208
246
  error: `service "${service}" wrote ${reports.length} file(s) to ${COVERAGE_CONTAINER_DIR}/ but ${
209
247
  anyReport ? `${malformed.slice(0, 5).join(", ")} ${malformed.length === 1 ? "is" : "are"} neither` : "none is"
210
- } an lcov report or a V8 coverage JSON document — only those two formats are read`,
248
+ } an lcov report, a V8 coverage JSON document, or a \`.units\` list — only those are read`,
211
249
  };
212
250
  }
213
251
  return { service, reports };
package/src/index.ts CHANGED
@@ -159,6 +159,7 @@ export {
159
159
  } from "./coverage.js";
160
160
  import { applyCoverageAdapters, validateCoverage, type ServiceCoverage } from "./coverage.js";
161
161
  import { resolveExistingProjectPath } from "./project-files.js";
162
+ export { ca } from "./environment-ca.js";
162
163
 
163
164
  // Low-level ingress primitives + the framework lowering that the friendly
164
165
  // `tls` / `hostnames` fields and `defineFake(...)` are built on. See