decy-llm 2.2.0

LLM context builder for Decy C-to-Rust transpiler
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! Tests for LLM code generation (DECY-099).
//!
//! Verifies LLM-guided Rust generation with analysis context.

use decy_llm::{CodegenPrompt, ContextBuilder, LlmCodegen};

// ============================================================================
// TEST 1: Create codegen prompt
// ============================================================================

#[test]
fn test_create_codegen_prompt() {
    let context = ContextBuilder::new().build();
    let c_source = "int add(int a, int b) { return a + b; }";

    let prompt = CodegenPrompt::new(c_source, context);

    assert_eq!(prompt.c_source, c_source);
    assert!(prompt.instructions.is_empty());
}

// ============================================================================
// TEST 2: Add instructions to prompt
// ============================================================================

#[test]
fn test_prompt_with_instructions() {
    let context = ContextBuilder::new().build();
    let c_source = "void process(int* data) { *data = 0; }";

    let prompt = CodegenPrompt::new(c_source, context)
        .with_instructions("Generate safe Rust using references");

    assert!(prompt.instructions.contains("references"));
}

// ============================================================================
// TEST 3: Render prompt includes context
// ============================================================================

#[test]
fn test_render_prompt_includes_context() {
    let mut builder = ContextBuilder::new();
    builder.add_function("transfer", "void transfer(int* dest, int* src)").add_ownership(
        "transfer",
        "dest",
        "mutable_borrow",
        0.9,
        "Modified",
    );

    let prompt =
        CodegenPrompt::new("void transfer(int* dest, int* src) { *dest = *src; }", builder.build());

    let rendered = prompt.render();

    // Should include C source
    assert!(rendered.contains("transfer"));
    // Should include ownership info
    assert!(rendered.contains("mutable_borrow") || rendered.contains("ownership"));
}

// ============================================================================
// TEST 4: Create LLM codegen
// ============================================================================

#[test]
fn test_create_llm_codegen() {
    let _codegen = LlmCodegen::new("test-model");
    // Just verify creation doesn't panic
}

// ============================================================================
// TEST 5: Parse valid LLM response
// ============================================================================

#[test]
fn test_parse_valid_response() {
    let codegen = LlmCodegen::new("test-model");

    let response = r#"
```rust
fn add(a: i32, b: i32) -> i32 {
    a + b
}
```

This is a simple addition function that takes two i32 parameters and returns their sum.
"#;

    let result = codegen.parse_response(response);
    assert!(result.is_ok());

    let generated = result.unwrap();
    assert!(generated.code.contains("fn add"));
    assert!(generated.code.contains("a + b"));
}

// ============================================================================
// TEST 6: Parse response with JSON format
// ============================================================================

#[test]
fn test_parse_json_response() {
    let codegen = LlmCodegen::new("test-model");

    let response = r#"
{
    "code": "fn add(a: i32, b: i32) -> i32 { a + b }",
    "confidence": 0.95,
    "reasoning": "Simple arithmetic conversion",
    "warnings": []
}
"#;

    let result = codegen.parse_response(response);
    assert!(result.is_ok());

    let generated = result.unwrap();
    assert!(generated.code.contains("fn add"));
    assert!((generated.confidence - 0.95).abs() < 0.01);
}

// ============================================================================
// TEST 7: Parse malformed response returns error
// ============================================================================

#[test]
fn test_parse_malformed_response() {
    let codegen = LlmCodegen::new("test-model");

    let response = "This response has no code at all, just random text.";

    let result = codegen.parse_response(response);
    assert!(result.is_err());
}

// ============================================================================
// TEST 8: Validate valid Rust code
// ============================================================================

#[test]
fn test_validate_valid_code() {
    let codegen = LlmCodegen::new("test-model");

    let code = "fn add(a: i32, b: i32) -> i32 { a + b }";

    let result = codegen.validate_code(code);
    assert!(result.is_ok());
}

// ============================================================================
// TEST 9: Validate invalid Rust code returns error
// ============================================================================

#[test]
fn test_validate_invalid_code() {
    let codegen = LlmCodegen::new("test-model");

    let code = "fn add(a: i32, b: { a + b }"; // Missing return type

    let result = codegen.validate_code(code);
    assert!(result.is_err());
}

// ============================================================================
// TEST 10: Default model is claude-3-sonnet
// ============================================================================

#[test]
fn test_default_model() {
    let _codegen = LlmCodegen::default();
    // Should create without panic using default model
}

// ============================================================================
// TEST 11: Render prompt with ownership data for multiple functions
// ============================================================================

#[test]
fn test_render_prompt_multiple_functions_with_ownership() {
    let mut builder = ContextBuilder::new();
    builder
        .add_function("init", "void init(int* buf, size_t len)")
        .add_ownership("init", "buf", "mutable_borrow", 0.95, "Modified in loop")
        .add_function("process", "void process(int* data)")
        .add_ownership("process", "data", "owning", 0.8, "Freed at end");

    let prompt = CodegenPrompt::new(
        "void init(int* buf, size_t len) {}\nvoid process(int* data) {}",
        builder.build(),
    );

    let rendered = prompt.render();

    // Should include both function headings
    assert!(rendered.contains("Function: init"), "Got: {}", rendered);
    assert!(rendered.contains("Function: process"), "Got: {}", rendered);
    // Should include ownership details
    assert!(rendered.contains("mutable_borrow"), "Got: {}", rendered);
    assert!(rendered.contains("owning"), "Got: {}", rendered);
    assert!(rendered.contains("95%"), "Got: {}", rendered);
    assert!(rendered.contains("80%"), "Got: {}", rendered);
}

// ============================================================================
// TEST 12: Render prompt with instructions
// ============================================================================

#[test]
fn test_render_prompt_with_instructions_section() {
    let context = ContextBuilder::new().build();
    let prompt = CodegenPrompt::new("int x = 5;", context)
        .with_instructions("Prefer safe Rust patterns. Avoid raw pointers.");

    let rendered = prompt.render();

    assert!(rendered.contains("Additional Instructions"), "Got: {}", rendered);
    assert!(rendered.contains("Prefer safe Rust patterns"), "Got: {}", rendered);
    assert!(rendered.contains("Task"), "Got: {}", rendered);
}

// ============================================================================
// TEST 13: Render prompt with empty functions (no ownership data)
// ============================================================================

#[test]
fn test_render_prompt_functions_without_ownership() {
    let mut builder = ContextBuilder::new();
    builder.add_function("empty_func", "void empty_func()");

    let prompt = CodegenPrompt::new("void empty_func() {}", builder.build());

    let rendered = prompt.render();

    // Should NOT include function ownership heading since ownership is empty
    assert!(
        !rendered.contains("Function: empty_func"),
        "Should skip functions with no ownership data, Got: {}",
        rendered
    );
    // Should still have the task section
    assert!(rendered.contains("Task"), "Got: {}", rendered);
}

// ============================================================================
// TEST 14: Validate empty code returns error
// ============================================================================

#[test]
fn test_validate_empty_code() {
    let codegen = LlmCodegen::new("test-model");
    let result = codegen.validate_code("");
    assert!(result.is_err());
    let err = format!("{}", result.unwrap_err());
    assert!(err.contains("Empty"), "Got: {}", err);
}

// ============================================================================
// TEST 15: Validate whitespace-only code returns error
// ============================================================================

#[test]
fn test_validate_whitespace_only_code() {
    let codegen = LlmCodegen::new("test-model");
    let result = codegen.validate_code("   \n\t  \n  ");
    assert!(result.is_err());
}

// ============================================================================
// TEST 16: Validate non-fn non-empty code succeeds
// ============================================================================

#[test]
fn test_validate_non_fn_non_empty_code() {
    let codegen = LlmCodegen::new("test-model");
    // Code without `fn ` but non-empty should pass (line 248)
    let result = codegen.validate_code("let x: i32 = 42;");
    assert!(result.is_ok());
}

// ============================================================================
// TEST 17: Validate unbalanced parentheses
// ============================================================================

#[test]
fn test_validate_unbalanced_parens() {
    let codegen = LlmCodegen::new("test-model");
    let result = codegen.validate_code("fn add(a: i32, b: i32 { a + b }");
    assert!(result.is_err());
    let err = format!("{}", result.unwrap_err());
    assert!(err.contains("parentheses"), "Got: {}", err);
}

// ============================================================================
// TEST 18: Generate returns API error stub
// ============================================================================

#[test]
fn test_generate_returns_api_error() {
    let codegen = LlmCodegen::new("test-model");
    let context = ContextBuilder::new().build();
    let prompt = CodegenPrompt::new("int x;", context);

    let result = codegen.generate(&prompt);
    assert!(result.is_err());
    let err = format!("{}", result.unwrap_err());
    assert!(err.contains("test-model"), "Got: {}", err);
}

// ============================================================================
// TEST 19: Parse response with plain code block (no rust marker)
// ============================================================================

#[test]
fn test_parse_response_plain_code_block() {
    let codegen = LlmCodegen::new("test-model");

    let response = "Here's the result:\n```\nlet x: i32 = 42;\n```\nDone.";

    let result = codegen.parse_response(response);
    assert!(result.is_ok());
    let generated = result.unwrap();
    assert!(generated.code.contains("let x"), "Got: {}", generated.code);
    assert!((generated.confidence - 0.8).abs() < 0.01);
}

// ============================================================================
// TEST 20: Parse response extracts reasoning after code
// ============================================================================

#[test]
fn test_parse_response_extracts_reasoning() {
    let codegen = LlmCodegen::new("test-model");

    let response = "```rust\nfn hello() {}\n```\nThis converts the C function to idiomatic Rust.";

    let result = codegen.parse_response(response);
    assert!(result.is_ok());
    let generated = result.unwrap();
    assert!(
        generated.reasoning.contains("idiomatic Rust"),
        "Got reasoning: {}",
        generated.reasoning
    );
}

// ============================================================================
// TEST 21: Render prompt with JSON context section
// ============================================================================

#[test]
fn test_render_prompt_includes_json_context() {
    let mut builder = ContextBuilder::new();
    builder.add_function("test_fn", "int test_fn()");

    let prompt = CodegenPrompt::new("int test_fn() { return 0; }", builder.build());
    let rendered = prompt.render();

    // Should have JSON context block
    assert!(rendered.contains("Static Analysis Context"), "Got: {}", rendered);
    assert!(rendered.contains("```json"), "Got: {}", rendered);
    assert!(rendered.contains("test_fn"), "Got: {}", rendered);
}

// ============================================================================
// TEST 22: Validate unbalanced braces (lines 224-227)
// ============================================================================

#[test]
fn test_validate_unbalanced_braces() {
    let codegen = LlmCodegen::new("test-model");
    let result = codegen.validate_code("fn foo() { { }"); // 2 open, 1 close
    assert!(result.is_err());
    let err = format!("{}", result.unwrap_err());
    assert!(err.contains("braces"), "Got: {}", err);
}

// ============================================================================
// TEST 23: Parse response with empty code block (lines 191-192)
// ============================================================================

#[test]
fn test_parse_response_empty_code_block() {
    let codegen = LlmCodegen::new("test-model");
    // Code block with empty content between markers
    let response = "```rust\n\n```\nSome reasoning";
    let result = codegen.parse_response(response);
    // Should fail since code is empty
    assert!(result.is_err(), "Should fail on empty code block");
}

// ============================================================================
// TEST 24: Parse response with no closing fence (extract_reasoning fallback)
// ============================================================================

#[test]
fn test_parse_response_reasoning_fallback() {
    let codegen = LlmCodegen::new("test-model");
    // Response with code block but nothing after last ```
    let response = "```rust\nfn main() {}\n```";
    let result = codegen.parse_response(response);
    assert!(result.is_ok());
    let generated = result.unwrap();
    // Reasoning should be the fallback since there's nothing after last ```
    assert_eq!(generated.reasoning, "Generated from C source");
}