Skip to content

Commit 0cf12f0

Browse files
committed
feat: Update task management DTOs and services for enhanced credential validation and response handling
1 parent 528ea08 commit 0cf12f0

12 files changed

Lines changed: 124 additions & 54 deletions

File tree

java-ecosystem/libs/core/src/main/java/org/rostilos/codecrow/core/dto/taskmanagement/TaskManagementConnectionRequest.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

77
/**
88
* Request DTO for creating or updating a task management connection.
9+
* <p>
10+
* Provider-specific credential requirements are validated at the service layer,
11+
* since different providers need different credential fields:
12+
* <ul>
13+
* <li>Jira Cloud: {@code email} + {@code apiToken}</li>
14+
* <li>Jira Data Center (future): {@code apiToken} only (Personal Access Token)</li>
15+
* </ul>
916
*/
1017
public record TaskManagementConnectionRequest(
1118
@NotBlank(message = "Connection name is required")
@@ -19,10 +26,10 @@ public record TaskManagementConnectionRequest(
1926
@Size(max = 512, message = "Base URL must be at most 512 characters")
2027
String baseUrl,
2128

22-
@NotBlank(message = "Email is required for Jira Cloud authentication")
29+
/** Required for Jira Cloud; may be null for other providers. */
2330
String email,
2431

25-
@NotBlank(message = "API token is required")
32+
/** API token (Jira Cloud) or Personal Access Token (Jira Data Center). */
2633
String apiToken
2734
) {
2835
}

java-ecosystem/libs/core/src/main/java/org/rostilos/codecrow/core/dto/taskmanagement/TaskManagementConnectionResponse.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package org.rostilos.codecrow.core.dto.taskmanagement;
22

3-
import java.time.LocalDateTime;
3+
import java.time.OffsetDateTime;
44

55
/**
66
* Response DTO for task management connection details.
@@ -14,7 +14,7 @@ public record TaskManagementConnectionResponse(
1414
String baseUrl,
1515
/** Masked email (e.g. "j***@example.com") — never returns raw credentials */
1616
String maskedEmail,
17-
LocalDateTime createdAt,
18-
LocalDateTime updatedAt
17+
OffsetDateTime createdAt,
18+
OffsetDateTime updatedAt
1919
) {
2020
}

java-ecosystem/libs/core/src/main/java/org/rostilos/codecrow/core/model/taskmanagement/TaskManagementConnection.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import org.hibernate.type.SqlTypes;
99
import org.rostilos.codecrow.core.model.workspace.Workspace;
1010

11-
import java.time.LocalDateTime;
11+
import java.time.OffsetDateTime;
1212
import java.util.Map;
1313

1414
/**
@@ -77,11 +77,11 @@ public class TaskManagementConnection {
7777

7878
@CreationTimestamp
7979
@Column(name = "created_at")
80-
private LocalDateTime createdAt;
80+
private OffsetDateTime createdAt;
8181

8282
@UpdateTimestamp
8383
@Column(name = "updated_at")
84-
private LocalDateTime updatedAt;
84+
private OffsetDateTime updatedAt;
8585

8686
@Version
8787
@Column(name = "version", nullable = false, columnDefinition = "BIGINT DEFAULT 0")
@@ -145,11 +145,11 @@ public void setCredentials(Map<String, String> credentials) {
145145
this.credentials = credentials;
146146
}
147147

148-
public LocalDateTime getCreatedAt() {
148+
public OffsetDateTime getCreatedAt() {
149149
return createdAt;
150150
}
151151

152-
public LocalDateTime getUpdatedAt() {
152+
public OffsetDateTime getUpdatedAt() {
153153
return updatedAt;
154154
}
155155

java-ecosystem/libs/core/src/main/java/org/rostilos/codecrow/core/util/RetryExecutor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,12 +107,12 @@ public static <T> T withExponentialBackoff(int maxRetries, long initialBackoffMs
107107
: new IOException("RetryExecutor: all " + maxRetries + " attempts failed");
108108
}
109109

110-
private static void sleep(long ms) {
110+
private static void sleep(long ms) throws IOException {
111111
try {
112112
Thread.sleep(ms);
113113
} catch (InterruptedException ie) {
114114
Thread.currentThread().interrupt();
115-
log.warn("Retry sleep interrupted");
115+
throw new IOException("Retry sleep interrupted", ie);
116116
}
117117
}
118118
}

java-ecosystem/libs/task-management/src/main/java/org/rostilos/codecrow/taskmanagement/jira/cloud/JiraCloudClient.java

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -376,69 +376,97 @@ private ObjectNode buildListItem(String text) {
376376

377377
/**
378378
* Parse inline Markdown formatting into ADF inline nodes.
379-
* Handles: {@code **bold**}, {@code *italic*}/{@code _italic_}, {@code `code`}.
379+
* <p>
380+
* Handles: {@code **bold**}, {@code __bold__}, {@code ***bold-italic***},
381+
* {@code *italic*}/{@code _italic_}, {@code `code`}, and backslash escapes
382+
* (e.g. {@code \*not italic\*}).
383+
* </p>
380384
*/
381385
private ArrayNode buildInlineContent(String text) {
382386
ArrayNode nodes = objectMapper.createArrayNode();
383-
// Regex for inline patterns: **bold**, *italic*, _italic_, `code`
384-
// Process left-to-right, handling nested marks
385387
int pos = 0;
386388
int len = text.length();
389+
StringBuilder plain = new StringBuilder();
387390

388391
while (pos < len) {
389-
// ── Inline code: `...` ──
390-
if (text.charAt(pos) == '`') {
392+
char c = text.charAt(pos);
393+
394+
// ── Backslash escape: \* \_ \` \\ ──
395+
if (c == '\\' && pos + 1 < len) {
396+
char next = text.charAt(pos + 1);
397+
if (next == '*' || next == '_' || next == '`' || next == '\\') {
398+
plain.append(next);
399+
pos += 2;
400+
continue;
401+
}
402+
}
403+
404+
// ── Inline code: `...` (no nesting, no escapes inside) ──
405+
if (c == '`') {
391406
int end = text.indexOf('`', pos + 1);
392407
if (end > pos) {
408+
flushPlain(nodes, plain);
393409
addTextNode(nodes, text.substring(pos + 1, end), List.of("code"));
394410
pos = end + 1;
395411
continue;
396412
}
397413
}
398414

399-
// ── Bold: **...** ──
400-
if (pos + 1 < len && text.charAt(pos) == '*' && text.charAt(pos + 1) == '*') {
401-
int end = text.indexOf("**", pos + 2);
415+
// ── Bold-italic: ***...*** ──
416+
if (pos + 2 < len && c == '*' && text.charAt(pos + 1) == '*' && text.charAt(pos + 2) == '*') {
417+
int end = text.indexOf("***", pos + 3);
418+
if (end > pos) {
419+
flushPlain(nodes, plain);
420+
addTextNode(nodes, text.substring(pos + 3, end), List.of("strong", "em"));
421+
pos = end + 3;
422+
continue;
423+
}
424+
}
425+
426+
// ── Bold: **...** or __...__ ──
427+
if (pos + 1 < len && ((c == '*' && text.charAt(pos + 1) == '*')
428+
|| (c == '_' && text.charAt(pos + 1) == '_'))) {
429+
String marker = text.substring(pos, pos + 2);
430+
int end = text.indexOf(marker, pos + 2);
402431
if (end > pos) {
432+
flushPlain(nodes, plain);
403433
addTextNode(nodes, text.substring(pos + 2, end), List.of("strong"));
404434
pos = end + 2;
405435
continue;
406436
}
407437
}
408438

409-
// ── Italic: *...* or _..._ ──
410-
if ((text.charAt(pos) == '*' || text.charAt(pos) == '_')) {
411-
char marker = text.charAt(pos);
412-
// Avoid matching ** (already handled) or __ for bold
413-
if (!(pos + 1 < len && text.charAt(pos + 1) == marker)) {
414-
int end = text.indexOf(marker, pos + 1);
439+
// ── Italic: *...* or _..._ (single marker, not doubled) ──
440+
if (c == '*' || c == '_') {
441+
if (!(pos + 1 < len && text.charAt(pos + 1) == c)) {
442+
int end = text.indexOf(c, pos + 1);
415443
if (end > pos) {
444+
flushPlain(nodes, plain);
416445
addTextNode(nodes, text.substring(pos + 1, end), List.of("em"));
417446
pos = end + 1;
418447
continue;
419448
}
420449
}
421450
}
422451

423-
// ── Plain text: consume until next potential marker ──
424-
int nextMarker = len;
425-
for (int j = pos + 1; j < len; j++) {
426-
char c = text.charAt(j);
427-
if (c == '`' || c == '*' || c == '_') {
428-
nextMarker = j;
429-
break;
430-
}
431-
}
432-
String plain = text.substring(pos, nextMarker);
433-
if (!plain.isEmpty()) {
434-
addTextNode(nodes, plain, List.of());
435-
}
436-
pos = nextMarker;
452+
// ── Regular character ──
453+
plain.append(c);
454+
pos++;
437455
}
438456

457+
flushPlain(nodes, plain);
439458
return nodes;
440459
}
441460

461+
/** Flush accumulated plain text into a text node, then clear the buffer. */
462+
private void flushPlain(ArrayNode nodes, StringBuilder buffer) {
463+
if (buffer.isEmpty()) {
464+
return;
465+
}
466+
addTextNode(nodes, buffer.toString(), List.of());
467+
buffer.setLength(0);
468+
}
469+
442470
private void addTextNode(ArrayNode nodes, String text, List<String> markTypes) {
443471
ObjectNode textNode = objectMapper.createObjectNode();
444472
textNode.put("type", "text");

java-ecosystem/libs/task-management/src/main/java/org/rostilos/codecrow/taskmanagement/jira/cloud/JiraCloudConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public record JiraCloudConfig(
3131
*/
3232
public String basicAuthHeader() {
3333
String credentials = email + ":" + apiToken;
34-
return "Basic " + java.util.Base64.getEncoder().encodeToString(credentials.getBytes());
34+
return "Basic " + java.util.Base64.getEncoder().encodeToString(
35+
credentials.getBytes(java.nio.charset.StandardCharsets.UTF_8));
3536
}
3637
}

java-ecosystem/services/pipeline-agent/src/main/java/org/rostilos/codecrow/pipelineagent/qadoc/QaDocGenerationService.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,11 @@ private String callInferenceOrchestrator(Map<String, Object> payload) throws IOE
165165

166166
return documentation;
167167
} catch (Exception e) {
168-
// If parsing fails, treat the raw response as the document
169-
log.warn("Failed to parse QA doc response as JSON, using raw response");
170-
return responseBody;
168+
// Reject unparseable responses — posting raw HTML/error pages to Jira
169+
// would corrupt the ticket. Return null so the caller skips the update.
170+
log.error("Failed to parse QA doc response as JSON (length={}): {}",
171+
responseBody.length(), e.getMessage());
172+
return null;
171173
}
172174
}
173175

java-ecosystem/services/pipeline-agent/src/test/java/org/rostilos/codecrow/pipelineagent/qadoc/QaDocGenerationServiceTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ void shouldThrowOn5xx() {
253253
}
254254

255255
@Test
256-
@DisplayName("should return raw response when JSON parsing fails")
256+
@DisplayName("should return null when JSON parsing fails (rejects malformed responses)")
257257
void shouldReturnRawResponseWhenJsonInvalid() throws Exception {
258258
mockWebServer.enqueue(new MockResponse()
259259
.setResponseCode(200)
@@ -264,7 +264,7 @@ void shouldReturnRawResponseWhenJsonInvalid() throws Exception {
264264
project, 1L, 0, 0, Map.of(), baseConfig(), null, null, null
265265
);
266266

267-
assertThat(result).isEqualTo("Plain text documentation");
267+
assertThat(result).isNull();
268268
}
269269

270270
@Test

java-ecosystem/services/web-server/src/main/java/org/rostilos/codecrow/webserver/taskmanagement/controller/TaskManagementController.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,8 @@ public ResponseEntity<QaAutoDocConfig> updateQaAutoDocConfig(
102102
@PathVariable String workspaceSlug,
103103
@PathVariable Long projectId,
104104
@Valid @RequestBody QaAutoDocConfigRequest request) {
105-
// Workspace resolution for auth context
106-
resolveWorkspace(workspaceSlug);
107-
QaAutoDocConfig config = taskManagementService.updateQaAutoDocConfig(projectId, request);
105+
Workspace workspace = resolveWorkspace(workspaceSlug);
106+
QaAutoDocConfig config = taskManagementService.updateQaAutoDocConfig(workspace.getId(), projectId, request);
108107
return ResponseEntity.ok(config);
109108
}
110109

0 commit comments

Comments
 (0)