@warpgogol/forge 2.8.0 → 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.
- package/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.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 +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- 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/__tests__/resolve-terminology.test.ts +45 -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/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -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/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -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/__tests__/workspace-discovery.test.ts +134 -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/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -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 +54 -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 +221 -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/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -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/__tests__/note-orphan-detect.test.ts +94 -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,161 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
GENERATED_MARKER,
|
|
4
|
+
hasGeneratedMarker,
|
|
5
|
+
stripGeneratedMarker,
|
|
6
|
+
buildGeneratedHeader,
|
|
7
|
+
isGeneratedMarkerTextCandidate,
|
|
8
|
+
} from "../utils/generated-marker.ts";
|
|
9
|
+
|
|
10
|
+
describe("hasGeneratedMarker", () => {
|
|
11
|
+
test("returns true when marker is present", () => {
|
|
12
|
+
expect(hasGeneratedMarker(`// ${GENERATED_MARKER}`)).toBe(true);
|
|
13
|
+
expect(hasGeneratedMarker(`/* ${GENERATED_MARKER} */`)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("returns false when marker is absent", () => {
|
|
17
|
+
expect(hasGeneratedMarker("// some other comment")).toBe(false);
|
|
18
|
+
expect(hasGeneratedMarker("")).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("stripGeneratedMarker", () => {
|
|
23
|
+
test("strips line-slash comment with marker", () => {
|
|
24
|
+
const input = `// ${GENERATED_MARKER}\n// DO NOT EDIT\ncontent`;
|
|
25
|
+
const result = stripGeneratedMarker(input);
|
|
26
|
+
expect(result.changed).toBe(true);
|
|
27
|
+
expect(result.content).not.toContain(GENERATED_MARKER);
|
|
28
|
+
expect(result.content).toContain("content");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("strips block comment with marker", () => {
|
|
32
|
+
const input = `/*\n${GENERATED_MARKER}\n*/\ncontent`;
|
|
33
|
+
const result = stripGeneratedMarker(input);
|
|
34
|
+
expect(result.changed).toBe(true);
|
|
35
|
+
expect(result.content).toBe("content");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("strips hash comment with marker", () => {
|
|
39
|
+
const input = `# ${GENERATED_MARKER}\ncontent`;
|
|
40
|
+
const result = stripGeneratedMarker(input);
|
|
41
|
+
expect(result.changed).toBe(true);
|
|
42
|
+
expect(result.content).toBe("content");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("strips HTML comment with marker", () => {
|
|
46
|
+
const input = `<!-- ${GENERATED_MARKER} -->\ncontent`;
|
|
47
|
+
const result = stripGeneratedMarker(input);
|
|
48
|
+
expect(result.changed).toBe(true);
|
|
49
|
+
expect(result.content).toBe("content");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("returns unchanged when no marker", () => {
|
|
53
|
+
const input = "// regular comment\ncontent";
|
|
54
|
+
const result = stripGeneratedMarker(input);
|
|
55
|
+
expect(result.changed).toBe(false);
|
|
56
|
+
expect(result.content).toBe(input);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("strips full header block with advisory lines", () => {
|
|
60
|
+
const input = [
|
|
61
|
+
`// ${GENERATED_MARKER}`,
|
|
62
|
+
"// DO NOT EDIT THIS FILE. Changes are overwritten on the next build.",
|
|
63
|
+
"// Owner command: foo.generate",
|
|
64
|
+
"// Edit instead: template.ts",
|
|
65
|
+
"// Regenerate: forge foo.generate",
|
|
66
|
+
"",
|
|
67
|
+
"actual content",
|
|
68
|
+
].join("\n");
|
|
69
|
+
const result = stripGeneratedMarker(input);
|
|
70
|
+
expect(result.changed).toBe(true);
|
|
71
|
+
expect(result.content).not.toContain(GENERATED_MARKER);
|
|
72
|
+
expect(result.content.trim()).toBe("actual content");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("buildGeneratedHeader", () => {
|
|
77
|
+
test("line-slash style for .ts files", () => {
|
|
78
|
+
const header = buildGeneratedHeader({
|
|
79
|
+
filePath: "src/generated.ts",
|
|
80
|
+
ownerCommand: "foo.generate",
|
|
81
|
+
});
|
|
82
|
+
expect(header).toContain(`// ${GENERATED_MARKER}`);
|
|
83
|
+
expect(header).toContain("// DO NOT EDIT");
|
|
84
|
+
expect(header).toContain("Owner command: foo.generate");
|
|
85
|
+
expect(header).toContain("Regenerate: forge foo.generate");
|
|
86
|
+
expect(header.endsWith("\n")).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("line-hash style for .yml files", () => {
|
|
90
|
+
const header = buildGeneratedHeader({
|
|
91
|
+
filePath: "config/generated.yml",
|
|
92
|
+
ownerCommand: "foo.generate",
|
|
93
|
+
});
|
|
94
|
+
expect(header).toContain(`# ${GENERATED_MARKER}`);
|
|
95
|
+
expect(header).toContain("# DO NOT EDIT");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("block-html style for .md files", () => {
|
|
99
|
+
const header = buildGeneratedHeader({
|
|
100
|
+
filePath: "docs/generated.md",
|
|
101
|
+
ownerCommand: "foo.generate",
|
|
102
|
+
});
|
|
103
|
+
expect(header).toContain("<!--");
|
|
104
|
+
expect(header).toContain(GENERATED_MARKER);
|
|
105
|
+
expect(header).toContain("-->");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("block-css style for .css files", () => {
|
|
109
|
+
const header = buildGeneratedHeader({
|
|
110
|
+
filePath: "styles/generated.css",
|
|
111
|
+
ownerCommand: "foo.generate",
|
|
112
|
+
});
|
|
113
|
+
expect(header).toContain("/*");
|
|
114
|
+
expect(header).toContain(GENERATED_MARKER);
|
|
115
|
+
expect(header).toContain("*/");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("includes --site when site is provided", () => {
|
|
119
|
+
const header = buildGeneratedHeader({
|
|
120
|
+
filePath: "src/generated.ts",
|
|
121
|
+
ownerCommand: "foo.generate",
|
|
122
|
+
site: "warpgogol-com",
|
|
123
|
+
});
|
|
124
|
+
expect(header).toContain("--site warpgogol-com");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("uses custom commandPrefix", () => {
|
|
128
|
+
const header = buildGeneratedHeader({
|
|
129
|
+
filePath: "src/generated.ts",
|
|
130
|
+
ownerCommand: "foo.generate",
|
|
131
|
+
commandPrefix: "pnpm exec werkstatt run",
|
|
132
|
+
});
|
|
133
|
+
expect(header).toContain("pnpm exec werkstatt run foo.generate");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("includes templatePath in Edit instead line", () => {
|
|
137
|
+
const header = buildGeneratedHeader({
|
|
138
|
+
filePath: "src/generated.ts",
|
|
139
|
+
ownerCommand: "foo.generate",
|
|
140
|
+
templatePath: "templates/foo.ts",
|
|
141
|
+
});
|
|
142
|
+
expect(header).toContain("Edit instead: templates/foo.ts");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("isGeneratedMarkerTextCandidate", () => {
|
|
147
|
+
test("returns true for text file extensions", () => {
|
|
148
|
+
expect(isGeneratedMarkerTextCandidate("file.ts")).toBe(true);
|
|
149
|
+
expect(isGeneratedMarkerTextCandidate("file.js")).toBe(true);
|
|
150
|
+
expect(isGeneratedMarkerTextCandidate("file.md")).toBe(true);
|
|
151
|
+
expect(isGeneratedMarkerTextCandidate("file.yml")).toBe(true);
|
|
152
|
+
expect(isGeneratedMarkerTextCandidate("file.json")).toBe(true);
|
|
153
|
+
expect(isGeneratedMarkerTextCandidate("file.astro")).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("returns false for non-text extensions", () => {
|
|
157
|
+
expect(isGeneratedMarkerTextCandidate("file.png")).toBe(false);
|
|
158
|
+
expect(isGeneratedMarkerTextCandidate("file.webp")).toBe(false);
|
|
159
|
+
expect(isGeneratedMarkerTextCandidate("file.mp4")).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { byteHash } from "../utils/hash.ts";
|
|
3
|
+
|
|
4
|
+
describe("byteHash", () => {
|
|
5
|
+
test("returns sha256-prefixed hex string", () => {
|
|
6
|
+
const result = byteHash("hello");
|
|
7
|
+
expect(result).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("is deterministic for same input", () => {
|
|
11
|
+
expect(byteHash("test")).toBe(byteHash("test"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("differs for different input", () => {
|
|
15
|
+
expect(byteHash("a")).not.toBe(byteHash("b"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("accepts Uint8Array", () => {
|
|
19
|
+
const data = new Uint8Array([104, 101, 108, 108, 111]); // "hello"
|
|
20
|
+
expect(byteHash(data)).toBe(byteHash("hello"));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("matches known SHA-256 value", () => {
|
|
24
|
+
// SHA-256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
|
|
25
|
+
expect(byteHash("hello")).toBe(
|
|
26
|
+
"sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("empty string produces valid hash", () => {
|
|
31
|
+
expect(byteHash("")).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
32
|
+
// SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
|
33
|
+
expect(byteHash("")).toBe(
|
|
34
|
+
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
import { runRfcImplementStamp } from "../../os/rfc/handlers/implement-stamp.ts";
|
|
8
|
+
import type { ForgeCommandInput, ForgeRuntimeContext, ForgeLogger } from "../../src/types.ts";
|
|
9
|
+
|
|
10
|
+
// ─── Test helpers ─────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const noopLogger: ForgeLogger = {
|
|
13
|
+
section: () => {},
|
|
14
|
+
info: () => {},
|
|
15
|
+
warn: () => {},
|
|
16
|
+
error: () => {},
|
|
17
|
+
success: () => {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
21
|
+
return {
|
|
22
|
+
workspaceRoot,
|
|
23
|
+
logger: noopLogger,
|
|
24
|
+
dryRun: false,
|
|
25
|
+
outputFormat: "json",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeInput(id: string, commit: string, dryRun?: boolean): ForgeCommandInput {
|
|
30
|
+
return {
|
|
31
|
+
argv: [],
|
|
32
|
+
flags: {
|
|
33
|
+
id,
|
|
34
|
+
"implementation-commit": commit,
|
|
35
|
+
...(dryRun ? { "dry-run": true } : {}),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeInputAutoDetect(id: string, dryRun?: boolean): ForgeCommandInput {
|
|
41
|
+
return {
|
|
42
|
+
argv: [],
|
|
43
|
+
flags: {
|
|
44
|
+
id,
|
|
45
|
+
...(dryRun ? { "dry-run": true } : {}),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const RFC_BODY = (id: string, status: string, criteriaChecked: boolean): string => `---
|
|
51
|
+
id: ${id}
|
|
52
|
+
title: "Test RFC"
|
|
53
|
+
status: ${status}
|
|
54
|
+
kind: policy
|
|
55
|
+
scope: workspace
|
|
56
|
+
owners:
|
|
57
|
+
- architecture
|
|
58
|
+
reviewers:
|
|
59
|
+
- human:test
|
|
60
|
+
createdAt: 2026-07-21
|
|
61
|
+
updatedAt: 2026-07-21
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
# ${id}: Test RFC
|
|
65
|
+
|
|
66
|
+
## Context
|
|
67
|
+
|
|
68
|
+
Test context.
|
|
69
|
+
|
|
70
|
+
## Problem
|
|
71
|
+
|
|
72
|
+
Test problem.
|
|
73
|
+
|
|
74
|
+
## Decision
|
|
75
|
+
|
|
76
|
+
Test decision.
|
|
77
|
+
|
|
78
|
+
## Architectural fit
|
|
79
|
+
|
|
80
|
+
Test fit.
|
|
81
|
+
|
|
82
|
+
## Design
|
|
83
|
+
|
|
84
|
+
Test design.
|
|
85
|
+
|
|
86
|
+
## Rollout
|
|
87
|
+
|
|
88
|
+
Test rollout.
|
|
89
|
+
|
|
90
|
+
## Alternatives considered
|
|
91
|
+
|
|
92
|
+
None.
|
|
93
|
+
|
|
94
|
+
## Risks
|
|
95
|
+
|
|
96
|
+
None.
|
|
97
|
+
|
|
98
|
+
## Acceptance criteria
|
|
99
|
+
|
|
100
|
+
- [${criteriaChecked ? "x" : " "}] First criterion (evidence: src/foo.ts:10, unit test)
|
|
101
|
+
- [${criteriaChecked ? "x" : " "}] Second criterion (evidence: src/bar.ts:20, integration test)
|
|
102
|
+
- [${criteriaChecked ? "x" : " "}] Third criterion (evidence: src/baz.ts:30, e2e test)
|
|
103
|
+
|
|
104
|
+
## Implementation notes for agents
|
|
105
|
+
|
|
106
|
+
Test notes.
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
const RFC_BODY_WITH_DEPS = (
|
|
110
|
+
id: string,
|
|
111
|
+
status: string,
|
|
112
|
+
criteriaChecked: boolean,
|
|
113
|
+
dependsOn: string[],
|
|
114
|
+
): string => `---
|
|
115
|
+
id: ${id}
|
|
116
|
+
title: "Test RFC"
|
|
117
|
+
status: ${status}
|
|
118
|
+
kind: policy
|
|
119
|
+
scope: workspace
|
|
120
|
+
owners:
|
|
121
|
+
- architecture
|
|
122
|
+
reviewers:
|
|
123
|
+
- human:test
|
|
124
|
+
createdAt: 2026-07-21
|
|
125
|
+
updatedAt: 2026-07-21
|
|
126
|
+
dependsOn:
|
|
127
|
+
${dependsOn.map((d) => ` - ${d}`).join("\n")}
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
# ${id}: Test RFC
|
|
131
|
+
|
|
132
|
+
## Context
|
|
133
|
+
|
|
134
|
+
Test context.
|
|
135
|
+
|
|
136
|
+
## Problem
|
|
137
|
+
|
|
138
|
+
Test problem.
|
|
139
|
+
|
|
140
|
+
## Decision
|
|
141
|
+
|
|
142
|
+
Test decision.
|
|
143
|
+
|
|
144
|
+
## Architectural fit
|
|
145
|
+
|
|
146
|
+
Test fit.
|
|
147
|
+
|
|
148
|
+
## Design
|
|
149
|
+
|
|
150
|
+
Test design.
|
|
151
|
+
|
|
152
|
+
## Rollout
|
|
153
|
+
|
|
154
|
+
Test rollout.
|
|
155
|
+
|
|
156
|
+
## Alternatives considered
|
|
157
|
+
|
|
158
|
+
None.
|
|
159
|
+
|
|
160
|
+
## Risks
|
|
161
|
+
|
|
162
|
+
None.
|
|
163
|
+
|
|
164
|
+
## Acceptance criteria
|
|
165
|
+
|
|
166
|
+
- [${criteriaChecked ? "x" : " "}] First criterion (evidence: src/foo.ts:10, unit test)
|
|
167
|
+
- [${criteriaChecked ? "x" : " "}] Second criterion (evidence: src/bar.ts:20, integration test)
|
|
168
|
+
- [${criteriaChecked ? "x" : " "}] Third criterion (evidence: src/baz.ts:30, e2e test)
|
|
169
|
+
|
|
170
|
+
## Implementation notes for agents
|
|
171
|
+
|
|
172
|
+
Test notes.
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
async function makeGitRepo(dir: string): Promise<void> {
|
|
176
|
+
execFileSync("git", ["init"], { cwd: dir, timeout: 5000 });
|
|
177
|
+
execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: dir, timeout: 5000 });
|
|
178
|
+
execFileSync("git", ["config", "user.name", "Test"], { cwd: dir, timeout: 5000 });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function commitAll(dir: string, message: string): Promise<string> {
|
|
182
|
+
execFileSync("git", ["add", "-A"], { cwd: dir, timeout: 5000 });
|
|
183
|
+
execFileSync("git", ["commit", "-m", message], { cwd: dir, timeout: 5000 });
|
|
184
|
+
return execFileSync("git", ["rev-parse", "HEAD"], { cwd: dir, timeout: 5000 }).toString().trim();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
describe("rfc.implement.stamp", () => {
|
|
190
|
+
let workspaceRoot: string;
|
|
191
|
+
|
|
192
|
+
beforeEach(async () => {
|
|
193
|
+
workspaceRoot = await mkdtemp(join(tmpdir(), "rfc-stamp-test-"));
|
|
194
|
+
await mkdir(join(workspaceRoot, "docs", "rfcs"), { recursive: true });
|
|
195
|
+
await makeGitRepo(workspaceRoot);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
afterEach(async () => {
|
|
199
|
+
await rm(workspaceRoot, { recursive: true, force: true });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("rejects an RFC not in accepted status (RFC-IMP-01)", async () => {
|
|
203
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
204
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "draft", true));
|
|
205
|
+
const commit = await commitAll(workspaceRoot, "Initial commit RFC-0001");
|
|
206
|
+
|
|
207
|
+
const result = await runRfcImplementStamp(
|
|
208
|
+
makeInput("RFC-0001", commit),
|
|
209
|
+
makeContext(workspaceRoot),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(result.exitCode).toBe(1);
|
|
213
|
+
expect(result.data?.status).toBe("fail");
|
|
214
|
+
expect(result.data?.violations).toHaveLength(1);
|
|
215
|
+
expect(result.data?.violations[0]?.rule).toBe("RFC-IMP-01");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("rejects unchecked criteria (RFC-IMP-02)", async () => {
|
|
219
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
220
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", false));
|
|
221
|
+
const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
|
|
222
|
+
|
|
223
|
+
const result = await runRfcImplementStamp(
|
|
224
|
+
makeInput("RFC-0001", commit),
|
|
225
|
+
makeContext(workspaceRoot),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(result.exitCode).toBe(1);
|
|
229
|
+
expect(result.data?.status).toBe("fail");
|
|
230
|
+
const rules = result.data?.violations.map((v) => v.rule);
|
|
231
|
+
expect(rules).toContain("RFC-IMP-02");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("rejects an RFC file with uncommitted changes (RFC-IMP-04)", async () => {
|
|
235
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
236
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
237
|
+
const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
|
|
238
|
+
|
|
239
|
+
// Make an uncommitted edit to the RFC file itself
|
|
240
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true) + "\nextra\n");
|
|
241
|
+
|
|
242
|
+
const result = await runRfcImplementStamp(
|
|
243
|
+
makeInput("RFC-0001", commit),
|
|
244
|
+
makeContext(workspaceRoot),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(result.exitCode).toBe(1);
|
|
248
|
+
expect(result.data?.status).toBe("fail");
|
|
249
|
+
const rules = result.data?.violations.map((v) => v.rule);
|
|
250
|
+
expect(rules).toContain("RFC-IMP-04");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("ignores uncommitted changes in unrelated files (multi-agent)", async () => {
|
|
254
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
255
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
256
|
+
const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
|
|
257
|
+
|
|
258
|
+
// Create uncommitted files from other agents — must NOT block stamping
|
|
259
|
+
await writeFile(join(workspaceRoot, "dirty.txt"), "dirty");
|
|
260
|
+
await mkdir(join(workspaceRoot, "src"), { recursive: true });
|
|
261
|
+
await writeFile(join(workspaceRoot, "src", "other-agent.ts"), "// other agent\n");
|
|
262
|
+
|
|
263
|
+
const result = await runRfcImplementStamp(
|
|
264
|
+
makeInput("RFC-0001", commit),
|
|
265
|
+
makeContext(workspaceRoot),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
expect(result.exitCode).toBe(0);
|
|
269
|
+
expect(result.data?.status).toBe("pass");
|
|
270
|
+
expect(result.data?.violations).toEqual([]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("rejects a commit that does not reference the RFC (RFC-IMP-03)", async () => {
|
|
274
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
275
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
276
|
+
await commitAll(workspaceRoot, "Add RFC-0001");
|
|
277
|
+
|
|
278
|
+
// Make a separate commit that doesn't reference the RFC
|
|
279
|
+
await mkdir(join(workspaceRoot, "src"), { recursive: true });
|
|
280
|
+
await writeFile(join(workspaceRoot, "src", "change.ts"), "// unrelated\n");
|
|
281
|
+
const unrelatedCommit = await commitAll(workspaceRoot, "Some unrelated change");
|
|
282
|
+
|
|
283
|
+
const result = await runRfcImplementStamp(
|
|
284
|
+
makeInput("RFC-0001", unrelatedCommit),
|
|
285
|
+
makeContext(workspaceRoot),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
expect(result.exitCode).toBe(1);
|
|
289
|
+
expect(result.data?.status).toBe("fail");
|
|
290
|
+
const rules = result.data?.violations.map((v) => v.rule);
|
|
291
|
+
expect(rules).toContain("RFC-IMP-03");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("rejects a non-existent RFC (RFC-IMP-01)", async () => {
|
|
295
|
+
// Need at least one file to commit
|
|
296
|
+
await writeFile(join(workspaceRoot, "README.md"), "# test");
|
|
297
|
+
const commit = await commitAll(workspaceRoot, "Initial commit");
|
|
298
|
+
|
|
299
|
+
const result = await runRfcImplementStamp(
|
|
300
|
+
makeInput("RFC-9999", commit),
|
|
301
|
+
makeContext(workspaceRoot),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
expect(result.exitCode).toBe(1);
|
|
305
|
+
expect(result.data?.status).toBe("fail");
|
|
306
|
+
expect(result.data?.violations[0]?.rule).toBe("RFC-IMP-01");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("passes in dry-run mode for a valid accepted RFC", async () => {
|
|
310
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
311
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
312
|
+
const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
|
|
313
|
+
|
|
314
|
+
const result = await runRfcImplementStamp(
|
|
315
|
+
makeInput("RFC-0001", commit, true),
|
|
316
|
+
makeContext(workspaceRoot),
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(result.exitCode).toBe(0);
|
|
320
|
+
expect(result.data?.status).toBe("pass");
|
|
321
|
+
expect(result.data?.data?.rfcId).toBe("RFC-0001");
|
|
322
|
+
expect(result.data?.data?.criteriaChecked).toBe(3);
|
|
323
|
+
expect(result.data?.violations).toEqual([]);
|
|
324
|
+
|
|
325
|
+
// Verify the file was NOT mutated
|
|
326
|
+
const content = await readFile(rfcFile, "utf-8");
|
|
327
|
+
expect(content).toContain("status: accepted");
|
|
328
|
+
expect(content).not.toContain("status: implemented");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("atomically stamps a valid accepted RFC to implemented", async () => {
|
|
332
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
333
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
334
|
+
const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
|
|
335
|
+
|
|
336
|
+
const result = await runRfcImplementStamp(
|
|
337
|
+
makeInput("RFC-0001", commit),
|
|
338
|
+
makeContext(workspaceRoot),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(result.exitCode).toBe(0);
|
|
342
|
+
expect(result.data?.status).toBe("pass");
|
|
343
|
+
expect(result.data?.data?.rfcId).toBe("RFC-0001");
|
|
344
|
+
expect(result.data?.data?.implementationCommit).toBe(commit);
|
|
345
|
+
expect(result.data?.data?.criteriaChecked).toBe(3);
|
|
346
|
+
expect(result.data?.violations).toEqual([]);
|
|
347
|
+
|
|
348
|
+
// Verify the file WAS mutated
|
|
349
|
+
const content = await readFile(rfcFile, "utf-8");
|
|
350
|
+
expect(content).toContain("status: implemented");
|
|
351
|
+
expect(content).toMatch(/implementedAt: \d{4}-\d{2}-\d{2}/);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// ── RFC-0756: auto-detect tests ─────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
it("auto-detects the implementation commit when --implementation-commit is omitted (RFC-0756)", async () => {
|
|
357
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
358
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
359
|
+
const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
|
|
360
|
+
|
|
361
|
+
const result = await runRfcImplementStamp(
|
|
362
|
+
makeInputAutoDetect("RFC-0001"),
|
|
363
|
+
makeContext(workspaceRoot),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
expect(result.exitCode).toBe(0);
|
|
367
|
+
expect(result.data?.status).toBe("pass");
|
|
368
|
+
expect(result.data?.data?.rfcId).toBe("RFC-0001");
|
|
369
|
+
expect(result.data?.data?.implementationCommit).toBe(commit);
|
|
370
|
+
expect(result.data?.violations).toEqual([]);
|
|
371
|
+
|
|
372
|
+
const content = await readFile(rfcFile, "utf-8");
|
|
373
|
+
expect(content).toContain("status: implemented");
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("lists multiple candidate commits when auto-detect finds more than one (RFC-0756)", async () => {
|
|
377
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
378
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
379
|
+
await commitAll(workspaceRoot, "Implement RFC-0001 step 1");
|
|
380
|
+
await writeFile(join(workspaceRoot, "src.txt"), "step 2");
|
|
381
|
+
await commitAll(workspaceRoot, "Implement RFC-0001 step 2");
|
|
382
|
+
|
|
383
|
+
const result = await runRfcImplementStamp(
|
|
384
|
+
makeInputAutoDetect("RFC-0001"),
|
|
385
|
+
makeContext(workspaceRoot),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
expect(result.exitCode).toBe(1);
|
|
389
|
+
expect(result.data?.status).toBe("fail");
|
|
390
|
+
const imp03 = result.data?.violations.find((v) => v.rule === "RFC-IMP-03");
|
|
391
|
+
expect(imp03).toBeDefined();
|
|
392
|
+
expect(imp03?.message).toContain("Multiple commits reference RFC-0001");
|
|
393
|
+
expect(imp03?.message).toContain("Pass --implementation-commit");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("errors with a clear message when no commit references the RFC (RFC-0756)", async () => {
|
|
397
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
398
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
399
|
+
await commitAll(workspaceRoot, "Initial commit without RFC ref");
|
|
400
|
+
|
|
401
|
+
const result = await runRfcImplementStamp(
|
|
402
|
+
makeInputAutoDetect("RFC-0001"),
|
|
403
|
+
makeContext(workspaceRoot),
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
expect(result.exitCode).toBe(1);
|
|
407
|
+
expect(result.data?.status).toBe("fail");
|
|
408
|
+
const imp03 = result.data?.violations.find((v) => v.rule === "RFC-IMP-03");
|
|
409
|
+
expect(imp03).toBeDefined();
|
|
410
|
+
expect(imp03?.message).toContain("No commit referencing RFC-0001 found");
|
|
411
|
+
expect(imp03?.message).toContain("Pass --implementation-commit");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("explicit --implementation-commit overrides auto-detect when multiple commits exist (RFC-0756)", async () => {
|
|
415
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
416
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
417
|
+
await commitAll(workspaceRoot, "Implement RFC-0001 step 1");
|
|
418
|
+
await writeFile(join(workspaceRoot, "src.txt"), "step 2");
|
|
419
|
+
const secondCommit = await commitAll(workspaceRoot, "Implement RFC-0001 step 2");
|
|
420
|
+
|
|
421
|
+
const result = await runRfcImplementStamp(
|
|
422
|
+
makeInput("RFC-0001", secondCommit),
|
|
423
|
+
makeContext(workspaceRoot),
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
expect(result.exitCode).toBe(0);
|
|
427
|
+
expect(result.data?.status).toBe("pass");
|
|
428
|
+
expect(result.data?.data?.implementationCommit).toBe(secondCommit);
|
|
429
|
+
expect(result.data?.violations).toEqual([]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ── RFC-0795: RFC-IMP-07 dependsOn dependency gate tests ──────────────────
|
|
433
|
+
|
|
434
|
+
it("blocks stamping when a dependsOn RFC is not implemented (RFC-IMP-07)", async () => {
|
|
435
|
+
const depFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-dep.md");
|
|
436
|
+
await writeFile(depFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
437
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0002-main.md");
|
|
438
|
+
await writeFile(rfcFile, RFC_BODY_WITH_DEPS("RFC-0002", "accepted", true, ["RFC-0001"]));
|
|
439
|
+
const commit = await commitAll(workspaceRoot, "Implement RFC-0002");
|
|
440
|
+
|
|
441
|
+
const result = await runRfcImplementStamp(
|
|
442
|
+
makeInput("RFC-0002", commit),
|
|
443
|
+
makeContext(workspaceRoot),
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
expect(result.exitCode).toBe(1);
|
|
447
|
+
expect(result.data?.status).toBe("fail");
|
|
448
|
+
const imp07 = result.data?.violations.find((v) => v.rule === "RFC-IMP-07");
|
|
449
|
+
expect(imp07).toBeDefined();
|
|
450
|
+
expect(imp07?.message).toContain("RFC-0001");
|
|
451
|
+
expect(imp07?.message).toContain("not implemented");
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("blocks stamping when a dependsOn RFC does not exist (RFC-IMP-07)", async () => {
|
|
455
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0002-main.md");
|
|
456
|
+
await writeFile(rfcFile, RFC_BODY_WITH_DEPS("RFC-0002", "accepted", true, ["RFC-9999"]));
|
|
457
|
+
const commit = await commitAll(workspaceRoot, "Implement RFC-0002");
|
|
458
|
+
|
|
459
|
+
const result = await runRfcImplementStamp(
|
|
460
|
+
makeInput("RFC-0002", commit),
|
|
461
|
+
makeContext(workspaceRoot),
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
expect(result.exitCode).toBe(1);
|
|
465
|
+
expect(result.data?.status).toBe("fail");
|
|
466
|
+
const imp07 = result.data?.violations.find((v) => v.rule === "RFC-IMP-07");
|
|
467
|
+
expect(imp07).toBeDefined();
|
|
468
|
+
expect(imp07?.message).toContain("RFC-9999");
|
|
469
|
+
expect(imp07?.message).toContain("does not exist");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("passes when all dependsOn RFCs are implemented (RFC-IMP-07)", async () => {
|
|
473
|
+
const depFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-dep.md");
|
|
474
|
+
await writeFile(depFile, RFC_BODY("RFC-0001", "implemented", true));
|
|
475
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0002-main.md");
|
|
476
|
+
await writeFile(rfcFile, RFC_BODY_WITH_DEPS("RFC-0002", "accepted", true, ["RFC-0001"]));
|
|
477
|
+
const commit = await commitAll(workspaceRoot, "Implement RFC-0002");
|
|
478
|
+
|
|
479
|
+
const result = await runRfcImplementStamp(
|
|
480
|
+
makeInput("RFC-0002", commit),
|
|
481
|
+
makeContext(workspaceRoot),
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
expect(result.exitCode).toBe(0);
|
|
485
|
+
expect(result.data?.status).toBe("pass");
|
|
486
|
+
const imp07 = result.data?.violations.find((v) => v.rule === "RFC-IMP-07");
|
|
487
|
+
expect(imp07).toBeUndefined();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("passes when dependsOn is absent (RFC-IMP-07)", async () => {
|
|
491
|
+
const rfcFile = join(workspaceRoot, "docs", "rfcs", "rfc-0001-test-rfc.md");
|
|
492
|
+
await writeFile(rfcFile, RFC_BODY("RFC-0001", "accepted", true));
|
|
493
|
+
const commit = await commitAll(workspaceRoot, "Implement RFC-0001");
|
|
494
|
+
|
|
495
|
+
const result = await runRfcImplementStamp(
|
|
496
|
+
makeInput("RFC-0001", commit),
|
|
497
|
+
makeContext(workspaceRoot),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
expect(result.exitCode).toBe(0);
|
|
501
|
+
expect(result.data?.status).toBe("pass");
|
|
502
|
+
const imp07 = result.data?.violations.find((v) => v.rule === "RFC-IMP-07");
|
|
503
|
+
expect(imp07).toBeUndefined();
|
|
504
|
+
});
|
|
505
|
+
});
|