Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gitgalaxy/tools/cobol_to_java/cobol_to_java_build_forge.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ def generate_pom_xml(group_id: str, artifact_id: str) -> str:
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
60 changes: 60 additions & 0 deletions tests/test_agent_forge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import pytest

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'pytest' is not used.
from gitgalaxy.tools.cobol_to_java.cobol_to_java_agent_forge import generate_java_agent_ticket

def test_llm_hallucination_prevention():
"""
Verifies that the Agent Forge correctly extracts strict architectural constraints
(external dependencies and honesty flags) from the IR state and injects them
into the JSON ticket. This proves the LLM will not fly blind and hallucinate.
"""
# 1. Setup the Mock Inputs
prog_id = "calc-payroll"

# Mock a specific COBOL slice
mock_slice = {
"target_var": "WS-NET-PAY",
"business_rules": [
{"paragraph": "0100-CALC-TAXES", "statement": "COMPUTE WS-TAX = WS-GROSS * 0.20"},
{"paragraph": "0200-FINALIZE", "statement": "SUBTRACT WS-TAX FROM WS-GROSS GIVING WS-NET-PAY"}
]
}

# Mock the Global IR State containing the guardrails
mock_ir_state = {
"analysis": {
"honesty_flags": [
"[CRITICAL] Do not use Java floats for currency, use BigDecimal.",
"This module assumes EBCDIC encoding."
],
"lineage": {
"unresolved_calls": ["TAX-RATES-DB", "AUDIT-LOGGER"]
}
}
}

# 2. Forge the Ticket
ticket = generate_java_agent_ticket(mock_slice, prog_id, mock_ir_state)

# =====================================================================
# 3. INVARIANT ASSERTIONS (The Proof)
# =====================================================================

# A) Verify the Job ID was formatted correctly
assert ticket["job_id"] == "CALC-PAYROLL_JAVA_SERVICE_TRANSLATION"
assert ticket["target_variable"] == "WS-NET-PAY"

# B) Verify Business Rules were translated into the context array
context = ticket["context"]
assert len(context["business_rules_to_translate"]) == 2
assert "// Context: 0100-CALC-TAXES" in context["business_rules_to_translate"][0]

# C) THE HALLUCINATION GUARDS: Verify Dependencies were passed perfectly
assert len(context["external_dependencies"]) == 2
assert "TAX-RATES-DB" in context["external_dependencies"]

# D) THE HONESTY GUARDS: Verify warnings were passed AND cleaned
# The script has a specific line: `a.split(']', 1)[-1].strip() if ']' in a else a`
# We must prove this strip logic works so the LLM doesn't get confused by internal bracket tags
assert len(context["architectural_warnings"]) == 2
assert context["architectural_warnings"][0] == "Do not use Java floats for currency, use BigDecimal." # The [CRITICAL] tag should be stripped!
assert context["architectural_warnings"][1] == "This module assumes EBCDIC encoding."
102 changes: 102 additions & 0 deletions tests/test_batch_test_harness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import pytest
import subprocess
import sys
from pathlib import Path

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'Path' is not used.
from unittest.mock import patch, MagicMock

# Import your orchestrator script
from gitgalaxy.tools.cobol_to_java import batch_test_harness

@pytest.fixture
def mock_env(tmp_path):
"""
Sets up a fake directory structure in the OS temp folder.
This ensures the harness doesn't exit early when it globs for output folders.
"""
corpus = tmp_path / "legacy_corpus"
corpus.mkdir()

# Create one target repository
repo1 = corpus / "alpha_repo"
repo1.mkdir()

# Create the fake output folders the harness globs for in Steps 1 and 2
(corpus / "alpha_repo_gitgalaxy_clean_v1").mkdir()
(corpus / "alpha_repo_gitgalaxy_java_spring_v1").mkdir()

return corpus

@patch("gitgalaxy.tools.cobol_to_java.batch_test_harness.subprocess.run")
def test_happy_path(mock_run, mock_env):
"""
Simulates a flawless run where Refractor, Java Forge, and Maven all succeed.
"""
# 1. Setup the mock to always return a successful execution
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "Process completed successfully."
mock_result.stderr = ""
mock_run.return_value = mock_result

# 2. Patch sys.argv to simulate running the script from the CLI
test_args = ["batch_test_harness.py", str(mock_env)]
with patch.object(sys, 'argv', test_args):
batch_test_harness.main()

# 3. Assert subprocess was called exactly 3 times for our 1 repo
assert mock_run.call_count == 3

# 4. Verify the master log was created and contains the correct sample size
reports = list(mock_env.glob("batch_test_reports/master_batch_run_*.txt"))
assert len(reports) == 1
content = reports[0].read_text(encoding="utf-8")
assert "Sample Size: 1" in content

@patch("gitgalaxy.tools.cobol_to_java.batch_test_harness.subprocess.run")
def test_maven_failure_path(mock_run, mock_env):
"""
Simulates Steps 1 & 2 succeeding, but Step 3 (Maven) failing with a compile error.
"""
# 1. Create two distinct mock objects
success_mock = MagicMock(returncode=0, stdout="OK", stderr="")
failure_mock = MagicMock(returncode=1, stdout="[ERROR] COMPILATION ERROR", stderr="Fatal flaw in Java")

# 2. Use side_effect to return them in sequence (Success, Success, Fail)
mock_run.side_effect = [success_mock, success_mock, failure_mock]

test_args = ["batch_test_harness.py", str(mock_env)]
with patch.object(sys, 'argv', test_args):
batch_test_harness.main()

# 3. Verify the specific repo error log was created and caught the Maven stdout
error_logs = list(mock_env.glob("batch_test_reports/alpha_repo_error_*.log"))
assert len(error_logs) == 1
error_content = error_logs[0].read_text(encoding="utf-8")

assert "--- MAVEN STDERR/STDOUT ---" in error_content
assert "[ERROR] COMPILATION ERROR" in error_content

@patch("gitgalaxy.tools.cobol_to_java.batch_test_harness.subprocess.run")
def test_timeout_path(mock_run, mock_env):
"""
Simulates the external script hanging and triggering the 5-minute kill switch.
"""
# 1. Force subprocess.run to raise the specific Timeout exception
mock_run.side_effect = subprocess.TimeoutExpired(
cmd="python cobol_refractor_controller.py",
timeout=300,
output=b"Partial refactor log before the freeze..."
)

test_args = ["batch_test_harness.py", str(mock_env)]
with patch.object(sys, 'argv', test_args):
batch_test_harness.main()

# 2. Verify the harness caught the timeout safely and wrote it to the error log
error_logs = list(mock_env.glob("batch_test_reports/alpha_repo_error_*.log"))
assert len(error_logs) == 1
error_content = error_logs[0].read_text(encoding="utf-8")

assert "TIMEOUT: Command exceeded 5 minutes" in error_content
# Ensure it successfully decoded the partial output byte string
assert "Partial refactor log before the freeze..." in error_content
103 changes: 103 additions & 0 deletions tests/test_decoder_forge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import pytest

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'pytest' is not used.
from gitgalaxy.tools.cobol_to_java.cobol_to_java_decoder_forge import generate_decoder_util

# ==============================================================================
# GOLDEN IMAGE (The "Perfect" Expected Output)
# ==============================================================================
GOLDEN_DECODER_UTIL = """package com.gitgalaxy.modernized.util;

import java.math.BigDecimal;
import java.nio.charset.Charset;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class EbcdicDecoderUtil {

private static final Logger log = LoggerFactory.getLogger(EbcdicDecoderUtil.class);

// Cp1047 is the standard IBM EBCDIC character set
private static final Charset EBCDIC_CHARSET = Charset.forName("Cp1047");

/**
* Decodes a raw EBCDIC byte array into a standard Java UTF-8 String.
*/
public static String decodeEbcdicString(byte[] ebcdicBytes) {
if (ebcdicBytes == null) return null;
try {
return new String(ebcdicBytes, EBCDIC_CHARSET).trim();
} catch (Exception e) {
log.error("Failed to decode EBCDIC string", e);
return "";
}
}

/**
* Unpacks a COBOL COMP-3 (Packed Decimal) byte array into a Java BigDecimal.
* Includes strict hex-boundary validation to prevent runtime crashes from dirty legacy data.
*/
public static BigDecimal unpackComp3(byte[] packedBytes, int scale) {
if (packedBytes == null || packedBytes.length == 0) {
return BigDecimal.ZERO;
}

try {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < packedBytes.length; i++) {
int b = packedBytes[i] & 0xFF;

// Extract the high and low nibbles (4 bits each)
int highNibble = b >>> 4;
int lowNibble = b & 0x0F;

// The high nibble MUST be a number (0-9)
if (highNibble > 9) {
log.warn("Corrupt COMP-3 high nibble '{}' at byte index {}. Defaulting to ZERO.", Integer.toHexString(highNibble), i);
return BigDecimal.ZERO;
}
sb.append(highNibble);

// The low nibble is a number EXCEPT in the very last byte, where it's the sign
if (i == packedBytes.length - 1) {
boolean isNegative = (lowNibble == 0x0D || lowNibble == 0x0B);
if (isNegative) {
sb.insert(0, "-");
} else if (lowNibble < 0x0A) {
// The sign nibble should be A-F. If it's a number, the data is likely shifted or corrupt.
log.warn("Suspicious COMP-3 sign nibble '{}' at end of byte array.", Integer.toHexString(lowNibble));
}
} else {
if (lowNibble > 9) {
log.warn("Corrupt COMP-3 low nibble '{}' at byte index {}. Defaulting to ZERO.", Integer.toHexString(lowNibble), i);
return BigDecimal.ZERO;
}
sb.append(lowNibble);
}
}

BigDecimal result = new BigDecimal(sb.toString());
return result.movePointLeft(scale);

} catch (Exception e) {
log.error("Critical failure unpacking COMP-3 bytes. Defaulting to ZERO.", e);
return BigDecimal.ZERO;
}
}
}"""

# ==============================================================================
# THE TESTS
# ==============================================================================

def test_ebcdic_comp3_decoder_golden_image():
"""
Verifies that the generated EBCDIC and COMP-3 decoding logic perfectly matches
the mathematically proven Golden Image. This prevents fatal regressions in
mainframe bitwise unpacking logic.
"""
# 1. Generate the code
generated_java = generate_decoder_util("com.gitgalaxy.modernized")

# 2. Compare against the Golden Image
# Using strip() to neutralize OS-level newline differences (CRLF vs LF)
assert generated_java.strip() == GOLDEN_DECODER_UTIL.strip(), \
"FATAL REGRESSION: The Mainframe Decoder utility drifted from the Golden Image! Check bitwise math and hex boundaries."
113 changes: 113 additions & 0 deletions tests/test_golden_forge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import pytest

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'pytest' is not used.
import json

Check notice

Code scanning / CodeQL

Unused import Note test

Import of 'json' is not used.

# Import your Forge generators
from gitgalaxy.tools.cobol_to_java.cobol_to_java_api_contract_forge import generate_rest_controller
from gitgalaxy.tools.cobol_to_java.cobol_to_java_spring_forge import generate_java_entity

# ==============================================================================
# INLINE FIXTURES (The "Known Good" Inputs)
# ==============================================================================
MOCK_IR_STATE = {
"metadata": {"file_name": "process-payroll.cbl"},
"analysis": {
"base_intent": {"files_requested": [], "is_cics": False},
"lineage": {
"inputs": ["EMPLOYEE-RECORD", "TIMECARD-DATA"],
"outputs": ["PAYROLL-RECEIPT"]
}
}
}

MOCK_SCHEMA_STATE = {
"title": "EMPLOYEE_TABLE",
"properties": {
"EMP-ID": {"type": "integer", "description": "PIC 9(6)"},
"EMP-NAME": {"type": "string", "description": "PIC X(50)"},
"SALARY": {"type": "decimal", "description": "PIC 9(5)V99"}
}
}

# ==============================================================================
# GOLDEN IMAGES (The "Perfect" Expected Outputs)
# ==============================================================================
GOLDEN_CONTROLLER = """package com.gitgalaxy.modernized.controller;

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import lombok.RequiredArgsConstructor;
import com.gitgalaxy.modernized.service.ProcessPayrollService;

@RestController
@RequestMapping("/api/v1/process-payroll")
@RequiredArgsConstructor
public class ProcessPayrollController {

private final ProcessPayrollService processPayrollService;

@PostMapping("/execute")
public ResponseEntity<?> executeProcessPayroll( @RequestBody EmployeeRecordDTO employeeRecordData,
@RequestBody TimecardDataDTO timecardDataData ) { // ⚡ TRANSACTIONAL PARADIGM DETECTED
processPayrollService.executeProcessPayroll(/* pass DTOs here */);

// Expected Outputs: PAYROLL-RECEIPT
return ResponseEntity.ok().build(); }
}"""

GOLDEN_ENTITY = """package com.gitgalaxy.modernized.entity;

import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.persistence.*;
import java.math.BigDecimal;

@Data
@NoArgsConstructor
@Entity
@Table(name = "EMPLOYEE_TABLE")
public class EmployeeTable {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "sys_id")
private Long sysId;

@Column(name = "EMP-ID")
private Integer empId;

@Column(name = "EMP-NAME", length = 50)
private String empName;

@Column(name = "SALARY", precision = 7, scale = 2)
private BigDecimal salary;

}"""

# ==============================================================================
# THE TESTS
# ==============================================================================

def test_api_contract_golden_image():
"""
Feeds a known IR state into the API Contract Forge and verifies the
resulting Java code matches our Golden Image byte-for-byte.
"""
# 1. Generate the code using the mock IR
generated_java = generate_rest_controller(MOCK_IR_STATE, "com.gitgalaxy.modernized")

# 2. Compare against the Golden Image
# We collapse whitespace to prevent OS line-ending differences (CRLF vs LF) from failing the test
assert " ".join(generated_java.split()) == " ".join(GOLDEN_CONTROLLER.split()), \
"API Contract generation drifted from the Golden Image! Did someone alter the string formatting?"

def test_spring_entity_golden_image():
"""
Feeds a known Schema state into the Spring Entity Forge and verifies the
resulting JPA Entity (with PIC constraints) matches our Golden Image.
"""
# 1. Generate the entity using the mock schema
generated_java = generate_java_entity(MOCK_SCHEMA_STATE, "com.gitgalaxy.modernized")

# 2. Compare against the Golden Image
assert " ".join(generated_java.split()) == " ".join(GOLDEN_ENTITY.split()), \
"Spring Entity generation drifted from the Golden Image! Check PIC clause parsing logic."
Loading
Loading