@warpgogol/forge 5.2.3 → 5.2.4
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 +2 -0
- package/dist/os/core/core.module.d.ts.map +1 -1
- package/dist/os/core/core.module.js +14 -0
- package/dist/os/core/core.module.js.map +1 -1
- package/dist/src/config/forge-config.d.ts +9 -0
- package/dist/src/config/forge-config.d.ts.map +1 -1
- package/dist/src/config/forge-config.js +7 -0
- package/dist/src/config/forge-config.js.map +1 -1
- package/dist/src/onboarding/agents-generate.d.ts +2 -0
- package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/agents-generate.js +14 -18
- package/dist/src/onboarding/agents-generate.js.map +1 -1
- package/dist/src/onboarding/doctor.d.ts +6 -0
- package/dist/src/onboarding/doctor.d.ts.map +1 -1
- package/dist/src/onboarding/doctor.js +117 -8
- package/dist/src/onboarding/doctor.js.map +1 -1
- package/dist/src/onboarding/memory-compact.d.ts +22 -0
- package/dist/src/onboarding/memory-compact.d.ts.map +1 -0
- package/dist/src/onboarding/memory-compact.js +147 -0
- package/dist/src/onboarding/memory-compact.js.map +1 -0
- package/dist/src/onboarding/memory-scaffold.d.ts +1 -0
- package/dist/src/onboarding/memory-scaffold.d.ts.map +1 -1
- package/dist/src/onboarding/memory-scaffold.js +1 -1
- package/dist/src/onboarding/memory-scaffold.js.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.d.ts.map +1 -1
- package/dist/src/onboarding/nested-agents-generate.js +1 -1
- package/dist/src/onboarding/nested-agents-generate.js.map +1 -1
- package/dist/src/onboarding/workspace-discovery.d.ts +1 -1
- package/dist/src/onboarding/workspace-discovery.d.ts.map +1 -1
- package/dist/src/onboarding/workspace-discovery.js +14 -2
- package/dist/src/onboarding/workspace-discovery.js.map +1 -1
- package/docs/reference/forge-yaml.md +12 -0
- package/os/core/core.module.ts +15 -0
- package/package.json +1 -1
- package/profiles/forge-shell.yaml +3 -0
- package/profiles/godot-game.yaml +3 -0
- package/profiles/phaser-game.yaml +3 -0
- package/profiles/site-workshop.yaml +567 -564
- package/src/config/forge-config.ts +11 -0
- package/src/onboarding/agents-generate.ts +18 -18
- package/src/onboarding/doctor.ts +134 -9
- package/src/onboarding/memory-compact.ts +188 -0
- package/src/onboarding/memory-scaffold.ts +1 -1
- package/src/onboarding/nested-agents-generate.ts +5 -1
- package/src/onboarding/workspace-discovery.ts +15 -1
- package/src/tests/agents-generate.test.ts +26 -3
- package/src/tests/doctor-dist-freshness.test.ts +110 -0
- package/src/tests/doctor-fix.test.ts +145 -0
- package/src/tests/doctor-nested-agents.test.ts +108 -0
- package/src/tests/memory-compact.test.ts +143 -0
- package/src/tests/workspace-discovery.test.ts +28 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for the dist-freshness doctor check — warns when a forge
|
|
4
|
+
source checkout's dist/ is older than the newest src/ file (RFC-1151).</purpose>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<CHANGE_SUMMARY>
|
|
7
|
+
<item>RFC-1151: initial dist-freshness tests.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, rm, mkdir, writeFile, utimes } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { runDoctor } from "../onboarding/doctor.ts";
|
|
16
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tempDir = await mkdtemp(join(tmpdir(), "doctor-dist-test-"));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function makeContext(): ForgeRuntimeContext {
|
|
29
|
+
return {
|
|
30
|
+
workspaceRoot: tempDir,
|
|
31
|
+
// forgeRoot falls back to <workspaceRoot>/packages/forge when unresolvable
|
|
32
|
+
forgeRoot: join(tempDir, "packages", "forge"),
|
|
33
|
+
logger: {
|
|
34
|
+
section: () => {},
|
|
35
|
+
success: () => {},
|
|
36
|
+
warn: () => {},
|
|
37
|
+
error: () => {},
|
|
38
|
+
info: () => {},
|
|
39
|
+
},
|
|
40
|
+
dryRun: false,
|
|
41
|
+
outputFormat: "json",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const FORGE_YAML = `schema: "forge/config@1"
|
|
46
|
+
project:
|
|
47
|
+
name: test-project
|
|
48
|
+
stack: []
|
|
49
|
+
packageManager: pnpm
|
|
50
|
+
paths:
|
|
51
|
+
rfcsDir: docs/rfcs
|
|
52
|
+
adrsDir: docs/adrs
|
|
53
|
+
plansDir: docs/plans
|
|
54
|
+
auditsDir: docs/audits
|
|
55
|
+
specsDir: docs/specs
|
|
56
|
+
skillsDir: .agents/skills
|
|
57
|
+
sessionsDir: docs/sessions
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
async function makeForgeCheckout(srcMtime: Date, distMtime: Date): Promise<void> {
|
|
61
|
+
const forgeRoot = join(tempDir, "packages", "forge");
|
|
62
|
+
await mkdir(join(forgeRoot, "src"), { recursive: true });
|
|
63
|
+
await mkdir(join(forgeRoot, "dist"), { recursive: true });
|
|
64
|
+
await writeFile(join(forgeRoot, "package.json"), JSON.stringify({ name: "@warpgogol/forge" }));
|
|
65
|
+
await writeFile(join(forgeRoot, "src", "index.ts"), "export {};\n");
|
|
66
|
+
await writeFile(join(forgeRoot, "dist", "index.js"), "export {};\n");
|
|
67
|
+
await utimes(join(forgeRoot, "src", "index.ts"), srcMtime, srcMtime);
|
|
68
|
+
await utimes(join(forgeRoot, "dist"), distMtime, distMtime);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
test("dist-freshness warns when dist is older than newest src file", async () => {
|
|
72
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
73
|
+
const old = new Date("2026-01-01T00:00:00Z");
|
|
74
|
+
const recent = new Date("2026-09-01T00:00:00Z");
|
|
75
|
+
await makeForgeCheckout(recent, old);
|
|
76
|
+
|
|
77
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
78
|
+
const result = await runDoctor(input, makeContext());
|
|
79
|
+
|
|
80
|
+
const check = result.data!.checks.find((c) => c.name === "dist-freshness");
|
|
81
|
+
expect(check).toBeDefined();
|
|
82
|
+
expect(check!.status).toBe("warn");
|
|
83
|
+
expect(check!.message).toContain("pnpm run build");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("dist-freshness passes when dist is absent (npm consumer layout)", async () => {
|
|
87
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
88
|
+
// No packages/forge at all — forgeRoot fallback dir doesn't exist
|
|
89
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
90
|
+
const result = await runDoctor(input, makeContext());
|
|
91
|
+
|
|
92
|
+
const check = result.data!.checks.find((c) => c.name === "dist-freshness");
|
|
93
|
+
expect(check).toBeDefined();
|
|
94
|
+
expect(check!.status).toBe("pass");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("dist-freshness passes when dist is newer than src", async () => {
|
|
98
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
99
|
+
const old = new Date("2026-01-01T00:00:00Z");
|
|
100
|
+
const recent = new Date("2026-09-01T00:00:00Z");
|
|
101
|
+
await makeForgeCheckout(old, recent);
|
|
102
|
+
|
|
103
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
104
|
+
const result = await runDoctor(input, makeContext());
|
|
105
|
+
|
|
106
|
+
const check = result.data!.checks.find((c) => c.name === "dist-freshness");
|
|
107
|
+
expect(check).toBeDefined();
|
|
108
|
+
expect(check!.status).toBe("pass");
|
|
109
|
+
expect(check!.message).toContain("fresh");
|
|
110
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for forge doctor --fix — safe remediations for warn/fail
|
|
4
|
+
checks: nested-AGENTS.md regeneration and memory-layer compaction (RFC-1151).</purpose>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<CHANGE_SUMMARY>
|
|
7
|
+
<item>RFC-1151: initial doctor --fix tests.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { runDoctor } from "../onboarding/doctor.ts";
|
|
16
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tempDir = await mkdtemp(join(tmpdir(), "doctor-fix-test-"));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function makeContext(dryRun = false): ForgeRuntimeContext {
|
|
29
|
+
return {
|
|
30
|
+
workspaceRoot: tempDir,
|
|
31
|
+
logger: {
|
|
32
|
+
section: () => {},
|
|
33
|
+
success: () => {},
|
|
34
|
+
warn: () => {},
|
|
35
|
+
error: () => {},
|
|
36
|
+
info: () => {},
|
|
37
|
+
},
|
|
38
|
+
dryRun,
|
|
39
|
+
outputFormat: "json",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const FORGE_YAML = `schema: "forge/config@1"
|
|
44
|
+
project:
|
|
45
|
+
name: test-project
|
|
46
|
+
stack: []
|
|
47
|
+
packageManager: pnpm
|
|
48
|
+
paths:
|
|
49
|
+
rfcsDir: docs/rfcs
|
|
50
|
+
adrsDir: docs/adrs
|
|
51
|
+
plansDir: docs/plans
|
|
52
|
+
auditsDir: docs/audits
|
|
53
|
+
specsDir: docs/specs
|
|
54
|
+
skillsDir: .agents/skills
|
|
55
|
+
sessionsDir: docs/sessions
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const GENERATED_MARKER =
|
|
59
|
+
"<!--\n GENERATED. Do not change this line unless the file contains project specific changes.\n-->\n";
|
|
60
|
+
|
|
61
|
+
async function makeStaleWorkspace(): Promise<void> {
|
|
62
|
+
await mkdir(join(tempDir, "packages", "my-pkg"), { recursive: true });
|
|
63
|
+
await writeFile(
|
|
64
|
+
join(tempDir, "packages", "my-pkg", "package.json"),
|
|
65
|
+
JSON.stringify({ name: "@test/my-pkg" }),
|
|
66
|
+
);
|
|
67
|
+
// Generated marker + outdated content → stale
|
|
68
|
+
await writeFile(
|
|
69
|
+
join(tempDir, "packages", "my-pkg", "AGENTS.md"),
|
|
70
|
+
`${GENERATED_MARKER}# Old content\n`,
|
|
71
|
+
"utf8",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
test("doctor --fix regenerates stale nested AGENTS.md and subsequent run passes", async () => {
|
|
76
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
77
|
+
await makeStaleWorkspace();
|
|
78
|
+
|
|
79
|
+
const fixInput: ForgeCommandInput = { argv: [], flags: { fix: true } };
|
|
80
|
+
const fixResult = await runDoctor(fixInput, makeContext());
|
|
81
|
+
|
|
82
|
+
const fixEntry = fixResult.data!.fixes?.find((f) => f.check === "nested-AGENTS.md");
|
|
83
|
+
expect(fixEntry).toBeDefined();
|
|
84
|
+
expect(fixEntry!.action).toBe("fixed");
|
|
85
|
+
|
|
86
|
+
const regenerated = await readFile(
|
|
87
|
+
join(tempDir, "packages", "my-pkg", "AGENTS.md"),
|
|
88
|
+
"utf8",
|
|
89
|
+
);
|
|
90
|
+
expect(regenerated).toContain("`@test/my-pkg` — Agent Guide");
|
|
91
|
+
expect(regenerated).not.toContain("Old content");
|
|
92
|
+
|
|
93
|
+
// Subsequent doctor run: nested-AGENTS.md no longer reports this file stale
|
|
94
|
+
const verifyResult = await runDoctor({ argv: [], flags: {} }, makeContext());
|
|
95
|
+
const check = verifyResult.data!.checks.find((c) => c.name === "nested-AGENTS.md");
|
|
96
|
+
expect(check!.message).not.toContain("my-pkg/AGENTS.md stale");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("doctor --fix does not modify hand-written nested AGENTS.md", async () => {
|
|
100
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
101
|
+
await mkdir(join(tempDir, "packages", "my-pkg"), { recursive: true });
|
|
102
|
+
await writeFile(
|
|
103
|
+
join(tempDir, "packages", "my-pkg", "package.json"),
|
|
104
|
+
JSON.stringify({ name: "@test/my-pkg" }),
|
|
105
|
+
);
|
|
106
|
+
const handwritten = "# Custom\nNo marker.\n";
|
|
107
|
+
await writeFile(join(tempDir, "packages", "my-pkg", "AGENTS.md"), handwritten, "utf8");
|
|
108
|
+
|
|
109
|
+
const result = await runDoctor({ argv: [], flags: { fix: true } }, makeContext());
|
|
110
|
+
expect(result.data!.fixes).toBeDefined();
|
|
111
|
+
|
|
112
|
+
const after = await readFile(join(tempDir, "packages", "my-pkg", "AGENTS.md"), "utf8");
|
|
113
|
+
expect(after).toBe(handwritten);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("doctor --fix compacts over-budget MEMORY.md", async () => {
|
|
117
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
118
|
+
await mkdir(join(tempDir, ".agents", "memory"), { recursive: true });
|
|
119
|
+
const env = Array.from({ length: 30 }, (_, i) => `- env ${i} ${"x".repeat(200)}`).join("\n");
|
|
120
|
+
await writeFile(
|
|
121
|
+
join(tempDir, ".agents", "memory", "MEMORY.md"),
|
|
122
|
+
`# Project Memory\n\n## Current focus\n\n- keep\n\n## Environment notes\n\n${env}\n`,
|
|
123
|
+
"utf8",
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const result = await runDoctor({ argv: [], flags: { fix: true } }, makeContext());
|
|
127
|
+
const fixEntry = result.data!.fixes?.find((f) => f.check === "memory-layer");
|
|
128
|
+
expect(fixEntry).toBeDefined();
|
|
129
|
+
expect(fixEntry!.action).toBe("fixed");
|
|
130
|
+
|
|
131
|
+
const after = await readFile(join(tempDir, ".agents", "memory", "MEMORY.md"), "utf8");
|
|
132
|
+
expect(after.length).toBeLessThanOrEqual(4096);
|
|
133
|
+
expect(after).toContain("- keep");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("doctor without --fix produces no fixes array and does not mutate", async () => {
|
|
137
|
+
await writeFile(join(tempDir, "forge.yaml"), FORGE_YAML, "utf8");
|
|
138
|
+
await makeStaleWorkspace();
|
|
139
|
+
|
|
140
|
+
const result = await runDoctor({ argv: [], flags: {} }, makeContext());
|
|
141
|
+
expect(result.data!.fixes).toBeUndefined();
|
|
142
|
+
|
|
143
|
+
const after = await readFile(join(tempDir, "packages", "my-pkg", "AGENTS.md"), "utf8");
|
|
144
|
+
expect(after).toContain("Old content");
|
|
145
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for the nested-AGENTS.md doctor check — consumer-declared
|
|
4
|
+
workspace discovery exclusions via bindings.workspaces.skipDirs (RFC-1150).</purpose>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<CHANGE_SUMMARY>
|
|
7
|
+
<item>RFC-1150: initial nested-AGENTS.md skipDirs tests.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { runDoctor } from "../onboarding/doctor.ts";
|
|
16
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tempDir = await mkdtemp(join(tmpdir(), "doctor-nested-test-"));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function makeContext(): ForgeRuntimeContext {
|
|
29
|
+
return {
|
|
30
|
+
workspaceRoot: tempDir,
|
|
31
|
+
logger: {
|
|
32
|
+
section: () => {},
|
|
33
|
+
success: () => {},
|
|
34
|
+
warn: () => {},
|
|
35
|
+
error: () => {},
|
|
36
|
+
info: () => {},
|
|
37
|
+
},
|
|
38
|
+
dryRun: false,
|
|
39
|
+
outputFormat: "json",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function forgeYaml(skipDirs: string[]): string {
|
|
44
|
+
const skip = skipDirs.length > 0 ? `\n workspaces:\n skipDirs: [${skipDirs.join(", ")}]` : "";
|
|
45
|
+
return `schema: "forge/config@1"
|
|
46
|
+
project:
|
|
47
|
+
name: test-project
|
|
48
|
+
stack: []
|
|
49
|
+
packageManager: pnpm
|
|
50
|
+
paths:
|
|
51
|
+
rfcsDir: docs/rfcs
|
|
52
|
+
adrsDir: docs/adrs
|
|
53
|
+
plansDir: docs/plans
|
|
54
|
+
auditsDir: docs/audits
|
|
55
|
+
specsDir: docs/specs
|
|
56
|
+
skillsDir: .agents/skills
|
|
57
|
+
sessionsDir: docs/sessions
|
|
58
|
+
bindings:
|
|
59
|
+
schema: forge/bindings@1
|
|
60
|
+
commands:
|
|
61
|
+
validateRfc: null
|
|
62
|
+
validateAdr: null
|
|
63
|
+
implementStamp: null
|
|
64
|
+
typecheck: null
|
|
65
|
+
test: null
|
|
66
|
+
scopedBuild: null
|
|
67
|
+
specValidate: null
|
|
68
|
+
sessionSave: null
|
|
69
|
+
paths:
|
|
70
|
+
invariantsFile: null
|
|
71
|
+
compassDocs: []
|
|
72
|
+
reviewsDir: null
|
|
73
|
+
handoffsDir: null
|
|
74
|
+
sessionsDir: null${skip}
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test("nested-AGENTS.md check excludes dirs declared in workspaces.skipDirs", async () => {
|
|
79
|
+
await writeFile(join(tempDir, "forge.yaml"), forgeYaml(["builds"]), "utf8");
|
|
80
|
+
// Artifact dir that would otherwise be discovered as a workspace
|
|
81
|
+
await mkdir(join(tempDir, "builds", "out"), { recursive: true });
|
|
82
|
+
await writeFile(join(tempDir, "builds", "out", "package.json"), "{}");
|
|
83
|
+
// Real workspace without AGENTS.md — must still be reported missing
|
|
84
|
+
await mkdir(join(tempDir, "packages", "real-pkg"), { recursive: true });
|
|
85
|
+
await writeFile(join(tempDir, "packages", "real-pkg", "package.json"), "{}");
|
|
86
|
+
|
|
87
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
88
|
+
const result = await runDoctor(input, makeContext());
|
|
89
|
+
|
|
90
|
+
const check = result.data!.checks.find((c) => c.name === "nested-AGENTS.md");
|
|
91
|
+
expect(check).toBeDefined();
|
|
92
|
+
// missing-only reports pass (warn requires stale>0) — assert on message
|
|
93
|
+
expect(check!.message).toContain("packages/real-pkg");
|
|
94
|
+
expect(check!.message).not.toContain("builds");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("nested-AGENTS.md check reports artifact dir as missing without skipDirs", async () => {
|
|
98
|
+
await writeFile(join(tempDir, "forge.yaml"), forgeYaml([]), "utf8");
|
|
99
|
+
await mkdir(join(tempDir, "builds", "out"), { recursive: true });
|
|
100
|
+
await writeFile(join(tempDir, "builds", "out", "package.json"), "{}");
|
|
101
|
+
|
|
102
|
+
const input: ForgeCommandInput = { argv: [], flags: {} };
|
|
103
|
+
const result = await runDoctor(input, makeContext());
|
|
104
|
+
|
|
105
|
+
const check = result.data!.checks.find((c) => c.name === "nested-AGENTS.md");
|
|
106
|
+
expect(check).toBeDefined();
|
|
107
|
+
expect(check!.message).toContain("builds");
|
|
108
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for forge memory.compact — mechanical MEMORY.md budget
|
|
4
|
+
enforcement via oldest-first Environment notes truncation (RFC-1151).</purpose>
|
|
5
|
+
</MODULE_CONTRACT>
|
|
6
|
+
<CHANGE_SUMMARY>
|
|
7
|
+
<item>RFC-1151: initial memory.compact tests.</item>
|
|
8
|
+
</CHANGE_SUMMARY>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
12
|
+
import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { runMemoryCompact, compactMemoryMd } from "../onboarding/memory-compact.ts";
|
|
16
|
+
import type { ForgeRuntimeContext } from "../types.ts";
|
|
17
|
+
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tempDir = await mkdtemp(join(tmpdir(), "memory-compact-test-"));
|
|
22
|
+
await mkdir(join(tempDir, ".agents", "memory"), { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function makeContext(dryRun = false): ForgeRuntimeContext {
|
|
30
|
+
return {
|
|
31
|
+
workspaceRoot: tempDir,
|
|
32
|
+
logger: {
|
|
33
|
+
section: () => {},
|
|
34
|
+
success: () => {},
|
|
35
|
+
warn: () => {},
|
|
36
|
+
error: () => {},
|
|
37
|
+
info: () => {},
|
|
38
|
+
},
|
|
39
|
+
dryRun,
|
|
40
|
+
outputFormat: "json",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function memoryMd(envBullets: number, bulletSize = 200): string {
|
|
45
|
+
const env = Array.from(
|
|
46
|
+
{ length: envBullets },
|
|
47
|
+
(_, i) => `- env note ${i} ${"x".repeat(bulletSize)}`,
|
|
48
|
+
).join("\n");
|
|
49
|
+
return [
|
|
50
|
+
"# Project Memory",
|
|
51
|
+
"",
|
|
52
|
+
"## Current focus",
|
|
53
|
+
"",
|
|
54
|
+
"- keep me",
|
|
55
|
+
"",
|
|
56
|
+
"## Decisions in flight",
|
|
57
|
+
"",
|
|
58
|
+
"- keep me too",
|
|
59
|
+
"",
|
|
60
|
+
"## Environment notes",
|
|
61
|
+
"",
|
|
62
|
+
env,
|
|
63
|
+
"",
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test("compactMemoryMd removes oldest Environment notes bullets until within budget", () => {
|
|
68
|
+
const content = memoryMd(20);
|
|
69
|
+
const budget = 2000;
|
|
70
|
+
const { lines, removedLines, fitsBudget } = compactMemoryMd(content, budget);
|
|
71
|
+
|
|
72
|
+
expect(fitsBudget).toBe(true);
|
|
73
|
+
expect(removedLines.length).toBeGreaterThan(0);
|
|
74
|
+
// Oldest-first: env note 0 removed before env note 19
|
|
75
|
+
expect(removedLines[0]).toContain("env note 0");
|
|
76
|
+
expect(lines.join("\n").length).toBeLessThanOrEqual(budget);
|
|
77
|
+
// Protected sections untouched
|
|
78
|
+
expect(lines.join("\n")).toContain("- keep me");
|
|
79
|
+
expect(lines.join("\n")).toContain("- keep me too");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("compactMemoryMd returns empty removedLines when already within budget", () => {
|
|
83
|
+
const content = memoryMd(2, 10);
|
|
84
|
+
const { removedLines, fitsBudget } = compactMemoryMd(content, 4096);
|
|
85
|
+
expect(removedLines).toHaveLength(0);
|
|
86
|
+
expect(fitsBudget).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("runMemoryCompact writes compacted file and reports removed count", async () => {
|
|
90
|
+
const content = memoryMd(30);
|
|
91
|
+
await writeFile(join(tempDir, ".agents", "memory", "MEMORY.md"), content, "utf8");
|
|
92
|
+
|
|
93
|
+
const result = await runMemoryCompact({ argv: [], flags: {} }, makeContext());
|
|
94
|
+
expect(result.exitCode).toBe(0);
|
|
95
|
+
expect(result.data?.status).toBe("pass");
|
|
96
|
+
expect(result.data?.removed).toBeGreaterThan(0);
|
|
97
|
+
expect(result.data?.remainingChars).toBeLessThanOrEqual(result.data!.budget);
|
|
98
|
+
|
|
99
|
+
const after = await readFile(join(tempDir, ".agents", "memory", "MEMORY.md"), "utf8");
|
|
100
|
+
expect(after.length).toBeLessThanOrEqual(result.data!.budget);
|
|
101
|
+
expect(after).toContain("- keep me");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("runMemoryCompact dryRun reports without writing", async () => {
|
|
105
|
+
const content = memoryMd(30);
|
|
106
|
+
await writeFile(join(tempDir, ".agents", "memory", "MEMORY.md"), content, "utf8");
|
|
107
|
+
|
|
108
|
+
const result = await runMemoryCompact({ argv: [], flags: {} }, makeContext(true));
|
|
109
|
+
expect(result.exitCode).toBe(0);
|
|
110
|
+
expect(result.data?.removed).toBeGreaterThan(0);
|
|
111
|
+
expect(result.data?.removedLines?.length).toBe(result.data?.removed);
|
|
112
|
+
|
|
113
|
+
const after = await readFile(join(tempDir, ".agents", "memory", "MEMORY.md"), "utf8");
|
|
114
|
+
expect(after).toBe(content);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("runMemoryCompact passes when MEMORY.md absent", async () => {
|
|
118
|
+
const result = await runMemoryCompact({ argv: [], flags: {} }, makeContext());
|
|
119
|
+
expect(result.exitCode).toBe(0);
|
|
120
|
+
expect(result.data?.removed).toBe(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("runMemoryCompact fails when protected sections alone exceed budget", async () => {
|
|
124
|
+
// Huge Current focus — Environment notes empty, nothing removable
|
|
125
|
+
const content = [
|
|
126
|
+
"# Project Memory",
|
|
127
|
+
"",
|
|
128
|
+
"## Current focus",
|
|
129
|
+
"",
|
|
130
|
+
`- ${"y".repeat(6000)}`,
|
|
131
|
+
"",
|
|
132
|
+
"## Environment notes",
|
|
133
|
+
"",
|
|
134
|
+
"- small note",
|
|
135
|
+
"",
|
|
136
|
+
].join("\n");
|
|
137
|
+
await writeFile(join(tempDir, ".agents", "memory", "MEMORY.md"), content, "utf8");
|
|
138
|
+
|
|
139
|
+
const result = await runMemoryCompact({ argv: [], flags: {} }, makeContext());
|
|
140
|
+
expect(result.exitCode).toBe(1);
|
|
141
|
+
expect(result.data?.status).toBe("fail");
|
|
142
|
+
expect(result.data?.errors[0]).toContain("manual editorial compaction");
|
|
143
|
+
});
|
|
@@ -104,6 +104,34 @@ test("discoverWorkspaces skips node_modules, .git, dist, .turbo, .cache, .agents
|
|
|
104
104
|
expect(workspaces).toHaveLength(0);
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
test("discoverWorkspaces skips missions, test-fixtures, .stryker-tmp", async () => {
|
|
108
|
+
// Pipeline-managed workpieces/archives, fixture workshops, and mutation-testing
|
|
109
|
+
// sandboxes are not project workspaces — their AGENTS.md is owned elsewhere.
|
|
110
|
+
for (const skip of ["missions", "test-fixtures", ".stryker-tmp"]) {
|
|
111
|
+
await mkdir(join(tempDir, skip, "sub"), { recursive: true });
|
|
112
|
+
await writeFile(join(tempDir, skip, "sub", "package.json"), "{}");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const workspaces = discoverWorkspaces(tempDir);
|
|
116
|
+
expect(workspaces).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// RFC-1150: consumer-declared exclusions (bindings.workspaces.skipDirs)
|
|
120
|
+
|
|
121
|
+
test("discoverWorkspaces honors extraSkipDirs for consumer artifact dirs", async () => {
|
|
122
|
+
await mkdir(join(tempDir, "builds", "out"), { recursive: true });
|
|
123
|
+
await writeFile(join(tempDir, "builds", "out", "package.json"), "{}");
|
|
124
|
+
await mkdir(join(tempDir, "packages", "real-pkg"), { recursive: true });
|
|
125
|
+
await writeFile(join(tempDir, "packages", "real-pkg", "package.json"), "{}");
|
|
126
|
+
|
|
127
|
+
const without = discoverWorkspaces(tempDir);
|
|
128
|
+
expect(without.map((w) => w.path)).toContain(join("builds", "out"));
|
|
129
|
+
|
|
130
|
+
const withSkip = discoverWorkspaces(tempDir, undefined, ["builds"]);
|
|
131
|
+
expect(withSkip.map((w) => w.path)).not.toContain(join("builds", "out"));
|
|
132
|
+
expect(withSkip.map((w) => w.path)).toContain(join("packages", "real-pkg"));
|
|
133
|
+
});
|
|
134
|
+
|
|
107
135
|
test("discoverWorkspaces detects generated AGENTS.md", async () => {
|
|
108
136
|
await mkdir(join(tempDir, "packages", "my-pkg"), { recursive: true });
|
|
109
137
|
await writeFile(join(tempDir, "packages", "my-pkg", "package.json"), "{}");
|