@specific.dev/spectest 0.131.21 → 0.132.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 (49) hide show
  1. package/dist/change-rules.d.ts +45 -0
  2. package/dist/change-rules.js +179 -0
  3. package/dist/components/k3s-coverage.js +4 -4
  4. package/dist/components/supabase-coverage.js +12 -0
  5. package/dist/components/supabase.js +25 -12
  6. package/dist/coverage-bun.js +2 -1
  7. package/dist/coverage-next.d.ts +5 -0
  8. package/dist/coverage-next.js +27 -3
  9. package/dist/coverage-rust.d.ts +2 -0
  10. package/dist/coverage-rust.js +20 -0
  11. package/dist/coverage.d.ts +27 -3
  12. package/dist/coverage.js +68 -15
  13. package/dist/daemon.js +13 -10
  14. package/dist/harness/component-images.d.ts +5 -0
  15. package/dist/harness/component-images.js +15 -2
  16. package/dist/harness/coverage-target.d.ts +2 -0
  17. package/dist/harness/coverage-target.js +3 -2
  18. package/dist/harness/reach.d.ts +6 -1
  19. package/dist/harness/reach.js +8 -34
  20. package/dist/harness/shadow-builder.d.ts +2 -1
  21. package/dist/harness/shadow-builder.js +6 -2
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.js +5 -0
  24. package/package.json +1 -1
  25. package/src/change-rules.fixtures.json +35 -0
  26. package/src/change-rules.test.ts +125 -0
  27. package/src/change-rules.ts +147 -0
  28. package/src/components/k3s-coverage.test.ts +32 -0
  29. package/src/components/k3s-coverage.ts +4 -4
  30. package/src/components/supabase-coverage.ts +12 -0
  31. package/src/components/supabase-dir.test.ts +26 -0
  32. package/src/components/supabase-functions-reach.test.ts +41 -1
  33. package/src/components/supabase-migrations.test.ts +1 -1
  34. package/src/components/supabase.ts +26 -12
  35. package/src/coverage-bun.ts +2 -1
  36. package/src/coverage-next.ts +29 -3
  37. package/src/coverage-rust.test.ts +34 -0
  38. package/src/coverage-rust.ts +22 -0
  39. package/src/coverage.test.ts +2 -1
  40. package/src/coverage.ts +76 -7
  41. package/src/daemon.ts +14 -9
  42. package/src/harness/component-images.test.ts +42 -0
  43. package/src/harness/component-images.ts +20 -3
  44. package/src/harness/coverage-target.ts +5 -2
  45. package/src/harness/reach.test.ts +40 -6
  46. package/src/harness/reach.ts +13 -3
  47. package/src/harness/shadow-builder.test.ts +8 -0
  48. package/src/harness/shadow-builder.ts +7 -2
  49. package/src/index.ts +11 -0
@@ -1,5 +1,6 @@
1
+ import { evaluateChangePolicy } from "../change-rules.js";
1
2
  import { describe, expect, test } from "bun:test";
2
- import { compactReach, compileDockerignore, isExcluded, projectReach, reachContains, renderReadsLcov, sourceOf } from "./reach.js";
3
+ import { compactReach, environmentReachSource, compileDockerignore, isExcluded, projectReach, reachContains, renderReadsLcov, sourceOf } from "./reach.js";
3
4
 
4
5
  describe("dockerignore semantics", () => {
5
6
  const ex = (text: string, rel: string) => isExcluded(compileDockerignore(text), rel);
@@ -87,7 +88,7 @@ describe("reach rules", () => {
87
88
  { service: "db", volumeSources: ["data", "/var/run/docker.sock"] },
88
89
  ];
89
90
  const reach = projectReach(files, sources);
90
- const rules = { version: 1 as const, sources, prefixes: compactReach(files, reach) };
91
+ const rules = { version: 2 as const, sources, prefixes: compactReach(files, reach) };
91
92
 
92
93
  test("compactReach folds a wholly reached directory into one prefix and keeps the odd file", () => {
93
94
  // packages/api holds README.md, which the ignore drops, so the
@@ -125,7 +126,7 @@ describe("reach rules", () => {
125
126
  expect(sourceOf([sources[1]!], "var/run/docker.sock")).toBeUndefined();
126
127
  // The Dockerfile named by path, and the one at the context root.
127
128
  expect(sourceOf(sources, "packages/api/Dockerfile")?.service).toBe("api");
128
- expect(reachContains({ version: 1, sources: [{ context: "packages/x", ignore: "**" }], prefixes: [] }, "packages/x/Dockerfile")).toBe(true);
129
+ expect(reachContains({ version: 2, sources: [{ context: "packages/x", ignore: "**" }], prefixes: [] }, "packages/x/Dockerfile")).toBe(true);
129
130
  });
130
131
 
131
132
  test("a service's declared inputs count: a lambda's source directory, a config file", () => {
@@ -134,14 +135,47 @@ describe("reach rules", () => {
134
135
  "lambda/add/index.js",
135
136
  "supabase/config.toml",
136
137
  ]);
137
- const rules = { version: 1 as const, sources: srcs, prefixes: [] };
138
+ const rules = { version: 2 as const, sources: srcs, prefixes: [] };
138
139
  expect(reachContains(rules, "lambda/add/new.js")).toBe(true);
139
140
  expect(sourceOf(srcs, "lambda/add/new.js")?.service).toBe("aws");
140
141
  expect(reachContains(rules, "lambda/other/index.js")).toBe(false);
141
142
  });
142
143
 
144
+ test("repository-root inputs and volumes reach existing and newly added files", () => {
145
+ for (const field of ["inputs", "volumeSources"] as const) {
146
+ for (const root of [".", "./"]) {
147
+ const source = { service: "root-consumer", [field]: [root] };
148
+ expect(projectReach(files, [source])).toEqual([...files].sort());
149
+ const rules = { version: 2 as const, sources: [source], prefixes: [] };
150
+ for (const path of ["new.ts", "new/directory/source.ts"]) {
151
+ expect(reachContains(rules, path)).toBe(true);
152
+ expect(sourceOf([source], path)?.service).toBe("root-consumer");
153
+ }
154
+ }
155
+ const absolute = { [field]: ["/", "/var/run/docker.sock"] };
156
+ expect(projectReach(files, [absolute])).toEqual([]);
157
+ expect(sourceOf([absolute], "var/run/docker.sock")).toBeUndefined();
158
+ expect(reachContains({ version: 2, sources: [absolute], prefixes: [] }, "new.ts")).toBe(false);
159
+ }
160
+ });
161
+
143
162
  test("the whole project as one prefix", () => {
144
- expect(reachContains({ version: 1, sources: [], prefixes: ["."] }, "anything/at/all")).toBe(true);
145
- expect(reachContains({ version: 1, sources: [], prefixes: [] }, "anything/at/all")).toBe(false);
163
+ expect(reachContains({ version: 2, sources: [], prefixes: ["."] }, "anything/at/all")).toBe(true);
164
+ expect(reachContains({ version: 2, sources: [], prefixes: [] }, "anything/at/all")).toBe(false);
146
165
  });
147
166
  });
167
+
168
+
169
+ test("environment policy owns Spectest Markdown outside narrow container contexts", () => {
170
+ const environment = environmentReachSource("__environment__");
171
+ const sources = [environment, {service:"app",context:"src"}];
172
+ const rules = {version:2 as const,sources,prefixes:[]};
173
+ for (const file of ["spectest/README.md", "spectest/tests/notes.md"]) {
174
+ expect(reachContains(rules,file)).toBe(true);
175
+ expect(sourceOf(sources,file)?.service).toBe("__environment__");
176
+ expect(evaluateChangePolicy(environment.changePolicy!,file)).toBe("ignore");
177
+ }
178
+ expect(evaluateChangePolicy(environment.changePolicy!,"spectest/index.ts")).toBe("full");
179
+ expect(evaluateChangePolicy(environment.changePolicy!,"spectest/tests/helper.ts")).toBe("coverage");
180
+ expect(reachContains(rules,"unrelated/file.ts")).toBe(false);
181
+ });
@@ -1,3 +1,5 @@
1
+ import { resolveChangePolicy } from "../change-rules.js";
2
+
1
3
  // What a change to a project file can reach — the pure part.
2
4
  //
3
5
  // A project file affects a test only through one of three doors: it is in
@@ -114,6 +116,8 @@ export function isExcluded(patterns: Pattern[], rel: string): boolean {
114
116
  export interface ReachSource {
115
117
  /** The service the source belongs to (for the shadow page's verdicts). */
116
118
  service?: string;
119
+ /** Frozen source-change policy, including adapter defaults and service overrides. */
120
+ changePolicy?: import("../change-rules.js").ChangePolicy;
117
121
  /** Build context directory, project-relative (`.` for the root); absent
118
122
  * for a registry image. */
119
123
  context?: string;
@@ -138,6 +142,12 @@ function normRel(p: string): string {
138
142
  return s === "." ? "" : s;
139
143
  }
140
144
 
145
+ /** Environment/test sources remain owned even when every container has a narrow
146
+ * build context. Their common policy ignores nearby documentation. */
147
+ export function environmentReachSource(service: string): ReachSource {
148
+ return {service,inputs:["spectest"],changePolicy:resolveChangePolicy({})};
149
+ }
150
+
141
151
  /**
142
152
  * The project's reach: every project file (`files`, project-relative)
143
153
  * some service's build context includes or some volume mounts, plus the
@@ -165,7 +175,7 @@ export function projectReach(files: readonly string[], sources: readonly ReachSo
165
175
  if (v.startsWith("/")) continue;
166
176
  const src = normRel(v);
167
177
  for (const f of files) {
168
- if (f === src || f.startsWith(`${src}/`)) out.add(f);
178
+ if (src === "" || f === src || f.startsWith(`${src}/`)) out.add(f);
169
179
  }
170
180
  }
171
181
  }
@@ -174,7 +184,7 @@ export function projectReach(files: readonly string[], sources: readonly ReachSo
174
184
 
175
185
  /** The `reach.json` report. Mirrors `reach::ReachRules` on the server. */
176
186
  export interface ReachRules {
177
- version: 1;
187
+ version: 2;
178
188
  /** Every service's contribution, as rules. */
179
189
  sources: ReachSource[];
180
190
  /** The current file reach, compacted: `dir/` for a directory every
@@ -259,7 +269,7 @@ export function sourceOf(sources: readonly ReachSource[], path: string): ReachSo
259
269
  for (const v of [...(s.volumeSources ?? []), ...(s.inputs ?? [])]) {
260
270
  if (v.startsWith("/")) continue;
261
271
  const src = normRel(v);
262
- if (p === src || p.startsWith(`${src}/`)) return s;
272
+ if (src === "" || p === src || p.startsWith(`${src}/`)) return s;
263
273
  }
264
274
  }
265
275
  return undefined;
@@ -30,3 +30,11 @@ test("ordinary SDK execution never needs remote builder configuration", async ()
30
30
  else process.env.SPECTEST_SHADOW_BUILD = previous;
31
31
  }
32
32
  });
33
+
34
+ // Switching formats must be an explicit controller-selected shadow artifact.
35
+ test("Nydus export requires the explicit shadow format and rejects unknown modes", () => {
36
+ const nydus = validateShadowBuilder({ ...config, imageFormat: "nydus-v6" });
37
+ expect(shadowOutput(shadowImage(nydus, "api"), nydus)).toContain("compression=nydus,force-compression=true");
38
+ expect(() => validateShadowBuilder({ ...config, imageFormat: "unknown" })).toThrow();
39
+ expect(shadowOutput(shadowImage(config, "api"), config)).toContain("compression=zstd");
40
+ });
@@ -7,12 +7,14 @@ export interface ShadowBuilder {
7
7
  builder: string;
8
8
  registry: string;
9
9
  experiment: string;
10
+ imageFormat?: "nydus-v6";
10
11
  }
11
12
  export function validateShadowBuilder(value: unknown): ShadowBuilder {
12
13
  const c = value as ShadowBuilder;
13
14
  if (!c || c.version !== 1 || !/^shadow-[a-z0-9-]+$/.test(c.builder)
14
15
  || !/^[a-z0-9.-]+(?::[0-9]+)?$/.test(c.registry)
15
- || !/^[a-z0-9-]{1,80}$/.test(c.experiment)) throw new Error("invalid shadow builder configuration");
16
+ || !/^[a-z0-9-]{1,80}$/.test(c.experiment)
17
+ || (c.imageFormat !== undefined && c.imageFormat !== "nydus-v6")) throw new Error("invalid shadow builder configuration");
16
18
  return c;
17
19
  }
18
20
  export async function shadowBuilder(): Promise<ShadowBuilder | undefined> {
@@ -26,7 +28,10 @@ export function shadowImage(c: ShadowBuilder, service: string): string {
26
28
  }
27
29
  // Convert inherited gzip layers too; the persistent builder caches the conversion.
28
30
  // Level 1 favors export latency over a small additional size reduction.
29
- export function shadowOutput(reference: string): string {
31
+ export function shadowOutput(reference: string, config?: ShadowBuilder): string {
32
+ if (config?.imageFormat === "nydus-v6") {
33
+ return `type=image,name=${reference},push=true,oci-mediatypes=true,compression=nydus,force-compression=true`;
34
+ }
30
35
  return `type=image,name=${reference},push=true,oci-mediatypes=true,compression=zstd,compression-level=1,force-compression=true`;
31
36
  }
32
37
  export function imageDigest(metadata: unknown): string {
package/src/index.ts CHANGED
@@ -176,6 +176,8 @@ export {
176
176
  type ServiceCoverage,
177
177
  } from "./coverage.js";
178
178
  import { applyCoverageAdapters, coverageAdapters, validateCoverage, type ServiceCoverage } from "./coverage.js";
179
+ export { type ChangeRule, type ChangeAction, COMMON_CHANGE_RULES } from "./change-rules.js";
180
+ import type { ChangeRule } from "./change-rules.js";
179
181
  import { resolveExistingProjectPath } from "./project-files.js";
180
182
  export { ca } from "./environment-ca.js";
181
183
 
@@ -365,6 +367,9 @@ export interface ServiceConfig {
365
367
  * `spectest docs /services/coverage`.
366
368
  */
367
369
  coverage?: ServiceCoverage;
370
+ /** Ordered source-change overrides, independent of coverage collection.
371
+ * Last matching rule wins. false disables inherited defaults. */
372
+ changeRules?: readonly ChangeRule[] | false;
368
373
  /**
369
374
  * Files seeded into the container's filesystem **before it starts**.
370
375
  * Each entry's `content` is written to a VM-host staging path and
@@ -1138,6 +1143,9 @@ function expandServiceGroups(input: InputServicesMap): ServicesMap {
1138
1143
  // Rewrite relative dependsOn to final keys; a spread keeps function
1139
1144
  // fields (helpers/setup) and the enumerable-symbol ingress decls.
1140
1145
  const final: ServiceConfig = { ...svc, ...(part === entry.primary ? primaryOverrides : {}) };
1146
+ // A group-level selection override applies to every generated owner.
1147
+ if (primaryOverrides.changeRules !== undefined) final.changeRules = primaryOverrides.changeRules === false
1148
+ ? false : [...(svc.changeRules || []), ...primaryOverrides.changeRules];
1141
1149
  // Component-owned nested containers keep their collection when callers
1142
1150
  // add ordinary service coverage (for example browser coverage on k3s).
1143
1151
  const collectors = coverageAdapters(svc.coverage).filter(adapter => adapter.collect);
@@ -1321,6 +1329,9 @@ export function normalizeServiceImage(image: ServiceImage): NormalizedServiceIma
1321
1329
 
1322
1330
  /** @internal Serializes build-only work with the owning component. */
1323
1331
  export interface ComponentImageBuild {
1332
+ /** Coverage and selection policy of the built application image. */
1333
+ coverage?: ServiceCoverage;
1334
+ changeRules?: readonly ChangeRule[] | false;
1324
1335
  reference: string;
1325
1336
  image: BuildImage;
1326
1337
  }