@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.
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 +72 -0
  16. package/dist/coverage.js +226 -6
  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 +15 -4
  22. package/dist/harness/coverage.js +48 -6
  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 +72 -2
  38. package/src/coverage.ts +270 -6
  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 +43 -6
  46. 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 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
+ });
@@ -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 spectest stores: lcov, V8 JSON, or a
128
- * node() script record. */
129
- 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);
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 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`,
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