rtfct 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.project/adrs/001-use-bun-typescript.md +52 -0
- package/.project/guardrails.md +65 -0
- package/.project/kanban/backlog.md +7 -0
- package/.project/kanban/done.md +240 -0
- package/.project/kanban/in-progress.md +11 -0
- package/.project/kickstart.md +63 -0
- package/.project/protocol.md +134 -0
- package/.project/specs/requirements.md +152 -0
- package/.project/testing/strategy.md +123 -0
- package/.project/theology.md +125 -0
- package/CLAUDE.md +119 -0
- package/README.md +143 -0
- package/package.json +31 -0
- package/src/args.ts +104 -0
- package/src/commands/add.ts +78 -0
- package/src/commands/init.ts +128 -0
- package/src/commands/praise.ts +19 -0
- package/src/commands/regenerate.ts +122 -0
- package/src/commands/status.ts +163 -0
- package/src/help.ts +52 -0
- package/src/index.ts +102 -0
- package/src/kanban.ts +83 -0
- package/src/manifest.ts +67 -0
- package/src/presets/base.ts +195 -0
- package/src/presets/elixir.ts +118 -0
- package/src/presets/github.ts +194 -0
- package/src/presets/index.ts +154 -0
- package/src/presets/typescript.ts +589 -0
- package/src/presets/zig.ts +494 -0
- package/tests/integration/add.test.ts +104 -0
- package/tests/integration/init.test.ts +197 -0
- package/tests/integration/praise.test.ts +36 -0
- package/tests/integration/regenerate.test.ts +154 -0
- package/tests/integration/status.test.ts +165 -0
- package/tests/unit/args.test.ts +144 -0
- package/tests/unit/kanban.test.ts +162 -0
- package/tests/unit/manifest.test.ts +155 -0
- package/tests/unit/presets.test.ts +295 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Tests for the Init Command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
6
|
+
import { mkdtemp, rm, stat, readFile, readdir } from "fs/promises";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { runInit, formatInit } from "../../src/commands/init";
|
|
9
|
+
|
|
10
|
+
describe("init command", () => {
|
|
11
|
+
let testDir: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
testDir = await mkdtemp("/tmp/rtfct-init-test-");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await rm(testDir, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("basic initialization", () => {
|
|
22
|
+
test("creates .project directory", async () => {
|
|
23
|
+
const result = await runInit(testDir);
|
|
24
|
+
|
|
25
|
+
expect(result.success).toBe(true);
|
|
26
|
+
const projectDir = join(testDir, ".project");
|
|
27
|
+
const stats = await stat(projectDir);
|
|
28
|
+
expect(stats.isDirectory()).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("creates protocol.md", async () => {
|
|
32
|
+
await runInit(testDir);
|
|
33
|
+
|
|
34
|
+
const filePath = join(testDir, ".project", "protocol.md");
|
|
35
|
+
const stats = await stat(filePath);
|
|
36
|
+
expect(stats.isFile()).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("creates theology.md", async () => {
|
|
40
|
+
await runInit(testDir);
|
|
41
|
+
|
|
42
|
+
const filePath = join(testDir, ".project", "theology.md");
|
|
43
|
+
const stats = await stat(filePath);
|
|
44
|
+
expect(stats.isFile()).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("creates kickstart.md", async () => {
|
|
48
|
+
await runInit(testDir);
|
|
49
|
+
|
|
50
|
+
const filePath = join(testDir, ".project", "kickstart.md");
|
|
51
|
+
const stats = await stat(filePath);
|
|
52
|
+
expect(stats.isFile()).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("creates guardrails.md", async () => {
|
|
56
|
+
await runInit(testDir);
|
|
57
|
+
|
|
58
|
+
const filePath = join(testDir, ".project", "guardrails.md");
|
|
59
|
+
const stats = await stat(filePath);
|
|
60
|
+
expect(stats.isFile()).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("creates kanban directory with files", async () => {
|
|
64
|
+
await runInit(testDir);
|
|
65
|
+
|
|
66
|
+
const kanbanDir = join(testDir, ".project", "kanban");
|
|
67
|
+
const stats = await stat(kanbanDir);
|
|
68
|
+
expect(stats.isDirectory()).toBe(true);
|
|
69
|
+
|
|
70
|
+
const files = await readdir(kanbanDir);
|
|
71
|
+
expect(files).toContain("backlog.md");
|
|
72
|
+
expect(files).toContain("in-progress.md");
|
|
73
|
+
expect(files).toContain("done.md");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("creates all subdirectories", async () => {
|
|
77
|
+
await runInit(testDir);
|
|
78
|
+
|
|
79
|
+
const dirs = [
|
|
80
|
+
"specs",
|
|
81
|
+
"design",
|
|
82
|
+
"adrs",
|
|
83
|
+
"kanban",
|
|
84
|
+
"testing",
|
|
85
|
+
"references",
|
|
86
|
+
"presets",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const dir of dirs) {
|
|
90
|
+
const dirPath = join(testDir, ".project", dir);
|
|
91
|
+
const stats = await stat(dirPath);
|
|
92
|
+
expect(stats.isDirectory()).toBe(true);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("existing project handling", () => {
|
|
98
|
+
test("fails if .project already exists", async () => {
|
|
99
|
+
// First init
|
|
100
|
+
await runInit(testDir);
|
|
101
|
+
|
|
102
|
+
// Second init should fail
|
|
103
|
+
const result = await runInit(testDir);
|
|
104
|
+
expect(result.success).toBe(false);
|
|
105
|
+
expect(result.message).toContain("already exist");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("succeeds with --force flag", async () => {
|
|
109
|
+
// First init
|
|
110
|
+
await runInit(testDir);
|
|
111
|
+
|
|
112
|
+
// Second init with force
|
|
113
|
+
const result = await runInit(testDir, { force: true });
|
|
114
|
+
expect(result.success).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("with presets", () => {
|
|
119
|
+
test("installs single preset", async () => {
|
|
120
|
+
const result = await runInit(testDir, { presets: ["zig"] });
|
|
121
|
+
|
|
122
|
+
expect(result.success).toBe(true);
|
|
123
|
+
|
|
124
|
+
const presetDir = join(testDir, ".project", "presets", "zig");
|
|
125
|
+
const stats = await stat(presetDir);
|
|
126
|
+
expect(stats.isDirectory()).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("installs multiple presets", async () => {
|
|
130
|
+
const result = await runInit(testDir, {
|
|
131
|
+
presets: ["zig", "typescript"],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result.success).toBe(true);
|
|
135
|
+
|
|
136
|
+
const zigDir = join(testDir, ".project", "presets", "zig");
|
|
137
|
+
const tsDir = join(testDir, ".project", "presets", "typescript");
|
|
138
|
+
|
|
139
|
+
const zigStats = await stat(zigDir);
|
|
140
|
+
const tsStats = await stat(tsDir);
|
|
141
|
+
|
|
142
|
+
expect(zigStats.isDirectory()).toBe(true);
|
|
143
|
+
expect(tsStats.isDirectory()).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("reports errors for unknown presets", async () => {
|
|
147
|
+
const result = await runInit(testDir, { presets: ["unknown"] });
|
|
148
|
+
|
|
149
|
+
expect(result.success).toBe(true); // Still succeeds overall
|
|
150
|
+
expect(result.presetErrors).toBeDefined();
|
|
151
|
+
expect(result.presetErrors!.length).toBeGreaterThan(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("installs valid presets even if some fail", async () => {
|
|
155
|
+
const result = await runInit(testDir, {
|
|
156
|
+
presets: ["zig", "unknown"],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result.success).toBe(true);
|
|
160
|
+
|
|
161
|
+
// Zig should be installed
|
|
162
|
+
const zigDir = join(testDir, ".project", "presets", "zig");
|
|
163
|
+
const zigStats = await stat(zigDir);
|
|
164
|
+
expect(zigStats.isDirectory()).toBe(true);
|
|
165
|
+
|
|
166
|
+
// But we should have an error for unknown
|
|
167
|
+
expect(result.presetErrors!.length).toBe(1);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("output formatting", () => {
|
|
172
|
+
test("formats success message", async () => {
|
|
173
|
+
const result = await runInit(testDir);
|
|
174
|
+
const output = formatInit(result);
|
|
175
|
+
|
|
176
|
+
expect(output).toContain("✓");
|
|
177
|
+
expect(output).toContain("consecrated");
|
|
178
|
+
expect(output).toContain("Omnissiah");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("formats failure message", async () => {
|
|
182
|
+
await runInit(testDir);
|
|
183
|
+
const result = await runInit(testDir); // Second init fails
|
|
184
|
+
const output = formatInit(result);
|
|
185
|
+
|
|
186
|
+
expect(output).toContain("✗");
|
|
187
|
+
expect(output).toContain("already exist");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("formats preset warnings", async () => {
|
|
191
|
+
const result = await runInit(testDir, { presets: ["unknown"] });
|
|
192
|
+
const output = formatInit(result);
|
|
193
|
+
|
|
194
|
+
expect(output).toContain("warning");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Tests for the Praise Command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect } from "bun:test";
|
|
6
|
+
import { runPraise } from "../../src/commands/praise";
|
|
7
|
+
|
|
8
|
+
describe("praise command", () => {
|
|
9
|
+
test("outputs the litany", () => {
|
|
10
|
+
const output = runPraise();
|
|
11
|
+
|
|
12
|
+
expect(output).toContain("The flesh is weak, but the protocol is strong.");
|
|
13
|
+
expect(output).toContain("The code is temporary, but the spec endures.");
|
|
14
|
+
expect(output).toContain(
|
|
15
|
+
"The tests do not lie, and the agent does not tire."
|
|
16
|
+
);
|
|
17
|
+
expect(output).toContain(
|
|
18
|
+
"From specification, code. From code, verification. From verification, truth."
|
|
19
|
+
);
|
|
20
|
+
expect(output).toContain("The Omnissiah provides.");
|
|
21
|
+
expect(output).toContain("Praise the Machine Spirit.");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("outputs exactly the expected litany", () => {
|
|
25
|
+
const output = runPraise();
|
|
26
|
+
|
|
27
|
+
const expected = `The flesh is weak, but the protocol is strong.
|
|
28
|
+
The code is temporary, but the spec endures.
|
|
29
|
+
The tests do not lie, and the agent does not tire.
|
|
30
|
+
From specification, code. From code, verification. From verification, truth.
|
|
31
|
+
The Omnissiah provides.
|
|
32
|
+
Praise the Machine Spirit.`;
|
|
33
|
+
|
|
34
|
+
expect(output).toBe(expected);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Tests for the Regenerate Command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
6
|
+
import { mkdtemp, rm, stat, mkdir, writeFile } from "fs/promises";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { runInit } from "../../src/commands/init";
|
|
9
|
+
import {
|
|
10
|
+
runRegenerate,
|
|
11
|
+
formatRegenerate,
|
|
12
|
+
} from "../../src/commands/regenerate";
|
|
13
|
+
|
|
14
|
+
describe("regenerate command", () => {
|
|
15
|
+
let testDir: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
testDir = await mkdtemp("/tmp/rtfct-regen-test-");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await rm(testDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("without existing project", () => {
|
|
26
|
+
test("fails if .project does not exist", async () => {
|
|
27
|
+
const result = await runRegenerate(testDir);
|
|
28
|
+
|
|
29
|
+
expect(result.success).toBe(false);
|
|
30
|
+
expect(result.message).toContain("No .project/ folder found");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("confirmation behavior", () => {
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
await runInit(testDir);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("requires confirmation without --yes", async () => {
|
|
40
|
+
const result = await runRegenerate(testDir);
|
|
41
|
+
|
|
42
|
+
expect(result.success).toBe(true);
|
|
43
|
+
expect(result.requiresConfirmation).toBe(true);
|
|
44
|
+
expect(result.pathsToDelete).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("proceeds with --yes flag", async () => {
|
|
48
|
+
const result = await runRegenerate(testDir, { yes: true });
|
|
49
|
+
|
|
50
|
+
expect(result.success).toBe(true);
|
|
51
|
+
expect(result.requiresConfirmation).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("path deletion", () => {
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
await runInit(testDir);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("deletes default paths when no presets", async () => {
|
|
61
|
+
// Create src/ and tests/
|
|
62
|
+
await mkdir(join(testDir, "src"));
|
|
63
|
+
await writeFile(join(testDir, "src", "index.ts"), "// code");
|
|
64
|
+
await mkdir(join(testDir, "tests"));
|
|
65
|
+
await writeFile(join(testDir, "tests", "test.ts"), "// test");
|
|
66
|
+
|
|
67
|
+
const result = await runRegenerate(testDir, { yes: true });
|
|
68
|
+
|
|
69
|
+
expect(result.success).toBe(true);
|
|
70
|
+
expect(result.deletedPaths).toContain("src/");
|
|
71
|
+
expect(result.deletedPaths).toContain("tests/");
|
|
72
|
+
|
|
73
|
+
// Verify paths are deleted
|
|
74
|
+
try {
|
|
75
|
+
await stat(join(testDir, "src"));
|
|
76
|
+
expect(true).toBe(false); // Should not reach here
|
|
77
|
+
} catch {
|
|
78
|
+
// Expected - directory should not exist
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("deletes paths from preset manifests", async () => {
|
|
83
|
+
await runInit(testDir, { presets: ["zig"], force: true });
|
|
84
|
+
|
|
85
|
+
// Create files that match the zig preset's generated_paths
|
|
86
|
+
await mkdir(join(testDir, "src"));
|
|
87
|
+
await writeFile(join(testDir, "src", "main.zig"), "// zig code");
|
|
88
|
+
await writeFile(join(testDir, "build.zig"), "// build");
|
|
89
|
+
|
|
90
|
+
const result = await runRegenerate(testDir, { yes: true });
|
|
91
|
+
|
|
92
|
+
expect(result.success).toBe(true);
|
|
93
|
+
expect(result.deletedPaths).toContain("src/");
|
|
94
|
+
expect(result.deletedPaths).toContain("build.zig");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("handles non-existent paths gracefully", async () => {
|
|
98
|
+
// Don't create src/ or tests/
|
|
99
|
+
const result = await runRegenerate(testDir, { yes: true });
|
|
100
|
+
|
|
101
|
+
expect(result.success).toBe(true);
|
|
102
|
+
// Paths might be empty since nothing existed to delete
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("preserves .project directory", async () => {
|
|
106
|
+
await mkdir(join(testDir, "src"));
|
|
107
|
+
|
|
108
|
+
await runRegenerate(testDir, { yes: true });
|
|
109
|
+
|
|
110
|
+
// .project should still exist
|
|
111
|
+
const projectStats = await stat(join(testDir, ".project"));
|
|
112
|
+
expect(projectStats.isDirectory()).toBe(true);
|
|
113
|
+
|
|
114
|
+
// And its files should still exist
|
|
115
|
+
const protocolStats = await stat(join(testDir, ".project", "protocol.md"));
|
|
116
|
+
expect(protocolStats.isFile()).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("output formatting", () => {
|
|
121
|
+
beforeEach(async () => {
|
|
122
|
+
await runInit(testDir);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("formats confirmation request", async () => {
|
|
126
|
+
const result = await runRegenerate(testDir);
|
|
127
|
+
const output = formatRegenerate(result);
|
|
128
|
+
|
|
129
|
+
expect(output).toContain("Purification");
|
|
130
|
+
expect(output).toContain("Run with --yes");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("formats success output", async () => {
|
|
134
|
+
await mkdir(join(testDir, "src"));
|
|
135
|
+
|
|
136
|
+
const result = await runRegenerate(testDir, { yes: true });
|
|
137
|
+
const output = formatRegenerate(result);
|
|
138
|
+
|
|
139
|
+
expect(output).toContain("✓");
|
|
140
|
+
expect(output).toContain("purified");
|
|
141
|
+
expect(output).toContain("Omnissiah");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("formats failure output", async () => {
|
|
145
|
+
await rm(testDir, { recursive: true, force: true });
|
|
146
|
+
testDir = await mkdtemp("/tmp/rtfct-regen-test-");
|
|
147
|
+
|
|
148
|
+
const result = await runRegenerate(testDir);
|
|
149
|
+
const output = formatRegenerate(result);
|
|
150
|
+
|
|
151
|
+
expect(output).toContain("✗");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Tests for the Status Command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
6
|
+
import { mkdtemp, rm, writeFile, mkdir } from "fs/promises";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import { runInit } from "../../src/commands/init";
|
|
9
|
+
import { runStatus, formatStatus } from "../../src/commands/status";
|
|
10
|
+
|
|
11
|
+
describe("status command", () => {
|
|
12
|
+
let testDir: string;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
testDir = await mkdtemp("/tmp/rtfct-status-test-");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await rm(testDir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("without existing project", () => {
|
|
23
|
+
test("fails if .project does not exist", async () => {
|
|
24
|
+
const result = await runStatus(testDir);
|
|
25
|
+
|
|
26
|
+
expect(result.success).toBe(false);
|
|
27
|
+
expect(result.message).toContain("No .project/ folder found");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("with existing project", () => {
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
await runInit(testDir);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns success", async () => {
|
|
37
|
+
const result = await runStatus(testDir);
|
|
38
|
+
expect(result.success).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns project name", async () => {
|
|
42
|
+
const result = await runStatus(testDir);
|
|
43
|
+
expect(result.data).toBeDefined();
|
|
44
|
+
expect(result.data!.projectName).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("counts tasks from default kanban files", async () => {
|
|
48
|
+
const result = await runStatus(testDir);
|
|
49
|
+
|
|
50
|
+
// Default template has 1 task in backlog
|
|
51
|
+
expect(result.data!.backlogCount).toBe(1);
|
|
52
|
+
expect(result.data!.inProgressCount).toBe(0);
|
|
53
|
+
expect(result.data!.doneCount).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("returns last activity date", async () => {
|
|
57
|
+
const result = await runStatus(testDir);
|
|
58
|
+
expect(result.data!.lastActivity).not.toBeNull();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("with custom kanban content", () => {
|
|
63
|
+
beforeEach(async () => {
|
|
64
|
+
await runInit(testDir);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("counts multiple backlog tasks", async () => {
|
|
68
|
+
const backlogContent = `# Backlog
|
|
69
|
+
|
|
70
|
+
## [TASK-001] First Task
|
|
71
|
+
|
|
72
|
+
## [TASK-002] Second Task
|
|
73
|
+
|
|
74
|
+
## [TASK-003] Third Task
|
|
75
|
+
`;
|
|
76
|
+
await writeFile(
|
|
77
|
+
join(testDir, ".project", "kanban", "backlog.md"),
|
|
78
|
+
backlogContent
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const result = await runStatus(testDir);
|
|
82
|
+
expect(result.data!.backlogCount).toBe(3);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("extracts current task from in-progress", async () => {
|
|
86
|
+
const inProgressContent = `# In Progress
|
|
87
|
+
|
|
88
|
+
## [TASK-004] Implement user auth
|
|
89
|
+
|
|
90
|
+
Working on it.
|
|
91
|
+
`;
|
|
92
|
+
await writeFile(
|
|
93
|
+
join(testDir, ".project", "kanban", "in-progress.md"),
|
|
94
|
+
inProgressContent
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const result = await runStatus(testDir);
|
|
98
|
+
expect(result.data!.inProgressCount).toBe(1);
|
|
99
|
+
expect(result.data!.currentTask).not.toBeNull();
|
|
100
|
+
expect(result.data!.currentTask!.id).toBe("TASK-004");
|
|
101
|
+
expect(result.data!.currentTask!.title).toBe("Implement user auth");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("counts completed tasks", async () => {
|
|
105
|
+
const doneContent = `# Done
|
|
106
|
+
|
|
107
|
+
## [TASK-001] First Completed
|
|
108
|
+
|
|
109
|
+
## [TASK-002] Second Completed
|
|
110
|
+
`;
|
|
111
|
+
await writeFile(
|
|
112
|
+
join(testDir, ".project", "kanban", "done.md"),
|
|
113
|
+
doneContent
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const result = await runStatus(testDir);
|
|
117
|
+
expect(result.data!.doneCount).toBe(2);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("output formatting", () => {
|
|
122
|
+
beforeEach(async () => {
|
|
123
|
+
await runInit(testDir);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("formats success output", async () => {
|
|
127
|
+
const result = await runStatus(testDir);
|
|
128
|
+
const output = formatStatus(result);
|
|
129
|
+
|
|
130
|
+
expect(output).toContain("rtfct:");
|
|
131
|
+
expect(output).toContain("Litany of Tasks");
|
|
132
|
+
expect(output).toContain("Backlog:");
|
|
133
|
+
expect(output).toContain("In Progress:");
|
|
134
|
+
expect(output).toContain("Completed:");
|
|
135
|
+
expect(output).toContain("Omnissiah");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("formats current task", async () => {
|
|
139
|
+
const inProgressContent = `# In Progress
|
|
140
|
+
|
|
141
|
+
## [TASK-007] The Holy Task
|
|
142
|
+
`;
|
|
143
|
+
await writeFile(
|
|
144
|
+
join(testDir, ".project", "kanban", "in-progress.md"),
|
|
145
|
+
inProgressContent
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const result = await runStatus(testDir);
|
|
149
|
+
const output = formatStatus(result);
|
|
150
|
+
|
|
151
|
+
expect(output).toContain("[TASK-007]");
|
|
152
|
+
expect(output).toContain("The Holy Task");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("formats failure output", async () => {
|
|
156
|
+
await rm(testDir, { recursive: true, force: true });
|
|
157
|
+
testDir = await mkdtemp("/tmp/rtfct-status-test-");
|
|
158
|
+
|
|
159
|
+
const result = await runStatus(testDir);
|
|
160
|
+
const output = formatStatus(result);
|
|
161
|
+
|
|
162
|
+
expect(output).toContain("✗");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit Tests for the Argument Parser
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect } from "bun:test";
|
|
6
|
+
import { parseArgs } from "../../src/args";
|
|
7
|
+
|
|
8
|
+
describe("parseArgs", () => {
|
|
9
|
+
describe("commands", () => {
|
|
10
|
+
test("parses init command", () => {
|
|
11
|
+
const result = parseArgs(["init"]);
|
|
12
|
+
expect(result.command).toBe("init");
|
|
13
|
+
expect(result.error).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("parses add command", () => {
|
|
17
|
+
const result = parseArgs(["add"]);
|
|
18
|
+
expect(result.command).toBe("add");
|
|
19
|
+
expect(result.error).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("parses status command", () => {
|
|
23
|
+
const result = parseArgs(["status"]);
|
|
24
|
+
expect(result.command).toBe("status");
|
|
25
|
+
expect(result.error).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("parses regenerate command", () => {
|
|
29
|
+
const result = parseArgs(["regenerate"]);
|
|
30
|
+
expect(result.command).toBe("regenerate");
|
|
31
|
+
expect(result.error).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("parses praise command", () => {
|
|
35
|
+
const result = parseArgs(["praise"]);
|
|
36
|
+
expect(result.command).toBe("praise");
|
|
37
|
+
expect(result.error).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("returns error for unknown command", () => {
|
|
41
|
+
const result = parseArgs(["unknown"]);
|
|
42
|
+
expect(result.error).toBe("Unknown command: unknown");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("no command returns undefined", () => {
|
|
46
|
+
const result = parseArgs([]);
|
|
47
|
+
expect(result.command).toBeUndefined();
|
|
48
|
+
expect(result.error).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("flags", () => {
|
|
53
|
+
test("parses --help flag", () => {
|
|
54
|
+
const result = parseArgs(["--help"]);
|
|
55
|
+
expect(result.flags.help).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("parses -h flag", () => {
|
|
59
|
+
const result = parseArgs(["-h"]);
|
|
60
|
+
expect(result.flags.help).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("parses --version flag", () => {
|
|
64
|
+
const result = parseArgs(["--version"]);
|
|
65
|
+
expect(result.flags.version).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("parses -v flag", () => {
|
|
69
|
+
const result = parseArgs(["-v"]);
|
|
70
|
+
expect(result.flags.version).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("parses --force flag", () => {
|
|
74
|
+
const result = parseArgs(["init", "--force"]);
|
|
75
|
+
expect(result.flags.force).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("parses -f flag", () => {
|
|
79
|
+
const result = parseArgs(["init", "-f"]);
|
|
80
|
+
expect(result.flags.force).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("parses --yes flag", () => {
|
|
84
|
+
const result = parseArgs(["regenerate", "--yes"]);
|
|
85
|
+
expect(result.flags.yes).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("parses -y flag", () => {
|
|
89
|
+
const result = parseArgs(["regenerate", "-y"]);
|
|
90
|
+
expect(result.flags.yes).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("parses --with flag with single preset", () => {
|
|
94
|
+
const result = parseArgs(["init", "--with", "zig"]);
|
|
95
|
+
expect(result.flags.with).toEqual(["zig"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("parses --with flag with multiple presets", () => {
|
|
99
|
+
const result = parseArgs(["init", "--with", "zig,typescript"]);
|
|
100
|
+
expect(result.flags.with).toEqual(["zig", "typescript"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("returns error for unknown flag", () => {
|
|
104
|
+
const result = parseArgs(["--unknown"]);
|
|
105
|
+
expect(result.error).toBe("Unknown flag: --unknown");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("returns error for unknown short flag", () => {
|
|
109
|
+
const result = parseArgs(["-x"]);
|
|
110
|
+
expect(result.error).toBe("Unknown flag: -x");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("arguments", () => {
|
|
115
|
+
test("collects arguments after command", () => {
|
|
116
|
+
const result = parseArgs(["add", "zig"]);
|
|
117
|
+
expect(result.command).toBe("add");
|
|
118
|
+
expect(result.args).toEqual(["zig"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("collects multiple arguments", () => {
|
|
122
|
+
const result = parseArgs(["add", "zig", "typescript"]);
|
|
123
|
+
expect(result.args).toEqual(["zig", "typescript"]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("combined", () => {
|
|
128
|
+
test("parses command with flags and arguments", () => {
|
|
129
|
+
const result = parseArgs(["init", "--force", "--with", "zig"]);
|
|
130
|
+
expect(result.command).toBe("init");
|
|
131
|
+
expect(result.flags.force).toBe(true);
|
|
132
|
+
expect(result.flags.with).toEqual(["zig"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("flags default to false", () => {
|
|
136
|
+
const result = parseArgs(["init"]);
|
|
137
|
+
expect(result.flags.help).toBe(false);
|
|
138
|
+
expect(result.flags.version).toBe(false);
|
|
139
|
+
expect(result.flags.force).toBe(false);
|
|
140
|
+
expect(result.flags.yes).toBe(false);
|
|
141
|
+
expect(result.flags.with).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|