diff --git a/Documentation.md b/Documentation.md index c795966..e23a2da 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 2.0.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/install.sh b/install.sh index a2dd4ad..f2399db 100755 --- a/install.sh +++ b/install.sh @@ -45,7 +45,7 @@ rows=( text=( "" "" - "${RED}${BOLD}GitAgent v1.1.1${RESET}" + "${RED}${BOLD}GitAgent v2.0.2${RESET}" "${GRAY}A universal git-native multimodal always learning AI Agent${RESET}" "${GRAY}(TinyHuman)${RESET}" "" @@ -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 0901d27..b44ee94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@open-gitagent/gitagent", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@open-gitagent/gitagent", - "version": "2.0.1", + "version": "2.0.2", "license": "MIT", "dependencies": { "@mariozechner/pi-agent-core": "^0.70.2", @@ -34,6 +34,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.0.0", "@types/node-cron": "^3.0.11", + "tsx": "^4.20.6", "typescript": "^5.7.0" }, "engines": { @@ -481,6 +482,448 @@ "node": ">=6.9.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@google/genai": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", @@ -2310,6 +2753,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2607,6 +3092,21 @@ "node": ">= 0.8" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3757,6 +4257,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.1.tgz", + "integrity": "sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", diff --git a/package.json b/package.json index 8661cd3..6a89b84 100644 --- a/package.json +++ b/package.json @@ -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" }, "engines": { "node": ">=20" @@ -79,6 +79,7 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^22.0.0", "@types/node-cron": "^3.0.11", + "tsx": "^4.20.6", "typescript": "^5.7.0" } } diff --git a/src/__tests__/telemetry.test.ts b/src/__tests__/telemetry.test.ts deleted file mode 100644 index 232554d..0000000 --- a/src/__tests__/telemetry.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { describe, it } from "node:test"; - -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"); -}); diff --git a/src/telemetry.ts b/src/telemetry.ts index 10cf562..43c72ef 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -71,6 +71,11 @@ const _slots = { export async function initTelemetry(opts: TelemetryOptions): Promise { if (_initialized) return; + // When there is no endpoint configured and no test provider, telemetry + // has nowhere to send data — skip the dynamic SDK imports entirely. + const hasEndpoint = opts.exporterEndpoint || process.env.OTEL_EXPORTER_OTLP_ENDPOINT; + if (!opts._testProvider && !hasEndpoint) return; + try { // Test path — register a caller-supplied TracerProvider directly. if (opts._testProvider) { diff --git a/src/tools/__tests__/memory.test.ts b/src/tools/__tests__/memory.test.ts deleted file mode 100644 index 1a2269e..0000000 --- a/src/tools/__tests__/memory.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it } from "node:test"; - -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"); -}); diff --git a/test/memory.test.ts b/test/memory.test.ts new file mode 100644 index 0000000..76ce582 --- /dev/null +++ b/test/memory.test.ts @@ -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 { + 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 { + 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); + } + }); + }); +}); diff --git a/test/telemetry-init.test.ts b/test/telemetry-init.test.ts new file mode 100644 index 0000000..6d09184 --- /dev/null +++ b/test/telemetry-init.test.ts @@ -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); + }); +});