@specific.dev/spectest 0.83.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 +72 -0
- package/dist/coverage.js +226 -6
- 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 +15 -4
- package/dist/harness/coverage.js +48 -6
- 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 +72 -2
- package/src/coverage.ts +270 -6
- 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 +43 -6
- package/src/index.ts +1 -0
|
@@ -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
|
@@ -124,19 +124,56 @@ export function isSpectestScriptRecord(text: string): boolean {
|
|
|
124
124
|
return /^\s*\{\s*"spectestScript"\s*:/.test(text);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
/** True when `text` is a report
|
|
128
|
-
*
|
|
129
|
-
|
|
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);
|
|
130
140
|
return isLcovShaped(text) || isV8CoverageJson(text) || isSpectestScriptRecord(text);
|
|
131
141
|
}
|
|
132
142
|
|
|
133
143
|
/** True when a well-formed report names no source file at all (an lcov
|
|
134
|
-
* 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). */
|
|
135
146
|
export function isEmptyReport(text: string): boolean {
|
|
136
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;
|
|
137
150
|
return !hasLcovSourceFile(text);
|
|
138
151
|
}
|
|
139
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
|
+
|
|
140
177
|
/** Every regular file under `dir`, recursively, as paths relative to it.
|
|
141
178
|
* Hidden files are skipped (a tool's own bookkeeping — `.seq`, `.lock`,
|
|
142
179
|
* a tmp file mid-write — is not a report). */
|
|
@@ -198,7 +235,7 @@ export async function readCoverageDir(
|
|
|
198
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`,
|
|
199
236
|
};
|
|
200
237
|
}
|
|
201
|
-
if (isWellFormedReport(content)) anyReport = true;
|
|
238
|
+
if (isWellFormedReport(content, name)) anyReport = true;
|
|
202
239
|
else malformed.push(name);
|
|
203
240
|
reports.push({ name, content });
|
|
204
241
|
}
|
|
@@ -208,7 +245,7 @@ export async function readCoverageDir(
|
|
|
208
245
|
reports,
|
|
209
246
|
error: `service "${service}" wrote ${reports.length} file(s) to ${COVERAGE_CONTAINER_DIR}/ but ${
|
|
210
247
|
anyReport ? `${malformed.slice(0, 5).join(", ")} ${malformed.length === 1 ? "is" : "are"} neither` : "none is"
|
|
211
|
-
} an lcov report
|
|
248
|
+
} an lcov report, a V8 coverage JSON document, or a \`.units\` list — only those are read`,
|
|
212
249
|
};
|
|
213
250
|
}
|
|
214
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
|