@warpgogol/forge 2.7.0 → 2.8.1
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/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/src/config/forge-config.d.ts +1 -1
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +2 -2
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/agents-generate.d.ts +2 -1
- package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/agents-generate.js +24 -4
- package/dist/src/onboarding/agents-generate.js.map +1 -1
- package/dist/src/profiles/profile-schema.d.ts +2 -0
- package/dist/src/profiles/profile-schema.d.ts.map +1 -1
- package/dist/src/profiles/profile-schema.js +1 -0
- package/dist/src/profiles/profile-schema.js.map +1 -1
- package/dist/src/profiles/stack-profile.d.ts +1 -0
- package/dist/src/profiles/stack-profile.d.ts.map +1 -1
- package/dist/src/profiles/stack-profile.js +2 -0
- package/dist/src/profiles/stack-profile.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +5 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/profiles/root-agents-godot.md +133 -0
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +51 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +217 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for forge.create command — in-place scaffolding via scaffold + init + package-manager post-processing.</purpose>
|
|
4
|
+
</MODULE_CONTRACT>
|
|
5
|
+
<CHANGE_SUMMARY>
|
|
6
|
+
<item>RFC-0544: initial forge.create tests.</item>
|
|
7
|
+
<item>RFC-0877: rewrite tests for --in-place mode, add strict empty-directory check and name-derivation tests.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { runCreate } from "../onboarding/create.ts";
|
|
17
|
+
import { loadForgeConfig } from "../config/forge-config.ts";
|
|
18
|
+
import type { ForgeRuntimeContext } from "../types.ts";
|
|
19
|
+
|
|
20
|
+
const silentLogger = {
|
|
21
|
+
section: () => {},
|
|
22
|
+
info: () => {},
|
|
23
|
+
warn: () => {},
|
|
24
|
+
error: () => {},
|
|
25
|
+
success: () => {},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const FORGE_ROOT = join(import.meta.dirname, "..", "..");
|
|
29
|
+
const _WORKSPACE_ROOT = join(FORGE_ROOT, "..", "..");
|
|
30
|
+
|
|
31
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
32
|
+
return {
|
|
33
|
+
workspaceRoot,
|
|
34
|
+
logger: silentLogger as never,
|
|
35
|
+
dryRun: false,
|
|
36
|
+
outputFormat: "json",
|
|
37
|
+
forgeRoot: FORGE_ROOT,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let tempDir: string;
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
tempDir = await mkdtemp(join(tmpdir(), "create-test-"));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("forge create --in-place scaffolds in cwd with forge.yaml and docs dirs", async () => {
|
|
52
|
+
const result = await runCreate(
|
|
53
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
54
|
+
makeContext(tempDir),
|
|
55
|
+
);
|
|
56
|
+
expect(result.exitCode).toBe(0);
|
|
57
|
+
expect(result.data?.status).toBe("pass");
|
|
58
|
+
|
|
59
|
+
expect(existsSync(join(tempDir, "forge.yaml"))).toBe(true);
|
|
60
|
+
expect(existsSync(join(tempDir, "docs", "rfcs"))).toBe(true);
|
|
61
|
+
expect(existsSync(join(tempDir, "docs", "adrs"))).toBe(true);
|
|
62
|
+
expect(existsSync(join(tempDir, "PREFERENCES.md"))).toBe(true);
|
|
63
|
+
expect(existsSync(join(tempDir, "package.json"))).toBe(true);
|
|
64
|
+
expect(existsSync(join(tempDir, "scripts", "clean.mjs"))).toBe(true);
|
|
65
|
+
}, 30000);
|
|
66
|
+
|
|
67
|
+
test("forge create --in-place refuses when forge artifacts already exist", async () => {
|
|
68
|
+
await writeFile(join(tempDir, "forge.yaml"), "existing: true", "utf8");
|
|
69
|
+
|
|
70
|
+
const result = await runCreate(
|
|
71
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
72
|
+
makeContext(tempDir),
|
|
73
|
+
);
|
|
74
|
+
expect(result.exitCode).toBe(1);
|
|
75
|
+
expect(result.data?.status).toBe("fail");
|
|
76
|
+
expect(result.data?.errors[0]).toContain("not empty");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("forge create --in-place refuses when non-forge files exist in target directory", async () => {
|
|
80
|
+
await writeFile(join(tempDir, "some-file.txt"), "hello", "utf8");
|
|
81
|
+
|
|
82
|
+
const result = await runCreate(
|
|
83
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
84
|
+
makeContext(tempDir),
|
|
85
|
+
);
|
|
86
|
+
expect(result.exitCode).toBe(1);
|
|
87
|
+
expect(result.data?.status).toBe("fail");
|
|
88
|
+
expect(result.data?.errors[0]).toContain("not empty");
|
|
89
|
+
expect(result.data?.errors[0]).toContain("some-file.txt");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("forge create --in-place tolerates only .git directory in target directory", async () => {
|
|
93
|
+
await mkdir(join(tempDir, ".git"), { recursive: true });
|
|
94
|
+
|
|
95
|
+
const result = await runCreate(
|
|
96
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
97
|
+
makeContext(tempDir),
|
|
98
|
+
);
|
|
99
|
+
expect(result.exitCode).toBe(0);
|
|
100
|
+
expect(result.data?.status).toBe("pass");
|
|
101
|
+
expect(existsSync(join(tempDir, "forge.yaml"))).toBe(true);
|
|
102
|
+
expect(existsSync(join(tempDir, ".git"))).toBe(true);
|
|
103
|
+
}, 30000);
|
|
104
|
+
|
|
105
|
+
test("forge create fails on non-kebab-case name", async () => {
|
|
106
|
+
const result = await runCreate(
|
|
107
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "MyProject" } },
|
|
108
|
+
makeContext(tempDir),
|
|
109
|
+
);
|
|
110
|
+
expect(result.exitCode).toBe(1);
|
|
111
|
+
expect(result.data?.status).toBe("fail");
|
|
112
|
+
expect(result.data?.errors[0]).toContain("kebab-case");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("forge create fails on missing --in-place flag", async () => {
|
|
116
|
+
const result = await runCreate(
|
|
117
|
+
{ argv: [], flags: { profile: "forge-shell", name: "my-project" } },
|
|
118
|
+
makeContext(tempDir),
|
|
119
|
+
);
|
|
120
|
+
expect(result.exitCode).toBe(1);
|
|
121
|
+
expect(result.data?.status).toBe("fail");
|
|
122
|
+
expect(result.data?.errors[0]).toContain("--in-place");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("forge create fails on missing --profile flag", async () => {
|
|
126
|
+
const result = await runCreate(
|
|
127
|
+
{ argv: [], flags: { "in-place": true, name: "my-project" } },
|
|
128
|
+
makeContext(tempDir),
|
|
129
|
+
);
|
|
130
|
+
expect(result.exitCode).toBe(1);
|
|
131
|
+
expect(result.data?.status).toBe("fail");
|
|
132
|
+
expect(result.data?.errors[0]).toContain("--profile");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("forge create --in-place --profile forge-shell uses forge-shell profile", async () => {
|
|
136
|
+
const result = await runCreate(
|
|
137
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
138
|
+
makeContext(tempDir),
|
|
139
|
+
);
|
|
140
|
+
expect(result.exitCode).toBe(0);
|
|
141
|
+
expect(result.data?.profile).toBe("forge-shell");
|
|
142
|
+
}, 30000);
|
|
143
|
+
|
|
144
|
+
test("forge create --package-manager npm writes npm into forge.yaml with npx bindings", async () => {
|
|
145
|
+
const result = await runCreate(
|
|
146
|
+
{
|
|
147
|
+
argv: [],
|
|
148
|
+
flags: {
|
|
149
|
+
"in-place": true,
|
|
150
|
+
profile: "forge-shell",
|
|
151
|
+
name: "my-project",
|
|
152
|
+
"package-manager": "npm",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
makeContext(tempDir),
|
|
156
|
+
);
|
|
157
|
+
expect(result.exitCode).toBe(0);
|
|
158
|
+
|
|
159
|
+
const config = loadForgeConfig(tempDir);
|
|
160
|
+
expect(config.project.packageManager).toBe("npm");
|
|
161
|
+
expect(config.bindings?.commands.validateRfc).toContain("npx");
|
|
162
|
+
expect(config.bindings?.commands.validateRfc).toContain("forge rfc.validate");
|
|
163
|
+
}, 30000);
|
|
164
|
+
|
|
165
|
+
test("forge create --package-manager pnpm writes pnpm into forge.yaml", async () => {
|
|
166
|
+
const result = await runCreate(
|
|
167
|
+
{
|
|
168
|
+
argv: [],
|
|
169
|
+
flags: {
|
|
170
|
+
"in-place": true,
|
|
171
|
+
profile: "forge-shell",
|
|
172
|
+
name: "my-project",
|
|
173
|
+
"package-manager": "pnpm",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
makeContext(tempDir),
|
|
177
|
+
);
|
|
178
|
+
expect(result.exitCode).toBe(0);
|
|
179
|
+
|
|
180
|
+
const config = loadForgeConfig(tempDir);
|
|
181
|
+
expect(config.project.packageManager).toBe("pnpm");
|
|
182
|
+
expect(config.bindings?.commands.validateRfc).toContain("pnpm exec");
|
|
183
|
+
}, 30000);
|
|
184
|
+
|
|
185
|
+
test("forge.yaml has non-null forge-CLI bindings and null stack bindings", async () => {
|
|
186
|
+
await runCreate(
|
|
187
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
188
|
+
makeContext(tempDir),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const config = loadForgeConfig(tempDir);
|
|
192
|
+
expect(config.bindings?.commands.validateRfc).not.toBeNull();
|
|
193
|
+
expect(config.bindings?.commands.validateAdr).not.toBeNull();
|
|
194
|
+
expect(config.bindings?.commands.implementStamp).not.toBeNull();
|
|
195
|
+
expect(config.bindings?.commands.specValidate).not.toBeNull();
|
|
196
|
+
expect(config.bindings?.commands.sessionSave).not.toBeNull();
|
|
197
|
+
expect(config.bindings?.commands.typecheck).toBeNull();
|
|
198
|
+
expect(config.bindings?.commands.test).toBeNull();
|
|
199
|
+
expect(config.bindings?.commands.scopedBuild).toBeNull();
|
|
200
|
+
}, 30000);
|
|
201
|
+
|
|
202
|
+
test("forge.yaml has forge.syncedVersion set", async () => {
|
|
203
|
+
await runCreate(
|
|
204
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
205
|
+
makeContext(tempDir),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const config = loadForgeConfig(tempDir);
|
|
209
|
+
expect(config.forge?.syncedVersion).toBeTruthy();
|
|
210
|
+
}, 30000);
|
|
211
|
+
|
|
212
|
+
test("result includes nextSteps with forge-bootstrap", async () => {
|
|
213
|
+
const result = await runCreate(
|
|
214
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
215
|
+
makeContext(tempDir),
|
|
216
|
+
);
|
|
217
|
+
expect(result.exitCode).toBe(0);
|
|
218
|
+
expect(result.nextSteps).toBeDefined();
|
|
219
|
+
expect(result.nextSteps?.some((s) => s.action.includes("forge-bootstrap"))).toBe(true);
|
|
220
|
+
expect(result.nextSteps?.some((s) => s.kind === "required")).toBe(true);
|
|
221
|
+
}, 30000);
|
|
222
|
+
|
|
223
|
+
test("forge create generates AGENTS.md with behavioral layer (RFC-0548)", async () => {
|
|
224
|
+
const result = await runCreate(
|
|
225
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
226
|
+
makeContext(tempDir),
|
|
227
|
+
);
|
|
228
|
+
expect(result.exitCode).toBe(0);
|
|
229
|
+
|
|
230
|
+
const agentsMdPath = join(tempDir, "AGENTS.md");
|
|
231
|
+
expect(existsSync(agentsMdPath)).toBe(true);
|
|
232
|
+
|
|
233
|
+
const { readFile: readFileAsync } = await import("node:fs/promises");
|
|
234
|
+
const agentsMd = await readFileAsync(agentsMdPath, "utf8");
|
|
235
|
+
expect(agentsMd).toContain("<!-- forge:begin behavioral-layer -->");
|
|
236
|
+
expect(agentsMd).toContain("<!-- forge:end behavioral-layer -->");
|
|
237
|
+
expect(agentsMd).toContain("### Intent-to-skill routing");
|
|
238
|
+
}, 30000);
|
|
239
|
+
|
|
240
|
+
test("forge create writes NEXT_STEPS.md with greenfield and transplant guidance (RFC-0550)", async () => {
|
|
241
|
+
const result = await runCreate(
|
|
242
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
243
|
+
makeContext(tempDir),
|
|
244
|
+
);
|
|
245
|
+
expect(result.exitCode).toBe(0);
|
|
246
|
+
|
|
247
|
+
const nextStepsPath = join(tempDir, "NEXT_STEPS.md");
|
|
248
|
+
expect(existsSync(nextStepsPath)).toBe(true);
|
|
249
|
+
|
|
250
|
+
const { readFile: readFileAsync } = await import("node:fs/promises");
|
|
251
|
+
const nextSteps = await readFileAsync(nextStepsPath, "utf8");
|
|
252
|
+
expect(nextSteps).toContain("required");
|
|
253
|
+
expect(nextSteps).toContain("forge-bootstrap");
|
|
254
|
+
expect(nextSteps).toContain("language");
|
|
255
|
+
expect(result.data?.filesCreated).toContain("NEXT_STEPS.md");
|
|
256
|
+
}, 30000);
|
|
257
|
+
|
|
258
|
+
test("forge create root package.json has scripts and replaced project name", async () => {
|
|
259
|
+
const result = await runCreate(
|
|
260
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell", name: "my-project" } },
|
|
261
|
+
makeContext(tempDir),
|
|
262
|
+
);
|
|
263
|
+
expect(result.exitCode).toBe(0);
|
|
264
|
+
|
|
265
|
+
const { readFile: readFileAsync } = await import("node:fs/promises");
|
|
266
|
+
const pkgJson = JSON.parse(await readFileAsync(join(tempDir, "package.json"), "utf8"));
|
|
267
|
+
expect(pkgJson.name).toBe("my-project");
|
|
268
|
+
expect(pkgJson.scripts.clean).toBe("node scripts/clean.mjs");
|
|
269
|
+
expect(pkgJson.scripts.format).toBe("prettier --write .");
|
|
270
|
+
expect(pkgJson.scripts["format:check"]).toBe("prettier --check .");
|
|
271
|
+
expect(pkgJson.scripts.test).toBe("vitest run --passWithNoTests");
|
|
272
|
+
expect(pkgJson.scripts["upgrade-packages"]).toBe("pnpm up");
|
|
273
|
+
}, 30000);
|
|
274
|
+
|
|
275
|
+
test("forge create --in-place derives name from folder name when --name omitted", async () => {
|
|
276
|
+
const namedDir = join(tempDir, "my-derived-project");
|
|
277
|
+
await mkdir(namedDir, { recursive: true });
|
|
278
|
+
|
|
279
|
+
const result = await runCreate(
|
|
280
|
+
{ argv: [], flags: { "in-place": true, profile: "forge-shell" } },
|
|
281
|
+
makeContext(namedDir),
|
|
282
|
+
);
|
|
283
|
+
expect(result.exitCode).toBe(0);
|
|
284
|
+
expect(result.data?.status).toBe("pass");
|
|
285
|
+
|
|
286
|
+
const { readFile: readFileAsync } = await import("node:fs/promises");
|
|
287
|
+
const pkgJson = JSON.parse(await readFileAsync(join(namedDir, "package.json"), "utf8"));
|
|
288
|
+
expect(pkgJson.name).toBe("my-derived-project");
|
|
289
|
+
}, 30000);
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for forge.doctor autonomy guard — verifies @warpgogol/* import detection.</purpose>
|
|
4
|
+
</MODULE_CONTRACT>
|
|
5
|
+
<CHANGE_SUMMARY>
|
|
6
|
+
<item>RFC-0391: initial doctor autonomy guard tests.</item>
|
|
7
|
+
</CHANGE_SUMMARY>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { runDoctor } from "../onboarding/doctor.ts";
|
|
15
|
+
import type { ForgeRuntimeContext, ForgeLogger } from "../types.ts";
|
|
16
|
+
|
|
17
|
+
const mockLogger: ForgeLogger = {
|
|
18
|
+
section: () => {},
|
|
19
|
+
info: () => {},
|
|
20
|
+
warn: () => {},
|
|
21
|
+
error: () => {},
|
|
22
|
+
success: () => {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const mockContext = (workspaceRoot: string): ForgeRuntimeContext => ({
|
|
26
|
+
workspaceRoot,
|
|
27
|
+
logger: mockLogger,
|
|
28
|
+
dryRun: false,
|
|
29
|
+
outputFormat: "json",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let tempDir: string;
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
tempDir = await mkdtemp(join(tmpdir(), "forge-doctor-test-"));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("doctor passes on clean tree with no @warpgogol/* imports", async () => {
|
|
43
|
+
await mkdir(join(tempDir, "packages", "forge", "src"), { recursive: true });
|
|
44
|
+
await writeFile(
|
|
45
|
+
join(tempDir, "packages", "forge", "package.json"),
|
|
46
|
+
'{"name":"@warpgogol/forge"}',
|
|
47
|
+
"utf8",
|
|
48
|
+
);
|
|
49
|
+
await writeFile(
|
|
50
|
+
join(tempDir, "packages", "forge", "src", "index.ts"),
|
|
51
|
+
"export const x = 1;",
|
|
52
|
+
"utf8",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
56
|
+
const autonomyCheck = result.data?.checks.find((c) => c.name === "autonomy-guard");
|
|
57
|
+
expect(autonomyCheck?.status).toBe("pass");
|
|
58
|
+
expect(result.data?.forbiddenImports).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("doctor detects @warpgogol/* import specifiers", async () => {
|
|
62
|
+
await mkdir(join(tempDir, "packages", "forge", "src"), { recursive: true });
|
|
63
|
+
await writeFile(
|
|
64
|
+
join(tempDir, "packages", "forge", "package.json"),
|
|
65
|
+
'{"name":"@warpgogol/forge"}',
|
|
66
|
+
"utf8",
|
|
67
|
+
);
|
|
68
|
+
await writeFile(
|
|
69
|
+
join(tempDir, "packages", "forge", "src", "bad.ts"),
|
|
70
|
+
'import { foo } from "@warpgogol/werkstatt/kernel";\nexport const x = foo;',
|
|
71
|
+
"utf8",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
75
|
+
const autonomyCheck = result.data?.checks.find((c) => c.name === "autonomy-guard");
|
|
76
|
+
expect(autonomyCheck?.status).toBe("fail");
|
|
77
|
+
expect(result.data?.forbiddenImports.length).toBe(1);
|
|
78
|
+
expect(result.data?.forbiddenImports[0]?.specifier).toBe("@warpgogol/werkstatt/kernel");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("doctor ignores @warpgogol/* in comments", async () => {
|
|
82
|
+
await mkdir(join(tempDir, "packages", "forge", "src"), { recursive: true });
|
|
83
|
+
await writeFile(
|
|
84
|
+
join(tempDir, "packages", "forge", "package.json"),
|
|
85
|
+
'{"name":"@warpgogol/forge"}',
|
|
86
|
+
"utf8",
|
|
87
|
+
);
|
|
88
|
+
await writeFile(
|
|
89
|
+
join(tempDir, "packages", "forge", "src", "clean.ts"),
|
|
90
|
+
"/* Do not import from @warpgogol/werkstatt/kernel */\nexport const x = 1;",
|
|
91
|
+
"utf8",
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
95
|
+
const autonomyCheck = result.data?.checks.find((c) => c.name === "autonomy-guard");
|
|
96
|
+
expect(autonomyCheck?.status).toBe("pass");
|
|
97
|
+
expect(result.data?.forbiddenImports).toEqual([]);
|
|
98
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for forge.doctor defaultable-binding-null notices (RFC-0540).</purpose>
|
|
4
|
+
</MODULE_CONTRACT>
|
|
5
|
+
<CHANGE_SUMMARY>
|
|
6
|
+
<item>RFC-0540: initial doctor binding notices tests.</item>
|
|
7
|
+
</CHANGE_SUMMARY>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { runDoctor } from "../onboarding/doctor.ts";
|
|
15
|
+
import type { ForgeRuntimeContext, ForgeLogger } from "../types.ts";
|
|
16
|
+
|
|
17
|
+
const mockLogger: ForgeLogger = {
|
|
18
|
+
section: () => {},
|
|
19
|
+
info: () => {},
|
|
20
|
+
warn: () => {},
|
|
21
|
+
error: () => {},
|
|
22
|
+
success: () => {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const mockContext = (workspaceRoot: string): ForgeRuntimeContext => ({
|
|
26
|
+
workspaceRoot,
|
|
27
|
+
logger: mockLogger,
|
|
28
|
+
dryRun: false,
|
|
29
|
+
outputFormat: "json",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
let tempDir: string;
|
|
33
|
+
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
tempDir = await mkdtemp(join(tmpdir(), "forge-doctor-bindings-test-"));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
async function writeForgeYaml(dir: string, yaml: string): Promise<void> {
|
|
43
|
+
await writeFile(join(dir, "forge.yaml"), yaml, "utf8");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function setupMinimalProject(dir: string): Promise<void> {
|
|
47
|
+
await mkdir(join(dir, "packages", "forge", "src"), { recursive: true });
|
|
48
|
+
await writeFile(
|
|
49
|
+
join(dir, "packages", "forge", "package.json"),
|
|
50
|
+
'{"name":"@warpgogol/forge"}',
|
|
51
|
+
"utf8",
|
|
52
|
+
);
|
|
53
|
+
await writeFile(join(dir, "packages", "forge", "src", "index.ts"), "export const x = 1;", "utf8");
|
|
54
|
+
await mkdir(join(dir, ".agents", "skills"), { recursive: true });
|
|
55
|
+
await mkdir(join(dir, "docs", "rfcs"), { recursive: true });
|
|
56
|
+
await mkdir(join(dir, "docs", "adrs"), { recursive: true });
|
|
57
|
+
await writeFile(join(dir, "AGENTS.md"), "# Test", "utf8");
|
|
58
|
+
await writeFile(join(dir, "PREFERENCES.md"), "# Test", "utf8");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test("doctor emits defaultable-binding-null notices for all 5 forge-CLI bindings when null", async () => {
|
|
62
|
+
await setupMinimalProject(tempDir);
|
|
63
|
+
await writeForgeYaml(
|
|
64
|
+
tempDir,
|
|
65
|
+
`schema: forge/config@1
|
|
66
|
+
project:
|
|
67
|
+
name: test
|
|
68
|
+
stack: []
|
|
69
|
+
packageManager: pnpm
|
|
70
|
+
paths:
|
|
71
|
+
rfcsDir: docs/rfcs
|
|
72
|
+
adrsDir: docs/adrs
|
|
73
|
+
plansDir: docs/plans
|
|
74
|
+
auditsDir: docs/audits
|
|
75
|
+
specsDir: docs/specs
|
|
76
|
+
skillsDir: .agents/skills
|
|
77
|
+
bindings:
|
|
78
|
+
schema: forge/bindings@1
|
|
79
|
+
commands:
|
|
80
|
+
validateRfc: null
|
|
81
|
+
validateAdr: null
|
|
82
|
+
implementStamp: null
|
|
83
|
+
typecheck: null
|
|
84
|
+
test: null
|
|
85
|
+
scopedBuild: null
|
|
86
|
+
specValidate: null
|
|
87
|
+
sessionSave: null
|
|
88
|
+
paths:
|
|
89
|
+
invariantsFile: null
|
|
90
|
+
compassDocs: []
|
|
91
|
+
reviewsDir: null
|
|
92
|
+
handoffsDir: null
|
|
93
|
+
sessionsDir: null
|
|
94
|
+
`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
98
|
+
const notices = result.data?.bindings?.notices ?? [];
|
|
99
|
+
expect(notices).toHaveLength(5);
|
|
100
|
+
const keys = notices.map((n) => n.key);
|
|
101
|
+
expect(keys).toContain("commands.validateRfc");
|
|
102
|
+
expect(keys).toContain("commands.validateAdr");
|
|
103
|
+
expect(keys).toContain("commands.implementStamp");
|
|
104
|
+
expect(keys).toContain("commands.specValidate");
|
|
105
|
+
expect(keys).toContain("commands.sessionSave");
|
|
106
|
+
for (const notice of notices) {
|
|
107
|
+
expect(notice.rule).toBe("defaultable-binding-null");
|
|
108
|
+
expect(notice.suggestion).toContain("pnpm exec forge");
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("doctor does NOT emit notices for non-null forge-CLI bindings (operator override)", async () => {
|
|
113
|
+
await setupMinimalProject(tempDir);
|
|
114
|
+
await writeForgeYaml(
|
|
115
|
+
tempDir,
|
|
116
|
+
`schema: forge/config@1
|
|
117
|
+
project:
|
|
118
|
+
name: test
|
|
119
|
+
stack: []
|
|
120
|
+
packageManager: pnpm
|
|
121
|
+
paths:
|
|
122
|
+
rfcsDir: docs/rfcs
|
|
123
|
+
adrsDir: docs/adrs
|
|
124
|
+
plansDir: docs/plans
|
|
125
|
+
auditsDir: docs/audits
|
|
126
|
+
specsDir: docs/specs
|
|
127
|
+
skillsDir: .agents/skills
|
|
128
|
+
bindings:
|
|
129
|
+
schema: forge/bindings@1
|
|
130
|
+
commands:
|
|
131
|
+
validateRfc: "custom rfc validate command"
|
|
132
|
+
validateAdr: "custom adr validate command"
|
|
133
|
+
implementStamp: "custom stamp command"
|
|
134
|
+
typecheck: null
|
|
135
|
+
test: null
|
|
136
|
+
scopedBuild: null
|
|
137
|
+
specValidate: "custom spec validate command"
|
|
138
|
+
sessionSave: "custom session save command"
|
|
139
|
+
paths:
|
|
140
|
+
invariantsFile: null
|
|
141
|
+
compassDocs: []
|
|
142
|
+
reviewsDir: null
|
|
143
|
+
handoffsDir: null
|
|
144
|
+
sessionsDir: null
|
|
145
|
+
`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
149
|
+
const notices = result.data?.bindings?.notices ?? [];
|
|
150
|
+
expect(notices).toHaveLength(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("doctor emits zero notices when all forge-CLI bindings are non-null", async () => {
|
|
154
|
+
await setupMinimalProject(tempDir);
|
|
155
|
+
await writeForgeYaml(
|
|
156
|
+
tempDir,
|
|
157
|
+
`schema: forge/config@1
|
|
158
|
+
project:
|
|
159
|
+
name: test
|
|
160
|
+
stack: []
|
|
161
|
+
packageManager: npm
|
|
162
|
+
paths:
|
|
163
|
+
rfcsDir: docs/rfcs
|
|
164
|
+
adrsDir: docs/adrs
|
|
165
|
+
plansDir: docs/plans
|
|
166
|
+
auditsDir: docs/audits
|
|
167
|
+
specsDir: docs/specs
|
|
168
|
+
skillsDir: .agents/skills
|
|
169
|
+
bindings:
|
|
170
|
+
schema: forge/bindings@1
|
|
171
|
+
commands:
|
|
172
|
+
validateRfc: "npx forge rfc.validate --id {id} --json"
|
|
173
|
+
validateAdr: "npx forge adr.validate --id {id} --json"
|
|
174
|
+
implementStamp: "npx forge rfc.implement.stamp --id {id} --implementation-commit {commit}"
|
|
175
|
+
typecheck: null
|
|
176
|
+
test: null
|
|
177
|
+
scopedBuild: null
|
|
178
|
+
specValidate: "npx forge spec.validate --spec={id} --json"
|
|
179
|
+
sessionSave: "npx forge session.save --json"
|
|
180
|
+
paths:
|
|
181
|
+
invariantsFile: null
|
|
182
|
+
compassDocs: []
|
|
183
|
+
reviewsDir: null
|
|
184
|
+
handoffsDir: null
|
|
185
|
+
sessionsDir: null
|
|
186
|
+
`,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
190
|
+
const notices = result.data?.bindings?.notices ?? [];
|
|
191
|
+
expect(notices).toHaveLength(0);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("doctor notice suggestion uses npm runner when packageManager is npm", async () => {
|
|
195
|
+
await setupMinimalProject(tempDir);
|
|
196
|
+
await writeForgeYaml(
|
|
197
|
+
tempDir,
|
|
198
|
+
`schema: forge/config@1
|
|
199
|
+
project:
|
|
200
|
+
name: test
|
|
201
|
+
stack: []
|
|
202
|
+
packageManager: npm
|
|
203
|
+
paths:
|
|
204
|
+
rfcsDir: docs/rfcs
|
|
205
|
+
adrsDir: docs/adrs
|
|
206
|
+
plansDir: docs/plans
|
|
207
|
+
auditsDir: docs/audits
|
|
208
|
+
specsDir: docs/specs
|
|
209
|
+
skillsDir: .agents/skills
|
|
210
|
+
bindings:
|
|
211
|
+
schema: forge/bindings@1
|
|
212
|
+
commands:
|
|
213
|
+
validateRfc: null
|
|
214
|
+
validateAdr: null
|
|
215
|
+
implementStamp: null
|
|
216
|
+
typecheck: null
|
|
217
|
+
test: null
|
|
218
|
+
scopedBuild: null
|
|
219
|
+
specValidate: null
|
|
220
|
+
sessionSave: null
|
|
221
|
+
paths:
|
|
222
|
+
invariantsFile: null
|
|
223
|
+
compassDocs: []
|
|
224
|
+
reviewsDir: null
|
|
225
|
+
handoffsDir: null
|
|
226
|
+
sessionsDir: null
|
|
227
|
+
`,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
231
|
+
const notices = result.data?.bindings?.notices ?? [];
|
|
232
|
+
expect(notices.length).toBeGreaterThan(0);
|
|
233
|
+
for (const notice of notices) {
|
|
234
|
+
expect(notice.suggestion).toContain("npx forge");
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("doctor detects directories as existing paths (regression: pathExists used readFile which throws EISDIR on dirs)", async () => {
|
|
239
|
+
await setupMinimalProject(tempDir);
|
|
240
|
+
await mkdir(join(tempDir, "docs", "sessions"), { recursive: true });
|
|
241
|
+
await writeForgeYaml(
|
|
242
|
+
tempDir,
|
|
243
|
+
`schema: forge/config@1
|
|
244
|
+
project:
|
|
245
|
+
name: test
|
|
246
|
+
stack: []
|
|
247
|
+
packageManager: pnpm
|
|
248
|
+
paths:
|
|
249
|
+
rfcsDir: docs/rfcs
|
|
250
|
+
adrsDir: docs/adrs
|
|
251
|
+
plansDir: docs/plans
|
|
252
|
+
auditsDir: docs/audits
|
|
253
|
+
specsDir: docs/specs
|
|
254
|
+
skillsDir: .agents/skills
|
|
255
|
+
sessionsDir: docs/sessions
|
|
256
|
+
bindings:
|
|
257
|
+
schema: forge/bindings@1
|
|
258
|
+
commands:
|
|
259
|
+
validateRfc: "pnpm exec forge rfc.validate --id {id} --json"
|
|
260
|
+
validateAdr: "pnpm exec forge adr.validate --id {id} --json"
|
|
261
|
+
implementStamp: "pnpm exec forge rfc.implement.stamp --id {id} --implementation-commit {commit}"
|
|
262
|
+
typecheck: null
|
|
263
|
+
test: null
|
|
264
|
+
scopedBuild: null
|
|
265
|
+
specValidate: "pnpm exec forge spec.validate --spec={id} --json"
|
|
266
|
+
sessionSave: "pnpm exec forge session.save --json"
|
|
267
|
+
paths:
|
|
268
|
+
invariantsFile: null
|
|
269
|
+
compassDocs: []
|
|
270
|
+
reviewsDir: null
|
|
271
|
+
handoffsDir: null
|
|
272
|
+
sessionsDir: docs/sessions
|
|
273
|
+
`,
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const result = await runDoctor({ argv: [], flags: {} }, mockContext(tempDir));
|
|
277
|
+
const checks = result.data?.checks ?? [];
|
|
278
|
+
const skillsCheck = checks.find((c) => c.name === ".agents/skills/");
|
|
279
|
+
expect(skillsCheck?.status).toBe("pass");
|
|
280
|
+
const rfcsCheck = checks.find((c) => c.name === "docs/rfcs/");
|
|
281
|
+
expect(rfcsCheck?.status).toBe("pass");
|
|
282
|
+
const adrsCheck = checks.find((c) => c.name === "docs/adrs/");
|
|
283
|
+
expect(adrsCheck?.status).toBe("pass");
|
|
284
|
+
const bindingsCheck = checks.find((c) => c.name === "bindings");
|
|
285
|
+
expect(bindingsCheck?.status).toBe("pass");
|
|
286
|
+
});
|