wdyt 0.1.11 → 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 +3 -2
- 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
- package/src/parseExpression.ts +20 -46
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symbol extraction module for wdyt
|
|
3
|
+
*
|
|
4
|
+
* Extracts function names, class names, type definitions, and interface names
|
|
5
|
+
* from source files in multiple languages.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Symbol types that can be extracted from source code */
|
|
9
|
+
export type SymbolType = "function" | "class" | "type" | "interface" | "const";
|
|
10
|
+
|
|
11
|
+
/** Extracted symbol information */
|
|
12
|
+
export interface Symbol {
|
|
13
|
+
name: string;
|
|
14
|
+
type: SymbolType;
|
|
15
|
+
line: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Language-specific regex patterns for symbol extraction */
|
|
19
|
+
interface LanguagePatterns {
|
|
20
|
+
patterns: Array<{
|
|
21
|
+
regex: RegExp;
|
|
22
|
+
type: SymbolType;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* TypeScript/JavaScript patterns
|
|
28
|
+
* Matches: export function, async function, class, type, interface, const
|
|
29
|
+
*/
|
|
30
|
+
const TYPESCRIPT_PATTERNS: LanguagePatterns = {
|
|
31
|
+
patterns: [
|
|
32
|
+
{
|
|
33
|
+
regex: /(?:export\s+)?(?:async\s+)?function\s+(\w+)/g,
|
|
34
|
+
type: "function",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
regex: /(?:export\s+)?class\s+(\w+)/g,
|
|
38
|
+
type: "class",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
regex: /(?:export\s+)?type\s+(\w+)/g,
|
|
42
|
+
type: "type",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
regex: /(?:export\s+)?interface\s+(\w+)/g,
|
|
46
|
+
type: "interface",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
// Match const with optional type annotation: const FOO: Type = or const FOO =
|
|
50
|
+
regex: /(?:export\s+)?const\s+(\w+)(?:\s*:\s*[^=]+)?\s*=/g,
|
|
51
|
+
type: "const",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Python patterns
|
|
58
|
+
* Matches: def, async def, class (including indented methods)
|
|
59
|
+
*/
|
|
60
|
+
const PYTHON_PATTERNS: LanguagePatterns = {
|
|
61
|
+
patterns: [
|
|
62
|
+
{
|
|
63
|
+
// Match def at any indentation level (for methods inside classes)
|
|
64
|
+
// Use [ \t]* for horizontal whitespace only (not newlines)
|
|
65
|
+
regex: /^[ \t]*(?:async\s+)?def\s+(\w+)/gm,
|
|
66
|
+
type: "function",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
regex: /^class\s+(\w+)/gm,
|
|
70
|
+
type: "class",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Go patterns
|
|
77
|
+
* Matches: func, type struct, type interface
|
|
78
|
+
*/
|
|
79
|
+
const GO_PATTERNS: LanguagePatterns = {
|
|
80
|
+
patterns: [
|
|
81
|
+
{
|
|
82
|
+
regex: /^func\s+(?:\([^)]+\)\s+)?(\w+)/gm,
|
|
83
|
+
type: "function",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
regex: /^type\s+(\w+)\s+struct/gm,
|
|
87
|
+
type: "class",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
regex: /^type\s+(\w+)\s+interface/gm,
|
|
91
|
+
type: "interface",
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Rust patterns
|
|
98
|
+
* Matches: fn, pub fn, struct, pub struct, trait, pub trait, impl, type alias
|
|
99
|
+
*/
|
|
100
|
+
const RUST_PATTERNS: LanguagePatterns = {
|
|
101
|
+
patterns: [
|
|
102
|
+
{
|
|
103
|
+
regex: /(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/g,
|
|
104
|
+
type: "function",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
regex: /(?:pub\s+)?struct\s+(\w+)/g,
|
|
108
|
+
type: "class",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
regex: /(?:pub\s+)?trait\s+(\w+)/g,
|
|
112
|
+
type: "interface",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
// Match type alias with optional generic params: type Foo<T> = or type Foo =
|
|
116
|
+
regex: /(?:pub\s+)?type\s+(\w+)(?:<[^>]*>)?\s*=/g,
|
|
117
|
+
type: "type",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/** File extensions to language pattern mapping */
|
|
123
|
+
const EXTENSION_PATTERNS: Record<string, LanguagePatterns> = {
|
|
124
|
+
".ts": TYPESCRIPT_PATTERNS,
|
|
125
|
+
".tsx": TYPESCRIPT_PATTERNS,
|
|
126
|
+
".js": TYPESCRIPT_PATTERNS,
|
|
127
|
+
".jsx": TYPESCRIPT_PATTERNS,
|
|
128
|
+
".mjs": TYPESCRIPT_PATTERNS,
|
|
129
|
+
".cjs": TYPESCRIPT_PATTERNS,
|
|
130
|
+
".py": PYTHON_PATTERNS,
|
|
131
|
+
".go": GO_PATTERNS,
|
|
132
|
+
".rs": RUST_PATTERNS,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the file extension from a file path
|
|
137
|
+
*/
|
|
138
|
+
function getExtension(filePath: string): string {
|
|
139
|
+
const lastDot = filePath.lastIndexOf(".");
|
|
140
|
+
if (lastDot === -1) return "";
|
|
141
|
+
return filePath.slice(lastDot).toLowerCase();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get line number for a given character index in content
|
|
146
|
+
*/
|
|
147
|
+
function getLineNumber(content: string, charIndex: number): number {
|
|
148
|
+
let lineNumber = 1;
|
|
149
|
+
for (let i = 0; i < charIndex && i < content.length; i++) {
|
|
150
|
+
if (content[i] === "\n") {
|
|
151
|
+
lineNumber++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return lineNumber;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Extract symbols from file content
|
|
159
|
+
*
|
|
160
|
+
* @param content - The source file content
|
|
161
|
+
* @param filePath - The file path (used to determine language from extension)
|
|
162
|
+
* @returns Array of extracted symbols with name, type, and line number
|
|
163
|
+
*/
|
|
164
|
+
export function extractSymbols(content: string, filePath: string): Symbol[] {
|
|
165
|
+
const extension = getExtension(filePath);
|
|
166
|
+
const languagePatterns = EXTENSION_PATTERNS[extension];
|
|
167
|
+
|
|
168
|
+
if (!languagePatterns) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const symbols: Symbol[] = [];
|
|
173
|
+
const seen = new Set<string>();
|
|
174
|
+
|
|
175
|
+
for (const { regex, type } of languagePatterns.patterns) {
|
|
176
|
+
// Reset regex lastIndex for global patterns
|
|
177
|
+
regex.lastIndex = 0;
|
|
178
|
+
|
|
179
|
+
let match: RegExpExecArray | null;
|
|
180
|
+
while ((match = regex.exec(content)) !== null) {
|
|
181
|
+
const name = match[1];
|
|
182
|
+
const key = `${name}:${type}:${match.index}`;
|
|
183
|
+
|
|
184
|
+
// Avoid duplicates at the same position
|
|
185
|
+
if (seen.has(key)) continue;
|
|
186
|
+
seen.add(key);
|
|
187
|
+
|
|
188
|
+
symbols.push({
|
|
189
|
+
name,
|
|
190
|
+
type,
|
|
191
|
+
line: getLineNumber(content, match.index),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Sort by line number
|
|
197
|
+
symbols.sort((a, b) => a.line - b.line);
|
|
198
|
+
|
|
199
|
+
return symbols;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extract symbols from a file path
|
|
204
|
+
*
|
|
205
|
+
* @param filePath - Absolute path to the source file
|
|
206
|
+
* @returns Array of extracted symbols
|
|
207
|
+
*/
|
|
208
|
+
export async function extractSymbolsFromFile(
|
|
209
|
+
filePath: string
|
|
210
|
+
): Promise<Symbol[]> {
|
|
211
|
+
const file = Bun.file(filePath);
|
|
212
|
+
|
|
213
|
+
if (!(await file.exists())) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const content = await file.text();
|
|
218
|
+
return extractSymbols(content, filePath);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if a file extension is supported for symbol extraction
|
|
223
|
+
*/
|
|
224
|
+
export function isSupported(filePath: string): boolean {
|
|
225
|
+
const extension = getExtension(filePath);
|
|
226
|
+
return extension in EXTENSION_PATTERNS;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get list of supported file extensions
|
|
231
|
+
*/
|
|
232
|
+
export function getSupportedExtensions(): string[] {
|
|
233
|
+
return Object.keys(EXTENSION_PATTERNS);
|
|
234
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow module exports
|
|
3
|
+
*
|
|
4
|
+
* Provides Flow-Next task spec loading and related utilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
parseTaskId,
|
|
9
|
+
getEpicId,
|
|
10
|
+
isTaskId,
|
|
11
|
+
getSpecPath,
|
|
12
|
+
loadTaskSpec,
|
|
13
|
+
formatSpecXml,
|
|
14
|
+
getTaskSpecContext,
|
|
15
|
+
extractTaskIdFromPayload,
|
|
16
|
+
type TaskSpecResult,
|
|
17
|
+
type LoadTaskSpecOptions,
|
|
18
|
+
} from "./specs";
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Flow-Next spec loading module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
|
|
6
|
+
import { mkdirSync, rmSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
import {
|
|
9
|
+
parseTaskId,
|
|
10
|
+
getEpicId,
|
|
11
|
+
isTaskId,
|
|
12
|
+
getSpecPath,
|
|
13
|
+
loadTaskSpec,
|
|
14
|
+
formatSpecXml,
|
|
15
|
+
getTaskSpecContext,
|
|
16
|
+
extractTaskIdFromPayload,
|
|
17
|
+
} from "./specs";
|
|
18
|
+
|
|
19
|
+
// Test fixtures
|
|
20
|
+
const TEST_DIR = join(import.meta.dir, "..", "..", ".test-flow");
|
|
21
|
+
const TEST_FLOW_DIR = join(TEST_DIR, ".flow");
|
|
22
|
+
|
|
23
|
+
// Sample spec content - using valid fn-N or fn-N-suffix ID format
|
|
24
|
+
const SAMPLE_SPEC = `# fn-99-test.1 Test Task
|
|
25
|
+
|
|
26
|
+
## Description
|
|
27
|
+
This is a test task for unit testing.
|
|
28
|
+
|
|
29
|
+
### Requirements
|
|
30
|
+
- Requirement 1
|
|
31
|
+
- Requirement 2
|
|
32
|
+
|
|
33
|
+
### Acceptance Criteria
|
|
34
|
+
- [ ] Criteria 1
|
|
35
|
+
- [ ] Criteria 2
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const SAMPLE_EPIC_SPEC = `# fn-99-test Test Epic
|
|
39
|
+
|
|
40
|
+
## Overview
|
|
41
|
+
This is a test epic.
|
|
42
|
+
|
|
43
|
+
## Tasks
|
|
44
|
+
- fn-99-test.1 - Test Task
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
describe("parseTaskId", () => {
|
|
48
|
+
it("parses simple task ID", () => {
|
|
49
|
+
expect(parseTaskId("fn-1.2")).toBe("fn-1.2");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("parses task ID with suffix", () => {
|
|
53
|
+
expect(parseTaskId("fn-2-vth.7")).toBe("fn-2-vth.7");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("parses epic ID", () => {
|
|
57
|
+
expect(parseTaskId("fn-1")).toBe("fn-1");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("parses epic ID with suffix", () => {
|
|
61
|
+
expect(parseTaskId("fn-2-vth")).toBe("fn-2-vth");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("handles uppercase", () => {
|
|
65
|
+
expect(parseTaskId("FN-1.2")).toBe("fn-1.2");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("handles whitespace", () => {
|
|
69
|
+
expect(parseTaskId(" fn-1.2 ")).toBe("fn-1.2");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("rejects invalid formats", () => {
|
|
73
|
+
expect(parseTaskId("invalid")).toBeNull();
|
|
74
|
+
expect(parseTaskId("fn1.2")).toBeNull();
|
|
75
|
+
expect(parseTaskId("fn-")).toBeNull();
|
|
76
|
+
expect(parseTaskId("")).toBeNull();
|
|
77
|
+
expect(parseTaskId(null as unknown as string)).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("getEpicId", () => {
|
|
82
|
+
it("extracts epic from simple task", () => {
|
|
83
|
+
expect(getEpicId("fn-1.2")).toBe("fn-1");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("extracts epic from task with suffix", () => {
|
|
87
|
+
expect(getEpicId("fn-2-vth.7")).toBe("fn-2-vth");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns epic unchanged", () => {
|
|
91
|
+
expect(getEpicId("fn-1")).toBe("fn-1");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns epic with suffix unchanged", () => {
|
|
95
|
+
expect(getEpicId("fn-2-vth")).toBe("fn-2-vth");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("isTaskId", () => {
|
|
100
|
+
it("identifies task IDs", () => {
|
|
101
|
+
expect(isTaskId("fn-1.2")).toBe(true);
|
|
102
|
+
expect(isTaskId("fn-2-vth.7")).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("identifies epic IDs", () => {
|
|
106
|
+
expect(isTaskId("fn-1")).toBe(false);
|
|
107
|
+
expect(isTaskId("fn-2-vth")).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("getSpecPath", () => {
|
|
112
|
+
it("returns task spec path", () => {
|
|
113
|
+
const path = getSpecPath("fn-1.2", { cwd: "/project" });
|
|
114
|
+
expect(path).toBe("/project/.flow/tasks/fn-1.2.md");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns epic spec path", () => {
|
|
118
|
+
const path = getSpecPath("fn-1", { cwd: "/project" });
|
|
119
|
+
expect(path).toBe("/project/.flow/specs/fn-1.md");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("uses custom flow directory", () => {
|
|
123
|
+
const path = getSpecPath("fn-1.2", { flowDir: "/custom/.flow" });
|
|
124
|
+
expect(path).toBe("/custom/.flow/tasks/fn-1.2.md");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("loadTaskSpec", () => {
|
|
129
|
+
beforeAll(() => {
|
|
130
|
+
// Create test directory structure
|
|
131
|
+
mkdirSync(join(TEST_FLOW_DIR, "tasks"), { recursive: true });
|
|
132
|
+
mkdirSync(join(TEST_FLOW_DIR, "specs"), { recursive: true });
|
|
133
|
+
|
|
134
|
+
// Write test spec files
|
|
135
|
+
Bun.write(join(TEST_FLOW_DIR, "tasks", "fn-99-test.1.md"), SAMPLE_SPEC);
|
|
136
|
+
Bun.write(join(TEST_FLOW_DIR, "specs", "fn-99-test.md"), SAMPLE_EPIC_SPEC);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
afterAll(() => {
|
|
140
|
+
// Clean up test directory
|
|
141
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("loads existing task spec", async () => {
|
|
145
|
+
const result = await loadTaskSpec("fn-99-test.1", { flowDir: TEST_FLOW_DIR });
|
|
146
|
+
|
|
147
|
+
expect(result.found).toBe(true);
|
|
148
|
+
expect(result.taskId).toBe("fn-99-test.1");
|
|
149
|
+
expect(result.content).toBe(SAMPLE_SPEC);
|
|
150
|
+
expect(result.path).toContain("fn-99-test.1.md");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("loads existing epic spec", async () => {
|
|
154
|
+
const result = await loadTaskSpec("fn-99-test", { flowDir: TEST_FLOW_DIR });
|
|
155
|
+
|
|
156
|
+
expect(result.found).toBe(true);
|
|
157
|
+
expect(result.taskId).toBe("fn-99-test");
|
|
158
|
+
expect(result.content).toBe(SAMPLE_EPIC_SPEC);
|
|
159
|
+
expect(result.path).toContain("fn-99-test.md");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("returns not found for missing spec", async () => {
|
|
163
|
+
const result = await loadTaskSpec("fn-88.99", { flowDir: TEST_FLOW_DIR });
|
|
164
|
+
|
|
165
|
+
expect(result.found).toBe(false);
|
|
166
|
+
expect(result.taskId).toBe("fn-88.99");
|
|
167
|
+
expect(result.error).toContain("not found");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("returns error for invalid task ID", async () => {
|
|
171
|
+
const result = await loadTaskSpec("invalid", { flowDir: TEST_FLOW_DIR });
|
|
172
|
+
|
|
173
|
+
expect(result.found).toBe(false);
|
|
174
|
+
expect(result.error).toContain("Invalid task ID");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("formatSpecXml", () => {
|
|
179
|
+
it("formats found spec as XML", () => {
|
|
180
|
+
const result = {
|
|
181
|
+
found: true,
|
|
182
|
+
taskId: "fn-99-test.1",
|
|
183
|
+
content: SAMPLE_SPEC,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const xml = formatSpecXml(result);
|
|
187
|
+
|
|
188
|
+
expect(xml).toContain("<task_spec>");
|
|
189
|
+
expect(xml).toContain("</task_spec>");
|
|
190
|
+
expect(xml).toContain("# fn-99-test.1 Test Task");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("returns empty string for not found", () => {
|
|
194
|
+
const result = {
|
|
195
|
+
found: false,
|
|
196
|
+
taskId: "fn-99-test.1",
|
|
197
|
+
error: "Not found",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const xml = formatSpecXml(result);
|
|
201
|
+
expect(xml).toBe("");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("getTaskSpecContext", () => {
|
|
206
|
+
beforeAll(() => {
|
|
207
|
+
// Create test directory structure
|
|
208
|
+
mkdirSync(join(TEST_FLOW_DIR, "tasks"), { recursive: true });
|
|
209
|
+
Bun.write(join(TEST_FLOW_DIR, "tasks", "fn-99-test.1.md"), SAMPLE_SPEC);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
afterAll(() => {
|
|
213
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("returns formatted XML for existing spec", async () => {
|
|
217
|
+
const xml = await getTaskSpecContext("fn-99-test.1", { flowDir: TEST_FLOW_DIR });
|
|
218
|
+
|
|
219
|
+
expect(xml).toContain("<task_spec>");
|
|
220
|
+
expect(xml).toContain("# fn-99-test.1 Test Task");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("returns empty string for missing spec (graceful fallback)", async () => {
|
|
224
|
+
const xml = await getTaskSpecContext("fn-88.99", { flowDir: TEST_FLOW_DIR });
|
|
225
|
+
|
|
226
|
+
expect(xml).toBe("");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("extractTaskIdFromPayload", () => {
|
|
231
|
+
it("extracts task_id field", () => {
|
|
232
|
+
const payload = { task_id: "fn-1.2" };
|
|
233
|
+
expect(extractTaskIdFromPayload(payload)).toBe("fn-1.2");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("extracts taskId field", () => {
|
|
237
|
+
const payload = { taskId: "fn-2-vth.7" };
|
|
238
|
+
expect(extractTaskIdFromPayload(payload)).toBe("fn-2-vth.7");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("extracts task field", () => {
|
|
242
|
+
const payload = { task: "fn-1.2" };
|
|
243
|
+
expect(extractTaskIdFromPayload(payload)).toBe("fn-1.2");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("returns null for missing field", () => {
|
|
247
|
+
const payload = { other: "value" };
|
|
248
|
+
expect(extractTaskIdFromPayload(payload)).toBeNull();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("returns null for invalid task ID", () => {
|
|
252
|
+
const payload = { task_id: "invalid" };
|
|
253
|
+
expect(extractTaskIdFromPayload(payload)).toBeNull();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("returns null for non-object", () => {
|
|
257
|
+
expect(extractTaskIdFromPayload(null)).toBeNull();
|
|
258
|
+
expect(extractTaskIdFromPayload("string")).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
});
|