Skip to content

Commit c645a8a

Browse files
committed
fix: address review feedback — import source, endpoint guard, test discoverability
- telemetry.test.ts: import InMemorySpanExporter/SimpleSpanProcessor from @opentelemetry/sdk-trace-node (listed dep) instead of sdk-trace-base (transitive only, would fail at runtime) - telemetry.ts: add early-return guard when no OTLP endpoint is configured and no _testProvider is given, so initTelemetry truly is a no-op - Move test files from src/__tests__/ to test/ so npm test glob finds them - Switch test runner to tsx (handles .js→.ts resolution that --experimental-strip-types cannot, required by memory.ts → shared.ts) - Revert tsconfig.json exclude change (tests no longer under src/)
1 parent 1f26b4e commit c645a8a

6 files changed

Lines changed: 130 additions & 164 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"build": "tsc",
4444
"dev": "tsc --watch",
4545
"start": "node dist/index.js",
46-
"test": "node --test test/*.test.ts --experimental-strip-types"
46+
"test": "npx tsx --test test/*.test.ts"
4747
},
4848
"engines": {
4949
"node": ">=20"

src/__tests__/telemetry.test.ts

Lines changed: 0 additions & 144 deletions
This file was deleted.

src/telemetry.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ const _slots = {
7171
export async function initTelemetry(opts: TelemetryOptions): Promise<void> {
7272
if (_initialized) return;
7373

74+
// When there is no endpoint configured and no test provider, telemetry
75+
// has nowhere to send data — skip the dynamic SDK imports entirely.
76+
const hasEndpoint = opts.exporterEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
77+
if (!opts._testProvider && !hasEndpoint) return;
78+
7479
try {
7580
// Test path — register a caller-supplied TracerProvider directly.
7681
if (opts._testProvider) {
Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,45 +12,38 @@ import { join } from "path";
1212
import { tmpdir } from "os";
1313
import { execSync } from "child_process";
1414

15-
let createMemoryTool: typeof import("../memory.ts").createMemoryTool;
15+
let createMemoryTool: typeof import("../src/tools/memory.ts").createMemoryTool;
1616

1717
before(async () => {
18-
const mod = await import("../memory.ts");
18+
const mod = await import("../src/tools/memory.ts");
1919
createMemoryTool = mod.createMemoryTool;
2020
});
2121

2222
describe("memory tool", () => {
23-
/** Create a temporary directory with git init and return the path. */
2423
async function setupRepo(): Promise<string> {
2524
const dir = await mkdtemp(join(tmpdir(), "gitagent-memory-test-"));
2625
execSync("git init -q", { cwd: dir });
27-
// Configure git user for commits
2826
execSync('git config --local user.email "test@gitagent.test"', { cwd: dir });
2927
execSync('git config --local user.name "Test Agent"', { cwd: dir });
3028
return dir;
3129
}
3230

33-
/** Clean up a temp directory. */
3431
async function cleanup(dir: string): Promise<void> {
3532
await rm(dir, { recursive: true, force: true }).catch(() => {});
3633
}
3734

38-
// ── load ─────────────────────────────────────────────────────────
39-
4035
describe("load", () => {
4136
it("returns stored memory content", async () => {
4237
const dir = await setupRepo();
4338
try {
4439
const tool = createMemoryTool(dir);
4540

46-
// First, save some memory content
4741
await tool.execute("call-1", {
4842
action: "save",
4943
content: "# Memory\n\n- Remember to buy milk\n- Project uses TypeScript",
5044
message: "Initial memory",
5145
});
5246

53-
// Now load it
5447
const result = await tool.execute("call-2", { action: "load" });
5548

5649
assert.ok(result.content);
@@ -67,7 +60,6 @@ describe("memory tool", () => {
6760
try {
6861
const tool = createMemoryTool(dir);
6962

70-
// Load from a repo with no memory file
7163
const result = await tool.execute("call-1", { action: "load" });
7264

7365
assert.equal(result.content[0].text, "No memories yet.");
@@ -79,7 +71,6 @@ describe("memory tool", () => {
7971
it("returns 'No memories yet.' when memory file has only heading", async () => {
8072
const dir = await setupRepo();
8173
try {
82-
// Write the default heading-only memory file
8374
await mkdir(join(dir, "memory"), { recursive: true });
8475
await writeFile(join(dir, "memory", "MEMORY.md"), "# Memory", "utf-8");
8576

@@ -93,8 +84,6 @@ describe("memory tool", () => {
9384
});
9485
});
9586

96-
// ── save ─────────────────────────────────────────────────────────
97-
9887
describe("save", () => {
9988
it("writes content and commits to git", async () => {
10089
const dir = await setupRepo();
@@ -113,15 +102,13 @@ describe("memory tool", () => {
113102
);
114103
assert.ok(result.content[0].text.includes("First save"));
115104

116-
// Verify the file was written
117105
const { readFile } = await import("fs/promises");
118106
const fileContent = await readFile(
119107
join(dir, "memory", "MEMORY.md"),
120108
"utf-8",
121109
);
122110
assert.ok(fileContent.includes("Saved entry one"));
123111

124-
// Verify the git commit exists
125112
const log = execSync("git log --oneline", {
126113
cwd: dir,
127114
encoding: "utf-8",
@@ -164,7 +151,6 @@ describe("memory tool", () => {
164151
() =>
165152
tool.execute("call-1", {
166153
action: "save",
167-
// content intentionally omitted
168154
}),
169155
/content is required for save action/,
170156
);
@@ -174,8 +160,6 @@ describe("memory tool", () => {
174160
});
175161
});
176162

177-
// ── abort signal ─────────────────────────────────────────────────
178-
179163
describe("abort signal", () => {
180164
it("throws when signal is already aborted", async () => {
181165
const dir = await setupRepo();

test/telemetry-init.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* Tests for the telemetry module (src/telemetry.ts) — init/shutdown/idempotency.
3+
*
4+
* These tests verify that initTelemetry correctly gates on the OTLP
5+
* endpoint environment variable: it MUST return without enabling telemetry
6+
* when no endpoint is configured, and it MUST successfully create an SDK
7+
* instance when an endpoint (or test provider) is provided.
8+
*/
9+
import { describe, it, before, afterEach } from "node:test";
10+
import assert from "node:assert/strict";
11+
import { trace } from "@opentelemetry/api";
12+
import {
13+
NodeTracerProvider,
14+
InMemorySpanExporter,
15+
SimpleSpanProcessor,
16+
} from "@opentelemetry/sdk-trace-node";
17+
18+
let initTelemetry: typeof import("../src/telemetry.ts").initTelemetry;
19+
let shutdownTelemetry: typeof import("../src/telemetry.ts").shutdownTelemetry;
20+
let isTelemetryEnabled: typeof import("../src/telemetry.ts").isTelemetryEnabled;
21+
22+
before(async () => {
23+
const mod = await import("../src/telemetry.ts");
24+
initTelemetry = mod.initTelemetry;
25+
shutdownTelemetry = mod.shutdownTelemetry;
26+
isTelemetryEnabled = mod.isTelemetryEnabled;
27+
});
28+
29+
afterEach(async () => {
30+
await shutdownTelemetry();
31+
try {
32+
trace.disable();
33+
} catch {
34+
/* ignore */
35+
}
36+
});
37+
38+
describe("telemetry init", () => {
39+
function makeTestProvider() {
40+
const exporter = new InMemorySpanExporter();
41+
const provider = new NodeTracerProvider({
42+
spanProcessors: [new SimpleSpanProcessor(exporter)],
43+
});
44+
return { exporter, provider };
45+
}
46+
47+
it("returns without enabling telemetry when no OTLP endpoint is configured", async () => {
48+
const saved = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
49+
const wasSet = "OTEL_EXPORTER_OTLP_ENDPOINT" in process.env;
50+
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
51+
52+
try {
53+
await assert.doesNotReject(
54+
() => initTelemetry({}),
55+
"initTelemetry must never throw, even without an endpoint",
56+
);
57+
58+
assert.equal(
59+
isTelemetryEnabled(),
60+
false,
61+
"telemetry must remain disabled when no endpoint is configured",
62+
);
63+
} finally {
64+
if (wasSet) {
65+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = saved;
66+
} else {
67+
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
68+
}
69+
}
70+
});
71+
72+
it("creates an SDK instance when endpoint is configured", async () => {
73+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318";
74+
const { exporter, provider } = makeTestProvider();
75+
76+
try {
77+
await initTelemetry({
78+
serviceName: "test-svc",
79+
_testProvider: provider,
80+
});
81+
82+
assert.equal(
83+
isTelemetryEnabled(),
84+
true,
85+
"telemetry must be enabled after initTelemetry with _testProvider",
86+
);
87+
88+
const tracer = trace.getTracer("test");
89+
const span = tracer.startSpan("test-span");
90+
span.end();
91+
92+
await provider.forceFlush();
93+
const spans = exporter.getFinishedSpans();
94+
assert.equal(spans.length, 1, "span should be exported");
95+
assert.equal(spans[0].name, "test-span");
96+
} finally {
97+
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
98+
}
99+
});
100+
101+
it("is idempotent", async () => {
102+
const { provider: provider1 } = makeTestProvider();
103+
const { provider: provider2 } = makeTestProvider();
104+
105+
await initTelemetry({ _testProvider: provider1 });
106+
assert.equal(isTelemetryEnabled(), true);
107+
108+
await initTelemetry({ _testProvider: provider2 });
109+
assert.equal(isTelemetryEnabled(), true);
110+
});
111+
112+
it("shutdownTelemetry resets the initialized state", async () => {
113+
const { provider } = makeTestProvider();
114+
115+
await initTelemetry({ _testProvider: provider });
116+
assert.equal(isTelemetryEnabled(), true);
117+
118+
await shutdownTelemetry();
119+
assert.equal(isTelemetryEnabled(), false);
120+
});
121+
});

0 commit comments

Comments
 (0)