diff --git a/gitgalaxy/tools/cobol_to_java/cobol_to_java_build_forge.py b/gitgalaxy/tools/cobol_to_java/cobol_to_java_build_forge.py
index 0aae5dbc..b2f4d82f 100644
--- a/gitgalaxy/tools/cobol_to_java/cobol_to_java_build_forge.py
+++ b/gitgalaxy/tools/cobol_to_java/cobol_to_java_build_forge.py
@@ -72,6 +72,12 @@ def generate_pom_xml(group_id: str, artifact_id: str) -> str:
spring-boot-starter-test
test
+
+
+ com.h2database
+ h2
+ test
+
diff --git a/tests/test_agent_forge.py b/tests/test_agent_forge.py
new file mode 100644
index 00000000..9c07e0f6
--- /dev/null
+++ b/tests/test_agent_forge.py
@@ -0,0 +1,60 @@
+import pytest
+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."
\ No newline at end of file
diff --git a/tests/test_batch_test_harness.py b/tests/test_batch_test_harness.py
new file mode 100644
index 00000000..6d142e9d
--- /dev/null
+++ b/tests/test_batch_test_harness.py
@@ -0,0 +1,102 @@
+import pytest
+import subprocess
+import sys
+from pathlib import Path
+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
\ No newline at end of file
diff --git a/tests/test_decoder_forge.py b/tests/test_decoder_forge.py
new file mode 100644
index 00000000..e0a9de46
--- /dev/null
+++ b/tests/test_decoder_forge.py
@@ -0,0 +1,103 @@
+import pytest
+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."
\ No newline at end of file
diff --git a/tests/test_golden_forge.py b/tests/test_golden_forge.py
new file mode 100644
index 00000000..09363368
--- /dev/null
+++ b/tests/test_golden_forge.py
@@ -0,0 +1,113 @@
+import pytest
+import json
+
+# 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."
\ No newline at end of file
diff --git a/tests/test_service_forge.py b/tests/test_service_forge.py
new file mode 100644
index 00000000..d101176b
--- /dev/null
+++ b/tests/test_service_forge.py
@@ -0,0 +1,60 @@
+import pytest
+from gitgalaxy.tools.cobol_to_java.cobol_to_java_service_forge import generate_service_skeleton
+
+# ==============================================================================
+# INLINE FIXTURES
+# ==============================================================================
+MOCK_IR_STATE = {
+ "metadata": {
+ "file_name": "payroll-processor.cbl"
+ },
+ "analysis": {
+ "lineage": {
+ # Two distinct COBOL-style program names with hyphens
+ "unresolved_calls": ["CALC-BENEFITS", "UPDATE-LEDGER"]
+ }
+ }
+}
+
+# ==============================================================================
+# GOLDEN IMAGE
+# ==============================================================================
+GOLDEN_SERVICE_SKELETON = """package com.gitgalaxy.modernized.service;
+
+import org.springframework.stereotype.Service;
+import lombok.RequiredArgsConstructor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Service
+@RequiredArgsConstructor
+public class PayrollProcessorService {
+
+ private static final Logger log = LoggerFactory.getLogger(PayrollProcessorService.class);
+
+ // ⚠️ UNRESOLVED EXTERNAL DEPENDENCIES (FROM DAG)
+ // TODO: AI AGENT - Implement or mock call to: CalcBenefitsService
+ // TODO: AI AGENT - Implement or mock call to: UpdateLedgerService
+
+ public void executePayrollProcessor(/* Parameters mapped from Controller */) {
+ log.info("Executing legacy business logic for payroll-processor");
+ // TODO: [AI AGENT] Implement extracted business rules here.
+ }
+}"""
+
+# ==============================================================================
+# THE TESTS
+# ==============================================================================
+
+def test_service_skeleton_dag_resolver():
+ """
+ Feeds a mock IR state with unresolved COBOL calls into the Service Forge.
+ Verifies that the Python script correctly translates COBOL hyphens into
+ Java CamelCase so the downstream AI Agent knows exactly what to auto-wire.
+ """
+ # 1. Generate the code using the mock IR
+ generated_java = generate_service_skeleton(MOCK_IR_STATE, "com.gitgalaxy.modernized")
+
+ # 2. Compare against the Golden Image
+ assert generated_java.strip() == GOLDEN_SERVICE_SKELETON.strip(), \
+ "Service Forge drifted from the Golden Image! Did the CamelCase/Hyphen parsing break?"
\ No newline at end of file