@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,481 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { normalizeTitle, detectDuplicatePrinciples, planPromotion } from "../knowledge/promote.ts";
|
|
3
|
+
import type {
|
|
4
|
+
ParsedKnowledgeFile,
|
|
5
|
+
KnowledgeEntryMeta,
|
|
6
|
+
KnowledgeEntry,
|
|
7
|
+
} from "../knowledge/schema.ts";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
const testDir = join(tmpdir(), "test");
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Helpers
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function makeMeta(overrides: Partial<KnowledgeEntryMeta> = {}): KnowledgeEntryMeta {
|
|
18
|
+
const layer = overrides.layer ?? "L2";
|
|
19
|
+
const base: KnowledgeEntryMeta = {
|
|
20
|
+
id: "K-0001",
|
|
21
|
+
layer,
|
|
22
|
+
created: "2026-01-01",
|
|
23
|
+
status: "active",
|
|
24
|
+
};
|
|
25
|
+
if (layer === "L2") {
|
|
26
|
+
base.confirmations = 1;
|
|
27
|
+
base.lastConfirmedAt = "2026-01-01";
|
|
28
|
+
}
|
|
29
|
+
return { ...base, ...overrides };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeEntry(
|
|
33
|
+
id: string,
|
|
34
|
+
title: string,
|
|
35
|
+
metaOverrides: Partial<KnowledgeEntryMeta> = {},
|
|
36
|
+
body = "Body text.",
|
|
37
|
+
): KnowledgeEntry {
|
|
38
|
+
return {
|
|
39
|
+
meta: makeMeta({ id, ...metaOverrides }),
|
|
40
|
+
title,
|
|
41
|
+
body,
|
|
42
|
+
lineStart: 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeParsedFile(
|
|
47
|
+
entries: KnowledgeEntry[],
|
|
48
|
+
overrides: Partial<ParsedKnowledgeFile> = {},
|
|
49
|
+
): ParsedKnowledgeFile {
|
|
50
|
+
return {
|
|
51
|
+
path: join(testDir, "learned-principles.md"),
|
|
52
|
+
layer: "L2",
|
|
53
|
+
preamble: "",
|
|
54
|
+
entries,
|
|
55
|
+
legacySections: [],
|
|
56
|
+
parseIssues: [],
|
|
57
|
+
isKnowledgeAdjacent: false,
|
|
58
|
+
...overrides,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// normalizeTitle
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
describe("normalizeTitle", () => {
|
|
67
|
+
test("lowercases", () => {
|
|
68
|
+
expect(normalizeTitle("Redact Sensitive Information")).toBe("redact sensitive information");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("strips punctuation", () => {
|
|
72
|
+
expect(normalizeTitle("Redact: Sensitive! Information?")).toBe("redact sensitive information");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("strips emoji", () => {
|
|
76
|
+
expect(normalizeTitle("🔒 Lock files before writing")).toBe("lock files before writing");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("collapses whitespace", () => {
|
|
80
|
+
expect(normalizeTitle(" Redact Sensitive Information ")).toBe(
|
|
81
|
+
"redact sensitive information",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("drops stop-words (the, a, an)", () => {
|
|
86
|
+
expect(normalizeTitle("The Redact a Sensitive an Information")).toBe(
|
|
87
|
+
"redact sensitive information",
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("preserves 'always' and 'never'", () => {
|
|
92
|
+
expect(normalizeTitle("Always verify before trusting")).toBe("always verify before trusting");
|
|
93
|
+
expect(normalizeTitle("Never trust raw input")).toBe("never trust raw input");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("empty string returns empty", () => {
|
|
97
|
+
expect(normalizeTitle("")).toBe("");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("only stop-words returns empty", () => {
|
|
101
|
+
expect(normalizeTitle("the a an")).toBe("");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// detectDuplicatePrinciples
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
describe("detectDuplicatePrinciples", () => {
|
|
110
|
+
test("exact match — two entries with identical normalized titles", () => {
|
|
111
|
+
const files = [
|
|
112
|
+
{
|
|
113
|
+
skill: "fo-session-save",
|
|
114
|
+
parsed: makeParsedFile([makeEntry("K-0001", "Redact sensitive information")]),
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
skill: "fo-memory-sync",
|
|
118
|
+
parsed: makeParsedFile([makeEntry("K-0005", "Redact sensitive information")]),
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
123
|
+
expect(pairs).toHaveLength(1);
|
|
124
|
+
expect(pairs[0].kind).toBe("exact");
|
|
125
|
+
expect(pairs[0].a.skill).toBe("fo-session-save");
|
|
126
|
+
expect(pairs[0].b.skill).toBe("fo-memory-sync");
|
|
127
|
+
expect(pairs[0].normalizedTitle).toBe("redact sensitive information");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("no match — different titles", () => {
|
|
131
|
+
const files = [
|
|
132
|
+
{
|
|
133
|
+
skill: "fo-session-save",
|
|
134
|
+
parsed: makeParsedFile([makeEntry("K-0001", "Redact sensitive information")]),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
skill: "fo-memory-sync",
|
|
138
|
+
parsed: makeParsedFile([makeEntry("K-0005", "Verify before trusting")]),
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
143
|
+
expect(pairs).toHaveLength(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("containment match — shorter is substring of longer, >= 20 chars and >= 60%", () => {
|
|
147
|
+
const shortTitle = "always verify auto-extracted ids before trusting";
|
|
148
|
+
const longTitle = "always verify auto-extracted ids before trusting them in production";
|
|
149
|
+
|
|
150
|
+
const files = [
|
|
151
|
+
{ skill: "fo-session-save", parsed: makeParsedFile([makeEntry("K-0001", shortTitle)]) },
|
|
152
|
+
{ skill: "grilling", parsed: makeParsedFile([makeEntry("K-0003", longTitle)]) },
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
156
|
+
expect(pairs).toHaveLength(1);
|
|
157
|
+
expect(pairs[0].kind).toBe("containment");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("containment bounded — shorter title < 20 chars produces no match", () => {
|
|
161
|
+
const shortTitle = "verify ids"; // 10 chars normalized
|
|
162
|
+
const longTitle = "verify ids before trusting them in production environments";
|
|
163
|
+
|
|
164
|
+
const files = [
|
|
165
|
+
{ skill: "fo-session-save", parsed: makeParsedFile([makeEntry("K-0001", shortTitle)]) },
|
|
166
|
+
{ skill: "grilling", parsed: makeParsedFile([makeEntry("K-0003", longTitle)]) },
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
170
|
+
expect(pairs).toHaveLength(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("containment bounded — shorter title < 60% of longer produces no match", () => {
|
|
174
|
+
const shortTitle = "always verify ids before trusting"; // 33 chars
|
|
175
|
+
const longTitle =
|
|
176
|
+
"always verify ids before trusting them in production environments with proper validation and care"; // 85 chars, 33/85 = 38.8%
|
|
177
|
+
|
|
178
|
+
const files = [
|
|
179
|
+
{ skill: "fo-session-save", parsed: makeParsedFile([makeEntry("K-0001", shortTitle)]) },
|
|
180
|
+
{ skill: "grilling", parsed: makeParsedFile([makeEntry("K-0003", longTitle)]) },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
184
|
+
expect(pairs).toHaveLength(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("exclusion — pairs linked by promotedTo are excluded", () => {
|
|
188
|
+
const files = [
|
|
189
|
+
{
|
|
190
|
+
skill: "fo-session-save",
|
|
191
|
+
parsed: makeParsedFile([
|
|
192
|
+
makeEntry("K-0001", "Redact sensitive information", { promotedTo: "shared/K-0001" }),
|
|
193
|
+
]),
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
skill: "fo-memory-sync",
|
|
197
|
+
parsed: makeParsedFile([
|
|
198
|
+
makeEntry("K-0005", "Redact sensitive information", { promotedTo: "shared/K-0001" }),
|
|
199
|
+
]),
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
204
|
+
expect(pairs).toHaveLength(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("exclusion — pairs linked by supersedes are excluded", () => {
|
|
208
|
+
const files = [
|
|
209
|
+
{
|
|
210
|
+
skill: "fo-session-save",
|
|
211
|
+
parsed: makeParsedFile([
|
|
212
|
+
makeEntry("K-0001", "Redact sensitive information", { supersedes: ["K-0005"] }),
|
|
213
|
+
]),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
skill: "fo-memory-sync",
|
|
217
|
+
parsed: makeParsedFile([makeEntry("K-0005", "Redact sensitive information")]),
|
|
218
|
+
},
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
222
|
+
expect(pairs).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("exclusion — entries with status: stale are excluded", () => {
|
|
226
|
+
const files = [
|
|
227
|
+
{
|
|
228
|
+
skill: "fo-session-save",
|
|
229
|
+
parsed: makeParsedFile([
|
|
230
|
+
makeEntry("K-0001", "Redact sensitive information", { status: "stale" }),
|
|
231
|
+
]),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
skill: "fo-memory-sync",
|
|
235
|
+
parsed: makeParsedFile([makeEntry("K-0005", "Redact sensitive information")]),
|
|
236
|
+
},
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
240
|
+
expect(pairs).toHaveLength(0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("exclusion — entries with status: archived are excluded", () => {
|
|
244
|
+
const files = [
|
|
245
|
+
{
|
|
246
|
+
skill: "fo-session-save",
|
|
247
|
+
parsed: makeParsedFile([
|
|
248
|
+
makeEntry("K-0001", "Redact sensitive information", { status: "archived" }),
|
|
249
|
+
]),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
skill: "fo-memory-sync",
|
|
253
|
+
parsed: makeParsedFile([makeEntry("K-0005", "Redact sensitive information")]),
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
258
|
+
expect(pairs).toHaveLength(0);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("multiple skills — detection across 3+ skills", () => {
|
|
262
|
+
const files = [
|
|
263
|
+
{
|
|
264
|
+
skill: "fo-session-save",
|
|
265
|
+
parsed: makeParsedFile([makeEntry("K-0001", "Redact sensitive information")]),
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
skill: "fo-memory-sync",
|
|
269
|
+
parsed: makeParsedFile([makeEntry("K-0005", "Redact sensitive information")]),
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
skill: "grilling",
|
|
273
|
+
parsed: makeParsedFile([makeEntry("K-0010", "Redact sensitive information")]),
|
|
274
|
+
},
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
278
|
+
expect(pairs).toHaveLength(3); // 3 pairs: (0,1), (0,2), (1,2)
|
|
279
|
+
expect(pairs.every((p) => p.kind === "exact")).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("knowledge-adjacent files are excluded", () => {
|
|
283
|
+
const files = [
|
|
284
|
+
{
|
|
285
|
+
skill: "fo-session-save",
|
|
286
|
+
parsed: makeParsedFile([makeEntry("K-0001", "Redact sensitive information")]),
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
skill: "fo-memory-sync",
|
|
290
|
+
parsed: makeParsedFile([makeEntry("K-0005", "Redact sensitive information")], {
|
|
291
|
+
isKnowledgeAdjacent: true,
|
|
292
|
+
}),
|
|
293
|
+
},
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
297
|
+
expect(pairs).toHaveLength(0);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("files with parse issues are excluded", () => {
|
|
301
|
+
const files = [
|
|
302
|
+
{
|
|
303
|
+
skill: "fo-session-save",
|
|
304
|
+
parsed: makeParsedFile([makeEntry("K-0001", "Redact sensitive information")]),
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
skill: "fo-memory-sync",
|
|
308
|
+
parsed: makeParsedFile([makeEntry("K-0005", "Redact sensitive information")], {
|
|
309
|
+
parseIssues: [{ line: 1, message: "bad" }],
|
|
310
|
+
}),
|
|
311
|
+
},
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
315
|
+
expect(pairs).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("empty normalized title is excluded", () => {
|
|
319
|
+
const files = [
|
|
320
|
+
{ skill: "fo-session-save", parsed: makeParsedFile([makeEntry("K-0001", "the a an")]) },
|
|
321
|
+
{ skill: "fo-memory-sync", parsed: makeParsedFile([makeEntry("K-0005", "the a an")]) },
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
const pairs = detectDuplicatePrinciples(files);
|
|
325
|
+
expect(pairs).toHaveLength(0);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// planPromotion
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
describe("planPromotion", () => {
|
|
334
|
+
test("summed confirmations — two sources with 3 and 5 produce 8", () => {
|
|
335
|
+
const sources = [
|
|
336
|
+
{
|
|
337
|
+
skill: "fo-session-save",
|
|
338
|
+
file: "/path/a.md",
|
|
339
|
+
entry: makeEntry("K-0001", "Redact sensitive information", { confirmations: 3 }),
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
skill: "fo-memory-sync",
|
|
343
|
+
file: "/path/b.md",
|
|
344
|
+
entry: makeEntry("K-0005", "Redact sensitive information", { confirmations: 5 }),
|
|
345
|
+
},
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
const plan = planPromotion(
|
|
349
|
+
sources,
|
|
350
|
+
{ title: "Redact sensitive information", body: "Merged body." },
|
|
351
|
+
"K-0001",
|
|
352
|
+
"2026-08-03",
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
expect(plan.sharedEntry.meta.confirmations).toBe(8);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("promotedFrom provenance — lists all source skill/id pairs", () => {
|
|
359
|
+
const sources = [
|
|
360
|
+
{
|
|
361
|
+
skill: "fo-session-save",
|
|
362
|
+
file: "/path/a.md",
|
|
363
|
+
entry: makeEntry("K-0001", "Redact sensitive information", { confirmations: 3 }),
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
skill: "fo-memory-sync",
|
|
367
|
+
file: "/path/b.md",
|
|
368
|
+
entry: makeEntry("K-0005", "Redact sensitive information", { confirmations: 5 }),
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
const plan = planPromotion(
|
|
373
|
+
sources,
|
|
374
|
+
{ title: "Redact sensitive information", body: "Merged body." },
|
|
375
|
+
"K-0001",
|
|
376
|
+
"2026-08-03",
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(plan.sharedEntry.meta.promotedFrom).toEqual([
|
|
380
|
+
"fo-session-save/K-0001",
|
|
381
|
+
"fo-memory-sync/K-0005",
|
|
382
|
+
]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("pointer list — one pointer per source entry", () => {
|
|
386
|
+
const sources = [
|
|
387
|
+
{
|
|
388
|
+
skill: "fo-session-save",
|
|
389
|
+
file: "/path/a.md",
|
|
390
|
+
entry: makeEntry("K-0001", "Redact sensitive information", { confirmations: 3 }),
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
skill: "fo-memory-sync",
|
|
394
|
+
file: "/path/b.md",
|
|
395
|
+
entry: makeEntry("K-0005", "Redact sensitive information", { confirmations: 5 }),
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
const plan = planPromotion(
|
|
400
|
+
sources,
|
|
401
|
+
{ title: "Redact sensitive information", body: "Merged body." },
|
|
402
|
+
"K-0001",
|
|
403
|
+
"2026-08-03",
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
expect(plan.localPointers).toHaveLength(2);
|
|
407
|
+
expect(plan.localPointers[0]).toEqual({
|
|
408
|
+
skill: "fo-session-save",
|
|
409
|
+
file: "/path/a.md",
|
|
410
|
+
entryId: "K-0001",
|
|
411
|
+
});
|
|
412
|
+
expect(plan.localPointers[1]).toEqual({
|
|
413
|
+
skill: "fo-memory-sync",
|
|
414
|
+
file: "/path/b.md",
|
|
415
|
+
entryId: "K-0005",
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("shared entry metadata — status active, created today, lastConfirmedAt today", () => {
|
|
420
|
+
const sources = [
|
|
421
|
+
{
|
|
422
|
+
skill: "fo-session-save",
|
|
423
|
+
file: "/path/a.md",
|
|
424
|
+
entry: makeEntry("K-0001", "Redact sensitive information", { confirmations: 3 }),
|
|
425
|
+
},
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
const plan = planPromotion(
|
|
429
|
+
sources,
|
|
430
|
+
{ title: "Redact sensitive information", body: "Merged body." },
|
|
431
|
+
"K-0001",
|
|
432
|
+
"2026-08-03",
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
expect(plan.sharedEntry.meta.status).toBe("active");
|
|
436
|
+
expect(plan.sharedEntry.meta.created).toBe("2026-08-03");
|
|
437
|
+
expect(plan.sharedEntry.meta.lastConfirmedAt).toBe("2026-08-03");
|
|
438
|
+
expect(plan.sharedEntry.meta.layer).toBe("L2");
|
|
439
|
+
expect(plan.sharedEntry.meta.id).toBe("K-0001");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("shared entry carries merged title and body", () => {
|
|
443
|
+
const sources = [
|
|
444
|
+
{
|
|
445
|
+
skill: "fo-session-save",
|
|
446
|
+
file: "/path/a.md",
|
|
447
|
+
entry: makeEntry("K-0001", "Redact sensitive information", { confirmations: 3 }),
|
|
448
|
+
},
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
const plan = planPromotion(
|
|
452
|
+
sources,
|
|
453
|
+
{ title: "Redact sensitive information", body: "Merged body." },
|
|
454
|
+
"K-0001",
|
|
455
|
+
"2026-08-03",
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
expect(plan.sharedEntry.title).toBe("Redact sensitive information");
|
|
459
|
+
expect(plan.sharedEntry.body).toBe("Merged body.");
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("single source — confirmations from one entry", () => {
|
|
463
|
+
const sources = [
|
|
464
|
+
{
|
|
465
|
+
skill: "grilling",
|
|
466
|
+
file: "/path/a.md",
|
|
467
|
+
entry: makeEntry("K-0001", "Test principle", { confirmations: 7 }),
|
|
468
|
+
},
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
const plan = planPromotion(
|
|
472
|
+
sources,
|
|
473
|
+
{ title: "Test principle", body: "Body." },
|
|
474
|
+
"K-0001",
|
|
475
|
+
"2026-08-03",
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
expect(plan.sharedEntry.meta.confirmations).toBe(7);
|
|
479
|
+
expect(plan.localPointers).toHaveLength(1);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { FORGE_SKILLS } from "../registry.ts";
|
|
3
|
+
|
|
4
|
+
describe("FORGE_SKILLS registry", () => {
|
|
5
|
+
test("is non-empty", () => {
|
|
6
|
+
expect(FORGE_SKILLS.length).toBeGreaterThan(10);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("has no duplicate names", () => {
|
|
10
|
+
const names = FORGE_SKILLS.map((s) => s.name);
|
|
11
|
+
const unique = new Set(names);
|
|
12
|
+
expect(unique.size).toBe(names.length);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("has no duplicate paths", () => {
|
|
16
|
+
const paths = FORGE_SKILLS.map((s) => s.path);
|
|
17
|
+
const unique = new Set(paths);
|
|
18
|
+
expect(unique.size).toBe(paths.length);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("all names are kebab-case", () => {
|
|
22
|
+
for (const skill of FORGE_SKILLS) {
|
|
23
|
+
expect(skill.name).toMatch(/^[a-z][a-z0-9-]*$/);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("all paths end with SKILL.md", () => {
|
|
28
|
+
for (const skill of FORGE_SKILLS) {
|
|
29
|
+
expect(skill.path.endsWith("SKILL.md")).toBe(true);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("all categories are valid", () => {
|
|
34
|
+
const validCategories = new Set(["fo", "shared", "meta"]);
|
|
35
|
+
for (const skill of FORGE_SKILLS) {
|
|
36
|
+
expect(validCategories.has(skill.category)).toBe(true);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("all invocations are valid", () => {
|
|
41
|
+
const validInvocations = new Set(["user", "model"]);
|
|
42
|
+
for (const skill of FORGE_SKILLS) {
|
|
43
|
+
expect(validInvocations.has(skill.invocation)).toBe(true);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("all concerns are valid", () => {
|
|
48
|
+
const validConcerns = new Set([
|
|
49
|
+
"read-only",
|
|
50
|
+
"document-only",
|
|
51
|
+
"content-mutation",
|
|
52
|
+
"code-mutation",
|
|
53
|
+
]);
|
|
54
|
+
for (const skill of FORGE_SKILLS) {
|
|
55
|
+
expect(validConcerns.has(skill.concerns)).toBe(true);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("dependsOn entries reference existing skill names", () => {
|
|
60
|
+
const knownNames = new Set(FORGE_SKILLS.map((s) => s.name));
|
|
61
|
+
for (const skill of FORGE_SKILLS) {
|
|
62
|
+
for (const dep of skill.dependsOn) {
|
|
63
|
+
expect(knownNames.has(dep)).toBe(true);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("path prefix matches category directory", () => {
|
|
69
|
+
for (const skill of FORGE_SKILLS) {
|
|
70
|
+
expect(skill.path.startsWith(`skills/${skill.category}/`)).toBe(true);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for forge.scaffold project command and forge.init --from flag.</purpose>
|
|
4
|
+
</MODULE_CONTRACT>
|
|
5
|
+
<CHANGE_SUMMARY>
|
|
6
|
+
<item>RFC-0392: initial scaffold-project and init --from tests.</item>
|
|
7
|
+
</CHANGE_SUMMARY>
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
11
|
+
import { mkdtemp, rm, readdir, mkdir, writeFile } from "node:fs/promises";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { runScaffoldProject } from "../onboarding/scaffold-project.ts";
|
|
16
|
+
import { runInit } from "../onboarding/init.ts";
|
|
17
|
+
import type { ForgeRuntimeContext } from "../types.ts";
|
|
18
|
+
|
|
19
|
+
const silentLogger = {
|
|
20
|
+
section: () => {},
|
|
21
|
+
info: () => {},
|
|
22
|
+
warn: () => {},
|
|
23
|
+
error: () => {},
|
|
24
|
+
success: () => {},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
28
|
+
return {
|
|
29
|
+
workspaceRoot,
|
|
30
|
+
logger: silentLogger as never,
|
|
31
|
+
dryRun: false,
|
|
32
|
+
outputFormat: "json",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Resolve forge root from this test file's location
|
|
37
|
+
const FORGE_ROOT = join(import.meta.dirname, "..", "..");
|
|
38
|
+
// In monorepo: FORGE_ROOT = packages/forge, workspace = monorepo root
|
|
39
|
+
// In extracted package: FORGE_ROOT = repo root, workspace = FORGE_ROOT
|
|
40
|
+
const monorepoRoot = join(FORGE_ROOT, "..", "..");
|
|
41
|
+
const WORKSPACE_ROOT = existsSync(join(monorepoRoot, "package.json")) ? monorepoRoot : FORGE_ROOT;
|
|
42
|
+
|
|
43
|
+
let tempDir: string;
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
tempDir = await mkdtemp(join(tmpdir(), "scaffold-test-"));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("forge.scaffold tolerates non-empty directory (RFC-0877 in-place mode)", async () => {
|
|
54
|
+
await writeFile(join(tempDir, "some-file.txt"), "hello", "utf8");
|
|
55
|
+
const result = await runScaffoldProject(
|
|
56
|
+
{ argv: [], flags: { profile: "forge-shell", name: "my-project" } },
|
|
57
|
+
{ ...makeContext(tempDir), forgeRoot: FORGE_ROOT },
|
|
58
|
+
);
|
|
59
|
+
expect(result.exitCode).toBe(0);
|
|
60
|
+
expect(result.data?.status).toBe("pass");
|
|
61
|
+
expect(existsSync(join(tempDir, "package.json"))).toBe(true);
|
|
62
|
+
expect(existsSync(join(tempDir, "some-file.txt"))).toBe(true);
|
|
63
|
+
}, 30000);
|
|
64
|
+
|
|
65
|
+
test("forge.scaffold fails on missing --profile", async () => {
|
|
66
|
+
const result = await runScaffoldProject(
|
|
67
|
+
{ argv: [], flags: { name: "my-site" } },
|
|
68
|
+
makeContext(tempDir),
|
|
69
|
+
);
|
|
70
|
+
expect(result.exitCode).toBe(1);
|
|
71
|
+
expect(result.data?.errors[0]).toContain("--profile");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("forge.scaffold derives name from folder when --name omitted", async () => {
|
|
75
|
+
const result = await runScaffoldProject(
|
|
76
|
+
{ argv: [], flags: { profile: "phaser-turborepo" } },
|
|
77
|
+
{ ...makeContext(tempDir), forgeRoot: FORGE_ROOT },
|
|
78
|
+
);
|
|
79
|
+
// Name is now derived from the folder name (consistent with forge.create)
|
|
80
|
+
// The temp dir name may not be kebab-case, so we accept either:
|
|
81
|
+
// 1. pass (if folder name happens to be kebab-case)
|
|
82
|
+
// 2. fail with kebab-case error (if folder name has uppercase)
|
|
83
|
+
if (result.exitCode === 0) {
|
|
84
|
+
expect(result.data?.status).toBe("pass");
|
|
85
|
+
} else {
|
|
86
|
+
expect(result.data?.errors[0]).toContain("kebab-case");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("forge.scaffold fails on non-kebab-case name", async () => {
|
|
91
|
+
const result = await runScaffoldProject(
|
|
92
|
+
{ argv: [], flags: { profile: "phaser-turborepo", name: "MySite" } },
|
|
93
|
+
makeContext(tempDir),
|
|
94
|
+
);
|
|
95
|
+
expect(result.exitCode).toBe(1);
|
|
96
|
+
expect(result.data?.errors[0]).toContain("kebab-case");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("forge.scaffold fails on unknown profile", async () => {
|
|
100
|
+
const result = await runScaffoldProject(
|
|
101
|
+
{ argv: [], flags: { profile: "nonexistent", name: "my-site" } },
|
|
102
|
+
makeContext(tempDir),
|
|
103
|
+
);
|
|
104
|
+
expect(result.exitCode).toBe(1);
|
|
105
|
+
expect(result.data?.errors[0]).toContain("Unknown profile");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("forge.scaffold creates workspace files in empty dir (no install)", async () => {
|
|
109
|
+
// Use a profile that has no install commands by testing the file creation directly.
|
|
110
|
+
// We can't run real pnpm install in tests, so we test with a profile that has empty install.
|
|
111
|
+
// Instead, test the non-install path by checking that files are created before install runs.
|
|
112
|
+
// For a full test we'd need a mock profile, but the handler runs real install commands.
|
|
113
|
+
// Skip this test if pnpm is not available — the file creation logic is tested above.
|
|
114
|
+
// Instead, verify the directory is empty first, then check that the handler at least
|
|
115
|
+
// starts creating files before potentially failing on install.
|
|
116
|
+
const entries = await readdir(tempDir);
|
|
117
|
+
expect(entries.length).toBe(0);
|
|
118
|
+
// We can't fully test scaffold without running pnpm install.
|
|
119
|
+
// The key acceptance criterion is the integration test with --lockfile-only.
|
|
120
|
+
// Here we just verify the empty-dir check passes.
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("forge.init --from detects stack from existing project", async () => {
|
|
124
|
+
// Create a fake project with phaser.config.ts
|
|
125
|
+
const fakeProject = join(tempDir, "fake-project");
|
|
126
|
+
await mkdir(fakeProject, { recursive: true });
|
|
127
|
+
await writeFile(join(fakeProject, "phaser.config.ts"), "export default {}", "utf8");
|
|
128
|
+
|
|
129
|
+
const result = runInit({ flags: { from: fakeProject } }, { workspaceRoot: WORKSPACE_ROOT });
|
|
130
|
+
expect(result.status).toBe("pass");
|
|
131
|
+
expect(result.detection).toBeDefined();
|
|
132
|
+
expect(result.detection?.profile).toBe("phaser-turborepo");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("forge.init --from reports null when stack undetectable", async () => {
|
|
136
|
+
const fakeProject = join(tempDir, "fake-project");
|
|
137
|
+
await mkdir(fakeProject, { recursive: true });
|
|
138
|
+
await writeFile(join(fakeProject, "Cargo.toml"), "[package]", "utf8");
|
|
139
|
+
|
|
140
|
+
const result = runInit({ flags: { from: fakeProject } }, { workspaceRoot: WORKSPACE_ROOT });
|
|
141
|
+
expect(result.status).toBe("pass");
|
|
142
|
+
expect(result.detection?.profile).toBeNull();
|
|
143
|
+
expect(result.detection?.unsupported).toEqual(["unknown"]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("forge.init --from fails on non-existent path", async () => {
|
|
147
|
+
const result = runInit({ flags: { from: "/nonexistent/path/xyz" } }, { workspaceRoot: tempDir });
|
|
148
|
+
expect(result.status).toBe("fail");
|
|
149
|
+
expect(result.errors[0]).toContain("does not exist");
|
|
150
|
+
});
|