fix: make plugin hooks run on Node 21+ and green the suite under modern Node (#2184)

ROOT CAUSE: hooks load plugin-hook-bootstrap.js via
`node -e "...; process.argv.splice(1,0,s); require(s)"`. On Node 21+,
require.main is `undefined` under --eval, so the `if (require.main === module)`
guard was false and main() never ran — every plugin hook silently no-op'd
(e.g. the MCP-health PreToolUse hook stopped blocking). CI (Node 18/20) hid
this; it only surfaces on Node 21+. Fix: also run main() when require.main is
undefined (the eval-bootstrap case), while staying dormant on real imports.

Also clears pre-existing main debt the full local suite enforces:
- catalog:sync — README/docs agent+skill counts drifted after recent merges
- tests/ci/supply-chain-watch-workflow: update checkout SHA to the merged v6.0.3 (#2183)
- markdownlint + check-unicode-safety --write across docs/skills

Suite: 2683/2683 green under Node v25; lint + unicode clean.

Co-authored-by: ECC Test <ecc@example.test>
This commit is contained in:
Affaan Mustafa
2026-06-07 16:05:28 +08:00
committed by GitHub
parent eef31ad39c
commit e755c5f72b
36 changed files with 613 additions and 609 deletions
+37 -37
View File
@@ -70,18 +70,18 @@ public class OrderProcessingService {
```java
@ApplicationScoped
public class ProcessingService {
public void processDocument(Document doc) {
LogContext logContext = CustomLog.getCurrentContext();
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
logContext.put("documentId", doc.getId().toString());
logContext.put("documentType", doc.getType());
logContext.put("userId", SecurityContext.getUserId());
log.info("Iniciando procesamiento de documento");
processInternal(doc);
log.info("Procesamiento de documento completado");
} catch (Exception e) {
log.error("Error en el procesamiento de documento", e);
@@ -101,7 +101,7 @@ public class ProcessingService {
<includeMdc>true</includeMdc>
</encoder>
</appender>
<logger name="com.example" level="INFO"/>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
@@ -118,7 +118,7 @@ public class ProcessingService {
public class EventService {
private final EventRepository eventRepository;
private final ObjectMapper objectMapper;
public void createSuccessEvent(Object payload, String eventType) {
Objects.requireNonNull(payload, "El payload no puede ser null");
Event event = new Event();
@@ -126,11 +126,11 @@ public class EventService {
event.setStatus(EventStatus.SUCCESS);
event.setPayload(serializePayload(payload));
event.setTimestamp(Instant.now());
eventRepository.persist(event);
log.info("Evento de éxito creado: {}", eventType);
}
public void createErrorEvent(Object payload, String eventType, String errorMessage) {
Objects.requireNonNull(payload, "El payload no puede ser null");
if (errorMessage == null || errorMessage.isBlank()) {
@@ -142,11 +142,11 @@ public class EventService {
event.setErrorMessage(errorMessage);
event.setPayload(serializePayload(payload));
event.setTimestamp(Instant.now());
eventRepository.persist(event);
log.error("Evento de error creado: {} - {}", eventType, errorMessage);
}
private String serializePayload(Object payload) {
try {
return objectMapper.writeValueAsString(payload);
@@ -165,10 +165,10 @@ public class EventService {
@RequiredArgsConstructor
public class BusinessRulesPublisher {
private final ProducerTemplate producerTemplate;
public void publishSync(BusinessRulesPayload payload) {
producerTemplate.sendBody(
"direct:business-rules-publisher",
"direct:business-rules-publisher",
payload
);
}
@@ -180,23 +180,23 @@ public class BusinessRulesPublisher {
```java
@ApplicationScoped
public class BusinessRulesRoute extends RouteBuilder {
@ConfigProperty(name = "camel.rabbitmq.queue.business-rules")
String businessRulesQueue;
@ConfigProperty(name = "rabbitmq.host")
String rabbitHost;
@ConfigProperty(name = "rabbitmq.port")
Integer rabbitPort;
@Override
public void configure() {
from("direct:business-rules-publisher")
.routeId("business-rules-publisher")
.log("Publicando mensaje en RabbitMQ: ${body}")
.marshal().json(JsonLibrary.Jackson)
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
.toF("spring-rabbitmq:%s?hostname=%s&portNumber=%d",
businessRulesQueue, rabbitHost, rabbitPort);
}
}
@@ -207,14 +207,14 @@ public class BusinessRulesRoute extends RouteBuilder {
```java
@ApplicationScoped
public class DocumentProcessingRoute extends RouteBuilder {
@Override
public void configure() {
onException(ValidationException.class)
.handled(true)
.to("direct:validation-error-handler")
.log("Error de validación: ${exception.message}");
from("direct:process-document")
.routeId("document-processing")
.log("Procesando documento: ${header.documentId}")
@@ -237,19 +237,19 @@ public class DocumentProcessingRoute extends RouteBuilder {
```java
@ApplicationScoped
public class FileMonitoringRoute extends RouteBuilder {
@ConfigProperty(name = "file.input.directory")
String inputDirectory;
@ConfigProperty(name = "file.processed.directory")
String processedDirectory;
@ConfigProperty(name = "file.error.directory")
String errorDirectory;
@Override
public void configure() {
from("file:" + inputDirectory + "?move=" + processedDirectory +
from("file:" + inputDirectory + "?move=" + processedDirectory +
"&moveFailed=" + errorDirectory + "&delay=5000")
.routeId("file-monitor")
.log("Procesando archivo: ${header.CamelFileName}")
@@ -302,7 +302,7 @@ public class DocumentResource {
```java
@ApplicationScoped
public class DocumentRepository implements PanacheRepository<Document> {
public List<Document> findByStatus(DocumentStatus status, int page, int size) {
return find("status = ?1 order by createdAt desc", status)
.page(page, size)
@@ -331,11 +331,11 @@ public class DocumentService {
document.setDescription(request.description());
document.setStatus(DocumentStatus.PENDING);
document.setCreatedAt(Instant.now());
repo.persist(document);
eventService.createSuccessEvent(document, "DOCUMENT_CREATED");
return document;
}
}
@@ -352,7 +352,7 @@ public record CreateDocumentRequest(
public record DocumentResponse(Long id, String referenceNumber, DocumentStatus status) {
public static DocumentResponse from(Document document) {
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
return new DocumentResponse(document.getId(), document.getReferenceNumber(),
document.getStatus());
}
}
@@ -368,7 +368,7 @@ public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViol
String message = exception.getConstraintViolations().stream()
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
.collect(Collectors.joining(", "));
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of("error", "validation_error", "message", message))
.build();
@@ -385,25 +385,25 @@ public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViol
public class FileStorageService {
private final S3Client s3Client;
private final ExecutorService executorService;
public CompletableFuture<StoredDocumentInfo> uploadOriginalFile(
InputStream inputStream,
long size,
InputStream inputStream,
long size,
LogContext logContext,
InvoiceFormat format) {
return CompletableFuture.supplyAsync(() -> {
try (SafeAutoCloseable ignored = CustomLog.startScope(logContext)) {
String path = generateStoragePath(format);
PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucketName)
.key(path)
.contentLength(size)
.build();
s3Client.putObject(request, RequestBody.fromInputStream(inputStream, size));
return new StoredDocumentInfo(path, size, Instant.now());
} catch (Exception e) {
log.error("Error al subir archivo a S3", e);
+19 -19
View File
@@ -28,7 +28,7 @@ Buenas prácticas para asegurar aplicaciones Quarkus con autenticación, autoriz
@Path("/api/protected")
@Authenticated
public class ProtectedResource {
@Inject
JsonWebToken jwt;
@@ -65,19 +65,19 @@ quarkus.oidc.credentials.secret=${OIDC_SECRET}
@Provider
@Priority(Priorities.AUTHENTICATION)
public class CustomAuthFilter implements ContainerRequestFilter {
@Inject
SecurityIdentity identity;
@Override
public void filter(ContainerRequestContext requestContext) {
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
return;
}
String token = authHeader.substring(7);
if (!validateToken(token)) {
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
@@ -98,7 +98,7 @@ public class CustomAuthFilter implements ContainerRequestFilter {
@Path("/api/admin")
@RolesAllowed("ADMIN")
public class AdminResource {
@GET
@Path("/users")
public List<UserDto> listUsers() {
@@ -116,7 +116,7 @@ public class AdminResource {
@Path("/api/users")
public class UserResource {
@Inject
SecurityIdentity securityIdentity;
@@ -124,7 +124,7 @@ public class UserResource {
@Path("/{id}")
@RolesAllowed("USER")
public Response getUser(@PathParam("id") Long id) {
if (!securityIdentity.hasRole("ADMIN") &&
if (!securityIdentity.hasRole("ADMIN") &&
!isOwner(id, securityIdentity.getPrincipal().getName())) {
return Response.status(Response.Status.FORBIDDEN).build();
}
@@ -138,7 +138,7 @@ public class UserResource {
```java
@ApplicationScoped
public class SecurityService {
@Inject
SecurityIdentity securityIdentity;
@@ -146,7 +146,7 @@ public class SecurityService {
if (securityIdentity.isAnonymous()) {
return false;
}
if (securityIdentity.hasRole("ADMIN")) {
return true;
}
@@ -216,7 +216,7 @@ List<User> users = User.list("email = ?1 and active = ?2", email, true);
Optional<User> user = User.find("username", username).firstResultOptional();
// BIEN: Parámetros nombrados
List<User> users = User.list("email = :email and age > :minAge",
List<User> users = User.list("email = :email and age > :minAge",
Parameters.with("email", email).and("minAge", 18));
```
@@ -243,7 +243,7 @@ public class User extends PanacheEntity {
```java
@ApplicationScoped
public class PasswordService {
public String hash(String plainPassword) {
return BcryptUtil.bcryptHash(plainPassword);
}
@@ -297,7 +297,7 @@ public class RateLimitFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) {
String clientId = getClientIdentifier();
RateLimiter limiter = limiters.computeIfAbsent(clientId,
RateLimiter limiter = limiters.computeIfAbsent(clientId,
k -> RateLimiter.create(100.0)); // 100 solicitudes por segundo
if (!limiter.tryAcquire()) {
@@ -324,17 +324,17 @@ public class RateLimitFilter implements ContainerRequestFilter {
```java
@Provider
public class SecurityHeadersFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext request, ContainerResponseContext response) {
MultivaluedMap<String, Object> headers = response.getHeaders();
headers.putSingle("X-Frame-Options", "DENY");
headers.putSingle("X-Content-Type-Options", "nosniff");
headers.putSingle("X-XSS-Protection", "1; mode=block");
headers.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
// CSP: evitar 'unsafe-inline' para script-src; usar nonces o hashes
headers.putSingle("Content-Security-Policy",
headers.putSingle("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
}
}
@@ -351,11 +351,11 @@ public class AuditService {
SecurityIdentity securityIdentity;
public void logAccess(String resource, String action) {
String user = securityIdentity.isAnonymous()
? "anonymous"
String user = securityIdentity.isAnonymous()
? "anonymous"
: securityIdentity.getPrincipal().getName();
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
LOG.infof("AUDIT: user=%s action=%s resource=%s timestamp=%s",
user, action, resource, Instant.now());
}
}
+33 -33
View File
@@ -32,19 +32,19 @@ Orientación TDD para servicios Quarkus 3.x con 80%+ de cobertura (unit + integr
@ExtendWith(MockitoExtension.class)
@DisplayName("Pruebas Unitarias de OrderService")
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private EventService eventService;
@Mock
private FulfillmentPublisher fulfillmentPublisher;
@InjectMocks
private OrderService orderService;
private CreateOrderCommand validCommand;
@BeforeEach
@@ -58,16 +58,16 @@ class OrderServiceTest {
@Nested
@DisplayName("Pruebas para createOrder")
class CreateOrder {
@Test
@DisplayName("Debe persistir orden y publicar evento de fulfillment")
void givenValidCommand_whenCreateOrder_thenPersistsAndPublishes() {
// ARRANGE
doNothing().when(orderRepository).persist(any(Order.class));
// ACT
OrderReceipt receipt = orderService.createOrder(validCommand);
// ASSERT
assertThat(receipt).isNotNull();
assertThat(receipt.customerId()).isEqualTo("customer-123");
@@ -81,7 +81,7 @@ class OrderServiceTest {
void givenMissingCustomerId_whenCreateOrder_thenThrowsBadRequest() {
// ARRANGE
CreateOrderCommand invalid = new CreateOrderCommand("", validCommand.lines());
// ACT & ASSERT
WebApplicationException exception = assertThrows(
WebApplicationException.class,
@@ -99,13 +99,13 @@ class OrderServiceTest {
// ARRANGE
doThrow(new PersistenceException("base de datos no disponible"))
.when(orderRepository).persist(any(Order.class));
// ACT & ASSERT
PersistenceException exception = assertThrows(
PersistenceException.class,
() -> orderService.createOrder(validCommand)
);
assertThat(exception.getMessage()).contains("base de datos no disponible");
verify(eventService).createErrorEvent(
eq(validCommand),
@@ -168,20 +168,20 @@ class BusinessRulesRouteTest {
// ARRANGE
MockEndpoint mockRabbitMQ = camelContext.getEndpoint("mock:rabbitmq", MockEndpoint.class);
mockRabbitMQ.expectedMessageCount(1);
camelContext.getRouteController().stopRoute("business-rules-publisher");
AdviceWith.adviceWith(camelContext, "business-rules-publisher", advice -> {
advice.replaceFromWith("direct:business-rules-publisher");
advice.weaveByToString(".*spring-rabbitmq.*").replace().to("mock:rabbitmq");
});
camelContext.getRouteController().startRoute("business-rules-publisher");
// ACT
producerTemplate.sendBody("direct:business-rules-publisher", testPayload);
// ASSERT
mockRabbitMQ.assertIsSatisfied(5000);
assertThat(mockRabbitMQ.getExchanges()).hasSize(1);
String body = mockRabbitMQ.getExchanges().get(0).getIn().getBody(String.class);
assertThat(body).contains("\"documentId\":1");
@@ -199,17 +199,17 @@ class EventServiceTest {
@Mock
private EventRepository eventRepository;
@Mock
private ObjectMapper objectMapper;
@InjectMocks
private EventService eventService;
@Nested
@DisplayName("Pruebas para createSuccessEvent")
class CreateSuccessEvent {
@Test
@DisplayName("Debe crear evento de éxito con atributos correctos")
void givenValidPayload_whenCreateSuccessEvent_thenEventPersisted() throws Exception {
@@ -217,13 +217,13 @@ class EventServiceTest {
BusinessRulesPayload testPayload = new BusinessRulesPayload();
testPayload.setDocumentId(1L);
when(objectMapper.writeValueAsString(testPayload)).thenReturn("{\"documentId\":1}");
// ACT
assertDoesNotThrow(() ->
assertDoesNotThrow(() ->
eventService.createSuccessEvent(testPayload, "DOCUMENT_PROCESSED"));
// ASSERT
verify(eventRepository).persist(argThat(event ->
verify(eventRepository).persist(argThat(event ->
event.getType().equals("DOCUMENT_PROCESSED") &&
event.getStatus() == EventStatus.SUCCESS &&
event.getTimestamp() != null
@@ -235,13 +235,13 @@ class EventServiceTest {
void givenNullPayload_whenCreateSuccessEvent_thenThrowsException() {
// ARRANGE
Object nullPayload = null;
// ACT & ASSERT
NullPointerException exception = assertThrows(
NullPointerException.class,
() -> eventService.createSuccessEvent(nullPayload, "EVENT_TYPE")
);
assertThat(exception.getMessage()).isEqualTo("Payload cannot be null");
verify(eventRepository, never()).persist(any());
}
@@ -250,7 +250,7 @@ class EventServiceTest {
@Nested
@DisplayName("Pruebas para createErrorEvent")
class CreateErrorEvent {
@ParameterizedTest
@DisplayName("Debe rechazar mensajes de error inválidos")
@ValueSource(strings = {"", " "})
@@ -263,7 +263,7 @@ class EventServiceTest {
IllegalArgumentException.class,
() -> eventService.createErrorEvent(testPayload, "ERROR", blankMessage)
);
assertThat(exception.getMessage()).contains("Error message cannot be blank");
}
}
@@ -278,10 +278,10 @@ class FileStorageServiceTest {
@Mock
private S3Client s3Client;
@Mock
private ExecutorService executorService;
@InjectMocks
private FileStorageService fileStorageService;
@@ -293,15 +293,15 @@ class FileStorageServiceTest {
((Runnable) invocation.getArgument(0)).run();
return null;
}).when(executorService).execute(any(Runnable.class));
when(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class)))
.thenThrow(new StorageException("S3 no disponible"));
// ACT
CompletableFuture<StoredDocumentInfo> future =
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
CompletableFuture<StoredDocumentInfo> future =
fileStorageService.uploadOriginalFile(testInputStream, 1024L,
testLogContext, InvoiceFormat.UBL);
// ASSERT
assertThatThrownBy(() -> future.join())
.isInstanceOf(CompletionException.class)