From 9ef746aa4fad65cbb344262d50903ee430b858bf Mon Sep 17 00:00:00 2001 From: andreferraro Date: Tue, 11 Aug 2026 15:03:49 -0300 Subject: [PATCH] fix: publish LCOV in Sonar CI so PR quality gate passes Co-authored-by: Cursor --- .gitea/workflows/sonar.yml | 11 +++++++++++ .gitignore | 1 + package.json | 3 ++- sonar-project.properties | 2 ++ src/env.test.ts | 25 +++++++++++++++++++++++++ src/hello.test.ts | 18 +++++++++++++++++- src/telemetry.test.ts | 26 ++++++++++++++++++++++++++ 7 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 src/env.test.ts create mode 100644 src/telemetry.test.ts diff --git a/.gitea/workflows/sonar.yml b/.gitea/workflows/sonar.yml index 3857cec..d57181e 100644 --- a/.gitea/workflows/sonar.yml +++ b/.gitea/workflows/sonar.yml @@ -22,6 +22,17 @@ jobs: with: fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Install, test, coverage + run: | + npm ci + npm run test:coverage + - name: SonarQube Scan + Quality Gate uses: sonarsource/sonarqube-scan-action@v5 env: diff --git a/.gitignore b/.gitignore index 515425a..096edc8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ data/ *.sqlite-journal *.sqlite-wal *.sqlite-shm +coverage/ .scannerwork/ graphify-out/ _plane* diff --git a/package.json b/package.json index 0b8a444..128845e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "start": "node dist/hello.js", "typecheck": "tsc --noEmit", "migrate": "npm run build && node dist/db/migrate.js", - "test": "npm run build && node --test dist/**/*.test.js" + "test": "npm run build && node --test dist/**/*.test.js", + "test:coverage": "npm run build && node -e \"require('fs').mkdirSync('coverage',{recursive:true})\" && node --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test dist/**/*.test.js" }, "dependencies": { "@opentelemetry/api": "^1.9.1", diff --git a/sonar-project.properties b/sonar-project.properties index c8d6954..89dd96b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -5,4 +5,6 @@ sonar.sources=src sonar.tests=src sonar.test.inclusions=**/*.test.ts sonar.exclusions=**/*.test.ts +sonar.javascript.lcov.reportPaths=coverage/lcov.info +sonar.coverage.exclusions=**/*.test.ts sonar.sourceEncoding=UTF-8 diff --git a/src/env.test.ts b/src/env.test.ts new file mode 100644 index 0000000..ad3421b --- /dev/null +++ b/src/env.test.ts @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import test from "node:test"; +import { loadDotEnv } from "./env.js"; + +test("loadDotEnv loads missing keys from .env without overriding", () => { + const dir = mkdtempSync(join(tmpdir(), "ticketlab-env-")); + const key = `TICKETLAB_ENV_TEST_${Date.now()}`; + try { + writeFileSync(join(dir, ".env"), `${key}=from-file\n# comment\nOTHER=1\n`, "utf8"); + delete process.env[key]; + loadDotEnv(dir); + assert.equal(process.env[key], "from-file"); + + process.env[key] = "already-set"; + writeFileSync(join(dir, ".env"), `${key}=should-not-win\n`, "utf8"); + loadDotEnv(dir); + assert.equal(process.env[key], "already-set"); + } finally { + delete process.env[key]; + rmSync(dir, { recursive: true, force: true }); + } +}); diff --git a/src/hello.test.ts b/src/hello.test.ts index 440d5df..9e1eaae 100644 --- a/src/hello.test.ts +++ b/src/hello.test.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import { greet } from "./hello.js"; +import { greet, main } from "./hello.js"; describe("greet", () => { it("returns the default greeting", () => { @@ -11,3 +11,19 @@ describe("greet", () => { assert.equal(greet("World"), "Hello, World!"); }); }); + +it("main emits telemetry and logs greeting", async () => { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = + process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://127.0.0.1:4318"; + const logs: unknown[] = []; + const original = console.log; + console.log = (...args: unknown[]) => { + logs.push(args); + }; + try { + await main(); + assert.deepEqual(logs, [["Hello, TicketLab MCP!"]]); + } finally { + console.log = original; + } +}); diff --git a/src/telemetry.test.ts b/src/telemetry.test.ts new file mode 100644 index 0000000..1b22dff --- /dev/null +++ b/src/telemetry.test.ts @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { SeverityNumber, startTelemetry } from "./telemetry.js"; + +test("startTelemetry emits and shuts down without throwing", async () => { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = + process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://127.0.0.1:4318"; + process.env.OTEL_SERVICE_NAME = "ticketlab-test"; + + const telemetry = startTelemetry("ticketlab-test"); + const counter = telemetry.meter.createCounter("ticketlab_test_runs_total"); + + await telemetry.tracer.startActiveSpan("ticketlab.test", async (span) => { + counter.add(1, { component: "test" }); + telemetry.logger.emit({ + severityNumber: SeverityNumber.INFO, + severityText: "INFO", + body: "telemetry self-check", + attributes: { "ticketlab.event": "test" }, + }); + span.end(); + }); + + await telemetry.shutdown(); + assert.equal(typeof SeverityNumber.INFO, "number"); +});