Skip to content
Merged

Dev #119

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
33 changes: 33 additions & 0 deletions .github/scripts/inspect-artifacts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -eu
run_id=${1:-}
if [ -z "$run_id" ]; then
echo "run id required as first arg"
exit 1
fi
repo="${GITHUB_REPOSITORY:-}"
token="${GITHUB_TOKEN:-}"
if [ -z "$repo" ] || [ -z "$token" ]; then
echo "GITHUB_REPOSITORY or GITHUB_TOKEN not set"
exit 1
fi

# Download and inspect artifacts that match test-results-*
for name in $(curl -s -H "Authorization: token $token" \
"https://api.github.com/repos/$repo/actions/runs/$run_id/artifacts" \
| jq -r '.artifacts[].name' || true); do
if [[ "$name" == test-results-* ]]; then
echo "Found artifact: $name"
mkdir -p debug-artifacts
id=$(curl -s -H "Authorization: token $token" \
"https://api.github.com/repos/$repo/actions/runs/$run_id/artifacts" \
| jq -r ".artifacts[] | select(.name==\"$name\") | .id")
echo "Downloading artifact id=$id"
curl -s -L -H "Authorization: token $token" \
"https://api.github.com/repos/$repo/actions/artifacts/${id}/zip" \
-o "debug-artifacts/${name}.zip"
unzip -l "debug-artifacts/${name}.zip" || true
unzip -p "debug-artifacts/${name}.zip" "**/*.trx" 2>/dev/null | head -c 200 || true
unzip -p "debug-artifacts/${name}.zip" "**/*.xml" 2>/dev/null | head -c 200 || true
fi
done
16 changes: 16 additions & 0 deletions .github/scripts/list-artifacts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -eu
run_id=${1:-}
if [ -z "$run_id" ]; then
echo "run id required as first arg"
exit 1
fi
repo="${GITHUB_REPOSITORY:-}"
token="${GITHUB_TOKEN:-}"
if [ -z "$repo" ] || [ -z "$token" ]; then
echo "GITHUB_REPOSITORY or GITHUB_TOKEN not set"
exit 1
fi
curl -s -H "Authorization: token $token" \
"https://api.github.com/repos/$repo/actions/runs/$run_id/artifacts" \
| jq -r '.artifacts[] | "- \(.name) (id=\(.id), size=\(.size_in_bytes))"' || true
32 changes: 32 additions & 0 deletions .github/scripts/sanitize-test-results.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import xml.etree.ElementTree as ET
from pathlib import Path

def root_tag_name(elem):
# strip namespace if present
tag = elem.tag
if '}' in tag:
return tag.split('}', 1)[1]
return tag

VALID_ROOTS = {'testsuite', 'testsuites', 'TestRun'}

src = Path('test-results')
dst = Path('test-results-clean')
dst.mkdir(exist_ok=True)

for f in src.rglob('*'):
if f.is_file() and f.suffix.lower() in ('.xml', '.trx'):
try:
tree = ET.parse(f)
root = tree.getroot()
tag = root_tag_name(root)
if tag not in VALID_ROOTS:
print('Skipping non-junit/trx file (root="%s"): %s' % (tag, f))
continue
target = dst / f.relative_to(src)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(f.read_bytes())
print('Kept:', f)
except Exception as e:
print('Skipping invalid test file:', f, '->', e)
83 changes: 74 additions & 9 deletions .github/workflows/test-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,29 @@ jobs:
- progress-monitoring-service
- time-logging-service
steps:
- uses: dorny/test-reporter@v2
- name: 'Unit Test Results - ${{ matrix.service }}'
uses: dorny/test-reporter@v2
with:
artifact: test-results-${{ matrix.service }}
name: 'Unit Tests - ${{ matrix.service }}'
# only include actual JUnit TEST-*.xml files (exclude summary files)
path: '**/surefire-reports/TEST-*.xml'
reporter: java-junit
fail-on-error: false
fail-on-empty: false
only-summary: false
list-suites: 'failed'
list-tests: 'failed'
max-annotations: 50
badge-title: '${{ matrix.service }}'

- name: 'Integration Test Results - ${{ matrix.service }}'
uses: dorny/test-reporter@v2
with:
artifact: test-results-${{ matrix.service }}
name: 'Test Results - ${{ matrix.service }}'
path: '**/*.xml'
name: 'Integration Tests - ${{ matrix.service }}'
# include only failing integration test result files generated by failsafe
path: '**/failsafe-reports/TEST-*.xml'
reporter: java-junit
fail-on-error: false
fail-on-empty: false
Expand Down Expand Up @@ -77,17 +95,64 @@ jobs:
needs: [report-java-services, report-dotnet-services]
if: ${{ always() && github.event.workflow_run.conclusion != 'cancelled' }}
steps:
- uses: dorny/test-reporter@v2
- name: "Debug: list artifacts from triggering run"
env:
RUN_ID: ${{ github.event.workflow_run.id }}
run: bash .github/scripts/list-artifacts.sh "$RUN_ID"

- name: "Debug: show matching artifact files (first lines)"
env:
RUN_ID: ${{ github.event.workflow_run.id }}
run: bash .github/scripts/inspect-artifacts.sh "$RUN_ID"

- name: Download all test-results-* artifacts
uses: actions/download-artifact@v4
with:
pattern: 'test-results-*'
path: ./test-results

- name: Sanitize test result files (skip malformed XML/TRX)
run: |
set -eux
python3 .github/scripts/sanitize-test-results.py

- name: List sanitized test result files
run: |
set -eux
echo "Sanitized files (test-results-clean):"
ls -R test-results-clean || true
echo "--- File previews ---"
for f in $(find test-results-clean -type f -name '*.xml' -o -name '*.trx' 2>/dev/null); do
echo "-- $f --"
head -c 400 "$f" || true
echo
done

- name: Generate combined test report - JUnit XML
uses: dorny/test-reporter@v2
continue-on-error: true
with:
artifact: /test-results-(.*)/
name: 'Combined Test Report - All Services'
path: '**/*.xml,**/*.trx'
name: 'Combined Test Report - All Services (XML)'
path: 'test-results-clean/**/*.xml'
reporter: java-junit
fail-on-error: false
fail-on-empty: false
only-summary: true
badge-title: 'All Services'
report-title: '📊 Complete Test Suite Results'
badge-title: 'All Services (XML)'
report-title: '📊 Complete Test Suite Results (XML)'

- name: Generate combined test report - .TRX
uses: dorny/test-reporter@v2
continue-on-error: true
with:
name: 'Combined Test Report - All Services (TRX)'
path: 'test-results-clean/**/*.trx'
reporter: dotnet-trx
fail-on-error: false
fail-on-empty: false
only-summary: true
badge-title: 'All Services (TRX)'
report-title: '📊 Complete Test Suite Results (TRX)'

# Status check
report-status:
Expand Down
4 changes: 2 additions & 2 deletions chatbot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,8 @@
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
<source>21</source>
<target>21</target>
<source>${java.version}</source>
<target>${java.version}</target>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package com.voidsquad.chatbot.service;import org.junit.jupiter.api.Test;import org.springframework.mock.web.MockMultipartFile;import java.util.List;import static org.assertj.core.api.Assertions.assertThat;class AIServiceCSVEdgeTest { @Test void readStaticInfoFromCSV_handlesMissingHeadersGracefully() throws Exception { AIService service = new AIService(null,null,null,null,null,null,null,null,null,null); String csv = "wrong,headers\nA,B\n"; MockMultipartFile file = new MockMultipartFile("file","f.csv","text/csv", csv.getBytes()); List<?> list = service.ReadStaticInfoFromCSV(file); assertThat(list).isEmpty(); } @Test void readStaticInfoFromCSV_skipsInvalidRows(){ AIService service = new AIService(null,null,null,null,null,null,null,null,null,null); String csv = "topic,description\n,Missing topic\nValid,Desc\nBad,\n"; MockMultipartFile file = new MockMultipartFile("file","f2.csv","text/csv", csv.getBytes()); try { var list = service.ReadStaticInfoFromCSV(file); assertThat(list).hasSize(1); assertThat(list.get(0).getTopic()).isEqualTo("Valid"); } catch (Exception e) { throw new RuntimeException(e); } }}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package com.voidsquad.chatbot.service;import com.voidsquad.chatbot.service.language.LanguageProcessor;import com.voidsquad.chatbot.service.promptmanager.core.OutputFormat;import com.voidsquad.chatbot.service.promptmanager.core.ProcessingResult;import com.voidsquad.chatbot.service.promptmanager.core.ProcessingType;import org.junit.jupiter.api.BeforeEach;import org.junit.jupiter.api.Test;import org.junit.jupiter.api.extension.ExtendWith;import org.mockito.Mock;import org.mockito.junit.jupiter.MockitoExtension;import static org.assertj.core.api.Assertions.assertThat;import static org.mockito.ArgumentMatchers.any;import static org.mockito.Mockito.when;@ExtendWith(MockitoExtension.class)class AIServiceGenerationTest { @Mock LanguageProcessor processor; private AIService aiService; @BeforeEach void setUp(){ aiService = new AIService(null,null,processor,null,null,null,null,null,null,null); } @Test void generation_handlesException(){ when(processor.evaluateSimpleReply(any(), any(), any())).thenThrow(new RuntimeException("boom")); String out = aiService.generation("hi"); assertThat(out).isEqualTo("Error!"); } @Test void generation_returnsNoResponseMessageWhenNull(){ when(processor.evaluateSimpleReply(any(), any(), any())).thenReturn(null); String out = aiService.generation("hi"); assertThat(out).isEqualTo("No response from model"); } @Test void generation_returnsOutput(){ ProcessingResult pr = new ProcessingResult("OK", OutputFormat.TEXT, ProcessingType.SIMPLE_CHAT, java.util.Map.of()); when(processor.evaluateSimpleReply(any(), any(), any())).thenReturn(pr); String out = aiService.generation("hi"); assertThat(out).isEqualTo("OK"); }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.voidsquad.chatbot.service;

import com.voidsquad.chatbot.entities.StaticInfo;
import com.voidsquad.chatbot.exception.JsonDecodeException;
import com.voidsquad.chatbot.exception.NoAnswerException;
import com.voidsquad.chatbot.model.SimpleChatStrategyResponse;
import com.voidsquad.chatbot.model.ToolCall;
import com.voidsquad.chatbot.repository.StaticInfoRepository;
import com.voidsquad.chatbot.repository.WorkflowStepRepository;
import com.voidsquad.chatbot.service.auth.AuthInfo;
import com.voidsquad.chatbot.service.language.LanguageProcessor;
import com.voidsquad.chatbot.service.promptmanager.core.OutputFormat;
import com.voidsquad.chatbot.service.promptmanager.core.ProcessingResult;
import com.voidsquad.chatbot.service.promptmanager.core.ProcessingType;
import com.voidsquad.chatbot.service.tool.ToolCallResult;
import com.voidsquad.chatbot.service.tool.ToolExecutionService;
import com.voidsquad.chatbot.service.tool.ToolRegistry;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class AIServiceRequestFlowTest {

@Mock LanguageProcessor languageProcessor;
@Mock StaticInfoRepository staticInfoRepository;
@Mock WorkflowStepRepository workflowStepRepository;
@Mock ToolRegistry toolRegistry;
@Mock ToolExecutionService toolExecutionService;
@Mock com.voidsquad.chatbot.decoder.SimpleAiResponseDecoder simpleDecoder;
@Mock com.voidsquad.chatbot.decoder.FinalOutputStrategyDecoder finalDecoder;
@Mock com.voidsquad.chatbot.decoder.ToolCallResponseDecoder toolCallDecoder;
@Mock com.voidsquad.chatbot.service.embedding.EmbeddingService embeddingService;

@InjectMocks AIService aiService;

@Test
void requestHandler_simplePath_returnsSimpleData() throws JsonDecodeException, NoAnswerException, IOException {
// Embedding + static hits
when(embeddingService.generateEmbedding(any())).thenReturn(new float[]{1f,2f,3f});
when(staticInfoRepository.findSimilarStaticInfo(any(), anyInt())).thenReturn(List.of(
StaticInfo.builder().topic("T").description("D").embedding(new float[]{1f}).build()
));
// First LLM call
ProcessingResult simpleResult = new ProcessingResult("{\"isSimple\":true,\"data\":\"Hi\"}", OutputFormat.JSON, ProcessingType.SIMPLE_CHAT, Map.of());
when(languageProcessor.evaluateSimpleReply(any(), any(), any())).thenReturn(simpleResult);
when(simpleDecoder.decode(simpleResult)).thenReturn(new SimpleChatStrategyResponse(true, "Hi"));

String out = aiService.requestHandler("hello", AuthInfo.builder().role("USER").firstName("A").userId(1L).build());
assertThat(out).isEqualTo("Hi");
}

@Test
void requestHandler_complexPath_executesToolsAndGeneratesFinalOutput() throws Exception {
when(embeddingService.generateEmbedding(any())).thenReturn(new float[]{1f,2f,3f});
when(staticInfoRepository.findSimilarStaticInfo(any(), anyInt())).thenReturn(List.of());

ProcessingResult first = new ProcessingResult("{\"isSimple\":false,\"data\":\"Need tools\"}", OutputFormat.JSON, ProcessingType.SIMPLE_CHAT, Map.of());
when(languageProcessor.evaluateSimpleReply(any(), any(), any())).thenReturn(first);
when(simpleDecoder.decode(first)).thenReturn(new SimpleChatStrategyResponse(false, "Need tools"));

// Workflow step embeddings search
when(workflowStepRepository.findSimilarSteps(any(), anyInt())).thenReturn(List.of());

// Tool call identification
ProcessingResult toolIdentify = new ProcessingResult("{\"tool_calls\":[{" +
"\"toolName\":\"echo\",\"parameters\":{}}]}", OutputFormat.JSON, ProcessingType.TOOL_CALL_IDENTIFICATION, Map.of());
when(languageProcessor.findHelperToolCalls(any(), any(), any())).thenReturn(toolIdentify);
when(toolCallDecoder.decode(any())).thenReturn(List.of(new ToolCall("echo", Map.of(), "explanation")));
when(toolExecutionService.executeAll(any(), any(), any())).thenReturn(List.of(ToolCallResult.success("res", "echo")));

// Final output
ProcessingResult finalProc = new ProcessingResult("{\"isComplete\":true,\"data\":\"DONE\"}", OutputFormat.JSON, ProcessingType.FINAL_OUTPUT_GENERATION, Map.of());
when(languageProcessor.finalOutputPrepWithData(any(), any(), any())).thenReturn(finalProc);
when(finalDecoder.decode(finalProc)).thenReturn(new com.voidsquad.chatbot.decoder.FinalOutputStrategyResponse(true, "DONE"));

String out = aiService.requestHandler("complex", AuthInfo.builder().role("ADMIN").firstName("B").userId(5L).build());
assertThat(out).isEqualTo("DONE");
}

@Test
void requestHandler_withoutCoreDeps_fallsBackToGeneration() throws Exception {
// EmbeddingService mocked but we simulate missing other deps by setting them null via reflection (simpler: return null ProcessingResult)
when(languageProcessor.evaluateSimpleReply(any(), any(), any())).thenReturn(new ProcessingResult("fallback", OutputFormat.TEXT, ProcessingType.SIMPLE_CHAT, Map.of()));
AIService local = new AIService(embeddingService, null, languageProcessor, null, simpleDecoder, finalDecoder, workflowStepRepository, toolCallDecoder, toolRegistry, toolExecutionService);
String out = local.requestHandler("x", AuthInfo.builder().role("X").build());
assertThat(out).isEqualTo("fallback");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.voidsquad.chatbot.service;

import com.voidsquad.chatbot.entities.StaticInfo;
import com.voidsquad.chatbot.repository.StaticInfoRepository;
import com.voidsquad.chatbot.service.embedding.EmbeddingService;
import com.voidsquad.chatbot.service.language.LanguageProcessor;
import com.voidsquad.chatbot.service.promptmanager.core.OutputFormat;
import com.voidsquad.chatbot.service.promptmanager.core.ProcessingResult;
import com.voidsquad.chatbot.service.promptmanager.core.ProcessingType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;

import java.io.IOException;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class AIServiceTest {

@Mock
EmbeddingService embeddingService;

@Mock
StaticInfoRepository staticInfoRepository;

@Mock
LanguageProcessor languageProcessor;

@InjectMocks
AIService aiService;

@Test
void generation_returnsModelOutput_whenProcessorReturnsResult() {
ProcessingResult res = new ProcessingResult("hello-world", OutputFormat.TEXT, ProcessingType.SIMPLE_CHAT, null);
when(languageProcessor.evaluateSimpleReply(any(), any(), any())).thenReturn(res);

String result = aiService.generation("hi");

assertThat(result).isEqualTo("hello-world");
}

@Test
void readStaticInfoFromCSV_parsesValidRows() throws IOException, Exception {
String csv = "topic,description\nFeature A,Does A\nFeature B,Does B\n";
MockMultipartFile file = new MockMultipartFile("file", "static.csv", "text/csv", csv.getBytes());

List<StaticInfo> out = aiService.ReadStaticInfoFromCSV(file);

assertThat(out).hasSize(2);
assertThat(out.get(0).getTopic()).isEqualTo("Feature A");
assertThat(out.get(1).getDescription()).isEqualTo("Does B");
}

@Test
void getAllStaticInfoByEmbeddings_formatsResults() {
when(embeddingService.generateEmbedding(any())).thenReturn(new float[]{1f, 2f, 3f});
when(staticInfoRepository.findSimilarStaticInfo(any(float[].class), org.mockito.ArgumentMatchers.anyInt())).thenReturn(
List.of(StaticInfo.builder().topic("T1").description("D1").build())
);

List<String> list = aiService.getAllStaticInfoByEmbeddings("keyword");

assertThat(list).hasSize(1);
assertThat(list.get(0)).contains("Topic T1").contains("Description: D1");
}
}
Loading
Loading