-
Notifications
You must be signed in to change notification settings - Fork 121
fix: update npm package name and Node version; test: implement memory and telemetry tests #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e912caf
d0295da
4486c21
a5a7036
1f26b4e
c645a8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,7 +43,7 @@ | |
| "build": "tsc", | ||
| "dev": "tsc --watch", | ||
| "start": "node dist/index.js", | ||
| "test": "node --test test/*.test.ts --experimental-strip-types" | ||
| "test": "npx tsx --test test/*.test.ts" | ||
| }, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tsx is not listed as a devDependency, but the test script invokes it via |
||
| "engines": { | ||
| "node": ">=20" | ||
|
|
||
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| /** | ||
| * Tests for the memory tool (src/tools/memory.ts). | ||
| * | ||
| * The memory tool provides git-backed persistent memory with load/save | ||
| * operations. Each save creates a git commit, giving full history of | ||
| * what the agent has remembered. | ||
| */ | ||
| import { describe, it, before } from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { mkdtemp, rm, writeFile, mkdir } from "fs/promises"; | ||
| import { join } from "path"; | ||
| import { tmpdir } from "os"; | ||
| import { execSync } from "child_process"; | ||
|
|
||
| let createMemoryTool: typeof import("../src/tools/memory.ts").createMemoryTool; | ||
|
|
||
| before(async () => { | ||
| const mod = await import("../src/tools/memory.ts"); | ||
| createMemoryTool = mod.createMemoryTool; | ||
| }); | ||
|
|
||
| describe("memory tool", () => { | ||
| async function setupRepo(): Promise<string> { | ||
| const dir = await mkdtemp(join(tmpdir(), "gitagent-memory-test-")); | ||
| execSync("git init -q", { cwd: dir }); | ||
| execSync('git config --local user.email "test@gitagent.test"', { cwd: dir }); | ||
| execSync('git config --local user.name "Test Agent"', { cwd: dir }); | ||
| return dir; | ||
| } | ||
|
|
||
| async function cleanup(dir: string): Promise<void> { | ||
| await rm(dir, { recursive: true, force: true }).catch(() => {}); | ||
| } | ||
|
|
||
| describe("load", () => { | ||
| it("returns stored memory content", async () => { | ||
| const dir = await setupRepo(); | ||
| try { | ||
| const tool = createMemoryTool(dir); | ||
|
|
||
| await tool.execute("call-1", { | ||
| action: "save", | ||
| content: "# Memory\n\n- Remember to buy milk\n- Project uses TypeScript", | ||
| message: "Initial memory", | ||
| }); | ||
|
|
||
| const result = await tool.execute("call-2", { action: "load" }); | ||
|
|
||
| assert.ok(result.content); | ||
| assert.equal(result.content.length, 1); | ||
| assert.ok(result.content[0].text.includes("Remember to buy milk")); | ||
| assert.ok(result.content[0].text.includes("Project uses TypeScript")); | ||
| } finally { | ||
| await cleanup(dir); | ||
| } | ||
| }); | ||
|
|
||
| it("returns 'No memories yet.' when memory file is empty or missing", async () => { | ||
| const dir = await setupRepo(); | ||
| try { | ||
| const tool = createMemoryTool(dir); | ||
|
|
||
| const result = await tool.execute("call-1", { action: "load" }); | ||
|
|
||
| assert.equal(result.content[0].text, "No memories yet."); | ||
| } finally { | ||
| await cleanup(dir); | ||
| } | ||
| }); | ||
|
|
||
| it("returns 'No memories yet.' when memory file has only heading", async () => { | ||
| const dir = await setupRepo(); | ||
| try { | ||
| await mkdir(join(dir, "memory"), { recursive: true }); | ||
| await writeFile(join(dir, "memory", "MEMORY.md"), "# Memory", "utf-8"); | ||
|
|
||
| const tool = createMemoryTool(dir); | ||
| const result = await tool.execute("call-1", { action: "load" }); | ||
|
|
||
| assert.equal(result.content[0].text, "No memories yet."); | ||
| } finally { | ||
| await cleanup(dir); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| describe("save", () => { | ||
| it("writes content and commits to git", async () => { | ||
| const dir = await setupRepo(); | ||
| try { | ||
| const tool = createMemoryTool(dir); | ||
|
|
||
| const result = await tool.execute("call-1", { | ||
| action: "save", | ||
| content: "# Memory\n\nSaved entry one.", | ||
| message: "First save", | ||
| }); | ||
|
|
||
| assert.equal(result.content.length, 1); | ||
| assert.ok( | ||
| result.content[0].text.includes("Memory saved and committed"), | ||
| ); | ||
| assert.ok(result.content[0].text.includes("First save")); | ||
|
|
||
| const { readFile } = await import("fs/promises"); | ||
| const fileContent = await readFile( | ||
| join(dir, "memory", "MEMORY.md"), | ||
| "utf-8", | ||
| ); | ||
| assert.ok(fileContent.includes("Saved entry one")); | ||
|
|
||
| const log = execSync("git log --oneline", { | ||
| cwd: dir, | ||
| encoding: "utf-8", | ||
| }); | ||
| assert.ok(log.includes("First save"), `git log should contain commit: ${log}`); | ||
| } finally { | ||
| await cleanup(dir); | ||
| } | ||
| }); | ||
|
|
||
| it("uses default commit message when message is omitted", async () => { | ||
| const dir = await setupRepo(); | ||
| try { | ||
| const tool = createMemoryTool(dir); | ||
|
|
||
| await tool.execute("call-1", { | ||
| action: "save", | ||
| content: "Memory without explicit message.", | ||
| }); | ||
|
|
||
| const log = execSync("git log --oneline", { | ||
| cwd: dir, | ||
| encoding: "utf-8", | ||
| }); | ||
| assert.ok( | ||
| log.includes("Update memory"), | ||
| `commit should default to "Update memory": ${log}`, | ||
| ); | ||
| } finally { | ||
| await cleanup(dir); | ||
| } | ||
| }); | ||
|
|
||
| it("requires content for save action", async () => { | ||
| const dir = await setupRepo(); | ||
| try { | ||
| const tool = createMemoryTool(dir); | ||
|
|
||
| await assert.rejects( | ||
| () => | ||
| tool.execute("call-1", { | ||
| action: "save", | ||
| }), | ||
| /content is required for save action/, | ||
| ); | ||
| } finally { | ||
| await cleanup(dir); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| describe("abort signal", () => { | ||
| it("throws when signal is already aborted", async () => { | ||
| const dir = await setupRepo(); | ||
| try { | ||
| const tool = createMemoryTool(dir); | ||
| const controller = new AbortController(); | ||
| controller.abort(); | ||
|
|
||
| await assert.rejects( | ||
| () => | ||
| tool.execute("call-1", { action: "load" }, controller.signal), | ||
| /Operation aborted/, | ||
| ); | ||
| } finally { | ||
| await cleanup(dir); | ||
| } | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| /** | ||
| * Tests for the telemetry module (src/telemetry.ts) — init/shutdown/idempotency. | ||
| * | ||
| * These tests verify that initTelemetry correctly gates on the OTLP | ||
| * endpoint environment variable: it MUST return without enabling telemetry | ||
| * when no endpoint is configured, and it MUST successfully create an SDK | ||
| * instance when an endpoint (or test provider) is provided. | ||
| */ | ||
| import { describe, it, before, afterEach } from "node:test"; | ||
| import assert from "node:assert/strict"; | ||
| import { trace } from "@opentelemetry/api"; | ||
| import { | ||
| NodeTracerProvider, | ||
| InMemorySpanExporter, | ||
| SimpleSpanProcessor, | ||
| } from "@opentelemetry/sdk-trace-node"; | ||
|
|
||
| let initTelemetry: typeof import("../src/telemetry.ts").initTelemetry; | ||
| let shutdownTelemetry: typeof import("../src/telemetry.ts").shutdownTelemetry; | ||
| let isTelemetryEnabled: typeof import("../src/telemetry.ts").isTelemetryEnabled; | ||
|
|
||
| before(async () => { | ||
| const mod = await import("../src/telemetry.ts"); | ||
| initTelemetry = mod.initTelemetry; | ||
| shutdownTelemetry = mod.shutdownTelemetry; | ||
| isTelemetryEnabled = mod.isTelemetryEnabled; | ||
| }); | ||
|
|
||
| afterEach(async () => { | ||
| await shutdownTelemetry(); | ||
| try { | ||
| trace.disable(); | ||
| } catch { | ||
| /* ignore */ | ||
| } | ||
| }); | ||
|
|
||
| describe("telemetry init", () => { | ||
| function makeTestProvider() { | ||
| const exporter = new InMemorySpanExporter(); | ||
| const provider = new NodeTracerProvider({ | ||
| spanProcessors: [new SimpleSpanProcessor(exporter)], | ||
| }); | ||
| return { exporter, provider }; | ||
| } | ||
|
|
||
| it("returns without enabling telemetry when no OTLP endpoint is configured", async () => { | ||
| const saved = process.env.OTEL_EXPORTER_OTLP_ENDPOINT; | ||
| const wasSet = "OTEL_EXPORTER_OTLP_ENDPOINT" in process.env; | ||
| delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT; | ||
|
|
||
| try { | ||
| await assert.doesNotReject( | ||
| () => initTelemetry({}), | ||
| "initTelemetry must never throw, even without an endpoint", | ||
| ); | ||
|
|
||
| assert.equal( | ||
| isTelemetryEnabled(), | ||
| false, | ||
| "telemetry must remain disabled when no endpoint is configured", | ||
| ); | ||
| } finally { | ||
| if (wasSet) { | ||
| process.env.OTEL_EXPORTER_OTLP_ENDPOINT = saved; | ||
| } else { | ||
| delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT; | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| it("creates an SDK instance when endpoint is configured", async () => { | ||
| process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "http://localhost:4318"; | ||
| const { exporter, provider } = makeTestProvider(); | ||
|
|
||
| try { | ||
| await initTelemetry({ | ||
| serviceName: "test-svc", | ||
| _testProvider: provider, | ||
| }); | ||
|
|
||
| assert.equal( | ||
| isTelemetryEnabled(), | ||
| true, | ||
| "telemetry must be enabled after initTelemetry with _testProvider", | ||
| ); | ||
|
|
||
| const tracer = trace.getTracer("test"); | ||
| const span = tracer.startSpan("test-span"); | ||
| span.end(); | ||
|
|
||
| await provider.forceFlush(); | ||
| const spans = exporter.getFinishedSpans(); | ||
| assert.equal(spans.length, 1, "span should be exported"); | ||
| assert.equal(spans[0].name, "test-span"); | ||
| } finally { | ||
| delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT; | ||
| } | ||
| }); | ||
|
|
||
| it("is idempotent", async () => { | ||
| const { provider: provider1 } = makeTestProvider(); | ||
| const { provider: provider2 } = makeTestProvider(); | ||
|
|
||
| await initTelemetry({ _testProvider: provider1 }); | ||
| assert.equal(isTelemetryEnabled(), true); | ||
|
|
||
| await initTelemetry({ _testProvider: provider2 }); | ||
| assert.equal(isTelemetryEnabled(), true); | ||
| }); | ||
|
|
||
| it("shutdownTelemetry resets the initialized state", async () => { | ||
| const { provider } = makeTestProvider(); | ||
|
|
||
| await initTelemetry({ _testProvider: provider }); | ||
| assert.equal(isTelemetryEnabled(), true); | ||
|
|
||
| await shutdownTelemetry(); | ||
| assert.equal(isTelemetryEnabled(), false); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The banner text array inside install.sh still reads
GitAgent v1.1.1(the spritetextarray, a few lines above the changed hunk). This PR updates the version in Documentation.md and README.md but leaves the hardcoded string in the installer banner stale. Additionally, Documentation.md was bumped to 1.5.2 while package.json is at 2.0.0 — the docs version is still wrong after this change.