wdyt 0.1.13 → 0.1.15
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/package.json +1 -1
- package/src/commands/chat.ts +65 -3
- package/src/context/hints.test.ts +135 -0
- package/src/context/hints.ts +264 -0
- package/src/context/index.ts +48 -0
- package/src/context/references.test.ts +341 -0
- package/src/context/references.ts +232 -0
- package/src/context/rereview.test.ts +135 -0
- package/src/context/rereview.ts +204 -0
- package/src/context/symbols.test.ts +550 -0
- package/src/context/symbols.ts +234 -0
- package/src/flow/index.ts +18 -0
- package/src/flow/specs.test.ts +260 -0
- package/src/flow/specs.ts +255 -0
- package/src/git/diff.test.ts +311 -0
- package/src/git/diff.ts +205 -0
- package/src/integration.test.ts +538 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow-Next spec loading module
|
|
3
|
+
*
|
|
4
|
+
* Loads task specs from the .flow/ directory for inclusion
|
|
5
|
+
* in code review context.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Task ID pattern: fn-N.M or fn-N-suffix.M
|
|
12
|
+
* Examples: fn-1.2, fn-2-vth.7, fn-123-abc.45
|
|
13
|
+
*/
|
|
14
|
+
const TASK_ID_REGEX = /^fn-\d+(?:-[a-z0-9]+)?(?:\.\d+)?$/i;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Result of loading a task spec
|
|
18
|
+
*/
|
|
19
|
+
export interface TaskSpecResult {
|
|
20
|
+
/** Whether the spec was found and loaded */
|
|
21
|
+
found: boolean;
|
|
22
|
+
/** The task ID that was parsed */
|
|
23
|
+
taskId: string;
|
|
24
|
+
/** The spec content (markdown) */
|
|
25
|
+
content?: string;
|
|
26
|
+
/** The file path that was loaded from */
|
|
27
|
+
path?: string;
|
|
28
|
+
/** Error message if loading failed */
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for loading task specs
|
|
34
|
+
*/
|
|
35
|
+
export interface LoadTaskSpecOptions {
|
|
36
|
+
/** Working directory (defaults to cwd) */
|
|
37
|
+
cwd?: string;
|
|
38
|
+
/** Base path for .flow directory (defaults to cwd/.flow) */
|
|
39
|
+
flowDir?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse and validate a task ID
|
|
44
|
+
*
|
|
45
|
+
* Valid formats:
|
|
46
|
+
* - fn-N (epic only, e.g., fn-1)
|
|
47
|
+
* - fn-N.M (simple task, e.g., fn-1.2)
|
|
48
|
+
* - fn-N-suffix (epic with suffix, e.g., fn-2-vth)
|
|
49
|
+
* - fn-N-suffix.M (task with suffix, e.g., fn-2-vth.7)
|
|
50
|
+
*
|
|
51
|
+
* @param input - The task ID string to parse
|
|
52
|
+
* @returns The validated task ID or null if invalid
|
|
53
|
+
*/
|
|
54
|
+
export function parseTaskId(input: string): string | null {
|
|
55
|
+
if (!input || typeof input !== "string") {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const trimmed = input.trim().toLowerCase();
|
|
60
|
+
|
|
61
|
+
if (TASK_ID_REGEX.test(trimmed)) {
|
|
62
|
+
return trimmed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extract the epic ID from a task ID
|
|
70
|
+
*
|
|
71
|
+
* Examples:
|
|
72
|
+
* - fn-1.2 -> fn-1
|
|
73
|
+
* - fn-2-vth.7 -> fn-2-vth
|
|
74
|
+
* - fn-1 -> fn-1
|
|
75
|
+
*
|
|
76
|
+
* @param taskId - The full task ID
|
|
77
|
+
* @returns The epic ID portion
|
|
78
|
+
*/
|
|
79
|
+
export function getEpicId(taskId: string): string {
|
|
80
|
+
// Remove the .M suffix if present
|
|
81
|
+
const dotIndex = taskId.lastIndexOf(".");
|
|
82
|
+
if (dotIndex > 0 && /^\d+$/.test(taskId.slice(dotIndex + 1))) {
|
|
83
|
+
return taskId.slice(0, dotIndex);
|
|
84
|
+
}
|
|
85
|
+
return taskId;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Determine if an ID is a task (has .M suffix) or an epic
|
|
90
|
+
*
|
|
91
|
+
* @param taskId - The ID to check
|
|
92
|
+
* @returns True if this is a task ID (has .M suffix)
|
|
93
|
+
*/
|
|
94
|
+
export function isTaskId(taskId: string): boolean {
|
|
95
|
+
const dotIndex = taskId.lastIndexOf(".");
|
|
96
|
+
if (dotIndex > 0) {
|
|
97
|
+
const suffix = taskId.slice(dotIndex + 1);
|
|
98
|
+
return /^\d+$/.test(suffix);
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the path to a task spec file
|
|
105
|
+
*
|
|
106
|
+
* Task specs are stored at: .flow/tasks/{task-id}.md
|
|
107
|
+
* Epic specs are stored at: .flow/specs/{epic-id}.md
|
|
108
|
+
*
|
|
109
|
+
* @param taskId - The task or epic ID
|
|
110
|
+
* @param options - Loading options
|
|
111
|
+
* @returns The absolute path to the spec file
|
|
112
|
+
*/
|
|
113
|
+
export function getSpecPath(taskId: string, options: LoadTaskSpecOptions = {}): string {
|
|
114
|
+
const { cwd = process.cwd() } = options;
|
|
115
|
+
const flowDir = options.flowDir || join(cwd, ".flow");
|
|
116
|
+
|
|
117
|
+
if (isTaskId(taskId)) {
|
|
118
|
+
// Task spec: .flow/tasks/fn-N.M.md
|
|
119
|
+
return join(flowDir, "tasks", `${taskId}.md`);
|
|
120
|
+
} else {
|
|
121
|
+
// Epic spec: .flow/specs/fn-N.md
|
|
122
|
+
return join(flowDir, "specs", `${taskId}.md`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Load a task spec from the .flow directory
|
|
128
|
+
*
|
|
129
|
+
* @param taskId - The task ID to load (e.g., fn-2-vth.7)
|
|
130
|
+
* @param options - Loading options
|
|
131
|
+
* @returns Result containing the spec content or error
|
|
132
|
+
*/
|
|
133
|
+
export async function loadTaskSpec(
|
|
134
|
+
taskId: string,
|
|
135
|
+
options: LoadTaskSpecOptions = {}
|
|
136
|
+
): Promise<TaskSpecResult> {
|
|
137
|
+
// Validate and normalize the task ID
|
|
138
|
+
const parsed = parseTaskId(taskId);
|
|
139
|
+
|
|
140
|
+
if (!parsed) {
|
|
141
|
+
return {
|
|
142
|
+
found: false,
|
|
143
|
+
taskId,
|
|
144
|
+
error: `Invalid task ID format: ${taskId}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const specPath = getSpecPath(parsed, options);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const file = Bun.file(specPath);
|
|
152
|
+
|
|
153
|
+
if (!(await file.exists())) {
|
|
154
|
+
return {
|
|
155
|
+
found: false,
|
|
156
|
+
taskId: parsed,
|
|
157
|
+
path: specPath,
|
|
158
|
+
error: `Spec file not found: ${specPath}`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const content = await file.text();
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
found: true,
|
|
166
|
+
taskId: parsed,
|
|
167
|
+
content,
|
|
168
|
+
path: specPath,
|
|
169
|
+
};
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
172
|
+
return {
|
|
173
|
+
found: false,
|
|
174
|
+
taskId: parsed,
|
|
175
|
+
path: specPath,
|
|
176
|
+
error: `Failed to read spec: ${message}`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Format a task spec as XML for inclusion in review context
|
|
183
|
+
*
|
|
184
|
+
* Output format:
|
|
185
|
+
* ```xml
|
|
186
|
+
* <task_spec>
|
|
187
|
+
* ## Task: fn-2.3 - Context hints generation
|
|
188
|
+
*
|
|
189
|
+
* ### Requirements
|
|
190
|
+
* ...
|
|
191
|
+
*
|
|
192
|
+
* ### Acceptance Criteria
|
|
193
|
+
* ...
|
|
194
|
+
* </task_spec>
|
|
195
|
+
* ```
|
|
196
|
+
*
|
|
197
|
+
* @param result - The task spec result from loadTaskSpec
|
|
198
|
+
* @returns Formatted XML string, or empty string if no spec found
|
|
199
|
+
*/
|
|
200
|
+
export function formatSpecXml(result: TaskSpecResult): string {
|
|
201
|
+
if (!result.found || !result.content) {
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return `<task_spec>\n${result.content.trim()}\n</task_spec>`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Load and format a task spec for inclusion in review context
|
|
210
|
+
*
|
|
211
|
+
* This is the main entry point for getting task spec context.
|
|
212
|
+
* Returns empty string if spec is not found (graceful fallback).
|
|
213
|
+
*
|
|
214
|
+
* @param taskId - The task ID to load (e.g., fn-2-vth.7)
|
|
215
|
+
* @param options - Loading options
|
|
216
|
+
* @returns Formatted XML spec string, or empty string if not found
|
|
217
|
+
*/
|
|
218
|
+
export async function getTaskSpecContext(
|
|
219
|
+
taskId: string,
|
|
220
|
+
options: LoadTaskSpecOptions = {}
|
|
221
|
+
): Promise<string> {
|
|
222
|
+
const result = await loadTaskSpec(taskId, options);
|
|
223
|
+
return formatSpecXml(result);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Extract task ID from a payload object
|
|
228
|
+
*
|
|
229
|
+
* Looks for task_id field in the payload.
|
|
230
|
+
*
|
|
231
|
+
* @param payload - Payload object that may contain task_id
|
|
232
|
+
* @returns The task ID or null if not found
|
|
233
|
+
*/
|
|
234
|
+
export function extractTaskIdFromPayload(payload: unknown): string | null {
|
|
235
|
+
if (!payload || typeof payload !== "object") {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const obj = payload as Record<string, unknown>;
|
|
240
|
+
|
|
241
|
+
// Check common field names
|
|
242
|
+
const taskIdFields = ["task_id", "taskId", "task"];
|
|
243
|
+
|
|
244
|
+
for (const field of taskIdFields) {
|
|
245
|
+
const value = obj[field];
|
|
246
|
+
if (typeof value === "string") {
|
|
247
|
+
const parsed = parseTaskId(value);
|
|
248
|
+
if (parsed) {
|
|
249
|
+
return parsed;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for git diff context module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
getDiffStat,
|
|
8
|
+
getCommits,
|
|
9
|
+
getChangedFiles,
|
|
10
|
+
getBranchName,
|
|
11
|
+
getGitDiffContext,
|
|
12
|
+
formatDiffContextXml,
|
|
13
|
+
getFormattedDiffContext,
|
|
14
|
+
type GitDiffContext,
|
|
15
|
+
} from "./diff";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { mkdir, writeFile, rm } from "fs/promises";
|
|
18
|
+
import { spawn } from "child_process";
|
|
19
|
+
|
|
20
|
+
// Test fixtures directory
|
|
21
|
+
const FIXTURES_DIR = join(import.meta.dir, "..", "..", ".test-fixtures-diff");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Helper to run git commands
|
|
25
|
+
*/
|
|
26
|
+
async function git(args: string[], cwd: string): Promise<void> {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const process = spawn("git", args, { cwd, stdio: "ignore" });
|
|
29
|
+
process.on("close", (code) => {
|
|
30
|
+
if (code === 0) resolve();
|
|
31
|
+
else reject(new Error(`git ${args.join(" ")} failed with code ${code}`));
|
|
32
|
+
});
|
|
33
|
+
process.on("error", reject);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("Git diff context", () => {
|
|
38
|
+
// Set up a temp git repo with commits on different branches
|
|
39
|
+
beforeAll(async () => {
|
|
40
|
+
await mkdir(FIXTURES_DIR, { recursive: true });
|
|
41
|
+
|
|
42
|
+
// Initialize git repo with explicit main branch
|
|
43
|
+
await git(["init", "-b", "main"], FIXTURES_DIR);
|
|
44
|
+
await git(["config", "user.email", "test@test.com"], FIXTURES_DIR);
|
|
45
|
+
await git(["config", "user.name", "Test User"], FIXTURES_DIR);
|
|
46
|
+
|
|
47
|
+
// Create initial commit on main
|
|
48
|
+
await writeFile(join(FIXTURES_DIR, "README.md"), "# Test Project\n");
|
|
49
|
+
await git(["add", "."], FIXTURES_DIR);
|
|
50
|
+
await git(["commit", "-m", "Initial commit"], FIXTURES_DIR);
|
|
51
|
+
|
|
52
|
+
// Create a feature branch
|
|
53
|
+
await git(["checkout", "-b", "feature/test"], FIXTURES_DIR);
|
|
54
|
+
|
|
55
|
+
// Add some changes
|
|
56
|
+
await mkdir(join(FIXTURES_DIR, "src"), { recursive: true });
|
|
57
|
+
await writeFile(
|
|
58
|
+
join(FIXTURES_DIR, "src/auth.ts"),
|
|
59
|
+
`export function authenticate() {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
);
|
|
64
|
+
await git(["add", "."], FIXTURES_DIR);
|
|
65
|
+
await git(["commit", "-m", "feat: add auth"], FIXTURES_DIR);
|
|
66
|
+
|
|
67
|
+
// Add another commit
|
|
68
|
+
await writeFile(
|
|
69
|
+
join(FIXTURES_DIR, "src/types.ts"),
|
|
70
|
+
`export type User = {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
};
|
|
74
|
+
`
|
|
75
|
+
);
|
|
76
|
+
await git(["add", "."], FIXTURES_DIR);
|
|
77
|
+
await git(["commit", "-m", "feat: add types"], FIXTURES_DIR);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterAll(async () => {
|
|
81
|
+
await rm(FIXTURES_DIR, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("getDiffStat", () => {
|
|
85
|
+
test("returns diff stat between branches", async () => {
|
|
86
|
+
const stat = await getDiffStat({
|
|
87
|
+
base: "main",
|
|
88
|
+
head: "HEAD",
|
|
89
|
+
cwd: FIXTURES_DIR,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(stat).toContain("src/auth.ts");
|
|
93
|
+
expect(stat).toContain("src/types.ts");
|
|
94
|
+
// Should show file stats like "+ insertions" pattern
|
|
95
|
+
expect(stat).toMatch(/\d+\s+(file|files)\s+changed/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("returns empty string for invalid base", async () => {
|
|
99
|
+
const stat = await getDiffStat({
|
|
100
|
+
base: "nonexistent-branch",
|
|
101
|
+
cwd: FIXTURES_DIR,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(stat).toBe("");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("uses default base of main", async () => {
|
|
108
|
+
const stat = await getDiffStat({
|
|
109
|
+
cwd: FIXTURES_DIR,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Should work with default main base
|
|
113
|
+
expect(typeof stat).toBe("string");
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("getCommits", () => {
|
|
118
|
+
test("returns commit history", async () => {
|
|
119
|
+
const commits = await getCommits({
|
|
120
|
+
base: "main",
|
|
121
|
+
cwd: FIXTURES_DIR,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(commits.length).toBe(2);
|
|
125
|
+
expect(commits.some((c) => c.includes("add auth"))).toBe(true);
|
|
126
|
+
expect(commits.some((c) => c.includes("add types"))).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("returns empty array for invalid base", async () => {
|
|
130
|
+
const commits = await getCommits({
|
|
131
|
+
base: "nonexistent-branch",
|
|
132
|
+
cwd: FIXTURES_DIR,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(commits).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("returns empty array when no commits between refs", async () => {
|
|
139
|
+
const commits = await getCommits({
|
|
140
|
+
base: "HEAD",
|
|
141
|
+
head: "HEAD",
|
|
142
|
+
cwd: FIXTURES_DIR,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(commits).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("getChangedFiles", () => {
|
|
150
|
+
test("returns list of changed files", async () => {
|
|
151
|
+
const files = await getChangedFiles({
|
|
152
|
+
base: "main",
|
|
153
|
+
cwd: FIXTURES_DIR,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
expect(files).toContain("src/auth.ts");
|
|
157
|
+
expect(files).toContain("src/types.ts");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("returns empty array for invalid base", async () => {
|
|
161
|
+
const files = await getChangedFiles({
|
|
162
|
+
base: "nonexistent-branch",
|
|
163
|
+
cwd: FIXTURES_DIR,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(files).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("getBranchName", () => {
|
|
171
|
+
test("returns current branch name", async () => {
|
|
172
|
+
const branch = await getBranchName(FIXTURES_DIR);
|
|
173
|
+
|
|
174
|
+
expect(branch).toBe("feature/test");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("returns empty string for invalid cwd", async () => {
|
|
178
|
+
const branch = await getBranchName("/nonexistent/path");
|
|
179
|
+
|
|
180
|
+
expect(branch).toBe("");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("getGitDiffContext", () => {
|
|
185
|
+
test("returns full git context", async () => {
|
|
186
|
+
const context = await getGitDiffContext({
|
|
187
|
+
base: "main",
|
|
188
|
+
cwd: FIXTURES_DIR,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(context.diffStat).toContain("src/auth.ts");
|
|
192
|
+
expect(context.commits.length).toBe(2);
|
|
193
|
+
expect(context.changedFiles).toContain("src/auth.ts");
|
|
194
|
+
expect(context.changedFiles).toContain("src/types.ts");
|
|
195
|
+
expect(context.branch).toBe("feature/test");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("handles missing base gracefully", async () => {
|
|
199
|
+
const context = await getGitDiffContext({
|
|
200
|
+
base: "nonexistent",
|
|
201
|
+
cwd: FIXTURES_DIR,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Should return empty values rather than throwing
|
|
205
|
+
expect(context.diffStat).toBe("");
|
|
206
|
+
expect(context.commits).toEqual([]);
|
|
207
|
+
expect(context.changedFiles).toEqual([]);
|
|
208
|
+
// Branch should still work
|
|
209
|
+
expect(context.branch).toBe("feature/test");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("formatDiffContextXml", () => {
|
|
214
|
+
test("formats context as XML", () => {
|
|
215
|
+
const context: GitDiffContext = {
|
|
216
|
+
diffStat:
|
|
217
|
+
" src/auth.ts | 45 +++\n src/types.ts | 12 ++\n 2 files changed, 57 insertions(+)",
|
|
218
|
+
commits: ["abc1234 feat: add auth", "def5678 feat: add types"],
|
|
219
|
+
changedFiles: ["src/auth.ts", "src/types.ts"],
|
|
220
|
+
branch: "feature/test",
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const xml = formatDiffContextXml(context);
|
|
224
|
+
|
|
225
|
+
expect(xml).toContain("<diff_summary>");
|
|
226
|
+
expect(xml).toContain("</diff_summary>");
|
|
227
|
+
expect(xml).toContain("<commits>");
|
|
228
|
+
expect(xml).toContain("</commits>");
|
|
229
|
+
expect(xml).toContain("<changed_files>");
|
|
230
|
+
expect(xml).toContain("</changed_files>");
|
|
231
|
+
|
|
232
|
+
expect(xml).toContain("src/auth.ts");
|
|
233
|
+
expect(xml).toContain("abc1234 feat: add auth");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("omits empty sections", () => {
|
|
237
|
+
const context: GitDiffContext = {
|
|
238
|
+
diffStat: "",
|
|
239
|
+
commits: [],
|
|
240
|
+
changedFiles: ["file.ts"],
|
|
241
|
+
branch: "main",
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const xml = formatDiffContextXml(context);
|
|
245
|
+
|
|
246
|
+
expect(xml).not.toContain("<diff_summary>");
|
|
247
|
+
expect(xml).not.toContain("<commits>");
|
|
248
|
+
expect(xml).toContain("<changed_files>");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("handles all empty context", () => {
|
|
252
|
+
const context: GitDiffContext = {
|
|
253
|
+
diffStat: "",
|
|
254
|
+
commits: [],
|
|
255
|
+
changedFiles: [],
|
|
256
|
+
branch: "",
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const xml = formatDiffContextXml(context);
|
|
260
|
+
|
|
261
|
+
expect(xml).toBe("");
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("getFormattedDiffContext", () => {
|
|
266
|
+
test("returns formatted XML context", async () => {
|
|
267
|
+
const xml = await getFormattedDiffContext({
|
|
268
|
+
base: "main",
|
|
269
|
+
cwd: FIXTURES_DIR,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(xml).toContain("<diff_summary>");
|
|
273
|
+
expect(xml).toContain("<commits>");
|
|
274
|
+
expect(xml).toContain("<changed_files>");
|
|
275
|
+
expect(xml).toContain("src/auth.ts");
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("Edge cases", () => {
|
|
280
|
+
test("handles cwd with spaces in path", async () => {
|
|
281
|
+
// This test verifies the module doesn't break with special paths
|
|
282
|
+
// The actual FIXTURES_DIR doesn't have spaces, but the code should handle it
|
|
283
|
+
const branch = await getBranchName(FIXTURES_DIR);
|
|
284
|
+
expect(typeof branch).toBe("string");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("all functions handle non-git directory", async () => {
|
|
288
|
+
const tempDir = "/tmp/non-git-test-" + Date.now();
|
|
289
|
+
await mkdir(tempDir, { recursive: true });
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const stat = await getDiffStat({ cwd: tempDir });
|
|
293
|
+
const commits = await getCommits({ cwd: tempDir });
|
|
294
|
+
const files = await getChangedFiles({ cwd: tempDir });
|
|
295
|
+
const branch = await getBranchName(tempDir);
|
|
296
|
+
const context = await getGitDiffContext({ cwd: tempDir });
|
|
297
|
+
const formatted = await getFormattedDiffContext({ cwd: tempDir });
|
|
298
|
+
|
|
299
|
+
// All should return empty/default values
|
|
300
|
+
expect(stat).toBe("");
|
|
301
|
+
expect(commits).toEqual([]);
|
|
302
|
+
expect(files).toEqual([]);
|
|
303
|
+
expect(branch).toBe("");
|
|
304
|
+
expect(context.diffStat).toBe("");
|
|
305
|
+
expect(formatted).toBe("");
|
|
306
|
+
} finally {
|
|
307
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
});
|