@warpgogol/forge 2.8.1 → 2.8.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.
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { discoverIgnoredFiles, formatSize } from "../ignored-files.ts";
3
+ import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ let tempDir: string;
8
+
9
+ beforeEach(async () => {
10
+ tempDir = await mkdtemp(join(tmpdir(), "forge-ignored-"));
11
+ });
12
+
13
+ afterEach(async () => {
14
+ await rm(tempDir, { recursive: true, force: true });
15
+ });
16
+
17
+ describe("formatSize", () => {
18
+ it("formats bytes", () => {
19
+ expect(formatSize(0)).toBe("0 B");
20
+ expect(formatSize(512)).toBe("512 B");
21
+ expect(formatSize(1023)).toBe("1023 B");
22
+ });
23
+
24
+ it("formats kilobytes", () => {
25
+ expect(formatSize(1024)).toBe("1.0 KB");
26
+ expect(formatSize(1536)).toBe("1.5 KB");
27
+ });
28
+
29
+ it("formats megabytes", () => {
30
+ expect(formatSize(1024 * 1024)).toBe("1.0 MB");
31
+ expect(formatSize(1024 * 1024 * 5)).toBe("5.0 MB");
32
+ });
33
+
34
+ it("formats gigabytes", () => {
35
+ expect(formatSize(1024 * 1024 * 1024)).toBe("1.0 GB");
36
+ });
37
+ });
38
+
39
+ describe("discoverIgnoredFiles", () => {
40
+ it("returns empty array when no .git directory exists", async () => {
41
+ const result = discoverIgnoredFiles(tempDir);
42
+ expect(result).toEqual([]);
43
+ });
44
+
45
+ it("returns empty array when .git exists but no ignored files", async () => {
46
+ await mkdir(join(tempDir, ".git"), { recursive: true });
47
+ await writeFile(join(tempDir, "file.txt"), "content", "utf8");
48
+ const result = discoverIgnoredFiles(tempDir);
49
+ expect(result).toEqual([]);
50
+ });
51
+ });
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { getAdapters, detectAdapter, detectAdapters } from "../registry.ts";
3
+ import { nodeTypescriptPnpmAdapter } from "../node-typescript-pnpm/index.ts";
4
+ import { phaserPnpmAdapter } from "../phaser-pnpm/index.ts";
5
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+
9
+ let tempDir: string;
10
+
11
+ beforeEach(async () => {
12
+ tempDir = await mkdtemp(join(tmpdir(), "forge-registry-"));
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await rm(tempDir, { recursive: true, force: true });
17
+ });
18
+
19
+ describe("getAdapters", () => {
20
+ it("returns built-in adapters", () => {
21
+ const adapters = getAdapters();
22
+ expect(adapters).toHaveLength(2);
23
+ const ids = adapters.map((a) => a.id);
24
+ expect(ids).toContain("node-typescript-pnpm");
25
+ expect(ids).toContain("phaser-pnpm");
26
+ });
27
+
28
+ it("returns built-in adapters even with undefined config", () => {
29
+ const adapters = getAdapters(undefined);
30
+ expect(adapters).toHaveLength(2);
31
+ });
32
+ });
33
+
34
+ describe("detectAdapter", () => {
35
+ it("detects node-typescript-pnpm when package.json + tsconfig.json + pnpm-lock.yaml exist", async () => {
36
+ await writeFile(join(tempDir, "package.json"), '{"name":"test"}', "utf8");
37
+ await writeFile(join(tempDir, "tsconfig.json"), "{}", "utf8");
38
+ await writeFile(join(tempDir, "pnpm-lock.yaml"), "", "utf8");
39
+
40
+ const adapter = detectAdapter(tempDir);
41
+ expect(adapter?.id).toBe("node-typescript-pnpm");
42
+ });
43
+
44
+ it("detects phaser-pnpm when phaser is in dependencies", async () => {
45
+ await writeFile(
46
+ join(tempDir, "package.json"),
47
+ '{"name":"game","dependencies":{"phaser":"^3.0.0"}}',
48
+ "utf8",
49
+ );
50
+ await writeFile(join(tempDir, "pnpm-lock.yaml"), "", "utf8");
51
+
52
+ const adapter = detectAdapter(tempDir);
53
+ expect(adapter?.id).toBe("phaser-pnpm");
54
+ });
55
+
56
+ it("returns null when no adapter matches", () => {
57
+ const adapter = detectAdapter(tempDir);
58
+ expect(adapter).toBeNull();
59
+ });
60
+
61
+ it("returns first matching adapter when multiple match", async () => {
62
+ await writeFile(
63
+ join(tempDir, "package.json"),
64
+ '{"name":"game","dependencies":{"phaser":"^3.0.0"}}',
65
+ "utf8",
66
+ );
67
+ await writeFile(join(tempDir, "tsconfig.json"), "{}", "utf8");
68
+ await writeFile(join(tempDir, "pnpm-lock.yaml"), "", "utf8");
69
+
70
+ const adapter = detectAdapter(tempDir);
71
+ expect(adapter).toBeDefined();
72
+ });
73
+ });
74
+
75
+ describe("detectAdapters", () => {
76
+ it("returns all matching adapters", async () => {
77
+ await writeFile(
78
+ join(tempDir, "package.json"),
79
+ '{"name":"game","dependencies":{"phaser":"^3.0.0"}}',
80
+ "utf8",
81
+ );
82
+ await writeFile(join(tempDir, "tsconfig.json"), "{}", "utf8");
83
+ await writeFile(join(tempDir, "pnpm-lock.yaml"), "", "utf8");
84
+
85
+ const adapters = detectAdapters(tempDir);
86
+ expect(adapters.length).toBeGreaterThanOrEqual(1);
87
+ });
88
+
89
+ it("returns empty array when nothing matches", () => {
90
+ expect(detectAdapters(tempDir)).toEqual([]);
91
+ });
92
+ });
93
+
94
+ describe("built-in adapter analyze", () => {
95
+ it("nodeTypescriptPnpmAdapter.analyze derives bindings from scripts", async () => {
96
+ await writeFile(
97
+ join(tempDir, "package.json"),
98
+ JSON.stringify({
99
+ name: "@scope/my-app",
100
+ scripts: {
101
+ build: "tsc",
102
+ test: "vitest",
103
+ typecheck: "tsc --noEmit",
104
+ },
105
+ }),
106
+ "utf8",
107
+ );
108
+
109
+ const analysis = nodeTypescriptPnpmAdapter.analyze(tempDir);
110
+ expect(analysis.packageManager).toBe("pnpm");
111
+ expect(analysis.appName).toBe("my-app");
112
+ expect(analysis.bindings.test).toBe("vitest");
113
+ expect(analysis.bindings.scopedBuild).toBe("tsc");
114
+ expect(analysis.placement).toBe("apps");
115
+ });
116
+
117
+ it("phaserPnpmAdapter.analyze detects phaser stack", async () => {
118
+ await writeFile(
119
+ join(tempDir, "package.json"),
120
+ JSON.stringify({
121
+ name: "my-game",
122
+ dependencies: { phaser: "^3.0.0" },
123
+ }),
124
+ "utf8",
125
+ );
126
+
127
+ const analysis = phaserPnpmAdapter.analyze(tempDir);
128
+ expect(analysis.stack).toContain("phaser");
129
+ expect(analysis.appName).toBe("my-game");
130
+ });
131
+ });
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ FORGE_PROTECTED_PATHS,
4
+ DEFAULT_EXCLUDE_PATTERNS,
5
+ type MigrationAdapter,
6
+ type AdapterAnalysis,
7
+ type MigrationResult,
8
+ type Conflict,
9
+ } from "../types.ts";
10
+
11
+ describe("FORGE_PROTECTED_PATHS", () => {
12
+ it("contains forge.yaml", () => {
13
+ expect(FORGE_PROTECTED_PATHS).toContain("forge.yaml");
14
+ });
15
+
16
+ it("contains .agents", () => {
17
+ expect(FORGE_PROTECTED_PATHS).toContain(".agents");
18
+ });
19
+
20
+ it("contains docs/rfcs", () => {
21
+ expect(FORGE_PROTECTED_PATHS).toContain("docs/rfcs");
22
+ });
23
+
24
+ it("contains PREFERENCES.md", () => {
25
+ expect(FORGE_PROTECTED_PATHS).toContain("PREFERENCES.md");
26
+ });
27
+ });
28
+
29
+ describe("DEFAULT_EXCLUDE_PATTERNS", () => {
30
+ it("excludes node_modules", () => {
31
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain("node_modules");
32
+ });
33
+
34
+ it("excludes dist", () => {
35
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain("dist");
36
+ });
37
+
38
+ it("excludes .git", () => {
39
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain(".git");
40
+ });
41
+
42
+ it("excludes .cache", () => {
43
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain(".cache");
44
+ });
45
+
46
+ it("excludes .turbo", () => {
47
+ expect(DEFAULT_EXCLUDE_PATTERNS).toContain(".turbo");
48
+ });
49
+ });
50
+
51
+ describe("type contracts", () => {
52
+ it("MigrationAdapter interface has required methods", () => {
53
+ const adapter: MigrationAdapter = {
54
+ id: "test",
55
+ detect: () => true,
56
+ analyze: (): AdapterAnalysis => ({
57
+ stack: ["typescript"],
58
+ packageManager: "pnpm",
59
+ bindings: { typecheck: null, test: null, scopedBuild: null },
60
+ placement: "apps",
61
+ appName: "test",
62
+ excludePatterns: [],
63
+ gitHistory: false,
64
+ }),
65
+ migrate: (): MigrationResult => ({
66
+ filesCopied: [],
67
+ filesSkipped: [],
68
+ conflicts: [],
69
+ workspaceUpdated: false,
70
+ }),
71
+ postSetup: () => {},
72
+ };
73
+ expect(adapter.id).toBe("test");
74
+ expect(adapter.detect("/")).toBe(true);
75
+ });
76
+
77
+ it("Conflict type has required fields", () => {
78
+ const conflict: Conflict = {
79
+ path: "src/file.ts",
80
+ sourceExists: true,
81
+ forgeExists: true,
82
+ resolution: "source-wins",
83
+ };
84
+ expect(conflict.resolution).toBe("source-wins");
85
+ });
86
+ });
@@ -0,0 +1,134 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { detectWorkspaceType, discoverWorkspaces } from "../workspace-discovery.ts";
3
+ import { GENERATED_MARKER } from "../../utils/index.ts";
4
+ import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+
8
+ let tempDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tempDir = await mkdtemp(join(tmpdir(), "forge-ws-discovery-"));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await rm(tempDir, { recursive: true, force: true });
16
+ });
17
+
18
+ describe("detectWorkspaceType", () => {
19
+ it("returns null when no package.json exists", () => {
20
+ expect(detectWorkspaceType(tempDir)).toBeNull();
21
+ });
22
+
23
+ it("detects 'app' when astro.config.mjs exists", async () => {
24
+ await writeFile(join(tempDir, "package.json"), '{"name":"app"}', "utf8");
25
+ await writeFile(join(tempDir, "astro.config.mjs"), "export default {}", "utf8");
26
+ expect(detectWorkspaceType(tempDir)).toBe("app");
27
+ });
28
+
29
+ it("detects 'app' when astro.config.ts exists", async () => {
30
+ await writeFile(join(tempDir, "package.json"), '{"name":"app"}', "utf8");
31
+ await writeFile(join(tempDir, "astro.config.ts"), "export default {}", "utf8");
32
+ expect(detectWorkspaceType(tempDir)).toBe("app");
33
+ });
34
+
35
+ it("detects 'service' when Dockerfile exists", async () => {
36
+ await writeFile(join(tempDir, "package.json"), '{"name":"svc"}', "utf8");
37
+ await writeFile(join(tempDir, "Dockerfile"), "FROM node:20", "utf8");
38
+ expect(detectWorkspaceType(tempDir)).toBe("service");
39
+ });
40
+
41
+ it("detects 'service' when service.config.yaml exists", async () => {
42
+ await writeFile(join(tempDir, "package.json"), '{"name":"svc"}', "utf8");
43
+ await writeFile(join(tempDir, "service.config.yaml"), "name: svc", "utf8");
44
+ expect(detectWorkspaceType(tempDir)).toBe("service");
45
+ });
46
+
47
+ it("defaults to 'package' when package.json exists but no markers", async () => {
48
+ await writeFile(join(tempDir, "package.json"), '{"name":"pkg"}', "utf8");
49
+ expect(detectWorkspaceType(tempDir)).toBe("package");
50
+ });
51
+
52
+ it("uses profile workspaceTypes when provided", async () => {
53
+ await writeFile(join(tempDir, "package.json"), '{"name":"pkg","dependencies":{"phaser":"^3"}}', "utf8");
54
+ const wst = [{ id: "game", detect: { packageJsonDep: "phaser" }, skills: [] }];
55
+ expect(detectWorkspaceType(tempDir, wst)).toBe("game");
56
+ });
57
+
58
+ it("returns null when profile workspaceTypes provided but no match", async () => {
59
+ await writeFile(join(tempDir, "package.json"), '{"name":"pkg"}', "utf8");
60
+ const wst = [{ id: "game", detect: { packageJsonDep: "phaser" }, skills: [] }];
61
+ expect(detectWorkspaceType(tempDir, wst)).toBeNull();
62
+ });
63
+
64
+ it("profile workspaceTypes with glob detection", async () => {
65
+ await writeFile(join(tempDir, "package.json"), '{"name":"pkg"}', "utf8");
66
+ await writeFile(join(tempDir, "game.config"), "config", "utf8");
67
+ const wst = [{ id: "game", detect: { glob: "game.*" }, skills: [] }];
68
+ expect(detectWorkspaceType(tempDir, wst)).toBe("game");
69
+ });
70
+
71
+ it("profile workspaceTypes with contains detection", async () => {
72
+ await writeFile(join(tempDir, "package.json"), '{"name":"pkg"}', "utf8");
73
+ await writeFile(join(tempDir, "marker.txt"), "marker", "utf8");
74
+ const wst = [{ id: "custom", detect: { contains: "marker.txt" }, skills: [] }];
75
+ expect(detectWorkspaceType(tempDir, wst)).toBe("custom");
76
+ });
77
+ });
78
+
79
+ describe("discoverWorkspaces", () => {
80
+ it("returns empty for directory with no workspaces", () => {
81
+ expect(discoverWorkspaces(tempDir)).toEqual([]);
82
+ });
83
+
84
+ it("discovers nested workspaces", async () => {
85
+ const appsDir = join(tempDir, "apps");
86
+ const appDir = join(appsDir, "my-app");
87
+ await mkdir(appDir, { recursive: true });
88
+ await writeFile(join(appDir, "package.json"), '{"name":"my-app"}', "utf8");
89
+ await writeFile(join(appDir, "astro.config.mjs"), "export default {}", "utf8");
90
+
91
+ const results = discoverWorkspaces(tempDir);
92
+ expect(results).toHaveLength(1);
93
+ expect(results[0].type).toBe("app");
94
+ expect(results[0].path).toContain("my-app");
95
+ });
96
+
97
+ it("skips node_modules, .git, dist, .turbo, .cache, .agents", async () => {
98
+ for (const skipDir of ["node_modules", ".git", "dist", ".turbo", ".cache", ".agents"]) {
99
+ const dir = join(tempDir, skipDir, "sub");
100
+ await mkdir(dir, { recursive: true });
101
+ await writeFile(join(dir, "package.json"), '{"name":"skip"}', "utf8");
102
+ }
103
+
104
+ const results = discoverWorkspaces(tempDir);
105
+ expect(results).toHaveLength(0);
106
+ });
107
+
108
+ it("detects hasAgentsMd and isGenerated", async () => {
109
+ const pkgDir = join(tempDir, "packages", "my-pkg");
110
+ await mkdir(pkgDir, { recursive: true });
111
+ await writeFile(join(pkgDir, "package.json"), '{"name":"my-pkg"}', "utf8");
112
+ await writeFile(
113
+ join(pkgDir, "AGENTS.md"),
114
+ `<!-- ${GENERATED_MARKER} -->\n# Test`,
115
+ "utf8",
116
+ );
117
+
118
+ const results = discoverWorkspaces(tempDir);
119
+ expect(results).toHaveLength(1);
120
+ expect(results[0].hasAgentsMd).toBe(true);
121
+ expect(results[0].isGenerated).toBe(true);
122
+ });
123
+
124
+ it("reports hasAgentsMd=false when no AGENTS.md", async () => {
125
+ const pkgDir = join(tempDir, "packages", "my-pkg");
126
+ await mkdir(pkgDir, { recursive: true });
127
+ await writeFile(join(pkgDir, "package.json"), '{"name":"my-pkg"}', "utf8");
128
+
129
+ const results = discoverWorkspaces(tempDir);
130
+ expect(results).toHaveLength(1);
131
+ expect(results[0].hasAgentsMd).toBe(false);
132
+ expect(results[0].isGenerated).toBe(false);
133
+ });
134
+ });
@@ -0,0 +1,228 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ profileInvariantSchema,
4
+ profileInvariantCheckSchema,
5
+ profileDevServerSchema,
6
+ profileReleaseSchema,
7
+ profilePrerequisiteSchema,
8
+ profileArtifactSchema,
9
+ UNIVERSAL_TERMINOLOGY_KEYS,
10
+ TERMINOLOGY_DEFAULTS,
11
+ } from "../profile-schema.ts";
12
+
13
+ describe("UNIVERSAL_TERMINOLOGY_KEYS", () => {
14
+ it("contains expected keys", () => {
15
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toContain("artifact");
16
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toContain("module");
17
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toContain("operator");
18
+ });
19
+ });
20
+
21
+ describe("TERMINOLOGY_DEFAULTS", () => {
22
+ it("has defaults for all universal keys", () => {
23
+ for (const key of UNIVERSAL_TERMINOLOGY_KEYS) {
24
+ expect(TERMINOLOGY_DEFAULTS[key]).toBeDefined();
25
+ }
26
+ });
27
+ });
28
+
29
+ describe("profileArtifactSchema", () => {
30
+ it("accepts minimal artifact with id and extensions", () => {
31
+ const result = profileArtifactSchema.safeParse({
32
+ id: "html-pages",
33
+ extensions: [".html"],
34
+ });
35
+ expect(result.success).toBe(true);
36
+ });
37
+
38
+ it("accepts artifact with produce and validate", () => {
39
+ const result = profileArtifactSchema.safeParse({
40
+ id: "html-pages",
41
+ extensions: [".html"],
42
+ produce: { command: "astro build", output: "dist" },
43
+ validate: { command: "check", outputFormat: "json" },
44
+ determinism: { hashable: true, inputs: ["src"] },
45
+ });
46
+ expect(result.success).toBe(true);
47
+ });
48
+
49
+ it("rejects artifact without id", () => {
50
+ const result = profileArtifactSchema.safeParse({ extensions: [".html"] });
51
+ expect(result.success).toBe(false);
52
+ });
53
+
54
+ it("accepts artifact with empty extensions array", () => {
55
+ const result = profileArtifactSchema.safeParse({ id: "test", extensions: [] });
56
+ expect(result.success).toBe(true);
57
+ });
58
+ });
59
+
60
+ describe("profileInvariantCheckSchema", () => {
61
+ it("accepts filename-pattern kind", () => {
62
+ const result = profileInvariantCheckSchema.safeParse({
63
+ kind: "filename-pattern",
64
+ glob: "*.ts",
65
+ pattern: "^[a-z-]+\\.ts$",
66
+ });
67
+ expect(result.success).toBe(true);
68
+ });
69
+
70
+ it("accepts attribute-pattern kind with required fields", () => {
71
+ const result = profileInvariantCheckSchema.safeParse({
72
+ kind: "attribute-pattern",
73
+ elements: ["img"],
74
+ attribute: "src",
75
+ pattern: "^/assets/",
76
+ });
77
+ expect(result.success).toBe(true);
78
+ });
79
+
80
+ it("rejects attribute-pattern without elements", () => {
81
+ const result = profileInvariantCheckSchema.safeParse({
82
+ kind: "attribute-pattern",
83
+ attribute: "src",
84
+ pattern: "^/assets/",
85
+ });
86
+ expect(result.success).toBe(false);
87
+ });
88
+
89
+ it("rejects attribute-pattern with empty elements array", () => {
90
+ const result = profileInvariantCheckSchema.safeParse({
91
+ kind: "attribute-pattern",
92
+ elements: [],
93
+ attribute: "src",
94
+ pattern: "^/assets/",
95
+ });
96
+ expect(result.success).toBe(false);
97
+ });
98
+
99
+ it("accepts frontmatter-required with fields", () => {
100
+ const result = profileInvariantCheckSchema.safeParse({
101
+ kind: "frontmatter-required",
102
+ fields: ["title", "created"],
103
+ });
104
+ expect(result.success).toBe(true);
105
+ });
106
+
107
+ it("rejects frontmatter-required without fields", () => {
108
+ const result = profileInvariantCheckSchema.safeParse({
109
+ kind: "frontmatter-required",
110
+ });
111
+ expect(result.success).toBe(false);
112
+ });
113
+ });
114
+
115
+ describe("profileInvariantSchema", () => {
116
+ it("accepts valid invariant with id matching pattern", () => {
117
+ const result = profileInvariantSchema.safeParse({
118
+ id: "VIDEO-01",
119
+ rule: "All videos must have captions",
120
+ severity: "error",
121
+ });
122
+ expect(result.success).toBe(true);
123
+ });
124
+
125
+ it("rejects id not matching ^[A-Z]+-\\d+$ pattern", () => {
126
+ const result = profileInvariantSchema.safeParse({
127
+ id: "video-01",
128
+ rule: "test",
129
+ severity: "error",
130
+ });
131
+ expect(result.success).toBe(false);
132
+ });
133
+
134
+ it("rejects invalid severity", () => {
135
+ const result = profileInvariantSchema.safeParse({
136
+ id: "VIDEO-01",
137
+ rule: "test",
138
+ severity: "critical",
139
+ });
140
+ expect(result.success).toBe(false);
141
+ });
142
+ });
143
+
144
+ describe("profileDevServerSchema", () => {
145
+ it("accepts minimal dev server with command", () => {
146
+ const result = profileDevServerSchema.safeParse({ command: "astro dev" });
147
+ expect(result.success).toBe(true);
148
+ });
149
+
150
+ it("accepts dev server with port and timeout", () => {
151
+ const result = profileDevServerSchema.safeParse({
152
+ command: "astro dev",
153
+ port: 4321,
154
+ readinessTimeout: 5000,
155
+ });
156
+ expect(result.success).toBe(true);
157
+ });
158
+
159
+ it("rejects empty command", () => {
160
+ const result = profileDevServerSchema.safeParse({ command: "" });
161
+ expect(result.success).toBe(false);
162
+ });
163
+
164
+ it("rejects negative port", () => {
165
+ const result = profileDevServerSchema.safeParse({ command: "dev", port: -1 });
166
+ expect(result.success).toBe(false);
167
+ });
168
+ });
169
+
170
+ describe("profileReleaseSchema", () => {
171
+ it("accepts local release target", () => {
172
+ const result = profileReleaseSchema.safeParse({
173
+ target: "local",
174
+ outputDir: "dist",
175
+ });
176
+ expect(result.success).toBe(true);
177
+ });
178
+
179
+ it("accepts r2 release target with required fields", () => {
180
+ const result = profileReleaseSchema.safeParse({
181
+ target: "r2",
182
+ outputDir: "dist",
183
+ r2: { bucket: "my-bucket", accountId: "123" },
184
+ });
185
+ expect(result.success).toBe(true);
186
+ expect(result.success && result.data?.manifestName).toBe("release-manifest.json");
187
+ });
188
+
189
+ it("rejects invalid target", () => {
190
+ const result = profileReleaseSchema.safeParse({
191
+ target: "invalid",
192
+ outputDir: "dist",
193
+ });
194
+ expect(result.success).toBe(false);
195
+ });
196
+ });
197
+
198
+ describe("profilePrerequisiteSchema", () => {
199
+ it("accepts minimal prerequisite", () => {
200
+ const result = profilePrerequisiteSchema.safeParse({
201
+ id: "ffmpeg",
202
+ name: "FFmpeg",
203
+ check: "ffmpeg -version",
204
+ });
205
+ expect(result.success).toBe(true);
206
+ expect(result.success && result.data?.severity).toBe("error");
207
+ });
208
+
209
+ it("accepts warning severity", () => {
210
+ const result = profilePrerequisiteSchema.safeParse({
211
+ id: "ffmpeg",
212
+ name: "FFmpeg",
213
+ check: "ffmpeg -version",
214
+ severity: "warning",
215
+ });
216
+ expect(result.success).toBe(true);
217
+ });
218
+
219
+ it("rejects invalid severity", () => {
220
+ const result = profilePrerequisiteSchema.safeParse({
221
+ id: "ffmpeg",
222
+ name: "FFmpeg",
223
+ check: "ffmpeg -version",
224
+ severity: "critical",
225
+ });
226
+ expect(result.success).toBe(false);
227
+ });
228
+ });