@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,391 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { runMissionArchive } from "./archive.ts";
|
|
7
|
+
import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../src/types.ts";
|
|
8
|
+
|
|
9
|
+
vi.mock("../../../src/utils/fs-trash.ts", () => ({
|
|
10
|
+
trashPath: (targetPath: string) => fs.rm(targetPath, { recursive: true, force: true }),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const execSyncMock = vi.fn<(cmd: string, opts?: Record<string, unknown>) => string | Buffer>();
|
|
14
|
+
|
|
15
|
+
vi.mock("node:child_process", () => ({
|
|
16
|
+
execSync: (cmd: string, opts?: Record<string, unknown>) => execSyncMock(cmd, opts),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
function makeContext(workspaceRoot: string): ForgeRuntimeContext {
|
|
20
|
+
return {
|
|
21
|
+
workspaceRoot,
|
|
22
|
+
logger: {
|
|
23
|
+
info: () => {},
|
|
24
|
+
success: () => {},
|
|
25
|
+
warn: () => {},
|
|
26
|
+
error: () => {},
|
|
27
|
+
debug: () => {},
|
|
28
|
+
},
|
|
29
|
+
outputFormat: "json",
|
|
30
|
+
dryRun: false,
|
|
31
|
+
} as unknown as ForgeRuntimeContext;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeInput(flags: Record<string, unknown> = {}): ForgeCommandInput {
|
|
35
|
+
return {
|
|
36
|
+
commandName: "mission.archive",
|
|
37
|
+
flags,
|
|
38
|
+
} as unknown as ForgeCommandInput;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unwrap<T>(result: { data?: T }): T {
|
|
42
|
+
if (!result.data) throw new Error("result.data is undefined");
|
|
43
|
+
return result.data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function writeMissionManifest(
|
|
47
|
+
missionsDir: string,
|
|
48
|
+
missionId: string,
|
|
49
|
+
state: string,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
const missionDir = path.join(missionsDir, missionId);
|
|
52
|
+
await fs.mkdir(missionDir, { recursive: true });
|
|
53
|
+
await fs.writeFile(
|
|
54
|
+
path.join(missionDir, "mission.yaml"),
|
|
55
|
+
`missionId: ${missionId}\nstate: ${state}\n`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let tmpDir: string;
|
|
60
|
+
let missionsDir: string;
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "mission-archive-test-"));
|
|
64
|
+
missionsDir = path.join(tmpDir, "missions");
|
|
65
|
+
await fs.mkdir(missionsDir, { recursive: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
afterEach(async () => {
|
|
69
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("mission.archive", () => {
|
|
73
|
+
test("terminal mission (closed) → moved to archive/closed/", async () => {
|
|
74
|
+
await writeMissionManifest(missionsDir, "test-m001", "closed");
|
|
75
|
+
|
|
76
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
77
|
+
|
|
78
|
+
expect(data.moved).toHaveLength(1);
|
|
79
|
+
expect(data.moved[0].missionId).toBe("test-m001");
|
|
80
|
+
expect(data.moved[0].state).toBe("closed");
|
|
81
|
+
expect(data.moved[0].direction).toBe("into-archive");
|
|
82
|
+
expect(existsSync(path.join(missionsDir, "test-m001"))).toBe(false);
|
|
83
|
+
expect(existsSync(path.join(missionsDir, "archive", "closed", "test-m001"))).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("terminal mission (aborted) → moved to archive/aborted/", async () => {
|
|
87
|
+
await writeMissionManifest(missionsDir, "test-m002", "aborted");
|
|
88
|
+
|
|
89
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
90
|
+
|
|
91
|
+
expect(data.moved).toHaveLength(1);
|
|
92
|
+
expect(data.moved[0].missionId).toBe("test-m002");
|
|
93
|
+
expect(data.moved[0].state).toBe("aborted");
|
|
94
|
+
expect(existsSync(path.join(missionsDir, "archive", "aborted", "test-m002"))).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("open mission → skipped (non-terminal)", async () => {
|
|
98
|
+
await writeMissionManifest(missionsDir, "test-m003", "open");
|
|
99
|
+
|
|
100
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
101
|
+
|
|
102
|
+
expect(data.moved).toHaveLength(0);
|
|
103
|
+
expect(data.skipped).toHaveLength(1);
|
|
104
|
+
expect(data.skipped[0].reason).toContain("non-terminal");
|
|
105
|
+
expect(existsSync(path.join(missionsDir, "test-m003"))).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("--status closed filter → only closed missions moved", async () => {
|
|
109
|
+
await writeMissionManifest(missionsDir, "test-m004", "closed");
|
|
110
|
+
await writeMissionManifest(missionsDir, "test-m005", "aborted");
|
|
111
|
+
|
|
112
|
+
const data = unwrap(
|
|
113
|
+
await runMissionArchive(makeInput({ status: "closed" }), makeContext(tmpDir)),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(data.moved).toHaveLength(1);
|
|
117
|
+
expect(data.moved[0].missionId).toBe("test-m004");
|
|
118
|
+
expect(existsSync(path.join(missionsDir, "test-m005"))).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("--dry-run → reports moves without touching filesystem", async () => {
|
|
122
|
+
await writeMissionManifest(missionsDir, "test-m006", "closed");
|
|
123
|
+
|
|
124
|
+
const data = unwrap(
|
|
125
|
+
await runMissionArchive(makeInput({ "dry-run": true }), makeContext(tmpDir)),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(data.moved).toHaveLength(1);
|
|
129
|
+
expect(data.dryRun).toBe(true);
|
|
130
|
+
expect(existsSync(path.join(missionsDir, "test-m006"))).toBe(true);
|
|
131
|
+
expect(existsSync(path.join(missionsDir, "archive", "closed", "test-m006"))).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("destination exists → skipped with 'destination exists' reason", async () => {
|
|
135
|
+
await writeMissionManifest(missionsDir, "test-m007", "closed");
|
|
136
|
+
const archiveDir = path.join(missionsDir, "archive", "closed", "test-m007");
|
|
137
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
138
|
+
await fs.writeFile(
|
|
139
|
+
path.join(archiveDir, "mission.yaml"),
|
|
140
|
+
"missionId: test-m007\nstate: closed\n",
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
144
|
+
|
|
145
|
+
expect(data.moved).toHaveLength(0);
|
|
146
|
+
expect(data.skipped).toHaveLength(2);
|
|
147
|
+
expect(data.skipped.some((s) => s.reason === "destination exists")).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("unreadable manifest → skipped with 'unreadable manifest' reason", async () => {
|
|
151
|
+
const missionDir = path.join(missionsDir, "test-m008");
|
|
152
|
+
await fs.mkdir(missionDir, { recursive: true });
|
|
153
|
+
// No mission.yaml — unreadable
|
|
154
|
+
|
|
155
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
156
|
+
|
|
157
|
+
expect(data.moved).toHaveLength(0);
|
|
158
|
+
expect(data.skipped).toHaveLength(1);
|
|
159
|
+
expect(data.skipped[0].reason).toBe("unreadable manifest");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("open mission in archive/ → moved back to missions/ (bidirectional)", async () => {
|
|
163
|
+
await writeMissionManifest(missionsDir, "test-m009", "open");
|
|
164
|
+
const archiveMissionDir = path.join(missionsDir, "archive", "closed", "test-m010");
|
|
165
|
+
await fs.mkdir(archiveMissionDir, { recursive: true });
|
|
166
|
+
await fs.writeFile(
|
|
167
|
+
path.join(archiveMissionDir, "mission.yaml"),
|
|
168
|
+
"missionId: test-m010\nstate: open\n",
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
172
|
+
|
|
173
|
+
const outOfArchive = data.moved.filter((m) => m.direction === "out-of-archive");
|
|
174
|
+
expect(outOfArchive).toHaveLength(1);
|
|
175
|
+
expect(outOfArchive[0].missionId).toBe("test-m010");
|
|
176
|
+
expect(existsSync(path.join(missionsDir, "test-m010"))).toBe(true);
|
|
177
|
+
expect(existsSync(archiveMissionDir)).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("resurrected source path (e.g. .astro/ cache) → cleaned up after rename", async () => {
|
|
181
|
+
await writeMissionManifest(missionsDir, "test-m011", "closed");
|
|
182
|
+
const missionDir = path.join(missionsDir, "test-m011");
|
|
183
|
+
|
|
184
|
+
// Simulate a watcher recreating a stale cache dir after rename by
|
|
185
|
+
// monkey-patching fs.rename to re-create .astro/ at the source.
|
|
186
|
+
const originalRename = fs.rename;
|
|
187
|
+
const astroCacheDir = path.join(missionDir, "workpiece", ".astro");
|
|
188
|
+
vi.spyOn(fs, "rename").mockImplementation(async (src, dest) => {
|
|
189
|
+
await originalRename(src, dest);
|
|
190
|
+
// Simulate IDE/watcher recreating stale cache at old path
|
|
191
|
+
await fs.mkdir(path.join(missionDir, "workpiece", ".astro"), { recursive: true });
|
|
192
|
+
await fs.writeFile(path.join(astroCacheDir, "content.d.ts"), "// stale cache\n");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
196
|
+
|
|
197
|
+
vi.mocked(fs.rename).mockRestore();
|
|
198
|
+
|
|
199
|
+
expect(data.moved).toHaveLength(1);
|
|
200
|
+
expect(data.moved[0].missionId).toBe("test-m011");
|
|
201
|
+
expect(existsSync(missionDir)).toBe(false);
|
|
202
|
+
expect(existsSync(path.join(missionsDir, "archive", "closed", "test-m011"))).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("invalid --status value → throws error", async () => {
|
|
206
|
+
await expect(
|
|
207
|
+
runMissionArchive(makeInput({ status: "invalid" }), makeContext(tmpDir)),
|
|
208
|
+
).rejects.toThrow('Invalid --status "invalid"');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("no missions/ directory → empty result", async () => {
|
|
212
|
+
const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), "mission-archive-empty-"));
|
|
213
|
+
try {
|
|
214
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(emptyDir)));
|
|
215
|
+
expect(data.moved).toHaveLength(0);
|
|
216
|
+
expect(data.skipped).toHaveLength(0);
|
|
217
|
+
} finally {
|
|
218
|
+
await fs.rm(emptyDir, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("RFC-0796: stale symlink in missions/ root → trashed", async () => {
|
|
223
|
+
// Create an archived mission
|
|
224
|
+
const archiveMissionDir = path.join(missionsDir, "archive", "closed", "test-m012");
|
|
225
|
+
await fs.mkdir(archiveMissionDir, { recursive: true });
|
|
226
|
+
await fs.writeFile(
|
|
227
|
+
path.join(archiveMissionDir, "mission.yaml"),
|
|
228
|
+
"missionId: test-m012\nstate: closed\n",
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Create a stale symlink pointing to the archived mission
|
|
232
|
+
const symlinkPath = path.join(missionsDir, "test-m012");
|
|
233
|
+
await fs.symlink(archiveMissionDir, symlinkPath);
|
|
234
|
+
|
|
235
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
236
|
+
|
|
237
|
+
// Symlink should be trashed
|
|
238
|
+
expect(existsSync(symlinkPath)).toBe(false);
|
|
239
|
+
// Archived mission should still exist
|
|
240
|
+
expect(existsSync(archiveMissionDir)).toBe(true);
|
|
241
|
+
// Should be reported as skipped with stale symlink reason
|
|
242
|
+
expect(data.skipped.some((s) => s.reason === "stale symlink — trashed")).toBe(true);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("RFC-0796: stale symlink in --dry-run → not trashed, reported", async () => {
|
|
246
|
+
const archiveMissionDir = path.join(missionsDir, "archive", "closed", "test-m013");
|
|
247
|
+
await fs.mkdir(archiveMissionDir, { recursive: true });
|
|
248
|
+
await fs.writeFile(
|
|
249
|
+
path.join(archiveMissionDir, "mission.yaml"),
|
|
250
|
+
"missionId: test-m013\nstate: closed\n",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
const symlinkPath = path.join(missionsDir, "test-m013");
|
|
254
|
+
await fs.symlink(archiveMissionDir, symlinkPath);
|
|
255
|
+
|
|
256
|
+
const data = unwrap(
|
|
257
|
+
await runMissionArchive(makeInput({ "dry-run": true }), makeContext(tmpDir)),
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// In dry-run, symlink should NOT be trashed
|
|
261
|
+
expect(existsSync(symlinkPath)).toBe(true);
|
|
262
|
+
// But should be reported as skipped
|
|
263
|
+
expect(data.skipped.some((s) => s.reason === "stale symlink — trashed")).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("RFC-0801: service folders deleted before move", async () => {
|
|
267
|
+
await writeMissionManifest(missionsDir, "test-m014", "closed");
|
|
268
|
+
const missionDir = path.join(missionsDir, "test-m014");
|
|
269
|
+
const workpieceDir = path.join(missionDir, "workpiece");
|
|
270
|
+
await fs.mkdir(workpieceDir, { recursive: true });
|
|
271
|
+
|
|
272
|
+
// Create service folders with dummy content
|
|
273
|
+
for (const folder of ["node_modules", "dist", ".astro", ".wrangler", ".cache", ".turbo"]) {
|
|
274
|
+
const folderPath = path.join(workpieceDir, folder);
|
|
275
|
+
await fs.mkdir(folderPath, { recursive: true });
|
|
276
|
+
await fs.writeFile(path.join(folderPath, "dummy.txt"), `content of ${folder}\n`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
280
|
+
|
|
281
|
+
expect(data.moved).toHaveLength(1);
|
|
282
|
+
expect(data.moved[0].missionId).toBe("test-m014");
|
|
283
|
+
|
|
284
|
+
const archivedWorkpiece = path.join(missionsDir, "archive", "closed", "test-m014", "workpiece");
|
|
285
|
+
for (const folder of ["node_modules", "dist", ".astro", ".wrangler", ".cache", ".turbo"]) {
|
|
286
|
+
expect(existsSync(path.join(archivedWorkpiece, folder))).toBe(false);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("RFC-0801: mission without workpiece — cleanup skipped gracefully", async () => {
|
|
291
|
+
await writeMissionManifest(missionsDir, "test-m015", "closed");
|
|
292
|
+
// No workpiece/ directory created — simulates aborted mission
|
|
293
|
+
|
|
294
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
295
|
+
|
|
296
|
+
expect(data.moved).toHaveLength(1);
|
|
297
|
+
expect(data.moved[0].missionId).toBe("test-m015");
|
|
298
|
+
expect(existsSync(path.join(missionsDir, "archive", "closed", "test-m015"))).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("RFC-0801: --dry-run does not delete service folders", async () => {
|
|
302
|
+
await writeMissionManifest(missionsDir, "test-m016", "closed");
|
|
303
|
+
const missionDir = path.join(missionsDir, "test-m016");
|
|
304
|
+
const workpieceDir = path.join(missionDir, "workpiece");
|
|
305
|
+
await fs.mkdir(workpieceDir, { recursive: true });
|
|
306
|
+
await fs.mkdir(path.join(workpieceDir, "node_modules"), { recursive: true });
|
|
307
|
+
await fs.writeFile(path.join(workpieceDir, "node_modules", "pkg.txt"), "content\n");
|
|
308
|
+
|
|
309
|
+
const data = unwrap(
|
|
310
|
+
await runMissionArchive(makeInput({ "dry-run": true }), makeContext(tmpDir)),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
expect(data.dryRun).toBe(true);
|
|
314
|
+
// Service folders should still exist at source in dry-run
|
|
315
|
+
expect(existsSync(path.join(workpieceDir, "node_modules"))).toBe(true);
|
|
316
|
+
expect(existsSync(path.join(workpieceDir, "node_modules", "pkg.txt"))).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("RFC-0804: pnpm install called after moves", async () => {
|
|
320
|
+
execSyncMock.mockReset();
|
|
321
|
+
execSyncMock.mockReturnValue("");
|
|
322
|
+
await writeMissionManifest(missionsDir, "test-m017", "closed");
|
|
323
|
+
|
|
324
|
+
await runMissionArchive(makeInput(), makeContext(tmpDir));
|
|
325
|
+
|
|
326
|
+
const installCall = execSyncMock.mock.calls.find(
|
|
327
|
+
(c) => typeof c[0] === "string" && c[0].startsWith("pnpm install"),
|
|
328
|
+
);
|
|
329
|
+
expect(installCall).toBeDefined();
|
|
330
|
+
expect(installCall![1]).toMatchObject({ cwd: tmpDir });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("RFC-0804: lockfile committed when dirty after install", async () => {
|
|
334
|
+
execSyncMock.mockReset();
|
|
335
|
+
// pnpm install succeeds, git status shows dirty lockfile, git add/commit succeed
|
|
336
|
+
execSyncMock.mockImplementation((cmd: string) => {
|
|
337
|
+
if (cmd.startsWith("pnpm install")) return "";
|
|
338
|
+
if (cmd.startsWith("git status --porcelain pnpm-lock.yaml")) return "M pnpm-lock.yaml";
|
|
339
|
+
return "";
|
|
340
|
+
});
|
|
341
|
+
await writeMissionManifest(missionsDir, "test-m018", "closed");
|
|
342
|
+
|
|
343
|
+
await runMissionArchive(makeInput(), makeContext(tmpDir));
|
|
344
|
+
|
|
345
|
+
const commitCall = execSyncMock.mock.calls.find(
|
|
346
|
+
(c) => typeof c[0] === "string" && c[0].startsWith("git commit"),
|
|
347
|
+
);
|
|
348
|
+
expect(commitCall).toBeDefined();
|
|
349
|
+
expect(commitCall![0]).toContain("refresh pnpm-lock.yaml after mission.archive");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("RFC-0804: pnpm install failure is non-fatal", async () => {
|
|
353
|
+
execSyncMock.mockReset();
|
|
354
|
+
execSyncMock.mockImplementation((cmd: string) => {
|
|
355
|
+
if (cmd.startsWith("pnpm install")) throw new Error("ECONNREFUSED");
|
|
356
|
+
return "";
|
|
357
|
+
});
|
|
358
|
+
await writeMissionManifest(missionsDir, "test-m019", "closed");
|
|
359
|
+
|
|
360
|
+
const data = unwrap(await runMissionArchive(makeInput(), makeContext(tmpDir)));
|
|
361
|
+
|
|
362
|
+
expect(data.moved).toHaveLength(1);
|
|
363
|
+
expect(data.moved[0].missionId).toBe("test-m019");
|
|
364
|
+
expect(existsSync(path.join(missionsDir, "archive", "closed", "test-m019"))).toBe(true);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("RFC-0804: dry-run skips lockfile refresh", async () => {
|
|
368
|
+
execSyncMock.mockReset();
|
|
369
|
+
execSyncMock.mockReturnValue("");
|
|
370
|
+
await writeMissionManifest(missionsDir, "test-m020", "closed");
|
|
371
|
+
|
|
372
|
+
await runMissionArchive(makeInput({ "dry-run": true }), makeContext(tmpDir));
|
|
373
|
+
|
|
374
|
+
const installCall = execSyncMock.mock.calls.find(
|
|
375
|
+
(c) => typeof c[0] === "string" && c[0].startsWith("pnpm install"),
|
|
376
|
+
);
|
|
377
|
+
expect(installCall).toBeUndefined();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("RFC-0804: no moves skips lockfile refresh", async () => {
|
|
381
|
+
execSyncMock.mockReset();
|
|
382
|
+
execSyncMock.mockReturnValue("");
|
|
383
|
+
|
|
384
|
+
await runMissionArchive(makeInput(), makeContext(tmpDir));
|
|
385
|
+
|
|
386
|
+
const installCall = execSyncMock.mock.calls.find(
|
|
387
|
+
(c) => typeof c[0] === "string" && c[0].startsWith("pnpm install"),
|
|
388
|
+
);
|
|
389
|
+
expect(installCall).toBeUndefined();
|
|
390
|
+
});
|
|
391
|
+
});
|