From e912caf98018a7ca9cb695a53b6757927a5385f9 Mon Sep 17 00:00:00 2001 From: Oxygen <1391083091@qq.com> Date: Fri, 5 Jun 2026 03:18:20 +0800 Subject: [PATCH 1/2] fix: correct npm package name, Node version check, and doc versions - install.sh: Update Node.js version check from >=18 to >=20 to match package.json engines field - Documentation.md: Update version string from 1.3.3 to 1.5.2 and npm update command to use scoped package @open-gitagent/gitagent - README.md: Update Node.js requirement from 18+ to 20+ in install instructions and FAQ, fix npm badge to use scoped package name - tsconfig.json: Exclude __tests__ directories from compilation Co-Authored-By: Claude Opus 4.8 --- Documentation.md | 4 ++-- README.md | 6 +++--- install.sh | 4 ++-- package-lock.json | 1 + tsconfig.json | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Documentation.md b/Documentation.md index d57eff4..617f7cf 100644 --- a/Documentation.md +++ b/Documentation.md @@ -1,7 +1,7 @@ # GitAgent Documentation > **GitAgent** — A universal git-native multimodal always-learning AI Agent -> Version 1.3.3 | MIT License | [github.com/open-gitagent/gitagent](https://github.com/open-gitagent/gitagent) +> Version 1.5.2 | MIT License | [github.com/open-gitagent/gitagent](https://github.com/open-gitagent/gitagent) --- @@ -83,7 +83,7 @@ The installer offers four options: curl -fsSL https://raw.githubusercontent.com/open-gitagent/gitagent/main/install.sh | bash # Or manually -npm update -g gitagent +npm update -g @open-gitagent/gitagent ``` --- diff --git a/README.md b/README.md index c684c6d..ea8baf9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- npm version + npm version node version license typescript @@ -56,7 +56,7 @@ This will: - Walk you through API key setup (Quick or Advanced mode) - Launch the voice UI in your browser at `http://localhost:3333` -> **Requirements:** Node.js 18+, npm, git +> **Requirements:** Node.js 20+, npm, git ### Or install manually: @@ -752,7 +752,7 @@ Your agent lives in a git repository with structured files: ### Installation & Setup **What are the requirements?** -Node.js 18+ (or 20+ recommended), npm, and git. Install globally with `npm install -g @open-gitagent/gitagent`. +Node.js 20+, npm, and git. Install globally with `npm install -g @open-gitagent/gitagent`. **How do I set up API keys?** Run the installer for guided setup: diff --git a/install.sh b/install.sh index 1325f44..472e700 100755 --- a/install.sh +++ b/install.sh @@ -91,8 +91,8 @@ check_cmd npm check_cmd git NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1) -if [ "$NODE_VERSION" -lt 18 ]; then - echo -e " ${RED}✗ Node.js 18+ required (found $(node -v))${NC}" +if [ "$NODE_VERSION" -lt 20 ]; then + echo -e " ${RED}✗ Node.js 20+ required (found $(node -v))${NC}" exit 1 fi diff --git a/package-lock.json b/package-lock.json index 1e81523..dd28020 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3257,6 +3257,7 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8" } diff --git a/tsconfig.json b/tsconfig.json index 64429fa..be84a50 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,5 +13,5 @@ "resolveJsonModule": true }, "include": ["src"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "**/__tests__"] } From d0295dad364dc79174a8c6d33f89641420fb463b Mon Sep 17 00:00:00 2001 From: Oxygen <1391083091@qq.com> Date: Fri, 5 Jun 2026 03:18:25 +0800 Subject: [PATCH 2/2] test: implement unit tests for memory and telemetry modules - src/tools/__tests__/memory.test.ts: Replace 3 it.todo() stubs with 7 real tests covering load, save (with git commits), default message, content validation, and abort signal handling - src/__tests__/telemetry.test.ts: Replace 2 it.todo() stubs with 4 real tests covering initTelemetry behavior without endpoint, with _testProvider, idempotency, and shutdown state reset Co-Authored-By: Claude Opus 4.8 --- src/__tests__/telemetry.test.ts | 134 ++++++++++++++++++- src/tools/__tests__/memory.test.ts | 198 ++++++++++++++++++++++++++++- 2 files changed, 325 insertions(+), 7 deletions(-) diff --git a/src/__tests__/telemetry.test.ts b/src/__tests__/telemetry.test.ts index 232554d..7aeff1f 100644 --- a/src/__tests__/telemetry.test.ts +++ b/src/__tests__/telemetry.test.ts @@ -1,6 +1,134 @@ -import { describe, it } from "node:test"; +/** + * Tests for the telemetry module (src/telemetry.ts). + * + * These tests verify that initTelemetry correctly gates on the OTLP + * endpoint environment variable: it MUST be a no-op when the endpoint + * is not 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("../telemetry.ts").initTelemetry; +let shutdownTelemetry: typeof import("../telemetry.ts").shutdownTelemetry; +let isTelemetryEnabled: typeof import("../telemetry.ts").isTelemetryEnabled; + +before(async () => { + const mod = await import("../telemetry.ts"); + initTelemetry = mod.initTelemetry; + shutdownTelemetry = mod.shutdownTelemetry; + isTelemetryEnabled = mod.isTelemetryEnabled; +}); + +afterEach(async () => { + // Always clean up telemetry after each test to avoid cross-test + // contamination from global state. + await shutdownTelemetry(); + try { + trace.disable(); + } catch { + /* ignore */ + } +}); describe("telemetry", () => { - it.todo("initTelemetry is a no-op when OTEL_EXPORTER_OTLP_ENDPOINT is not set"); - it.todo("initTelemetry creates an SDK instance when endpoint is configured"); + // ── Helpers ────────────────────────────────────────────────────── + + function makeTestProvider() { + const exporter = new InMemorySpanExporter(); + const provider = new NodeTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(exporter)], + }); + return { exporter, provider }; + } + + // ── initTelemetry no-op ────────────────────────────────────────── + + it("initTelemetry without OTLP endpoint does not throw and leaves module in a consistent state", async () => { + // Ensure the env var is not set for this test + const saved = process.env.OTEL_EXPORTER_OTLP_ENDPOINT; + delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT; + + try { + // Calling initTelemetry with no options does not throw — the + // module always wraps its body in try/catch so failures are + // logged, not thrown. + await assert.doesNotReject( + () => initTelemetry({}), + "initTelemetry must never throw, even without an endpoint", + ); + } finally { + if (saved !== undefined) { + process.env.OTEL_EXPORTER_OTLP_ENDPOINT = saved; + } + } + }); + + // ── initTelemetry with endpoint ────────────────────────────────── + + it("initTelemetry creates an SDK instance when endpoint is configured", async () => { + // Set a (bogus) OTLP endpoint so the init path proceeds past the + // no-op guard. Because we do not have a real collector, we also + // provide a _testProvider so the test is deterministic. + 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", + ); + + // Verify the provider was actually registered: create a span + // and confirm it flows through the InMemorySpanExporter. + const tracer = trace.getTracer("test"); + const span = tracer.startSpan("test-span"); + span.end(); + + // Force flush by shutting down (which is handled by afterEach) + 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; + } + }); + + // ── Idempotency ────────────────────────────────────────────────── + + it("initTelemetry is idempotent", async () => { + const { provider: provider1 } = makeTestProvider(); + const { provider: provider2 } = makeTestProvider(); + + await initTelemetry({ _testProvider: provider1 }); + assert.equal(isTelemetryEnabled(), true); + + // Second call should be a no-op — _initialized is already true + await initTelemetry({ _testProvider: provider2 }); + assert.equal(isTelemetryEnabled(), true); + }); + + // ── shutdownTelemetry ──────────────────────────────────────────── + + it("shutdownTelemetry resets the initialized state", async () => { + const { provider } = makeTestProvider(); + + await initTelemetry({ _testProvider: provider }); + assert.equal(isTelemetryEnabled(), true); + + await shutdownTelemetry(); + assert.equal(isTelemetryEnabled(), false); + }); }); diff --git a/src/tools/__tests__/memory.test.ts b/src/tools/__tests__/memory.test.ts index 1a2269e..9c7cd5f 100644 --- a/src/tools/__tests__/memory.test.ts +++ b/src/tools/__tests__/memory.test.ts @@ -1,7 +1,197 @@ -import { describe, it } from "node:test"; +/** + * 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("../../../dist/tools/memory.js").createMemoryTool; + +before(async () => { + const mod = await import("../../../dist/tools/memory.js"); + createMemoryTool = mod.createMemoryTool; +}); describe("memory tool", () => { - it.todo("load returns stored memory content"); - it.todo("save writes content and commits to git"); - it.todo("save requires content and message"); + /** Create a temporary directory with git init and return the path. */ + async function setupRepo(): Promise { + const dir = await mkdtemp(join(tmpdir(), "gitagent-memory-test-")); + execSync("git init -q", { cwd: dir }); + // Configure git user for commits + execSync('git config user.email "test@gitagent.test"', { cwd: dir }); + execSync('git config user.name "Test Agent"', { cwd: dir }); + return dir; + } + + /** Clean up a temp directory. */ + async function cleanup(dir: string): Promise { + await rm(dir, { recursive: true, force: true }).catch(() => {}); + } + + // ── load ───────────────────────────────────────────────────────── + + describe("load", () => { + it("returns stored memory content", async () => { + const dir = await setupRepo(); + try { + const tool = createMemoryTool(dir); + + // First, save some memory content + await tool.execute("call-1", { + action: "save", + content: "# Memory\n\n- Remember to buy milk\n- Project uses TypeScript", + message: "Initial memory", + }); + + // Now load it + 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); + + // Load from a repo with no memory file + 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 { + // Write the default heading-only memory file + 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); + } + }); + }); + + // ── save ───────────────────────────────────────────────────────── + + 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")); + + // Verify the file was written + const { readFile } = await import("fs/promises"); + const fileContent = await readFile( + join(dir, "memory", "MEMORY.md"), + "utf-8", + ); + assert.ok(fileContent.includes("Saved entry one")); + + // Verify the git commit exists + 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 intentionally omitted + }), + /content is required for save action/, + ); + } finally { + await cleanup(dir); + } + }); + }); + + // ── abort signal ───────────────────────────────────────────────── + + 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); + } + }); + }); });