@warpgogol/forge 5.3.4 → 6.2.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.
- package/CHANGELOG.md +8 -0
- package/bin/cli.ts +7 -2
- package/dist/bin/cli.js +7 -2
- package/dist/bin/cli.js.map +1 -1
- package/dist/os/adr/adr.module.d.ts.map +1 -1
- package/dist/os/adr/adr.module.js +2 -0
- package/dist/os/adr/adr.module.js.map +1 -1
- package/dist/os/audit/audit.module.d.ts.map +1 -1
- package/dist/os/audit/audit.module.js +2 -0
- package/dist/os/audit/audit.module.js.map +1 -1
- package/dist/os/compass/compass.module.d.ts.map +1 -1
- package/dist/os/compass/compass.module.js +47 -15
- package/dist/os/compass/compass.module.js.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
- package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
- package/dist/os/compass/handlers/compass-audit-handler.js +40 -10
- package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
- package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
- package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
- package/dist/os/compass/handlers/compass-docs-validate.js +328 -0
- package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
- package/dist/os/compass/handlers/summary-record.d.ts +11 -2
- package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
- package/dist/os/compass/handlers/summary-record.js +95 -15
- package/dist/os/compass/handlers/summary-record.js.map +1 -1
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +124 -73
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/os/core/handlers/file-size-lint.d.ts.map +1 -1
- package/dist/os/core/handlers/file-size-lint.js +2 -0
- package/dist/os/core/handlers/file-size-lint.js.map +1 -1
- package/dist/os/exploration/exploration.module.d.ts.map +1 -1
- package/dist/os/exploration/exploration.module.js +3 -0
- package/dist/os/exploration/exploration.module.js.map +1 -1
- package/dist/os/naming/naming-convention.d.ts.map +1 -1
- package/dist/os/naming/naming-convention.js +11 -1
- package/dist/os/naming/naming-convention.js.map +1 -1
- package/dist/os/naming/naming.module.d.ts.map +1 -1
- package/dist/os/naming/naming.module.js +9 -2
- package/dist/os/naming/naming.module.js.map +1 -1
- package/dist/os/notes/notes.module.d.ts.map +1 -1
- package/dist/os/notes/notes.module.js +17 -6
- package/dist/os/notes/notes.module.js.map +1 -1
- package/dist/os/plan/plan.module.d.ts.map +1 -1
- package/dist/os/plan/plan.module.js +2 -0
- package/dist/os/plan/plan.module.js.map +1 -1
- package/dist/os/plugin/plugin.module.d.ts.map +1 -1
- package/dist/os/plugin/plugin.module.js +9 -2
- package/dist/os/plugin/plugin.module.js.map +1 -1
- package/dist/os/program/program.module.d.ts.map +1 -1
- package/dist/os/program/program.module.js +19 -4
- package/dist/os/program/program.module.js.map +1 -1
- package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
- package/dist/os/queue/handlers/queue-validate.js +154 -7
- package/dist/os/queue/handlers/queue-validate.js.map +1 -1
- package/dist/os/queue/manifest.d.ts +17 -1
- package/dist/os/queue/manifest.d.ts.map +1 -1
- package/dist/os/queue/manifest.js +127 -6
- package/dist/os/queue/manifest.js.map +1 -1
- package/dist/os/queue/queue.module.d.ts.map +1 -1
- package/dist/os/queue/queue.module.js +32 -4
- package/dist/os/queue/queue.module.js.map +1 -1
- package/dist/os/queue/types.d.ts +100 -1
- package/dist/os/queue/types.d.ts.map +1 -1
- package/dist/os/queue/types.js +44 -0
- package/dist/os/queue/types.js.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
- package/dist/os/rfc/handlers/implement-stamp.js +30 -8
- package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
- package/dist/os/rfc/rfc.module.d.ts.map +1 -1
- package/dist/os/rfc/rfc.module.js +25 -6
- package/dist/os/rfc/rfc.module.js.map +1 -1
- package/dist/os/rfc/types.d.ts +12 -2
- package/dist/os/rfc/types.d.ts.map +1 -1
- package/dist/os/rfc/types.js +2 -2
- package/dist/os/rfc/types.js.map +1 -1
- package/dist/os/rfc/verification-evidence.d.ts +21 -0
- package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
- package/dist/os/rfc/verification-evidence.js +134 -23
- package/dist/os/rfc/verification-evidence.js.map +1 -1
- package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
- package/dist/os/rfc/verification-refresh.js +66 -37
- package/dist/os/rfc/verification-refresh.js.map +1 -1
- package/dist/os/session/handlers/save.d.ts.map +1 -1
- package/dist/os/session/handlers/save.js +42 -5
- package/dist/os/session/handlers/save.js.map +1 -1
- package/dist/os/session/session.module.d.ts.map +1 -1
- package/dist/os/session/session.module.js +5 -0
- package/dist/os/session/session.module.js.map +1 -1
- package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
- package/dist/os/spec/live-spec-merge.js +55 -134
- package/dist/os/spec/live-spec-merge.js.map +1 -1
- package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
- package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
- package/dist/os/spec/live-spec-rebuild.js +272 -0
- package/dist/os/spec/live-spec-rebuild.js.map +1 -0
- package/dist/os/spec/live-spec-shared.d.ts +62 -0
- package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
- package/dist/os/spec/live-spec-shared.js +315 -0
- package/dist/os/spec/live-spec-shared.js.map +1 -0
- package/dist/os/spec/live-spec-types.d.ts +17 -1
- package/dist/os/spec/live-spec-types.d.ts.map +1 -1
- package/dist/os/spec/live-spec-types.js +3 -0
- package/dist/os/spec/live-spec-types.js.map +1 -1
- package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
- package/dist/os/spec/live-spec-validate.js +105 -2
- package/dist/os/spec/live-spec-validate.js.map +1 -1
- package/dist/os/spec/spec-materialize.d.ts +2 -0
- package/dist/os/spec/spec-materialize.d.ts.map +1 -1
- package/dist/os/spec/spec-materialize.js +45 -5
- package/dist/os/spec/spec-materialize.js.map +1 -1
- package/dist/os/spec/spec-schema.d.ts +19 -0
- package/dist/os/spec/spec-schema.d.ts.map +1 -1
- package/dist/os/spec/spec-schema.js +30 -0
- package/dist/os/spec/spec-schema.js.map +1 -1
- package/dist/os/spec/spec-status.d.ts +4 -0
- package/dist/os/spec/spec-status.d.ts.map +1 -1
- package/dist/os/spec/spec-status.js +19 -1
- package/dist/os/spec/spec-status.js.map +1 -1
- package/dist/os/spec/spec-validate.d.ts.map +1 -1
- package/dist/os/spec/spec-validate.js +55 -8
- package/dist/os/spec/spec-validate.js.map +1 -1
- package/dist/os/spec/spec.module.d.ts.map +1 -1
- package/dist/os/spec/spec.module.js +84 -8
- package/dist/os/spec/spec.module.js.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
- package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
- package/dist/os/werkstatt/werkstatt.module.js +13 -4
- package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
- package/dist/os/workflow/workflow.module.d.ts.map +1 -1
- package/dist/os/workflow/workflow.module.js +13 -5
- package/dist/os/workflow/workflow.module.js.map +1 -1
- package/dist/src/cli-flags.d.ts.map +1 -1
- package/dist/src/cli-flags.js +25 -5
- package/dist/src/cli-flags.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +3 -0
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +2 -0
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +2 -3
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/init.d.ts.map +1 -1
- package/dist/src/onboarding/init.js +8 -3
- package/dist/src/onboarding/init.js.map +1 -1
- package/dist/src/pipeline-status.d.ts +14 -1
- package/dist/src/pipeline-status.d.ts.map +1 -1
- package/dist/src/pipeline-status.js +28 -8
- package/dist/src/pipeline-status.js.map +1 -1
- package/dist/src/types.d.ts +7 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils/generated-marker.d.ts +8 -0
- package/dist/src/utils/generated-marker.d.ts.map +1 -1
- package/dist/src/utils/generated-marker.js +25 -0
- package/dist/src/utils/generated-marker.js.map +1 -1
- package/dist/src/validators/skill-validate.d.ts.map +1 -1
- package/dist/src/validators/skill-validate.js +2 -4
- package/dist/src/validators/skill-validate.js.map +1 -1
- package/os/adr/adr.module.ts +2 -0
- package/os/audit/audit.module.ts +2 -0
- package/os/compass/compass.module.ts +49 -15
- package/os/compass/handlers/compass-audit-handler.ts +51 -10
- package/os/compass/handlers/compass-docs-validate.ts +361 -0
- package/os/compass/handlers/summary-record.ts +106 -15
- package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
- package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
- package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
- package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
- package/os/compass/handlers/tests/compass-policy.test.ts +7 -1
- package/os/compass/handlers/tests/summary-record.test.ts +127 -0
- package/os/core/core.module.ts +147 -75
- package/os/core/docs-archive.test.ts +136 -0
- package/os/core/handlers/file-size-lint.ts +2 -0
- package/os/exploration/exploration.module.ts +3 -0
- package/os/naming/naming-convention.ts +11 -1
- package/os/naming/naming.module.ts +9 -2
- package/os/notes/notes.module.ts +17 -6
- package/os/plan/plan-0000-template.md +150 -0
- package/os/plan/plan.module.ts +2 -0
- package/os/plugin/plugin.module.ts +9 -2
- package/os/program/program.module.ts +19 -4
- package/os/queue/decision-ledger.test.ts +163 -0
- package/os/queue/handlers/queue-validate.ts +190 -8
- package/os/queue/manifest.ts +201 -7
- package/os/queue/queue-validate.test.ts +303 -0
- package/os/queue/queue.module.ts +32 -4
- package/os/queue/types.ts +56 -1
- package/os/rfc/handlers/implement-stamp.ts +37 -8
- package/os/rfc/rfc-read-only-no-side-effects.test.ts +1 -0
- package/os/rfc/rfc.module.ts +27 -6
- package/os/rfc/types.ts +18 -3
- package/os/rfc/verification-emit.test.ts +305 -0
- package/os/rfc/verification-evidence.ts +149 -24
- package/os/rfc/verification-refresh.test.ts +222 -1
- package/os/rfc/verification-refresh.ts +74 -38
- package/os/session/handlers/save.ts +37 -5
- package/os/session/session.module.ts +5 -0
- package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
- package/os/spec/live-spec-list-show-validate.test.ts +10 -6
- package/os/spec/live-spec-merge.test.ts +187 -7
- package/os/spec/live-spec-merge.ts +71 -155
- package/os/spec/live-spec-rebuild.test.ts +317 -0
- package/os/spec/live-spec-rebuild.ts +356 -0
- package/os/spec/live-spec-shared.ts +378 -0
- package/os/spec/live-spec-types.ts +21 -1
- package/os/spec/live-spec-validate.test.ts +317 -0
- package/os/spec/live-spec-validate.ts +106 -2
- package/os/spec/spec-materialize.test.ts +192 -0
- package/os/spec/spec-materialize.ts +48 -4
- package/os/spec/spec-schema.ts +49 -0
- package/os/spec/spec-status.ts +24 -0
- package/os/spec/spec-validate.test.ts +66 -0
- package/os/spec/spec-validate.ts +62 -8
- package/os/spec/spec.module.ts +85 -8
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
- package/os/werkstatt/werkstatt.module.ts +13 -4
- package/os/workflow/workflow.module.ts +13 -5
- package/package.json +1 -1
- package/skills/_shared/fo-pipeline-conventions.md +126 -1
- package/skills/fo/fo-doc-audit/SKILL.md +4 -4
- package/skills/fo/fo-explore/SKILL.md +2 -1
- package/skills/fo/fo-fix/SKILL.md +11 -3
- package/skills/fo/fo-handoff/SKILL.md +4 -6
- package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
- package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
- package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
- package/skills/fo/fo-idea-implement/SKILL.md +53 -10
- package/skills/fo/fo-idea-plan/SKILL.md +28 -6
- package/skills/fo/fo-review/SKILL.md +69 -8
- package/skills/fo/fo-session-retro/SKILL.md +3 -2
- package/skills/fo/fo-step-commit/SKILL.md +2 -0
- package/skills/shared/grilling/SKILL.md +12 -0
- package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
- package/skills/shared/writing-great-skills/SKILL.md +2 -1
- package/src/cli-flags.ts +23 -5
- package/src/config/forge-config.ts +4 -0
- package/src/onboarding/doctor.ts +2 -3
- package/src/onboarding/init.ts +8 -3
- package/src/pipeline-status.ts +32 -8
- package/src/tests/cli-flags.test.ts +20 -0
- package/src/tests/cli-output.test.ts +13 -7
- package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
- package/src/tests/implement-stamp.test.ts +38 -0
- package/src/tests/session-handlers.test.ts +33 -0
- package/src/types.ts +13 -1
- package/src/utils/generated-marker.ts +28 -0
- package/src/validators/skill-validate.ts +2 -4
- package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
- package/skills/fo/fo-review/AXES.md +0 -70
|
@@ -16,6 +16,7 @@ import { join } from "node:path";
|
|
|
16
16
|
import { execFile } from "node:child_process";
|
|
17
17
|
import { promisify } from "node:util";
|
|
18
18
|
import { runCompassAuditValidate } from "../compass-audit-handler.ts";
|
|
19
|
+
import { ambientIo } from "../../../../src/utils/io.ts";
|
|
19
20
|
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
|
|
20
21
|
|
|
21
22
|
const execFileAsync = promisify(execFile);
|
|
@@ -31,15 +32,15 @@ const logger = {
|
|
|
31
32
|
},
|
|
32
33
|
};
|
|
33
34
|
|
|
34
|
-
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
35
|
+
function makeContext(workspaceRoot: string, dryRun = false): ForgeRuntimeContext {
|
|
35
36
|
return {
|
|
36
37
|
workspaceRoot,
|
|
37
38
|
site: undefined,
|
|
38
39
|
siteExplicit: false,
|
|
39
40
|
logger: logger as never,
|
|
40
|
-
dryRun
|
|
41
|
+
dryRun,
|
|
41
42
|
outputFormat: "json",
|
|
42
|
-
io:
|
|
43
|
+
io: ambientIo,
|
|
43
44
|
actualState: undefined as never,
|
|
44
45
|
fileIntents: [],
|
|
45
46
|
} as unknown as ForgeRuntimeContext;
|
|
@@ -131,3 +132,91 @@ describe("compass.audit.validate — RFC-1143 ledger-ineligible skips", () => {
|
|
|
131
132
|
expect(data.skippedIneligible).toBeGreaterThan(0);
|
|
132
133
|
});
|
|
133
134
|
});
|
|
135
|
+
|
|
136
|
+
// RFC-1242 AC-5/AC-6: --prune drops ledger entries for paths that no longer
|
|
137
|
+
// exist while preserving every surviving entry's verdict fields.
|
|
138
|
+
const DEAD_PATH = "packages/gone/deleted.ts";
|
|
139
|
+
const LEDGER_ENTRY = (path: string) =>
|
|
140
|
+
` - path: ${path}\n entityId: "eid-${path}"\n auditedRevision: 7\n auditedHash: "hash-${path}"\n auditedAt: "2026-01-01T00:00:00Z"\n verdict: pass\n agent: human:test\n`;
|
|
141
|
+
|
|
142
|
+
describe("compass.audit.validate --prune — RFC-1242", () => {
|
|
143
|
+
let root: string;
|
|
144
|
+
|
|
145
|
+
beforeEach(async () => {
|
|
146
|
+
root = await mkdtemp(join(tmpdir(), "compass-prune-"));
|
|
147
|
+
await mkdir(join(root, "packages", "foo"), { recursive: true });
|
|
148
|
+
await mkdir(join(root, "docs"), { recursive: true });
|
|
149
|
+
await writeFile(join(root, "packages", "foo", "index.ts"), "export const x = 1;\n", "utf8");
|
|
150
|
+
await writeFile(
|
|
151
|
+
join(root, "docs", "compass-audit-ledger.generated.yaml"),
|
|
152
|
+
`entries:\n${LEDGER_ENTRY(ELIGIBLE_FILE)}${LEDGER_ENTRY(DEAD_PATH)}`,
|
|
153
|
+
"utf8",
|
|
154
|
+
);
|
|
155
|
+
await execFileAsync("git", ["init"], { cwd: root });
|
|
156
|
+
await execFileAsync("git", ["config", "user.email", "test@test.com"], { cwd: root });
|
|
157
|
+
await execFileAsync("git", ["config", "user.name", "Test"], { cwd: root });
|
|
158
|
+
await execFileAsync("git", ["add", "."], { cwd: root });
|
|
159
|
+
await execFileAsync("git", ["commit", "-m", "init"], { cwd: root });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
afterEach(async () => {
|
|
163
|
+
await rm(root, { recursive: true, force: true });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("AC-5: ledger contains no entry whose path does not exist after prune", async () => {
|
|
167
|
+
const result = await runCompassAuditValidate(
|
|
168
|
+
makeInput({ prune: true, root: ["packages"] }),
|
|
169
|
+
makeContext(root),
|
|
170
|
+
);
|
|
171
|
+
const data = result.data as { pruned: number; prunedPaths: string[] };
|
|
172
|
+
expect(data.pruned).toBe(1);
|
|
173
|
+
expect(data.prunedPaths).toEqual([DEAD_PATH]);
|
|
174
|
+
const { readFile } = await import("node:fs/promises");
|
|
175
|
+
const ledgerText = await readFile(
|
|
176
|
+
join(root, "docs", "compass-audit-ledger.generated.yaml"),
|
|
177
|
+
"utf8",
|
|
178
|
+
);
|
|
179
|
+
expect(ledgerText).not.toContain(DEAD_PATH);
|
|
180
|
+
expect(ledgerText).toContain(ELIGIBLE_FILE);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("AC-6: surviving entries keep their verdict fields unchanged", async () => {
|
|
184
|
+
await runCompassAuditValidate(
|
|
185
|
+
makeInput({ prune: true, root: ["packages"] }),
|
|
186
|
+
makeContext(root),
|
|
187
|
+
);
|
|
188
|
+
const { readFile } = await import("node:fs/promises");
|
|
189
|
+
const { parse } = await import("yaml");
|
|
190
|
+
const ledger = parse(
|
|
191
|
+
await readFile(join(root, "docs", "compass-audit-ledger.generated.yaml"), "utf8"),
|
|
192
|
+
) as { entries: Array<Record<string, unknown>> };
|
|
193
|
+
const survivor = ledger.entries.find((e) => e.path === ELIGIBLE_FILE);
|
|
194
|
+
expect(
|
|
195
|
+
survivor,
|
|
196
|
+
"surviving ledger entry must keep its verdict fields — check the keep/drop partition",
|
|
197
|
+
).toMatchObject({
|
|
198
|
+
entityId: `eid-${ELIGIBLE_FILE}`,
|
|
199
|
+
auditedRevision: 7,
|
|
200
|
+
auditedHash: `hash-${ELIGIBLE_FILE}`,
|
|
201
|
+
auditedAt: "2026-01-01T00:00:00Z",
|
|
202
|
+
verdict: "pass",
|
|
203
|
+
agent: "human:test",
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("--prune --dry-run reports the drop set without rewriting the ledger", async () => {
|
|
208
|
+
const { readFile } = await import("node:fs/promises");
|
|
209
|
+
const ledgerPath = join(root, "docs", "compass-audit-ledger.generated.yaml");
|
|
210
|
+
const before = await readFile(ledgerPath, "utf8");
|
|
211
|
+
const result = await runCompassAuditValidate(
|
|
212
|
+
makeInput({ prune: true, root: ["packages"] }),
|
|
213
|
+
makeContext(root, true),
|
|
214
|
+
);
|
|
215
|
+
const data = result.data as { pruned: number };
|
|
216
|
+
expect(data.pruned).toBe(1);
|
|
217
|
+
expect(
|
|
218
|
+
await readFile(ledgerPath, "utf8"),
|
|
219
|
+
"dry-run must not rewrite the ledger — writes must route through context.io honoring context.dryRun",
|
|
220
|
+
).toBe(before);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>
|
|
4
|
+
RFC-1242 AC-1..AC-4: compass.docs.validate scans the docs/*.xml corpus and
|
|
5
|
+
must fail on unresolvable <path> entries (COMPASS-DOC-01) and dead workspace
|
|
6
|
+
ids (COMPASS-DOC-02), warn on unresolvable link targets (COMPASS-DOC-03),
|
|
7
|
+
and flag malformed XML (COMPASS-DOC-00). A green corpus exits 0 with
|
|
8
|
+
status-shaped data.
|
|
9
|
+
</purpose>
|
|
10
|
+
</MODULE_CONTRACT>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
14
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { runCompassDocsValidate } from "../compass-docs-validate.ts";
|
|
18
|
+
import { ambientIo } from "../../../../src/utils/io.ts";
|
|
19
|
+
import type { Diagnostic, ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
|
|
20
|
+
|
|
21
|
+
const logger = {
|
|
22
|
+
section() {},
|
|
23
|
+
info() {},
|
|
24
|
+
warn() {},
|
|
25
|
+
error() {},
|
|
26
|
+
success() {},
|
|
27
|
+
getEvents() {
|
|
28
|
+
return [];
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
33
|
+
return {
|
|
34
|
+
workspaceRoot,
|
|
35
|
+
site: undefined,
|
|
36
|
+
siteExplicit: false,
|
|
37
|
+
logger: logger as never,
|
|
38
|
+
dryRun: false,
|
|
39
|
+
outputFormat: "json",
|
|
40
|
+
io: ambientIo,
|
|
41
|
+
actualState: undefined as never,
|
|
42
|
+
fileIntents: [],
|
|
43
|
+
} as unknown as ForgeRuntimeContext;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeInput(): ForgeCommandInput {
|
|
47
|
+
return { argv: [], flags: {} } as unknown as ForgeCommandInput;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const GREEN_DOC = `<root>
|
|
51
|
+
<path>\`docs/other.xml\`</path>
|
|
52
|
+
<workspace id="pkg-foo"><path>\`packages/foo\`</path></workspace>
|
|
53
|
+
<node id="packages.foo" type="package"><path>\`packages/foo\`</path></node>
|
|
54
|
+
<node id="root.a" type="compass-doc"><link rel="binds-to" target="root.b" /></node>
|
|
55
|
+
<node id="root.b" type="compass-doc" />
|
|
56
|
+
</root>
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
describe("compass.docs.validate — RFC-1242", () => {
|
|
60
|
+
let root: string;
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
root = await mkdtemp(join(tmpdir(), "compass-docs-"));
|
|
64
|
+
await mkdir(join(root, "docs"), { recursive: true });
|
|
65
|
+
await mkdir(join(root, "packages", "foo"), { recursive: true });
|
|
66
|
+
await writeFile(join(root, "packages", "foo", "index.ts"), "export {};\n", "utf8");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(async () => {
|
|
70
|
+
await rm(root, { recursive: true, force: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("AC-2: green corpus exits 0 and reports scanned counts", async () => {
|
|
74
|
+
await writeFile(join(root, "docs", "a.xml"), GREEN_DOC, "utf8");
|
|
75
|
+
await writeFile(join(root, "docs", "other.xml"), "<root/>", "utf8");
|
|
76
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
77
|
+
expect(result.exitCode).toBe(0);
|
|
78
|
+
const data = result.data as {
|
|
79
|
+
scanned: { xmlFiles: number; pathsChecked: number; idsChecked: number };
|
|
80
|
+
diagnostics: Diagnostic[];
|
|
81
|
+
};
|
|
82
|
+
expect(data.scanned.xmlFiles).toBe(2);
|
|
83
|
+
expect(data.diagnostics).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("AC-3: dead <path> emits COMPASS-DOC-01 error", async () => {
|
|
87
|
+
await writeFile(
|
|
88
|
+
join(root, "docs", "a.xml"),
|
|
89
|
+
`<root><path>\`packages/gone\`</path></root>`,
|
|
90
|
+
"utf8",
|
|
91
|
+
);
|
|
92
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
93
|
+
const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
|
|
94
|
+
expect(
|
|
95
|
+
diagnostics.some((d) => d.ruleId === "COMPASS-DOC-01" && d.severity === "error"),
|
|
96
|
+
"dead <path> must emit COMPASS-DOC-01 — check pathTokens/pathCheckTarget resolution",
|
|
97
|
+
).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("AC-4: dead <path> produces a non-zero exit", async () => {
|
|
101
|
+
await writeFile(
|
|
102
|
+
join(root, "docs", "a.xml"),
|
|
103
|
+
`<root><path>\`packages/gone\`</path></root>`,
|
|
104
|
+
"utf8",
|
|
105
|
+
);
|
|
106
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
107
|
+
expect(result.exitCode).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("dead workspace id emits COMPASS-DOC-02 error", async () => {
|
|
111
|
+
await writeFile(join(root, "docs", "a.xml"), `<root><workspace id="pkg-gone"/></root>`, "utf8");
|
|
112
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
113
|
+
const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
|
|
114
|
+
expect(
|
|
115
|
+
diagnostics.some((d) => d.ruleId === "COMPASS-DOC-02" && d.message.includes("pkg-gone")),
|
|
116
|
+
"dead workspace id must emit COMPASS-DOC-02 — check workspaceIdTarget mapping",
|
|
117
|
+
).toBe(true);
|
|
118
|
+
expect(result.exitCode).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("glob-tail <path> resolves against the parent directory", async () => {
|
|
122
|
+
await writeFile(
|
|
123
|
+
join(root, "docs", "a.xml"),
|
|
124
|
+
`<root><path>\`packages/*\`</path></root>`,
|
|
125
|
+
"utf8",
|
|
126
|
+
);
|
|
127
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
128
|
+
expect(result.exitCode).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("unresolvable link target emits COMPASS-DOC-03 warning, never an error", async () => {
|
|
132
|
+
await writeFile(
|
|
133
|
+
join(root, "docs", "a.xml"),
|
|
134
|
+
`<root><node id="x"><link rel="binds-to" target="docs.rfcs.rfc-9999" /></node></root>`,
|
|
135
|
+
"utf8",
|
|
136
|
+
);
|
|
137
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
138
|
+
const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
|
|
139
|
+
expect(
|
|
140
|
+
diagnostics.some((d) => d.ruleId === "COMPASS-DOC-03" && d.severity === "warning"),
|
|
141
|
+
"dangling link target must warn via COMPASS-DOC-03",
|
|
142
|
+
).toBe(true);
|
|
143
|
+
expect(result.exitCode).toBe(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("link target resolving to an archived RFC file passes", async () => {
|
|
147
|
+
await mkdir(join(root, "docs", "rfcs", "archive"), { recursive: true });
|
|
148
|
+
await writeFile(join(root, "docs", "rfcs", "archive", "rfc-0097-old.md"), "x", "utf8");
|
|
149
|
+
await writeFile(
|
|
150
|
+
join(root, "docs", "a.xml"),
|
|
151
|
+
`<root><node id="x"><link rel="binds-to" target="docs.rfcs.rfc-0097" /></node></root>`,
|
|
152
|
+
"utf8",
|
|
153
|
+
);
|
|
154
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
155
|
+
expect(result.exitCode).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("malformed XML emits COMPASS-DOC-00 and skips ref checks for that file", async () => {
|
|
159
|
+
await writeFile(join(root, "docs", "a.xml"), `<root><name>oops</root>`, "utf8");
|
|
160
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
161
|
+
const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
|
|
162
|
+
expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-00")).toBe(true);
|
|
163
|
+
expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-01")).toBe(false);
|
|
164
|
+
expect(result.exitCode).toBe(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("quoted attribute value ending in / is not misread as self-closing", async () => {
|
|
168
|
+
await writeFile(
|
|
169
|
+
join(root, "docs", "a.xml"),
|
|
170
|
+
`<root><node id="x" type="compass-doc" path="docs/"><link rel="binds-to" target="x" /></node></root>`,
|
|
171
|
+
"utf8",
|
|
172
|
+
);
|
|
173
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
174
|
+
const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
|
|
175
|
+
expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-00")).toBe(false);
|
|
176
|
+
expect(result.exitCode).toBe(0);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("docs.plans.plan-rfc-* link target resolves to a plan-*.md basename", async () => {
|
|
180
|
+
await mkdir(join(root, "docs", "plans"), { recursive: true });
|
|
181
|
+
await writeFile(join(root, "docs", "plans", "plan-rfc-1242-x.md"), "x", "utf8");
|
|
182
|
+
await writeFile(
|
|
183
|
+
join(root, "docs", "a.xml"),
|
|
184
|
+
`<root><node id="x"><link rel="binds-to" target="docs.plans.plan-rfc-1242-x" /></node></root>`,
|
|
185
|
+
"utf8",
|
|
186
|
+
);
|
|
187
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
188
|
+
const diagnostics = (result.data as { diagnostics: Diagnostic[] }).diagnostics;
|
|
189
|
+
expect(diagnostics.some((d) => d.ruleId === "COMPASS-DOC-03")).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("AC-1: result data carries command-shaped payload fields", async () => {
|
|
193
|
+
await writeFile(join(root, "docs", "a.xml"), GREEN_DOC, "utf8");
|
|
194
|
+
await writeFile(join(root, "docs", "other.xml"), "<root/>", "utf8");
|
|
195
|
+
const result = await runCompassDocsValidate(makeInput(), makeContext(root));
|
|
196
|
+
const data = result.data as Record<string, unknown>;
|
|
197
|
+
expect(data).toHaveProperty("command", "compass.docs.validate");
|
|
198
|
+
expect(data).toHaveProperty("status", "pass");
|
|
199
|
+
expect(data).toHaveProperty("scanned");
|
|
200
|
+
expect(data).toHaveProperty("diagnostics");
|
|
201
|
+
expect(Array.isArray(data.diagnostics)).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -16,6 +16,7 @@ import { execFile } from "node:child_process";
|
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
import { parse as yamlParse } from "yaml";
|
|
18
18
|
import { runCompassAuditBaseline } from "../compass-audit-handler.ts";
|
|
19
|
+
import { ambientIo } from "../../../../src/utils/io.ts";
|
|
19
20
|
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../../src/types.ts";
|
|
20
21
|
|
|
21
22
|
const execFileAsync = promisify(execFile);
|
|
@@ -39,7 +40,7 @@ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
|
39
40
|
logger: logger as never,
|
|
40
41
|
dryRun: false,
|
|
41
42
|
outputFormat: "json",
|
|
42
|
-
io:
|
|
43
|
+
io: ambientIo,
|
|
43
44
|
actualState: undefined as never,
|
|
44
45
|
fileIntents: [],
|
|
45
46
|
} as unknown as ForgeRuntimeContext;
|
|
@@ -11,6 +11,7 @@ the named-config-error contract.</purpose>
|
|
|
11
11
|
<item>RFC-1096: initial coverage for resolveCompassPolicy (AC-1..AC-5).</item>
|
|
12
12
|
<item>RFC-1220: step 1 — widen governance-ID tail regex</item>
|
|
13
13
|
<item>RFC-1220: fo-review — F-1 shared forge-yaml fixture, F-2 temp-dir cleanup</item>
|
|
14
|
+
<item>RFC-1252: harden export pipeline — CI-parity verify-extract, --from-head publish-forge, EC-20 staged lint gate</item>
|
|
14
15
|
</CHANGE_SUMMARY>
|
|
15
16
|
*/
|
|
16
17
|
|
|
@@ -163,8 +164,13 @@ describe("resolveCompassPolicy (RFC-1096)", () => {
|
|
|
163
164
|
const inventoryFile = join(COMPASS_DIR, "handlers", "compass-inventory.ts");
|
|
164
165
|
|
|
165
166
|
const offenders: string[] = [];
|
|
167
|
+
// Compass contract blocks are mechanical governance metadata — a recorded
|
|
168
|
+
// item text may name a consumer command and still be a name, not a path
|
|
169
|
+
// literal. The documented exemption above only holds if those blocks are
|
|
170
|
+
// stripped before the substring scan.
|
|
171
|
+
const compassBlockRe = /<(MODULE_CONTRACT|KEY_DECISIONS|CHANGE_SUMMARY)>[\s\S]*?<\/\1>/g;
|
|
166
172
|
for (const file of collectTsFiles(COMPASS_DIR)) {
|
|
167
|
-
const source = readFileSync(file, "utf8");
|
|
173
|
+
const source = readFileSync(file, "utf8").replace(compassBlockRe, "");
|
|
168
174
|
const banned = file === inventoryFile ? bannedInInventory : bannedPathLiterals;
|
|
169
175
|
for (const literal of banned) {
|
|
170
176
|
if (source.includes(literal)) {
|
|
@@ -2,12 +2,14 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
5
6
|
import {
|
|
6
7
|
runCompassSummaryRecord,
|
|
7
8
|
parseChangeSummary,
|
|
8
9
|
mergeHistoryIds,
|
|
9
10
|
buildChangeSummaryBlock,
|
|
10
11
|
stripConventionalPrefix,
|
|
12
|
+
stripGitTrailers,
|
|
11
13
|
isValidGovernanceId,
|
|
12
14
|
CHANGE_SUMMARY_WINDOW,
|
|
13
15
|
} from "../summary-record.ts";
|
|
@@ -160,6 +162,51 @@ describe("compass.summary.record", () => {
|
|
|
160
162
|
expect(missing.exitCode).toBe(1);
|
|
161
163
|
});
|
|
162
164
|
|
|
165
|
+
it("skips auto-inject when the worktree diff already adds a same-ID item (id-in-diff)", async () => {
|
|
166
|
+
execFileSync("git", ["init"], { cwd: root });
|
|
167
|
+
execFileSync("git", ["config", "user.email", "t@t"], { cwd: root });
|
|
168
|
+
execFileSync("git", ["config", "user.name", "T"], { cwd: root });
|
|
169
|
+
await writeFile(join(root, "src", "a.ts"), TS_HEADER(["RFC-0001: existing item."]));
|
|
170
|
+
execFileSync("git", ["add", "-A"], { cwd: root });
|
|
171
|
+
execFileSync("git", ["commit", "-m", "baseline"], { cwd: root });
|
|
172
|
+
// The pending change carries a hand-authored descriptive item for the ID.
|
|
173
|
+
await writeFile(
|
|
174
|
+
join(root, "src", "a.ts"),
|
|
175
|
+
TS_HEADER(["RFC-0001: existing item.", "RFC-1095: descriptive hand-written entry"]),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const result = await runCompassSummaryRecord(
|
|
179
|
+
makeInput({ id: "RFC-1095", files: ["src/a.ts"], text: "generic subject line" }),
|
|
180
|
+
makeContext(root),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(result.data?.skipped).toEqual([{ file: "src/a.ts", reason: "id-in-diff" }]);
|
|
184
|
+
expect(result.data?.recorded).toEqual([]);
|
|
185
|
+
const source = await readFile(join(root, "src", "a.ts"), "utf8");
|
|
186
|
+
expect(source).not.toContain("generic subject line");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("still records when the same-ID item already exists at HEAD (consecutive-commit stack)", async () => {
|
|
190
|
+
execFileSync("git", ["init"], { cwd: root });
|
|
191
|
+
execFileSync("git", ["config", "user.email", "t@t"], { cwd: root });
|
|
192
|
+
execFileSync("git", ["config", "user.name", "T"], { cwd: root });
|
|
193
|
+
await writeFile(
|
|
194
|
+
join(root, "src", "a.ts"),
|
|
195
|
+
TS_HEADER(["RFC-1095: step 1 — recorded by the previous commit"]),
|
|
196
|
+
);
|
|
197
|
+
execFileSync("git", ["add", "-A"], { cwd: root });
|
|
198
|
+
execFileSync("git", ["commit", "-m", "baseline"], { cwd: root });
|
|
199
|
+
|
|
200
|
+
const result = await runCompassSummaryRecord(
|
|
201
|
+
makeInput({ id: "RFC-1095", files: ["src/a.ts"], text: "step 2" }),
|
|
202
|
+
makeContext(root),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(result.data?.recorded).toEqual(["src/a.ts"]);
|
|
206
|
+
const source = await readFile(join(root, "src", "a.ts"), "utf8");
|
|
207
|
+
expect(source).toContain("<item>RFC-1095: step 2</item>");
|
|
208
|
+
});
|
|
209
|
+
|
|
163
210
|
it("defaults item text to the bare ID when --text is absent", async () => {
|
|
164
211
|
await writeFile(join(root, "src", "a.ts"), TS_HEADER([]));
|
|
165
212
|
await runCompassSummaryRecord(
|
|
@@ -191,6 +238,65 @@ describe("compass.summary.record", () => {
|
|
|
191
238
|
expect(source).toContain("<item>RFC-1095: record command</item>");
|
|
192
239
|
expect(source).not.toContain("RFC-1095: RFC-1095");
|
|
193
240
|
});
|
|
241
|
+
|
|
242
|
+
it("strips Generated-with/Co-Authored-By trailer blocks from the recorded item (RFC-1233 AC-1)", async () => {
|
|
243
|
+
await writeFile(join(root, "src", "a.ts"), TS_HEADER([]));
|
|
244
|
+
await runCompassSummaryRecord(
|
|
245
|
+
makeInput({
|
|
246
|
+
id: "RFC-1233",
|
|
247
|
+
files: ["src/a.ts"],
|
|
248
|
+
text: "trailer stripping\n\nGenerated with [Devin](https://devin.ai)\nCo-Authored-By: Devin <bot@users.noreply.github.com>",
|
|
249
|
+
}),
|
|
250
|
+
makeContext(root),
|
|
251
|
+
);
|
|
252
|
+
const source = await readFile(join(root, "src", "a.ts"), "utf8");
|
|
253
|
+
expect(source).toContain("<item>RFC-1233: trailer stripping</item>");
|
|
254
|
+
expect(source).not.toContain("Co-Authored-By");
|
|
255
|
+
expect(source).not.toContain("Generated with");
|
|
256
|
+
expect(source).not.toContain("devin.ai");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("neutralizes a literal star-slash in --text so the item cannot terminate the block comment (RFC-1233 AC-2)", async () => {
|
|
260
|
+
await writeFile(join(root, "src", "a.ts"), TS_HEADER([]));
|
|
261
|
+
await runCompassSummaryRecord(
|
|
262
|
+
makeInput({
|
|
263
|
+
id: "RFC-1233",
|
|
264
|
+
files: ["src/a.ts"],
|
|
265
|
+
text: "glob src" + "/**/*.ts in comments",
|
|
266
|
+
}),
|
|
267
|
+
makeContext(root),
|
|
268
|
+
);
|
|
269
|
+
const source = await readFile(join(root, "src", "a.ts"), "utf8");
|
|
270
|
+
expect(source).toContain("RFC-1233: glob src");
|
|
271
|
+
expect(source.match(CS_BLOCK_RE)).not.toBeNull();
|
|
272
|
+
// The neutralized form keeps both characters readable but cannot close /*.
|
|
273
|
+
expect(source).toContain("glob src/**" + "∕*.ts in comments");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("keeps a single-line subject identical — negative regression (RFC-1233 AC-3)", async () => {
|
|
277
|
+
await writeFile(join(root, "src", "a.ts"), TS_HEADER([]));
|
|
278
|
+
await runCompassSummaryRecord(
|
|
279
|
+
makeInput({ id: "RFC-1233", files: ["src/a.ts"], text: "plain subject line" }),
|
|
280
|
+
makeContext(root),
|
|
281
|
+
);
|
|
282
|
+
const source = await readFile(join(root, "src", "a.ts"), "utf8");
|
|
283
|
+
expect(source).toContain("<item>RFC-1233: plain subject line</item>");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("preserves a Key: value-shaped line when it is not in the trailing block (RFC-1233 AC-4)", async () => {
|
|
287
|
+
await writeFile(join(root, "src", "a.ts"), TS_HEADER([]));
|
|
288
|
+
await runCompassSummaryRecord(
|
|
289
|
+
makeInput({
|
|
290
|
+
id: "RFC-1233",
|
|
291
|
+
files: ["src/a.ts"],
|
|
292
|
+
text: "fix the parser\n\nNote: this line is prose about the fix\nfinal summary line",
|
|
293
|
+
}),
|
|
294
|
+
makeContext(root),
|
|
295
|
+
);
|
|
296
|
+
const source = await readFile(join(root, "src", "a.ts"), "utf8");
|
|
297
|
+
expect(source).toContain("Note: this line is prose about the fix");
|
|
298
|
+
expect(source).toContain("final summary line");
|
|
299
|
+
});
|
|
194
300
|
});
|
|
195
301
|
|
|
196
302
|
describe("summary-record helpers", () => {
|
|
@@ -246,4 +352,25 @@ describe("summary-record helpers", () => {
|
|
|
246
352
|
expect(block).not.toContain("<history>");
|
|
247
353
|
expect(block).toContain("<item>RFC-1: a</item>");
|
|
248
354
|
});
|
|
355
|
+
|
|
356
|
+
it("stripGitTrailers strips a Token:-shaped tail block (RFC-1233)", () => {
|
|
357
|
+
expect(
|
|
358
|
+
stripGitTrailers("subject\n\nbody line\nCo-Authored-By: Devin <bot@x>\nX-RFC: RFC-1233"),
|
|
359
|
+
).toBe("subject\n\nbody line");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("stripGitTrailers keeps a lone markdown-link tail (no strict trailer line)", () => {
|
|
363
|
+
const msg = "subject\n\nsee [the docs](https://example.com)";
|
|
364
|
+
expect(stripGitTrailers(msg)).toBe(msg);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("stripGitTrailers on a trailer-only message returns empty", () => {
|
|
368
|
+
expect(stripGitTrailers("Co-Authored-By: Devin <bot@x>")).toBe("");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("stripGitTrailers strips empty-value trailers (Token:) — interpret-trailers allows them", () => {
|
|
372
|
+
expect(stripGitTrailers("subject\n\nReviewed-by:\nSigned-off-by: Devin <bot@x>")).toBe(
|
|
373
|
+
"subject",
|
|
374
|
+
);
|
|
375
|
+
});
|
|
249
376
|
});
|