@warpgogol/forge 2.8.1 → 2.9.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.
@@ -0,0 +1,192 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { serializeKnowledgeFile } from "../serialize.ts";
3
+ import type { ParsedKnowledgeFile, KnowledgeEntryMeta } from "../schema.ts";
4
+
5
+ function makeMeta(overrides: Partial<KnowledgeEntryMeta> = {}): KnowledgeEntryMeta {
6
+ return {
7
+ id: "entry-001",
8
+ layer: "L1",
9
+ created: "2026-01-01",
10
+ lastConfirmedAt: "2026-01-01",
11
+ confirmations: 1,
12
+ status: "active",
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ describe("serializeKnowledgeFile", () => {
18
+ it("serializes empty parsed file to single newline", () => {
19
+ const parsed: ParsedKnowledgeFile = {
20
+ path: "test.md",
21
+ layer: "L1",
22
+ preamble: "",
23
+ entries: [],
24
+ legacySections: [],
25
+ isKnowledgeAdjacent: false,
26
+ parseIssues: [],
27
+ };
28
+ expect(serializeKnowledgeFile(parsed)).toBe("\n");
29
+ });
30
+
31
+ it("serializes preamble only", () => {
32
+ const parsed: ParsedKnowledgeFile = {
33
+ path: "test.md",
34
+ layer: "L1",
35
+ preamble: "# My Knowledge File",
36
+ entries: [],
37
+ legacySections: [],
38
+ isKnowledgeAdjacent: false,
39
+ parseIssues: [],
40
+ };
41
+ expect(serializeKnowledgeFile(parsed)).toBe("# My Knowledge File\n");
42
+ });
43
+
44
+ it("serializes a single entry with meta and body", () => {
45
+ const parsed: ParsedKnowledgeFile = {
46
+ path: "test.md",
47
+ layer: "L1",
48
+ preamble: "",
49
+ entries: [
50
+ {
51
+ meta: makeMeta(),
52
+ title: "First Principle",
53
+ body: "This is the body text.",
54
+ lineStart: 0,
55
+ },
56
+ ],
57
+ legacySections: [],
58
+ isKnowledgeAdjacent: false,
59
+ parseIssues: [],
60
+ };
61
+ const result = serializeKnowledgeFile(parsed);
62
+ expect(result).toContain("### entry-001: First Principle");
63
+ expect(result).toContain("```knowledge-entry");
64
+ expect(result).toContain("id: entry-001");
65
+ expect(result).toContain("layer: L1");
66
+ expect(result).toContain("status: active");
67
+ expect(result).toContain("This is the body text.");
68
+ expect(result).toContain("```");
69
+ });
70
+
71
+ it("omits undefined meta fields", () => {
72
+ const parsed: ParsedKnowledgeFile = {
73
+ path: "test.md",
74
+ layer: "L1",
75
+ preamble: "",
76
+ entries: [
77
+ {
78
+ meta: makeMeta({ supersedes: undefined, promotedTo: undefined, expiresAt: undefined }),
79
+ title: "Minimal",
80
+ body: "",
81
+ lineStart: 0,
82
+ },
83
+ ],
84
+ legacySections: [],
85
+ isKnowledgeAdjacent: false,
86
+ parseIssues: [],
87
+ };
88
+ const result = serializeKnowledgeFile(parsed);
89
+ expect(result).not.toContain("supersedes");
90
+ expect(result).not.toContain("promotedTo");
91
+ expect(result).not.toContain("expiresAt");
92
+ });
93
+
94
+ it("serializes array meta values as inline arrays", () => {
95
+ const parsed: ParsedKnowledgeFile = {
96
+ path: "test.md",
97
+ layer: "L1",
98
+ preamble: "",
99
+ entries: [
100
+ {
101
+ meta: makeMeta({ supersedes: ["old-001", "old-002"] }),
102
+ title: "Merged",
103
+ body: "",
104
+ lineStart: 0,
105
+ },
106
+ ],
107
+ legacySections: [],
108
+ isKnowledgeAdjacent: false,
109
+ parseIssues: [],
110
+ };
111
+ const result = serializeKnowledgeFile(parsed);
112
+ expect(result).toContain("supersedes: [old-001, old-002]");
113
+ });
114
+
115
+ it("serializes empty array as []", () => {
116
+ const parsed: ParsedKnowledgeFile = {
117
+ path: "test.md",
118
+ layer: "L1",
119
+ preamble: "",
120
+ entries: [
121
+ {
122
+ meta: makeMeta({ supersedes: [] }),
123
+ title: "Empty",
124
+ body: "",
125
+ lineStart: 0,
126
+ },
127
+ ],
128
+ legacySections: [],
129
+ isKnowledgeAdjacent: false,
130
+ parseIssues: [],
131
+ };
132
+ const result = serializeKnowledgeFile(parsed);
133
+ expect(result).toContain("supersedes: []");
134
+ });
135
+
136
+ it("serializes null meta value as null", () => {
137
+ const parsed: ParsedKnowledgeFile = {
138
+ path: "test.md",
139
+ layer: "L1",
140
+ preamble: "",
141
+ entries: [
142
+ {
143
+ meta: makeMeta({ promotedTo: null }),
144
+ title: "Null",
145
+ body: "",
146
+ lineStart: 0,
147
+ },
148
+ ],
149
+ legacySections: [],
150
+ isKnowledgeAdjacent: false,
151
+ parseIssues: [],
152
+ };
153
+ const result = serializeKnowledgeFile(parsed);
154
+ expect(result).toContain("promotedTo: null");
155
+ });
156
+
157
+ it("serializes legacy sections", () => {
158
+ const parsed: ParsedKnowledgeFile = {
159
+ path: "test.md",
160
+ layer: "L1",
161
+ preamble: "",
162
+ entries: [],
163
+ legacySections: [{ text: "## Old Section\n\nSome content", lineStart: 0 }],
164
+ isKnowledgeAdjacent: false,
165
+ parseIssues: [],
166
+ };
167
+ const result = serializeKnowledgeFile(parsed);
168
+ expect(result).toContain("## Old Section");
169
+ expect(result).toContain("Some content");
170
+ });
171
+
172
+ it("collapses excessive newlines to max two", () => {
173
+ const parsed: ParsedKnowledgeFile = {
174
+ path: "test.md",
175
+ layer: "L1",
176
+ preamble: "preamble",
177
+ entries: [
178
+ {
179
+ meta: makeMeta(),
180
+ title: "Title",
181
+ body: "body",
182
+ lineStart: 0,
183
+ },
184
+ ],
185
+ legacySections: [],
186
+ isKnowledgeAdjacent: false,
187
+ parseIssues: [],
188
+ };
189
+ const result = serializeKnowledgeFile(parsed);
190
+ expect(result).not.toMatch(/\n{3,}/);
191
+ });
192
+ });
@@ -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
+ });