@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.
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/postgres-tls.d.ts +10 -0
- package/dist/components/postgres-tls.js +9 -0
- package/dist/components/postgres.d.ts +59 -1
- package/dist/components/postgres.js +5 -2
- package/dist/components/supabase-config.d.ts +5 -0
- package/dist/components/supabase-config.js +12 -4
- package/dist/components/supabase-webhooks.d.ts +1 -0
- package/dist/components/supabase-webhooks.js +262 -0
- package/dist/components/supabase.d.ts +36 -11
- package/dist/components/supabase.js +75 -18
- package/dist/components/temporal.d.ts +89 -0
- package/dist/components/temporal.js +185 -0
- package/dist/coverage.d.ts +78 -3
- package/dist/coverage.js +251 -11
- package/dist/daemon.js +71 -5
- package/dist/environment-ca.d.ts +8 -0
- package/dist/environment-ca.js +16 -0
- package/dist/harness/container-run.js +3 -0
- package/dist/harness/coverage.d.ts +19 -7
- package/dist/harness/coverage.js +52 -9
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
- package/src/components/index.ts +3 -0
- package/src/components/postgres-tls.test.ts +23 -0
- package/src/components/postgres-tls.ts +11 -0
- package/src/components/postgres.ts +8 -2
- package/src/components/supabase-config.ts +11 -2
- package/src/components/supabase-db.test.ts +64 -0
- package/src/components/supabase-dir.test.ts +71 -0
- package/src/components/supabase-webhooks.ts +263 -0
- package/src/components/supabase.ts +108 -28
- package/src/components/temporal.test.ts +86 -0
- package/src/components/temporal.ts +188 -0
- package/src/coverage.test.ts +93 -13
- package/src/coverage.ts +295 -11
- package/src/daemon.ts +68 -3
- package/src/environment-ca.test.ts +44 -0
- package/src/environment-ca.ts +20 -0
- package/src/harness/container-run.test.ts +14 -2
- package/src/harness/container-run.ts +3 -0
- package/src/harness/coverage.test.ts +32 -1
- package/src/harness/coverage.ts +47 -9
- 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
|
-
|
|
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
|
|
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
|
|
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
|
+
});
|
package/src/harness/coverage.ts
CHANGED
|
@@ -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
|
|
112
|
-
* `
|
|
113
|
-
*
|
|
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
|
|
127
|
-
*
|
|
128
|
-
|
|
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
|
|
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
|