@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.
- package/dist/change-rules.d.ts +45 -0
- package/dist/change-rules.js +179 -0
- package/dist/components/k3s-coverage.js +4 -4
- package/dist/components/supabase-coverage.js +12 -0
- package/dist/components/supabase.js +25 -12
- package/dist/coverage-bun.js +2 -1
- package/dist/coverage-next.d.ts +5 -0
- package/dist/coverage-next.js +27 -3
- package/dist/coverage-rust.d.ts +2 -0
- package/dist/coverage-rust.js +20 -0
- package/dist/coverage.d.ts +27 -3
- package/dist/coverage.js +68 -15
- package/dist/daemon.js +13 -10
- package/dist/harness/component-images.d.ts +5 -0
- package/dist/harness/component-images.js +15 -2
- package/dist/harness/coverage-target.d.ts +2 -0
- package/dist/harness/coverage-target.js +3 -2
- package/dist/harness/reach.d.ts +6 -1
- package/dist/harness/reach.js +8 -34
- package/dist/harness/shadow-builder.d.ts +2 -1
- package/dist/harness/shadow-builder.js +6 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/package.json +1 -1
- package/src/change-rules.fixtures.json +35 -0
- package/src/change-rules.test.ts +125 -0
- package/src/change-rules.ts +147 -0
- package/src/components/k3s-coverage.test.ts +32 -0
- package/src/components/k3s-coverage.ts +4 -4
- package/src/components/supabase-coverage.ts +12 -0
- package/src/components/supabase-dir.test.ts +26 -0
- package/src/components/supabase-functions-reach.test.ts +41 -1
- package/src/components/supabase-migrations.test.ts +1 -1
- package/src/components/supabase.ts +26 -12
- package/src/coverage-bun.ts +2 -1
- package/src/coverage-next.ts +29 -3
- package/src/coverage-rust.test.ts +34 -0
- package/src/coverage-rust.ts +22 -0
- package/src/coverage.test.ts +2 -1
- package/src/coverage.ts +76 -7
- package/src/daemon.ts +14 -9
- package/src/harness/component-images.test.ts +42 -0
- package/src/harness/component-images.ts +20 -3
- package/src/harness/coverage-target.ts +5 -2
- package/src/harness/reach.test.ts +40 -6
- package/src/harness/reach.ts +13 -3
- package/src/harness/shadow-builder.test.ts +8 -0
- package/src/harness/shadow-builder.ts +7 -2
- 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:
|
|
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:
|
|
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:
|
|
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:
|
|
145
|
-
expect(reachContains({ version:
|
|
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
|
+
});
|
package/src/harness/reach.ts
CHANGED
|
@@ -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:
|
|
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)
|
|
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
|
}
|