@specific.dev/spectest 0.131.1 → 0.131.2

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.
@@ -42,9 +42,11 @@ export declare function android(options: AndroidOptions): import("../index.js").
42
42
  type: "registry";
43
43
  reference: string;
44
44
  content?: undefined;
45
+ exclude?: undefined;
45
46
  } | {
46
47
  type: "dockerfile";
47
48
  content: string;
49
+ exclude: string[];
48
50
  reference?: undefined;
49
51
  };
50
52
  command: string;
@@ -70,9 +70,10 @@ export function android(options) {
70
70
  },
71
71
  },
72
72
  tools: {
73
+ // Tools are SDK-owned; the APK is a separate input below.
73
74
  image: options.toolsImage
74
75
  ? { type: "registry", reference: options.toolsImage }
75
- : { type: "dockerfile", content: ANDROID_TOOLS_DOCKERFILE },
76
+ : { type: "dockerfile", content: ANDROID_TOOLS_DOCKERFILE, exclude: ["**"] },
76
77
  command: "node /spectest-android.mjs",
77
78
  files: [
78
79
  {
@@ -28,6 +28,7 @@ export declare function apple(opts?: AppleOptions): {
28
28
  image: {
29
29
  type: "dockerfile";
30
30
  content: string;
31
+ exclude: string[];
31
32
  };
32
33
  command: string;
33
34
  files: {
@@ -89,6 +89,7 @@ export declare function emulatorService(spec: ProviderSpec): {
89
89
  image: {
90
90
  type: "dockerfile";
91
91
  content: string;
92
+ exclude: string[];
92
93
  };
93
94
  command: string;
94
95
  files: {
@@ -98,7 +98,8 @@ RUN echo '{"name":"spectest-provider","private":true}' > package.json \\
98
98
  * entry, and a provider can claim four names. */
99
99
  export function emulatorService(spec) {
100
100
  const service = {
101
- image: { type: "dockerfile", content: DOCKERFILE },
101
+ // The image installs packages; its entry script is injected via files.
102
+ image: { type: "dockerfile", content: DOCKERFILE, exclude: ["**"] },
102
103
  command: "bun /srv/auth.mjs",
103
104
  files: [{ path: "/srv/auth.mjs", content: ENTRY_SCRIPT }],
104
105
  env: { SPECTEST_AUTH_CONFIG: JSON.stringify({ port: PORT, providers: [spec] }) },
@@ -33,6 +33,7 @@ export declare function github(opts?: GithubOptions): {
33
33
  image: {
34
34
  type: "dockerfile";
35
35
  content: string;
36
+ exclude: string[];
36
37
  };
37
38
  command: string;
38
39
  files: {
@@ -38,6 +38,7 @@ export declare function google(opts?: GoogleOptions): {
38
38
  image: {
39
39
  type: "dockerfile";
40
40
  content: string;
41
+ exclude: string[];
41
42
  };
42
43
  command: string;
43
44
  files: {
@@ -36,6 +36,7 @@ export declare function microsoft(opts?: MicrosoftOptions): {
36
36
  image: {
37
37
  type: "dockerfile";
38
38
  content: string;
39
+ exclude: string[];
39
40
  };
40
41
  command: string;
41
42
  files: {
@@ -32,6 +32,7 @@ export declare function okta(opts: OktaOptions): {
32
32
  image: {
33
33
  type: "dockerfile";
34
34
  content: string;
35
+ exclude: string[];
35
36
  };
36
37
  command: string;
37
38
  files: {
@@ -91,6 +91,7 @@ export declare function s3(opts?: S3Options): {
91
91
  image: {
92
92
  type: "dockerfile";
93
93
  content: string;
94
+ exclude: string[];
94
95
  };
95
96
  ports: number[];
96
97
  };
@@ -61,7 +61,8 @@ export function s3(opts = {}) {
61
61
  const secretAccessKey = opts.secretAccessKey ?? "s3";
62
62
  const region = opts.region ?? "us-east-1";
63
63
  return {
64
- image: { type: "dockerfile", content: S3_DOCKERFILE },
64
+ // All sources are embedded above; no project file is a build input.
65
+ image: { type: "dockerfile", content: S3_DOCKERFILE, exclude: ["**"] },
65
66
  ports: [S3_PORT],
66
67
  ...(opts.env ? { env: opts.env } : {}),
67
68
  ...(opts.hosts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.131.1",
3
+ "version": "0.131.2",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -3,6 +3,8 @@ import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { tmpdir } from "node:os";
5
5
  import { android } from "./android.js";
6
+ import { serviceDockerignore } from "../harness/build-context.js";
7
+ import { compactReach, projectReach, reachContains } from "../harness/reach.js";
6
8
 
7
9
  test("Android expands into two VM services with an APK input and private Appium/ADB addresses", async () => {
8
10
  const dir = mkdtempSync(join(tmpdir(), "spectest-android-")),
@@ -45,6 +47,18 @@ test("Android expands into two VM services with an APK input and private Appium/
45
47
  expect(tools.env?.ANDROID_SHOW_TOUCHES).toBe("1");
46
48
  expect(tools.env?.ANDROID_CA_PATH).toBe(process.env.SPECTEST_CA_PATH ?? "/etc/spectest/ca.crt");
47
49
  expect(tools.inputs).toContain("app.apk");
50
+ const projectFiles = ["app.apk", "terraform/budgets.tf", "k8s/nodepools/README.md"];
51
+ const sources = [{
52
+ service: "phone",
53
+ context: tools.image.context ?? ".",
54
+ ignore: serviceDockerignore(null, tools.image.exclude),
55
+ inputs: tools.inputs,
56
+ }];
57
+ const reached = projectReach(projectFiles, sources);
58
+ expect(reached).toEqual(["app.apk"]);
59
+ const rules = { version: 1, sources, prefixes: compactReach(projectFiles, reached) };
60
+ expect(reachContains(rules, "app.apk")).toBe(true);
61
+ expect(reachContains(rules, "k8s/new-nodepool.yaml")).toBe(false);
48
62
  expect(tools.volumes).toContainEqual({
49
63
  source: apk,
50
64
  target: "/spectest/app.apk",
@@ -101,9 +101,10 @@ export function android(options: AndroidOptions) {
101
101
  },
102
102
  },
103
103
  tools: {
104
+ // Tools are SDK-owned; the APK is a separate input below.
104
105
  image: options.toolsImage
105
106
  ? { type: "registry" as const, reference: options.toolsImage }
106
- : { type: "dockerfile" as const, content: ANDROID_TOOLS_DOCKERFILE },
107
+ : { type: "dockerfile" as const, content: ANDROID_TOOLS_DOCKERFILE, exclude: ["**"] },
107
108
  command: "node /spectest-android.mjs",
108
109
  files: [
109
110
  {
@@ -0,0 +1,48 @@
1
+ import { expect, test } from "bun:test";
2
+ import { defineEnvironment } from "../index.js";
3
+ import { serviceDockerignore } from "../harness/build-context.js";
4
+ import { compactReach, projectReach, reachContains } from "../harness/reach.js";
5
+ import { s3 } from "./s3.js";
6
+ import { google } from "./google.js";
7
+ import { github } from "./github.js";
8
+ import { apple } from "./apple.js";
9
+ import { microsoft } from "./microsoft.js";
10
+ import { okta } from "./okta.js";
11
+
12
+ const unrelatedFiles = [
13
+ "terraform/budgets.tf",
14
+ "k8s/nodepools/README.md",
15
+ "docs/guide.md",
16
+ "packages/app/index.ts",
17
+ ];
18
+
19
+ for (const [name, component] of Object.entries({
20
+ s3: s3(),
21
+ google: google(),
22
+ github: github(),
23
+ apple: apple(),
24
+ microsoft: microsoft(),
25
+ okta: okta({ domain: "example.okta.com" }),
26
+ })) {
27
+ test(`${name}'s SDK-owned build does not make project files reachable`, () => {
28
+ const services = defineEnvironment({ name, services: { component } }).config.services;
29
+ for (const projectIgnore of [null, "**\n!terraform/**\n!packages/**"]) {
30
+ const sources = Object.entries(services).map(([service, svc]) => {
31
+ expect(svc.image.type).toBe("dockerfile");
32
+ if (svc.image.type !== "dockerfile") throw new Error("expected a built image");
33
+ return {
34
+ service,
35
+ context: svc.image.context ?? ".",
36
+ // A project's negations must not re-include files in this build.
37
+ ignore: serviceDockerignore(projectIgnore, svc.image.exclude),
38
+ };
39
+ });
40
+ const files = projectReach(unrelatedFiles, sources);
41
+ expect(files).toEqual([]);
42
+ const rules = { version: 1, sources, prefixes: compactReach(unrelatedFiles, files) };
43
+ for (const file of [...unrelatedFiles, "k8s/new-nodepool.yaml"]) {
44
+ expect(reachContains(rules, file)).toBe(false);
45
+ }
46
+ }
47
+ });
48
+ }
@@ -1,9 +1,11 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
2
  import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { join, relative } from 'node:path';
5
5
  import { electron } from './electron.js';
6
6
  import { desktopApp, isDesktopApp } from '../desktop.js';
7
+ import { serviceDockerignore } from '../harness/build-context.js';
8
+ import { projectReach, reachContains } from '../harness/reach.js';
7
9
 
8
10
  function fixture(run: (directory: string) => void) {
9
11
  const directory = mkdtempSync(join(tmpdir(), 'spectest-electron-'));
@@ -57,6 +59,24 @@ describe('Electron component in a monorepo', () => {
57
59
  expect(content).toContain('RUN cd apps/desktop && npm run build\n');
58
60
  expect(service.env?.SPECTEST_ELECTRON_APP_DIR).toBe('/app/apps/desktop');
59
61
  }));
62
+ test('the default app context excludes siblings while an explicit monorepo context honors ignore rules', () => monorepo(root => {
63
+ writeFileSync(join(root, 'apps', 'desktop', 'package.json'), JSON.stringify({ devDependencies: { electron: '44.0.0' } }));
64
+ for (const context of [undefined, root]) {
65
+ const image = electron({ appDir: join(root, 'apps/desktop'), context }).image;
66
+ // Fixtures use absolute paths; the guest receives project-relative paths.
67
+ const sources = [{
68
+ context: relative(root, image.context) || '.',
69
+ ignore: serviceDockerignore('docs\nterraform', image.exclude),
70
+ }];
71
+ const files = ['apps/desktop/main.ts', 'apps/desktop/node_modules/x/index.js', 'packages/shared/index.ts', 'docs/guide.md', 'terraform/budgets.tf'];
72
+ expect(projectReach(files, sources)).toEqual(context
73
+ ? ['apps/desktop/main.ts', 'packages/shared/index.ts']
74
+ : ['apps/desktop/main.ts']);
75
+ const rules = { version: 1, sources, prefixes: [] };
76
+ expect(reachContains(rules, 'apps/desktop/new-window.ts')).toBe(true);
77
+ expect(reachContains(rules, 'docs/new-guide.md')).toBe(false);
78
+ }
79
+ }));
60
80
  test('accepts electron declared at the context root only', () => monorepo(root => {
61
81
  expect(() => electron({ appDir: join(root, 'apps/desktop'), context: root })).not.toThrow();
62
82
  writeFileSync(join(root, 'package.json'), JSON.stringify({ workspaces: ['apps/*'] }));
@@ -174,7 +174,8 @@ RUN echo '{"name":"spectest-provider","private":true}' > package.json \\
174
174
  * entry, and a provider can claim four names. */
175
175
  export function emulatorService(spec: ProviderSpec) {
176
176
  const service = {
177
- image: { type: "dockerfile" as const, content: DOCKERFILE },
177
+ // The image installs packages; its entry script is injected via files.
178
+ image: { type: "dockerfile" as const, content: DOCKERFILE, exclude: ["**"] },
178
179
  command: "bun /srv/auth.mjs",
179
180
  files: [{ path: "/srv/auth.mjs", content: ENTRY_SCRIPT }],
180
181
  env: { SPECTEST_AUTH_CONFIG: JSON.stringify({ port: PORT, providers: [spec] }) },
@@ -11,6 +11,8 @@ import { join } from "node:path";
11
11
 
12
12
  import { expo, nodeMajorFromRange } from "./expo.js";
13
13
  import type { ServiceConfig } from "../index.js";
14
+ import { serviceDockerignore } from "../harness/build-context.js";
15
+ import { projectReach, reachContains } from "../harness/reach.js";
14
16
 
15
17
  let root = "";
16
18
  const prevWorkspace = process.env.SPECTEST_WORKSPACE;
@@ -53,6 +55,20 @@ function lineOf(text: string, needle: string): number {
53
55
  }
54
56
 
55
57
  describe("expo() Dockerfile caching", () => {
58
+ test("the app context excludes sibling infrastructure and docs but includes new app code", () => {
59
+ app({ "package.json": pkg(), "App.tsx": "export default 1" }, "apps/mobile");
60
+ const image = expo({ appDir: "apps/mobile" }).image;
61
+ if (image.type !== "dockerfile") throw new Error("expected a built image");
62
+ const sources = [{ context: image.context, ignore: serviceDockerignore(null, image.exclude) }];
63
+ expect(projectReach([
64
+ "apps/mobile/package.json", "apps/mobile/App.tsx", "apps/mobile/node_modules/x/index.js",
65
+ "apps/mobile/dist/index.html", "terraform/budgets.tf", "docs/guide.md", "apps/other/index.ts",
66
+ ], sources)).toEqual(["apps/mobile/App.tsx", "apps/mobile/package.json"]);
67
+ const rules = { version: 1, sources, prefixes: [] };
68
+ expect(reachContains(rules, "apps/mobile/new-screen.tsx")).toBe(true);
69
+ expect(reachContains(rules, "k8s/new-nodepool.yaml")).toBe(false);
70
+ });
71
+
56
72
  test("installs from the manifests before it copies the source", () => {
57
73
  app({ "package.json": pkg(), "package-lock.json": "{}", "App.js": "x" });
58
74
  const df = dockerfile(expo());
@@ -108,7 +108,8 @@ export function s3(opts: S3Options = {}) {
108
108
  const region = opts.region ?? "us-east-1";
109
109
 
110
110
  return {
111
- image: { type: "dockerfile", content: S3_DOCKERFILE },
111
+ // All sources are embedded above; no project file is a build input.
112
+ image: { type: "dockerfile", content: S3_DOCKERFILE, exclude: ["**"] },
112
113
  ports: [S3_PORT],
113
114
  ...(opts.env ? { env: opts.env } : {}),
114
115
  ...(opts.hosts
@@ -4,6 +4,8 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
 
6
6
  import { excludeAllBut, functionsReach, importMapLocalTargets, importSpecifiers } from "./supabase-functions-reach.js";
7
+ import { serviceDockerignore } from "../harness/build-context.js";
8
+ import { projectReach, reachContains } from "../harness/reach.js";
7
9
 
8
10
  let root: string;
9
11
 
@@ -82,6 +84,22 @@ describe("supabase() wiring", () => {
82
84
  expect(fn.image.exclude?.at(-1)).toBe("**/.venv/**");
83
85
  expect(fn.volumes?.[0]).toMatchObject({ source: root, reachExempt: true });
84
86
  expect((parts.db as { inputs?: readonly string[] }).inputs).toEqual(["supabase"]);
87
+ const sources = [
88
+ { context: fn.image.context, ignore: serviceDockerignore(null, fn.image.exclude) },
89
+ { inputs: [...(parts.db as { inputs: string[] }).inputs] },
90
+ ];
91
+ expect(projectReach([
92
+ "supabase/config.toml", "supabase/migrations/001.sql", "supabase/functions/hello/index.ts",
93
+ "packages/shared/src/greet.ts", "lib/db/client.ts", "packages/unrelated/index.ts",
94
+ "docs/guide.md", "site/app/page.tsx", "terraform/budgets.tf",
95
+ ], sources)).toEqual([
96
+ "lib/db/client.ts", "packages/shared/src/greet.ts", "supabase/config.toml",
97
+ "supabase/functions/hello/index.ts", "supabase/migrations/001.sql",
98
+ ]);
99
+ const rules = { version: 1, sources, prefixes: [] };
100
+ expect(reachContains(rules, "packages/shared/src/new-helper.ts")).toBe(true);
101
+ expect(reachContains(rules, "supabase/migrations/002.sql")).toBe(true);
102
+ expect(reachContains(rules, "k8s/new-nodepool.yaml")).toBe(false);
85
103
  } finally {
86
104
  if (prev === undefined) delete process.env.SPECTEST_WORKSPACE;
87
105
  else process.env.SPECTEST_WORKSPACE = prev;