From 7eaf743146b9fa9e69bb7c592effd6c3c4109c46 Mon Sep 17 00:00:00 2001 From: dawgdevv Date: Mon, 6 Jul 2026 14:17:09 +0530 Subject: [PATCH 1/6] Update package dependencies and add Vitest support --- package-lock.json | 1785 +++++++++++++++++++++++++++++++++++++++++---- package.json | 4 +- 2 files changed, 1632 insertions(+), 157 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30a72be..954f9ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,16 +20,101 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.15.3", - "typescript": "^5.8.3" + "@vitest/coverage-v8": "^4.1.10", + "typescript": "^5.8.3", + "vitest": "^4.1.10" }, "engines": { "node": ">=22.0.0 <27.0.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -529,6 +614,63 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -568,12 +710,6 @@ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "license": "BSD-3-Clause" }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", - "license": "BSD-3-Clause" - }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", @@ -592,142 +728,650 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, - "node_modules/@types/better-sqlite3": { - "version": "7.6.13", - "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", - "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@types/node": { - "version": "22.19.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", - "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/adm-zip": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", - "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=12.0" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" ], - "license": "MIT" - }, - "node_modules/better-sqlite3": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", - "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", - "hasInstallScript": true, + "dev": true, "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "prebuild-install": "^7.1.1" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT" - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" ], "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/chownr": { + "node_modules/@rolldown/binding-linux-ppc64-gnu": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/deep-extend": { - "version": "0.6.0", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", + "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.10", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.10", + "vitest": "4.1.10" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.4.tgz", + "integrity": "sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.11.1.tgz", + "integrity": "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", @@ -811,6 +1455,13 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "dev": true, + "license": "MIT" + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -829,6 +1480,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -838,6 +1499,34 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -856,6 +1545,21 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "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/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -913,6 +1617,16 @@ "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -922,46 +1636,372 @@ "es-define-property": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "license": "ISC" + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, "node_modules/long": { "version": "5.3.2", @@ -969,6 +2009,44 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -1008,6 +2086,25 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -1035,6 +2132,20 @@ "node": ">= 0.4" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1087,12 +2198,68 @@ "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==", "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/platform": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "license": "MIT" }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -1121,9 +2288,9 @@ } }, "node_modules/protobufjs": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.2.tgz", - "integrity": "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -1133,7 +2300,6 @@ "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", @@ -1200,6 +2366,40 @@ "node": ">=8.0" } }, + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1297,6 +2497,13 @@ "@img/sharp-win32-x64": "0.34.5" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -1342,12 +2549,36 @@ "simple-concat": "^1.0.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1366,6 +2597,19 @@ "node": ">=0.10.0" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -1394,6 +2638,50 @@ "node": ">=6" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tree-sitter-wasms": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/tree-sitter-wasms/-/tree-sitter-wasms-0.1.13.tgz", @@ -1460,12 +2748,197 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/web-tree-sitter": { "version": "0.24.7", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.24.7.tgz", "integrity": "sha512-CdC/TqVFbXqR+C51v38hv6wOPatKEUGxa39scAeFSm98wIhZxAYonhRQPSMmfZ2w7JDI0zQDdzdmgtNk06/krQ==", "license": "MIT" }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 43833c3..22d5d47 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,8 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.15.3", - "typescript": "^5.8.3" + "@vitest/coverage-v8": "^4.1.10", + "typescript": "^5.8.3", + "vitest": "^4.1.10" } } From 644999d92b856351dc85ae65bef24fab7ea9109f Mon Sep 17 00:00:00 2001 From: dawgdevv Date: Mon, 6 Jul 2026 15:52:10 +0530 Subject: [PATCH 2/6] Refactor testing setup and scripts for Vitest integration - Updated `package.json` to replace existing test scripts with Vitest commands. - Added `vitest.config.ts` for Vitest configuration. - Excluded `tests` and `node_modules` from build in `tsconfig.build.json`. - Included `tests/**/*.ts` in `tsconfig.json` for TypeScript compilation. - Removed outdated test scripts: `check-graph-view.js`, `check-install-options.js`, `check-proposal-validate.js`, `check-repo-context.js`, and `check-transcript-bundle.js`. - Introduced new integration tests for graph view, install options, proposal validation, repo context detection, and transcript bundling. --- package.json | 12 +- scripts/check-graph-view.js | 48 ----- scripts/check-install-options.js | 137 ------------ scripts/check-proposal-validate.js | 61 ------ scripts/check-repo-context.js | 58 ----- scripts/check-transcript-bundle.js | 221 -------------------- tests/integration/graph-view.test.ts | 52 +++++ tests/integration/install-options.test.ts | 148 +++++++++++++ tests/integration/proposal-validate.test.ts | 67 ++++++ tests/integration/repo-context.test.ts | 78 +++++++ tests/integration/transcript-bundle.test.ts | 174 +++++++++++++++ tsconfig.build.json | 3 +- tsconfig.json | 8 +- vitest.config.ts | 13 ++ 14 files changed, 550 insertions(+), 530 deletions(-) delete mode 100644 scripts/check-graph-view.js delete mode 100644 scripts/check-install-options.js delete mode 100644 scripts/check-proposal-validate.js delete mode 100644 scripts/check-repo-context.js delete mode 100644 scripts/check-transcript-bundle.js create mode 100644 tests/integration/graph-view.test.ts create mode 100644 tests/integration/install-options.test.ts create mode 100644 tests/integration/proposal-validate.test.ts create mode 100644 tests/integration/repo-context.test.ts create mode 100644 tests/integration/transcript-bundle.test.ts create mode 100644 vitest.config.ts diff --git a/package.json b/package.json index 22d5d47..65274fb 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,15 @@ "typecheck": "tsc --noEmit", "smoke:openhands": "npm run build && node scripts/smoke-openhands-install.mjs", "smoke:copilot": "npm run build && node scripts/smoke-copilot-install.mjs", - "test": "npm run build && node scripts/check-transcript-bundle.js && node scripts/check-repo-context.js && node scripts/check-install-options.js && node scripts/check-graph-view.js && node scripts/check-proposal-validate.js", - "test:transcript-bundle": "npm run build && node scripts/check-transcript-bundle.js", - "test:repo-context": "npm run build && node scripts/check-repo-context.js", + "test": "npm run build && vitest run", + "test:fast": "vitest run", + "test:watch": "vitest", + "test:coverage": "npm run build && vitest run --coverage", + "test:transcript-bundle": "npm run build && vitest run tests/integration/transcript-bundle.test.ts", + "test:repo-context": "npm run build && vitest run tests/integration/repo-context.test.ts", + "test:install-options": "npm run build && vitest run tests/integration/install-options.test.ts", + "test:graph-view": "npm run build && vitest run tests/integration/graph-view.test.ts", + "test:proposal-validate": "npm run build && vitest run tests/integration/proposal-validate.test.ts", "eval:bootstrap-current": "npm run build && node dist/evals/cases/bootstrap-current-repo-at-8038fe8/run.js", "eval:coding-proposal-apply-atomicity": "npm run build && node dist/evals/cases/coding-proposal-apply-atomicity-at-d6ebf80/run.js", "eval:curated-doc-bootstrap-vercel-chat": "npm run build && node dist/evals/cases/curated-doc-bootstrap-vercel-chat-at-f3de128/run.js", diff --git a/scripts/check-graph-view.js b/scripts/check-graph-view.js deleted file mode 100644 index 11661ed..0000000 --- a/scripts/check-graph-view.js +++ /dev/null @@ -1,48 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -const root = new URL("..", import.meta.url); -const { openDatabase } = await import(new URL("dist/libs/storage/sqlite/db.js", root)); -const { SqliteRepository } = await import(new URL("dist/libs/storage/sqlite/repository.js", root)); -const { KnowledgeGraphService } = await import(new URL("dist/libs/knowledge-graph/service.js", root)); - -const tmp = mkdtempSync(join(tmpdir(), "greplica-graph-view-test-")); -const db = openDatabase(join(tmp, "graph.db")); - -try { - const repository = new SqliteRepository(db); - const service = new KnowledgeGraphService(repository); - const repo = { - repo_root: join(tmp, "repo"), - repo_name: "graph-view-null-anchor", - default_branch: "main", - }; - - const initialized = service.initRepo(repo); - const memoryCommit = repository.createMemoryCommit({ - scope_id: initialized.working_scope_id, - title: "Seed null component anchor", - }); - - repository.createProposalRecords(initialized.working_scope_id, memoryCommit.id, { - title: "Seed null component anchor", - creates: { - components: [ - { - id: "component.no_anchor", - name: "Component Without Anchor", - }, - ], - }, - }); - - const html = service.buildGraphView(repo); - assert.match(html, /Component Without Anchor/); - assert.match(html, /Greplica graph view/); -} finally { - db.close(); -} - -console.log("Graph view checks passed."); diff --git a/scripts/check-install-options.js b/scripts/check-install-options.js deleted file mode 100644 index 15aa3aa..0000000 --- a/scripts/check-install-options.js +++ /dev/null @@ -1,137 +0,0 @@ -import assert from "node:assert/strict"; -import { execFileSync, spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -const root = new URL("..", import.meta.url); -const cli = new URL("dist/apps/cli/main.js", root); -const { greplicaHookGuidance } = await import(new URL("dist/libs/hooks/guidance.js", root)); -const { shouldRunAutoMemoryUpdates } = await import(new URL("dist/libs/hooks/worker.js", root)); - -const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); - -const autoSave = installInTempRepo("auto-save", ["--hooks", "enabled", "--auto-memory", "enabled"]); -assert.match(autoSave.output, /Hooks: installed for UserPromptSubmit, Stop\./); -assert.match(autoSave.output, /Automatic memory updates: enabled\./); -assert.ok(existsSync(join(autoSave.codexHome, "hooks.json"))); -assert.equal(readConfig(autoSave.greplicaHome).session.autoMemoryUpdates, true); -assert.equal(shouldRunAutoMemoryUpdates(readConfig(autoSave.greplicaHome)), true); - -const guidanceOnly = installInTempRepo("guidance-only", ["--hooks", "enabled", "--auto-memory", "disabled"]); -assert.match(guidanceOnly.output, /Hooks: installed for UserPromptSubmit, Stop\./); -assert.match(guidanceOnly.output, /Automatic memory updates: disabled\./); -assert.ok(existsSync(join(guidanceOnly.codexHome, "hooks.json"))); -assert.equal(readConfig(guidanceOnly.greplicaHome).session.autoMemoryUpdates, false); -assert.equal(shouldRunAutoMemoryUpdates(readConfig(guidanceOnly.greplicaHome)), false); - -const hookOutput = execFileSync( - process.execPath, - [cli.pathname, "hook", "ingest", "--platform", "codex"], - { - cwd: guidanceOnly.repo, - encoding: "utf8", - input: JSON.stringify({ - hook_event_name: "UserPromptSubmit", - session_id: "guidance-only-session", - cwd: guidanceOnly.repo, - }), - env: guidanceOnly.env, - }, -); -assert.match(hookOutput, /Greplica hook guidance/); -assert.match(hookOutput, /greplica graph context/); - -const noHooks = installInTempRepo("no-hooks", ["--hooks", "disabled"]); -assert.match(noHooks.output, /Hooks: not installed\./); -assert.match(noHooks.output, /Automatic memory updates: disabled\./); -assert.match(noHooks.output, /To give future agents Greplica guidance without hooks/); -assert.ok(noHooks.output.includes(greplicaHookGuidance)); -assert.equal(existsSync(join(noHooks.codexHome, "hooks.json")), false); -assert.equal(readConfig(noHooks.greplicaHome).session.autoMemoryUpdates, false); - -const unsupportedHooks = installInTempRepo("unsupported-hooks", ["--hooks", "enabled", "--auto-memory", "enabled"], "opencode"); -assert.match(unsupportedHooks.output, /Hooks: not installed for this platform\./); -assert.match(unsupportedHooks.output, /Automatic memory updates: disabled\./); -assert.equal(readConfig(unsupportedHooks.greplicaHome).session.autoMemoryUpdates, false); - -const copilotHooks = installInTempRepo("copilot-hooks", ["--hooks", "enabled", "--auto-memory", "enabled"], "copilot"); -assert.match(copilotHooks.output, /Installed Greplica for GitHub Copilot CLI\./); -assert.match(copilotHooks.output, /Hooks: installed for SessionStart, Stop\./); -assert.match(copilotHooks.output, /Automatic memory updates: enabled\./); -assert.ok(existsSync(join(copilotHooks.copilotHome, "hooks", "greplica.json"))); -assert.equal(readConfig(copilotHooks.greplicaHome).session.autoMemoryUpdates, true); - -const invalid = spawnSync( - process.execPath, - [ - cli.pathname, - "install", - "--platform", - "codex", - "--embedding", - "local", - "--hooks", - "disabled", - "--auto-memory", - "enabled", - ], - { - cwd: noHooks.repo, - encoding: "utf8", - env: noHooks.env, - }, -); -assert.notEqual(invalid.status, 0); -assert.match(invalid.stderr, /--auto-memory enabled requires --hooks enabled/); - -const invalidValue = spawnSync( - process.execPath, - [cli.pathname, "install", "--platform", "codex", "--embedding", "local", "--hooks", "sometimes"], - { - cwd: noHooks.repo, - encoding: "utf8", - env: noHooks.env, - }, -); -assert.notEqual(invalidValue.status, 0); -assert.match(invalidValue.stderr, /expected enabled or disabled/); - -console.log("Install option checks passed."); - -function installInTempRepo(name, flags, platform = "codex") { - const repo = join(tmp, name, "repo"); - const greplicaHome = join(tmp, name, "greplica-home"); - const codexHome = join(tmp, name, "codex-home"); - const copilotHome = join(tmp, name, "copilot-home"); - mkdirSync(repo, { recursive: true }); - execFileSync("git", ["init", "--quiet"], { cwd: repo, encoding: "utf8" }); - - const env = { - ...process.env, - GREPLICA_HOME: greplicaHome, - CODEX_HOME: codexHome, - COPILOT_HOME: copilotHome, - XDG_CONFIG_HOME: join(tmp, name, "xdg-config-home"), - GREPLICA_INSTALL_SKIP_PREWARM: "1", - }; - const output = execFileSync( - process.execPath, - [cli.pathname, "install", "--platform", platform, "--embedding", "local", ...flags], - { - cwd: repo, - encoding: "utf8", - env, - }, - ); - execFileSync(process.execPath, [cli.pathname, "doctor"], { - cwd: repo, - encoding: "utf8", - env, - }); - return { repo, greplicaHome, codexHome, copilotHome, output, env }; -} - -function readConfig(greplicaHome) { - return JSON.parse(readFileSync(join(greplicaHome, "config.json"), "utf8")); -} diff --git a/scripts/check-proposal-validate.js b/scripts/check-proposal-validate.js deleted file mode 100644 index 37229fe..0000000 --- a/scripts/check-proposal-validate.js +++ /dev/null @@ -1,61 +0,0 @@ -import assert from "node:assert/strict"; - -const root = new URL("..", import.meta.url); -const { normalizeProposal } = await import(new URL("dist/libs/knowledge-graph/proposal.js", root)); -const { validateProposal } = await import(new URL("dist/libs/knowledge-graph/validate-proposal.js", root)); - -const components = [ - { id: "component.a", name: "A" }, - { id: "component.b", name: "B" }, -]; - -// Edge written with canonical field names but no types (issue #82) must not crash -// normalization, and must surface as a validation error. -const malformedEdge = { - title: "Malformed edge repro", - creates: { - components, - edges: [{ from_id: "component.a", to_id: "component.b", kind: "contains" }], - }, -}; - -let normalized; -assert.doesNotThrow(() => { - normalized = normalizeProposal(malformedEdge); -}, "normalizeProposal must not throw on a malformed edge"); - -const malformedResult = validateProposal(normalized); -assert.equal(malformedResult.valid, false, "malformed edge proposal must be invalid"); -assert.ok( - malformedResult.errors.some((error) => error.includes("from'/'to")), - `expected a from/to guidance error, got: ${JSON.stringify(malformedResult.errors)}`, -); - -// Edge missing kind must also be reported, not crash. -const missingKindEdge = { - title: "Missing kind repro", - creates: { - components, - edges: [{ from: "component.a", to: "component.b" }], - }, -}; - -let normalizedMissingKind; -assert.doesNotThrow(() => { - normalizedMissingKind = normalizeProposal(missingKindEdge); -}, "normalizeProposal must not throw on an edge without kind"); -assert.equal(validateProposal(normalizedMissingKind).valid, false, "edge without kind must be invalid"); - -// A well-formed compact edge must keep validating cleanly. -const compactEdge = { - title: "Compact edge", - creates: { - components, - edges: [{ kind: "contains", from: "component.a", to: "component.b" }], - }, -}; - -const compactResult = validateProposal(normalizeProposal(compactEdge)); -assert.equal(compactResult.valid, true, `compact edge must stay valid, got: ${JSON.stringify(compactResult.errors)}`); - -console.log("check-proposal-validate: ok"); diff --git a/scripts/check-repo-context.js b/scripts/check-repo-context.js deleted file mode 100644 index 410a9eb..0000000 --- a/scripts/check-repo-context.js +++ /dev/null @@ -1,58 +0,0 @@ -import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; -import { mkdtempSync, mkdirSync, realpathSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { basename, join } from "node:path"; - -const root = new URL("..", import.meta.url); -const { detectRepoContext } = await import(new URL("dist/apps/cli/repo-context.js", root)); -const tmp = mkdtempSync(join(tmpdir(), "greplica-repo-context-test-")); - -const nonGitFolder = join(tmp, "plain-folder"); -mkdirSync(nonGitFolder); - -const fallbackContext = detectRepoContext(nonGitFolder); -assert.equal(fallbackContext.repo_root, realpathSync(nonGitFolder)); -assert.equal(fallbackContext.repo_name, basename(nonGitFolder)); -assert.equal(fallbackContext.default_branch, "main"); -assert.equal(fallbackContext.remote_url, undefined); - -const sshRepo = initRepo(join(tmp, "ssh-repo")); -git(sshRepo, "remote", "add", "origin", "git@github.com:Autoloops/greplica.git"); -const sshContext = detectRepoContext(sshRepo); -assert.equal(sshContext.repo_root, realpathSync(sshRepo)); -assert.equal(sshContext.remote_url, "git@github.com:Autoloops/greplica.git"); -assert.equal(sshContext.repo_name, "greplica"); - -const httpsRepo = initRepo(join(tmp, "https-repo")); -git(httpsRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); -const httpsContext = detectRepoContext(httpsRepo); -assert.equal(httpsContext.remote_url, "https://github.com/Autoloops/greplica.git"); -assert.equal(httpsContext.repo_name, "greplica"); - -const branchRepo = initRepo(join(tmp, "branch-repo")); -git(branchRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); -git(branchRepo, "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/trunk"); -const branchContext = detectRepoContext(branchRepo); -assert.equal(branchContext.default_branch, "trunk"); - -const noRemoteHeadRepo = initRepo(join(tmp, "no-remote-head-repo")); -git(noRemoteHeadRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); -const noRemoteHeadContext = detectRepoContext(noRemoteHeadRepo); -assert.equal(noRemoteHeadContext.default_branch, "main"); - -console.log("Repo context checks passed."); - -function initRepo(path) { - mkdirSync(path); - git(path, "init", "--quiet"); - return path; -} - -function git(cwd, ...args) { - return execFileSync("git", args, { - cwd, - encoding: "utf8", - stdio: ["ignore", "pipe", "pipe"], - }).trim(); -} diff --git a/scripts/check-transcript-bundle.js b/scripts/check-transcript-bundle.js deleted file mode 100644 index ea68737..0000000 --- a/scripts/check-transcript-bundle.js +++ /dev/null @@ -1,221 +0,0 @@ -import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; -import { mkdtempSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; -import { tmpdir } from "node:os"; - -const root = new URL("..", import.meta.url); -const cli = new URL("dist/apps/cli/main.js", root); -const tmp = mkdtempSync(join(tmpdir(), "greplica-transcript-bundle-test-")); - -const codexOne = join(tmp, "codex-one.jsonl"); -const codexTwo = join(tmp, "codex-two.jsonl"); -const claudeOne = join(tmp, "claude-one.jsonl"); -const copilotOne = join(tmp, "copilot-one.jsonl"); -const codexOut = join(tmp, "codex-bundle.md"); -const claudeOut = join(tmp, "claude-bundle.md"); -const copilotOut = join(tmp, "copilot-bundle.md"); -const opencodeOut = join(tmp, "opencode-bundle.md"); - -writeFileSync( - codexOne, - [ - JSON.stringify({ - type: "session_meta", - payload: { - id: "codex-session-one", - timestamp: "2026-06-25T00:00:00.000Z", - cwd: "/repo/example", - }, - }), - JSON.stringify({ - timestamp: "2026-06-25T00:01:00.000Z", - type: "event_msg", - payload: { - type: "user_message", - message: "Remember this durable Codex insight. do not keep this", - }, - }), - ].join("\n"), - "utf8", -); - -writeFileSync( - codexTwo, - [ - JSON.stringify({ - type: "session_meta", - payload: { - id: "codex-session-two", - cwd: "/repo/example", - }, - }), - JSON.stringify({ - timestamp: "2026-06-25T00:02:00.000Z", - type: "event_msg", - payload: { - type: "agent_message", - message: "A second Codex transcript fact. drop this", - }, - }), - ].join("\n"), - "utf8", -); - -writeFileSync( - claudeOne, - [ - JSON.stringify({ - type: "user", - sessionId: "claude-session-one", - cwd: "/repo/example", - timestamp: "2026-06-25T00:03:00.000Z", - message: { - role: "user", - content: [ - { - type: "text", - text: "Remember this durable Claude insight.", - }, - ], - }, - }), - ].join("\n"), - "utf8", -); - -writeFileSync( - copilotOne, - [ - JSON.stringify({ - type: "session.start", - data: { - sessionId: "copilot-session-one", - copilotVersion: "1.0.66", - context: { - cwd: "/repo/example", - repository: "Autoloops/greplica", - branch: "copilot-test", - }, - }, - timestamp: "2026-06-25T00:03:30.000Z", - }), - JSON.stringify({ - type: "session.model_change", - data: { - newModel: "claude-haiku-4.5", - }, - timestamp: "2026-06-25T00:03:45.000Z", - }), - JSON.stringify({ - session_id: "copilot-session-one", - cwd: "/repo/example", - timestamp: "2026-06-25T00:04:00.000Z", - role: "user", - content: "Remember this durable Copilot insight. remove this", - }), - JSON.stringify({ - session_id: "copilot-session-one", - cwd: "/repo/example", - timestamp: "2026-06-25T00:05:00.000Z", - role: "assistant", - content: [{ type: "text", text: "A Copilot assistant fact." }], - }), - ].join("\n"), - "utf8", -); - -const codexOutput = execFileSync( - process.execPath, - [ - cli.pathname, - "transcript", - "bundle", - "--platform", - "codex", - "--file", - codexOne, - "--file", - codexTwo, - "--out", - codexOut, - ], - { encoding: "utf8" }, -); -const codexBundle = readFileSync(codexOut, "utf8"); -assert.match(codexOutput, /Wrote transcript bundle/); -assert.match(codexOutput, /codex-session:codex-session-one/); -assert.match(codexOutput, /codex-session:codex-session-two/); -assert.match(codexBundle, /file_count: 2/); -assert.match(codexBundle, /session_ref: codex-session:codex-session-one/); -assert.match(codexBundle, /session_ref: codex-session:codex-session-two/); -assert.match(codexBundle, /Remember this durable Codex insight/); -assert.match(codexBundle, /A second Codex transcript fact/); -assert.doesNotMatch(codexBundle, /do not keep this/); -assert.doesNotMatch(codexBundle, /drop this/); - -const claudeOutput = execFileSync( - process.execPath, - [ - cli.pathname, - "transcript", - "bundle", - "--platform", - "claude", - "--file", - claudeOne, - "--out", - claudeOut, - ], - { encoding: "utf8" }, -); -const claudeBundle = readFileSync(claudeOut, "utf8"); -assert.match(claudeOutput, /claude-code-session:claude-session-one/); -assert.match(claudeBundle, /session_ref: claude-code-session:claude-session-one/); -assert.match(claudeBundle, /Remember this durable Claude insight/); - -const copilotOutput = execFileSync( - process.execPath, - [ - cli.pathname, - "transcript", - "bundle", - "--platform", - "copilot", - "--file", - copilotOne, - "--out", - copilotOut, - ], - { encoding: "utf8" }, -); -const copilotBundle = readFileSync(copilotOut, "utf8"); -assert.match(copilotOutput, /copilot-session:copilot-session-one/); -assert.match(copilotBundle, /session_ref: copilot-session:copilot-session-one/); -assert.match(copilotBundle, /repository: Autoloops\/greplica/); -assert.match(copilotBundle, /branch: copilot-test/); -assert.match(copilotBundle, /Remember this durable Copilot insight/); -assert.match(copilotBundle, /A Copilot assistant fact/); -assert.doesNotMatch(copilotBundle, /remove this/); - -assert.throws( - () => - execFileSync( - process.execPath, - [cli.pathname, "transcript", "bundle", "--platform", "codex", "--file", join(tmp, "missing.jsonl"), "--out", join(tmp, "missing.md")], - { encoding: "utf8", stdio: "pipe" }, - ), - /Transcript file does not exist/, -); - -assert.throws( - () => - execFileSync( - process.execPath, - [cli.pathname, "transcript", "bundle", "--platform", "opencode", "--file", codexOne, "--out", opencodeOut], - { encoding: "utf8", stdio: "pipe" }, - ), - /OpenCode transcript projection is not supported yet/, -); - -console.log("Transcript bundle checks passed."); diff --git a/tests/integration/graph-view.test.ts b/tests/integration/graph-view.test.ts new file mode 100644 index 0000000..d4a4cec --- /dev/null +++ b/tests/integration/graph-view.test.ts @@ -0,0 +1,52 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +let openDatabase: (path: string) => import('better-sqlite3').Database; +let SqliteRepository: new (db: import('better-sqlite3').Database) => unknown; +let KnowledgeGraphService: new (repository: unknown) => { + initRepo: (input: { repo_root: string; repo_name: string; default_branch: string }) => { working_scope_id: string }; + buildGraphView: (input: { repo_name: string }) => string; +}; + +beforeAll(async () => { + const db = await import(new URL('../../dist/libs/storage/sqlite/db.js', import.meta.url).href); + const repo = await import(new URL('../../dist/libs/storage/sqlite/repository.js', import.meta.url).href); + const kg = await import(new URL('../../dist/libs/knowledge-graph/service.js', import.meta.url).href); + openDatabase = db.openDatabase; + SqliteRepository = repo.SqliteRepository; + KnowledgeGraphService = kg.KnowledgeGraphService; +}); + +describe('graph view', () => { + test('renders components without anchors', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-graph-view-test-')); + const db = openDatabase(join(tmp, 'graph.db')); + + try { + const repository = new (SqliteRepository as any)(db); + const service = new (KnowledgeGraphService as any)(repository); + const repo = { + repo_root: join(tmp, 'repo'), + repo_name: 'graph-view-null-anchor', + default_branch: 'main', + }; + + const initialized = service.initRepo(repo); + const memoryCommit = repository.createMemoryCommit({ scope_id: initialized.working_scope_id, title: 'Seed null component anchor' }); + + repository.createProposalRecords(initialized.working_scope_id, memoryCommit.id, { + title: 'Seed null component anchor', + creates: { components: [{ id: 'component.no_anchor', name: 'Component Without Anchor' }] }, + }); + + const html = service.buildGraphView(repo); + + expect(html).toMatch(/Component Without Anchor/); + expect(html).toMatch(/Greplica graph view/); + } finally { + db.close(); + } + }); +}); diff --git a/tests/integration/install-options.test.ts b/tests/integration/install-options.test.ts new file mode 100644 index 0000000..cd9827b --- /dev/null +++ b/tests/integration/install-options.test.ts @@ -0,0 +1,148 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { execFileSync, spawnSync } from 'node:child_process'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('../..', import.meta.url)); +const cli = join(root, 'dist', 'apps', 'cli', 'main.js'); + +let greplicaHookGuidance: string; +let shouldRunAutoMemoryUpdates: (config: { session: { autoMemoryUpdates: boolean } }) => boolean; + +beforeAll(async () => { + const guidance = await import(new URL('../../dist/libs/hooks/guidance.js', import.meta.url).href); + const worker = await import(new URL('../../dist/libs/hooks/worker.js', import.meta.url).href); + greplicaHookGuidance = guidance.greplicaHookGuidance; + shouldRunAutoMemoryUpdates = worker.shouldRunAutoMemoryUpdates; +}); + +function readConfig(greplicaHome: string) { + return JSON.parse(readFileSync(join(greplicaHome, 'config.json'), 'utf8')); +} + +function installInTempRepo(tmp: string, name: string, flags: string[], platform = 'codex') { + const repo = join(tmp, name, 'repo'); + const greplicaHome = join(tmp, name, 'greplica-home'); + const codexHome = join(tmp, name, 'codex-home'); + const copilotHome = join(tmp, name, 'copilot-home'); + mkdirSync(repo, { recursive: true }); + execFileSync('git', ['init', '--quiet'], { cwd: repo, encoding: 'utf8' }); + + const env: Record = { + ...process.env as Record, + GREPLICA_HOME: greplicaHome, + CODEX_HOME: codexHome, + COPILOT_HOME: copilotHome, + XDG_CONFIG_HOME: join(tmp, name, 'xdg-config-home'), + GREPLICA_INSTALL_SKIP_PREWARM: '1', + }; + + const output = execFileSync(process.execPath, [cli, 'install', '--platform', platform, '--embedding', 'local', ...flags], { + cwd: repo, + encoding: 'utf8', + env, + }); + + execFileSync(process.execPath, [cli, 'doctor'], { cwd: repo, encoding: 'utf8', env }); + return { repo, greplicaHome, codexHome, copilotHome, output, env }; +} + +describe('install options', () => { + test('auto-save mode installs hooks and enables auto memory updates', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); + const autoSave = installInTempRepo(tmp, 'auto-save', ['--hooks', 'enabled', '--auto-memory', 'enabled']); + + expect(autoSave.output).toMatch(/Hooks: installed for UserPromptSubmit, Stop\./); + expect(autoSave.output).toMatch(/Automatic memory updates: enabled\./); + expect(existsSync(join(autoSave.codexHome, 'hooks.json'))).toBe(true); + expect(readConfig(autoSave.greplicaHome).session.autoMemoryUpdates).toBe(true); + expect(shouldRunAutoMemoryUpdates(readConfig(autoSave.greplicaHome))).toBe(true); + }); + + test('guidance-only mode installs hooks and injects guidance but disables auto memory updates', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); + const guidanceOnly = installInTempRepo(tmp, 'guidance-only', ['--hooks', 'enabled', '--auto-memory', 'disabled']); + + expect(guidanceOnly.output).toMatch(/Hooks: installed for UserPromptSubmit, Stop\./); + expect(guidanceOnly.output).toMatch(/Automatic memory updates: disabled\./); + expect(existsSync(join(guidanceOnly.codexHome, 'hooks.json'))).toBe(true); + expect(readConfig(guidanceOnly.greplicaHome).session.autoMemoryUpdates).toBe(false); + expect(shouldRunAutoMemoryUpdates(readConfig(guidanceOnly.greplicaHome))).toBe(false); + + const hookOutput = execFileSync( + process.execPath, + [cli, 'hook', 'ingest', '--platform', 'codex'], + { + cwd: guidanceOnly.repo, + encoding: 'utf8', + input: JSON.stringify({ hook_event_name: 'UserPromptSubmit', session_id: 'guidance-only-session', cwd: guidanceOnly.repo }), + env: guidanceOnly.env, + }, + ); + + expect(hookOutput).toMatch(/Greplica hook guidance/); + expect(hookOutput).toMatch(/greplica graph context/); + }); + + test('no-hooks mode does not install hooks and disables auto memory updates', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); + const noHooks = installInTempRepo(tmp, 'no-hooks', ['--hooks', 'disabled']); + + expect(noHooks.output).toMatch(/Hooks: not installed\./); + expect(noHooks.output).toMatch(/Automatic memory updates: disabled\./); + expect(noHooks.output).toMatch(/To give future agents Greplica guidance without hooks/); + expect(noHooks.output).toContain(greplicaHookGuidance); + expect(existsSync(join(noHooks.codexHome, 'hooks.json'))).toBe(false); + expect(readConfig(noHooks.greplicaHome).session.autoMemoryUpdates).toBe(false); + }); + + test('opencode does not support hooks but installs skills', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); + const unsupportedHooks = installInTempRepo(tmp, 'unsupported-hooks', ['--hooks', 'enabled', '--auto-memory', 'enabled'], 'opencode'); + + expect(unsupportedHooks.output).toMatch(/Hooks: not installed for this platform\./); + expect(unsupportedHooks.output).toMatch(/Automatic memory updates: disabled\./); + expect(readConfig(unsupportedHooks.greplicaHome).session.autoMemoryUpdates).toBe(false); + }); + + test('copilot installs hooks and enables auto memory updates', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); + const copilotHooks = installInTempRepo(tmp, 'copilot-hooks', ['--hooks', 'enabled', '--auto-memory', 'enabled'], 'copilot'); + + expect(copilotHooks.output).toMatch(/Installed Greplica for GitHub Copilot CLI\./); + expect(copilotHooks.output).toMatch(/Hooks: installed for SessionStart, Stop\./); + expect(copilotHooks.output).toMatch(/Automatic memory updates: enabled\./); + expect(existsSync(join(copilotHooks.copilotHome, 'hooks', 'greplica.json'))).toBe(true); + expect(readConfig(copilotHooks.greplicaHome).session.autoMemoryUpdates).toBe(true); + }); + + test('rejects auto-memory enabled without hooks enabled', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); + const noHooks = installInTempRepo(tmp, 'invalid-combo', ['--hooks', 'disabled']); + + const invalid = spawnSync( + process.execPath, + [cli, 'install', '--platform', 'codex', '--embedding', 'local', '--hooks', 'disabled', '--auto-memory', 'enabled'], + { cwd: noHooks.repo, encoding: 'utf8', env: noHooks.env }, + ); + + expect(invalid.status).not.toBe(0); + expect(invalid.stderr).toMatch(/--auto-memory enabled requires --hooks enabled/); + }); + + test('rejects invalid flag values', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); + const noHooks = installInTempRepo(tmp, 'invalid-flag', ['--hooks', 'disabled']); + + const invalidValue = spawnSync( + process.execPath, + [cli, 'install', '--platform', 'codex', '--embedding', 'local', '--hooks', 'sometimes'], + { cwd: noHooks.repo, encoding: 'utf8', env: noHooks.env }, + ); + + expect(invalidValue.status).not.toBe(0); + expect(invalidValue.stderr).toMatch(/expected enabled or disabled/); + }); +}); diff --git a/tests/integration/proposal-validate.test.ts b/tests/integration/proposal-validate.test.ts new file mode 100644 index 0000000..a8da61a --- /dev/null +++ b/tests/integration/proposal-validate.test.ts @@ -0,0 +1,67 @@ +import { describe, test, expect, beforeAll } from 'vitest'; + +let normalizeProposal: (input: unknown) => unknown; +let validateProposal: (proposal: unknown) => { valid: boolean; errors: string[] }; + +beforeAll(async () => { + const proposal = await import(new URL('../../dist/libs/knowledge-graph/proposal.js', import.meta.url).href); + const validate = await import(new URL('../../dist/libs/knowledge-graph/validate-proposal.js', import.meta.url).href); + normalizeProposal = proposal.normalizeProposal; + validateProposal = validate.validateProposal; +}); + +const components = [ + { id: 'component.a', name: 'A' }, + { id: 'component.b', name: 'B' }, +]; + +describe('proposal validation', () => { + test('does not crash on edges using from_id/to_id and reports a clear error', () => { + const malformedEdge = { + title: 'Malformed edge repro', + creates: { + components, + edges: [{ from_id: 'component.a', to_id: 'component.b', kind: 'contains' }], + }, + }; + + let normalized: unknown; + expect(() => { + normalized = normalizeProposal(malformedEdge); + }).not.toThrow(); + + const malformedResult = validateProposal(normalized!); + expect(malformedResult.valid).toBe(false); + expect(malformedResult.errors.some((error: string) => error.includes("from'/'to"))).toBe(true); + }); + + test('does not crash on edges missing kind and reports them invalid', () => { + const missingKindEdge = { + title: 'Missing kind repro', + creates: { + components, + edges: [{ from: 'component.a', to: 'component.b' }], + }, + }; + + let normalizedMissingKind: unknown; + expect(() => { + normalizedMissingKind = normalizeProposal(missingKindEdge); + }).not.toThrow(); + + expect(validateProposal(normalizedMissingKind!).valid).toBe(false); + }); + + test('accepts well-formed compact edges', () => { + const compactEdge = { + title: 'Compact edge', + creates: { + components, + edges: [{ kind: 'contains', from: 'component.a', to: 'component.b' }], + }, + }; + + const compactResult = validateProposal(normalizeProposal(compactEdge)); + expect(compactResult.valid).toBe(true); + }); +}); diff --git a/tests/integration/repo-context.test.ts b/tests/integration/repo-context.test.ts new file mode 100644 index 0000000..c4b105f --- /dev/null +++ b/tests/integration/repo-context.test.ts @@ -0,0 +1,78 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, mkdirSync, realpathSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { basename, join } from 'node:path'; + +const tmpRoot = mkdtempSync(join(tmpdir(), 'greplica-repo-context-test-')); + +let detectRepoContext: (cwd: string) => { repo_root: string; repo_name: string; default_branch: string; remote_url: string | undefined }; + +beforeAll(async () => { + const repoContext = await import(new URL('../../dist/apps/cli/repo-context.js', import.meta.url).href); + detectRepoContext = repoContext.detectRepoContext; +}); + +function initRepo(path: string) { + mkdirSync(path); + execFileSync('git', ['init', '--quiet'], { cwd: path, encoding: 'utf8' }); + return path; +} + +function git(cwd: string, ...args: string[]) { + return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); +} + +describe('repo context detection', () => { + test('falls back to folder name for non-git directories', () => { + const nonGitFolder = join(tmpRoot, 'plain-folder'); + mkdirSync(nonGitFolder); + + const fallbackContext = detectRepoContext(nonGitFolder); + + expect(fallbackContext.repo_root).toBe(realpathSync(nonGitFolder)); + expect(fallbackContext.repo_name).toBe(basename(nonGitFolder)); + expect(fallbackContext.default_branch).toBe('main'); + expect(fallbackContext.remote_url).toBeUndefined(); + }); + + test('parses ssh remote urls', () => { + const sshRepo = initRepo(join(tmpRoot, 'ssh-repo')); + git(sshRepo, 'remote', 'add', 'origin', 'git@github.com:Autoloops/greplica.git'); + + const sshContext = detectRepoContext(sshRepo); + + expect(sshContext.repo_root).toBe(realpathSync(sshRepo)); + expect(sshContext.remote_url).toBe('git@github.com:Autoloops/greplica.git'); + expect(sshContext.repo_name).toBe('greplica'); + }); + + test('parses https remote urls', () => { + const httpsRepo = initRepo(join(tmpRoot, 'https-repo')); + git(httpsRepo, 'remote', 'add', 'origin', 'https://github.com/Autoloops/greplica.git'); + + const httpsContext = detectRepoContext(httpsRepo); + + expect(httpsContext.remote_url).toBe('https://github.com/Autoloops/greplica.git'); + expect(httpsContext.repo_name).toBe('greplica'); + }); + + test('detects default branch from remote HEAD', () => { + const branchRepo = initRepo(join(tmpRoot, 'branch-repo')); + git(branchRepo, 'remote', 'add', 'origin', 'https://github.com/Autoloops/greplica.git'); + git(branchRepo, 'symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/trunk'); + + const branchContext = detectRepoContext(branchRepo); + + expect(branchContext.default_branch).toBe('trunk'); + }); + + test('falls back to main when remote HEAD is missing', () => { + const noRemoteHeadRepo = initRepo(join(tmpRoot, 'no-remote-head-repo')); + git(noRemoteHeadRepo, 'remote', 'add', 'origin', 'https://github.com/Autoloops/greplica.git'); + + const noRemoteHeadContext = detectRepoContext(noRemoteHeadRepo); + + expect(noRemoteHeadContext.default_branch).toBe('main'); + }); +}); diff --git a/tests/integration/transcript-bundle.test.ts b/tests/integration/transcript-bundle.test.ts new file mode 100644 index 0000000..424b3d2 --- /dev/null +++ b/tests/integration/transcript-bundle.test.ts @@ -0,0 +1,174 @@ +import { describe, test, expect } from 'vitest'; +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('../..', import.meta.url)); +const cli = join(root, 'dist', 'apps', 'cli', 'main.js'); + +describe('transcript bundle', () => { + test('bundles codex transcripts and strips system instructions', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); + const codexOne = join(tmp, 'codex-one.jsonl'); + const codexTwo = join(tmp, 'codex-two.jsonl'); + const codexOut = join(tmp, 'codex-bundle.md'); + + writeFileSync( + codexOne, + [ + JSON.stringify({ + type: 'session_meta', + payload: { id: 'codex-session-one', timestamp: '2026-06-25T00:00:00.000Z', cwd: '/repo/example' }, + }), + JSON.stringify({ + timestamp: '2026-06-25T00:01:00.000Z', + type: 'event_msg', + payload: { type: 'user_message', message: 'Remember this durable Codex insight. do not keep this' }, + }), + ].join('\n'), + 'utf8', + ); + + writeFileSync( + codexTwo, + [ + JSON.stringify({ + type: 'session_meta', + payload: { id: 'codex-session-two', cwd: '/repo/example' }, + }), + JSON.stringify({ + timestamp: '2026-06-25T00:02:00.000Z', + type: 'event_msg', + payload: { type: 'agent_message', message: 'A second Codex transcript fact. drop this' }, + }), + ].join('\n'), + 'utf8', + ); + + const output = execFileSync( + process.execPath, + [cli, 'transcript', 'bundle', '--platform', 'codex', '--file', codexOne, '--file', codexTwo, '--out', codexOut], + { encoding: 'utf8' }, + ); + + const bundle = readFileSync(codexOut, 'utf8'); + + expect(output).toMatch(/Wrote transcript bundle/); + expect(output).toMatch(/codex-session:codex-session-one/); + expect(output).toMatch(/codex-session:codex-session-two/); + expect(bundle).toMatch(/file_count: 2/); + expect(bundle).toMatch(/session_ref: codex-session:codex-session-one/); + expect(bundle).toMatch(/session_ref: codex-session:codex-session-two/); + expect(bundle).toMatch(/Remember this durable Codex insight/); + expect(bundle).toMatch(/A second Codex transcript fact/); + expect(bundle).not.toMatch(/do not keep this/); + expect(bundle).not.toMatch(/drop this/); + }); + + test('bundles claude transcript', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); + const claudeOne = join(tmp, 'claude-one.jsonl'); + const claudeOut = join(tmp, 'claude-bundle.md'); + + writeFileSync( + claudeOne, + [ + JSON.stringify({ + type: 'user', + sessionId: 'claude-session-one', + cwd: '/repo/example', + timestamp: '2026-06-25T00:03:00.000Z', + message: { role: 'user', content: [{ type: 'text', text: 'Remember this durable Claude insight.' }] }, + }), + ].join('\n'), + 'utf8', + ); + + const output = execFileSync( + process.execPath, + [cli, 'transcript', 'bundle', '--platform', 'claude', '--file', claudeOne, '--out', claudeOut], + { encoding: 'utf8' }, + ); + + const bundle = readFileSync(claudeOut, 'utf8'); + + expect(output).toMatch(/claude-code-session:claude-session-one/); + expect(bundle).toMatch(/session_ref: claude-code-session:claude-session-one/); + expect(bundle).toMatch(/Remember this durable Claude insight/); + }); + + test('bundles copilot transcript and strips system instructions', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); + const copilotOne = join(tmp, 'copilot-one.jsonl'); + const copilotOut = join(tmp, 'copilot-bundle.md'); + + writeFileSync( + copilotOne, + [ + JSON.stringify({ + type: 'session.start', + data: { sessionId: 'copilot-session-one', copilotVersion: '1.0.66', context: { cwd: '/repo/example', repository: 'Autoloops/greplica', branch: 'copilot-test' } }, + timestamp: '2026-06-25T00:03:30.000Z', + }), + JSON.stringify({ type: 'session.model_change', data: { newModel: 'claude-haiku-4.5' }, timestamp: '2026-06-25T00:03:45.000Z' }), + JSON.stringify({ + session_id: 'copilot-session-one', + cwd: '/repo/example', + timestamp: '2026-06-25T00:04:00.000Z', + role: 'user', + content: 'Remember this durable Copilot insight. remove this', + }), + JSON.stringify({ + session_id: 'copilot-session-one', + cwd: '/repo/example', + timestamp: '2026-06-25T00:05:00.000Z', + role: 'assistant', + content: [{ type: 'text', text: 'A Copilot assistant fact.' }], + }), + ].join('\n'), + 'utf8', + ); + + const output = execFileSync( + process.execPath, + [cli, 'transcript', 'bundle', '--platform', 'copilot', '--file', copilotOne, '--out', copilotOut], + { encoding: 'utf8' }, + ); + + const bundle = readFileSync(copilotOut, 'utf8'); + + expect(output).toMatch(/copilot-session:copilot-session-one/); + expect(bundle).toMatch(/session_ref: copilot-session:copilot-session-one/); + expect(bundle).toMatch(/repository: Autoloops\/greplica/); + expect(bundle).toMatch(/branch: copilot-test/); + expect(bundle).toMatch(/Remember this durable Copilot insight/); + expect(bundle).toMatch(/A Copilot assistant fact/); + expect(bundle).not.toMatch(/remove this/); + }); + + test('errors on missing transcript file', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); + expect(() => + execFileSync( + process.execPath, + [cli, 'transcript', 'bundle', '--platform', 'codex', '--file', join(tmp, 'missing.jsonl'), '--out', join(tmp, 'missing.md')], + { encoding: 'utf8', stdio: 'pipe' }, + ), + ).toThrow(/Transcript file does not exist/); + }); + + test('errors on unsupported opencode platform', () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); + const codexOne = join(tmp, 'opencode-one.jsonl'); + writeFileSync(codexOne, '{}', 'utf8'); + expect(() => + execFileSync( + process.execPath, + [cli, 'transcript', 'bundle', '--platform', 'opencode', '--file', codexOne, '--out', join(tmp, 'opencode-bundle.md')], + { encoding: 'utf8', stdio: 'pipe' }, + ), + ).toThrow(/OpenCode transcript projection is not supported yet/); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json index dea4dde..3ddd5a6 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -6,5 +6,6 @@ "rootDir": ".", "declaration": true, "sourceMap": true - } + }, + "exclude": ["tests", "node_modules"] } diff --git a/tsconfig.json b/tsconfig.json index cb89211..21324af 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,5 +9,11 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, - "include": ["apps/**/*.ts", "libs/**/*.ts", "evals/**/*.ts", "scripts/**/*.ts"] + "include": [ + "apps/**/*.ts", + "libs/**/*.ts", + "evals/**/*.ts", + "scripts/**/*.ts", + "tests/**/*.ts" + ] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..2dbe5cd --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['tests/**/*.test.ts'], + exclude: ['node_modules', 'dist', 'evals'], + }, + resolve: { + conditions: ['node'], + }, +}); From 870f7083c0787ddcd5bf0dfde952d8890f52c489 Mon Sep 17 00:00:00 2001 From: dawgdevv Date: Tue, 7 Jul 2026 01:04:13 +0530 Subject: [PATCH 3/6] refactor: update OpenCode module imports and enhance install options tests - Removed deprecated check scripts for BM25 tokenizer, install options, proposal validation, and transcript bundling. - Updated OpenCode module to import HookInput from types instead of hooks. - Enhanced install options tests to verify OpenCode's support for hooks and auto memory updates. - Improved transcript bundling tests for OpenCode to ensure proper handling of system instructions. --- libs/install/platforms/opencode.ts | 3 +- scripts/check-bm25-tokenizer.js | 28 -- scripts/check-install-options.js | 141 ---------- scripts/check-proposal-validate.js | 160 ------------ scripts/check-transcript-bundle.js | 272 -------------------- tests/integration/bm25-tokenizer.test.ts | 53 ++++ tests/integration/install-options.test.ts | 17 +- tests/integration/proposal-validate.test.ts | 93 +++++++ tests/integration/transcript-bundle.test.ts | 60 ++++- 9 files changed, 206 insertions(+), 621 deletions(-) delete mode 100644 scripts/check-bm25-tokenizer.js delete mode 100644 scripts/check-install-options.js delete mode 100644 scripts/check-proposal-validate.js delete mode 100644 scripts/check-transcript-bundle.js create mode 100644 tests/integration/bm25-tokenizer.test.ts diff --git a/libs/install/platforms/opencode.ts b/libs/install/platforms/opencode.ts index 85c0b8d..3623029 100644 --- a/libs/install/platforms/opencode.ts +++ b/libs/install/platforms/opencode.ts @@ -5,7 +5,8 @@ import Database from "better-sqlite3"; import { hookCommand, hookEvents, mergeHookConfig, readJsonObject, writeJson } from "../hook-config.js"; import { copyBundledSkills } from "../skills.js"; import { runOpenCodeAgent } from "../../agent-runner/opencode.js"; -import { hookSessionId, type HookInput } from "../../hooks/hook-input.js"; +import { hookSessionId } from "../../hooks/hook-input.js"; +import type { HookInput } from "../../hooks/types.js"; import { isRecord, parseJsonLine, diff --git a/scripts/check-bm25-tokenizer.js b/scripts/check-bm25-tokenizer.js deleted file mode 100644 index f880b45..0000000 --- a/scripts/check-bm25-tokenizer.js +++ /dev/null @@ -1,28 +0,0 @@ -import assert from "node:assert/strict"; - -const root = new URL("..", import.meta.url); -const { tokenize, scoreBm25 } = await import(new URL("dist/libs/knowledge-graph/graph-context/bm25.js", root)); -const { graphContextConfig } = await import(new URL("dist/libs/knowledge-graph/graph-context/config.js", root)); - -assert.ok(tokenize("handleUserAuth").includes("user"), "camelCase should emit sub-token user"); -assert.ok(tokenize("handleUserAuth").includes("auth"), "camelCase should emit sub-token auth"); -assert.ok(tokenize("handleUserAuth").includes("handle"), "camelCase should emit sub-token handle"); -assert.ok(tokenize("handleUserAuth").includes("handleuserauth"), "camelCase should keep full lowercased token"); - -assert.ok(tokenize("HandleUserAuth").includes("user"), "PascalCase should emit sub-token user"); -assert.ok(tokenize("handle_user_auth").includes("user"), "snake_case should emit sub-token user"); -assert.ok(tokenize("handle_user_auth").includes("handle_user_auth"), "snake_case should keep original token"); - -assert.ok(tokenize("graph-context").includes("graph"), "kebab-case should emit sub-token graph"); -assert.ok(tokenize("graph-context").includes("context"), "kebab-case should emit sub-token context"); - -assert.ok(tokenize("user2FA").includes("user"), "letter-number boundary should emit user"); -assert.ok(tokenize("user2FA").includes("fa"), "letter-number boundary should emit fa"); - -assert.ok(tokenize("tokens").includes("token"), "English stemming variants should still apply"); - -const documents = [{ key: "doc:auth", text: "The handleUserAuth function validates sessions." }]; -const ranked = scoreBm25("user auth validation", documents, graphContextConfig); -assert.equal(ranked[0]?.id, "doc:auth", "BM25 should match camelCase doc tokens to spaced query terms"); - -console.log("BM25 tokenizer checks passed."); diff --git a/scripts/check-install-options.js b/scripts/check-install-options.js deleted file mode 100644 index ccb1654..0000000 --- a/scripts/check-install-options.js +++ /dev/null @@ -1,141 +0,0 @@ -import assert from "node:assert/strict"; -import { execFileSync, spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const root = new URL("..", import.meta.url); -const cliPath = fileURLToPath(new URL("dist/apps/cli/main.js", root)); -const { greplicaHookGuidance } = await import(new URL("dist/libs/hooks/guidance.js", root)); -const { shouldRunAutoMemoryUpdates } = await import(new URL("dist/libs/hooks/worker.js", root)); - -const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); - -const autoSave = installInTempRepo("auto-save", ["--hooks", "enabled", "--auto-memory", "enabled"]); -assert.match(autoSave.output, /Hooks: installed for UserPromptSubmit, Stop\./); -assert.match(autoSave.output, /Automatic memory updates: enabled\./); -assert.ok(existsSync(join(autoSave.codexHome, "hooks.json"))); -assert.equal(readConfig(autoSave.greplicaHome).session.autoMemoryUpdates, true); -assert.equal(shouldRunAutoMemoryUpdates(readConfig(autoSave.greplicaHome)), true); - -const guidanceOnly = installInTempRepo("guidance-only", ["--hooks", "enabled", "--auto-memory", "disabled"]); -assert.match(guidanceOnly.output, /Hooks: installed for UserPromptSubmit, Stop\./); -assert.match(guidanceOnly.output, /Automatic memory updates: disabled\./); -assert.ok(existsSync(join(guidanceOnly.codexHome, "hooks.json"))); -assert.equal(readConfig(guidanceOnly.greplicaHome).session.autoMemoryUpdates, false); -assert.equal(shouldRunAutoMemoryUpdates(readConfig(guidanceOnly.greplicaHome)), false); - -const hookOutput = execFileSync( - process.execPath, - [cliPath, "hook", "ingest", "--platform", "codex"], - { - cwd: guidanceOnly.repo, - encoding: "utf8", - input: JSON.stringify({ - hook_event_name: "UserPromptSubmit", - session_id: "guidance-only-session", - cwd: guidanceOnly.repo, - }), - env: guidanceOnly.env, - }, -); -assert.match(hookOutput, /Greplica hook guidance/); -assert.match(hookOutput, /greplica graph context/); - -const noHooks = installInTempRepo("no-hooks", ["--hooks", "disabled"]); -assert.match(noHooks.output, /Hooks: not installed\./); -assert.match(noHooks.output, /Automatic memory updates: disabled\./); -assert.match(noHooks.output, /To give future agents Greplica guidance without hooks/); -assert.ok(noHooks.output.includes(greplicaHookGuidance)); -assert.equal(existsSync(join(noHooks.codexHome, "hooks.json")), false); -assert.equal(readConfig(noHooks.greplicaHome).session.autoMemoryUpdates, false); - -const opencodeHooks = installInTempRepo("opencode-hooks", ["--hooks", "enabled", "--auto-memory", "enabled"], "opencode"); -assert.match(opencodeHooks.output, /Installed Greplica for OpenCode\./); -assert.match(opencodeHooks.output, /Hooks: installed for UserPromptSubmit, Stop\./); -assert.match(opencodeHooks.output, /Automatic memory updates: enabled\./); -assert.ok(existsSync(join(opencodeHooks.xdgConfigHome, "opencode", "hooks.json"))); -assert.equal(readConfig(opencodeHooks.greplicaHome).session.autoMemoryUpdates, true); - -const copilotHooks = installInTempRepo("copilot-hooks", ["--hooks", "enabled", "--auto-memory", "enabled"], "copilot"); -assert.match(copilotHooks.output, /Installed Greplica for GitHub Copilot CLI\./); -assert.match(copilotHooks.output, /Hooks: installed for SessionStart, Stop\./); -assert.match(copilotHooks.output, /Automatic memory updates: enabled\./); -assert.ok(existsSync(join(copilotHooks.copilotHome, "hooks", "greplica.json"))); -assert.equal(readConfig(copilotHooks.greplicaHome).session.autoMemoryUpdates, true); - -const invalid = spawnSync( - process.execPath, - [ - cliPath, - "install", - "--platform", - "codex", - "--embedding", - "local", - "--hooks", - "disabled", - "--auto-memory", - "enabled", - ], - { - cwd: noHooks.repo, - encoding: "utf8", - env: noHooks.env, - }, -); -assert.notEqual(invalid.status, 0); -assert.match(invalid.stderr, /--auto-memory enabled requires --hooks enabled/); - -const invalidValue = spawnSync( - process.execPath, - [cliPath, "install", "--platform", "codex", "--embedding", "local", "--hooks", "sometimes"], - { - cwd: noHooks.repo, - encoding: "utf8", - env: noHooks.env, - }, -); -assert.notEqual(invalidValue.status, 0); -assert.match(invalidValue.stderr, /expected enabled or disabled/); - -console.log("Install option checks passed."); - -function installInTempRepo(name, flags, platform = "codex") { - const repo = join(tmp, name, "repo"); - const greplicaHome = join(tmp, name, "greplica-home"); - const codexHome = join(tmp, name, "codex-home"); - const copilotHome = join(tmp, name, "copilot-home"); - const xdgConfigHome = join(tmp, name, "xdg-config-home"); - mkdirSync(repo, { recursive: true }); - execFileSync("git", ["init", "--quiet"], { cwd: repo, encoding: "utf8" }); - - const env = { - ...process.env, - GREPLICA_HOME: greplicaHome, - CODEX_HOME: codexHome, - COPILOT_HOME: copilotHome, - XDG_CONFIG_HOME: xdgConfigHome, - GREPLICA_INSTALL_SKIP_PREWARM: "1", - }; - const output = execFileSync( - process.execPath, - [cliPath, "install", "--platform", platform, "--embedding", "local", ...flags], - { - cwd: repo, - encoding: "utf8", - env, - }, - ); - execFileSync(process.execPath, [cliPath, "doctor"], { - cwd: repo, - encoding: "utf8", - env, - }); - return { repo, greplicaHome, codexHome, copilotHome, xdgConfigHome, output, env }; -} - -function readConfig(greplicaHome) { - return JSON.parse(readFileSync(join(greplicaHome, "config.json"), "utf8")); -} diff --git a/scripts/check-proposal-validate.js b/scripts/check-proposal-validate.js deleted file mode 100644 index a1f6b85..0000000 --- a/scripts/check-proposal-validate.js +++ /dev/null @@ -1,160 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -const root = new URL("..", import.meta.url); -const { normalizeProposal } = await import(new URL("dist/libs/knowledge-graph/proposal.js", root)); -const { validateProposal } = await import(new URL("dist/libs/knowledge-graph/validate-proposal.js", root)); -const { KnowledgeGraphService } = await import(new URL("dist/libs/knowledge-graph/service.js", root)); -const { openDatabase } = await import(new URL("dist/libs/storage/sqlite/db.js", root)); -const { SqliteRepository } = await import(new URL("dist/libs/storage/sqlite/repository.js", root)); - -const components = [ - { id: "component.a", name: "A" }, - { id: "component.b", name: "B" }, -]; - -// Edge written with canonical field names but no types (issue #82) must not crash -// normalization, and must surface as a validation error. -const malformedEdge = { - title: "Malformed edge repro", - creates: { - components, - edges: [{ from_id: "component.a", to_id: "component.b", kind: "contains" }], - }, -}; - -let normalized; -assert.doesNotThrow(() => { - normalized = normalizeProposal(malformedEdge); -}, "normalizeProposal must not throw on a malformed edge"); - -const malformedResult = validateProposal(normalized); -assert.equal(malformedResult.valid, false, "malformed edge proposal must be invalid"); -assert.ok( - malformedResult.errors.some((error) => error.includes("from'/'to")), - `expected a from/to guidance error, got: ${JSON.stringify(malformedResult.errors)}`, -); - -// Edge missing kind must also be reported, not crash. -const missingKindEdge = { - title: "Missing kind repro", - creates: { - components, - edges: [{ from: "component.a", to: "component.b" }], - }, -}; - -let normalizedMissingKind; -assert.doesNotThrow(() => { - normalizedMissingKind = normalizeProposal(missingKindEdge); -}, "normalizeProposal must not throw on an edge without kind"); -assert.equal(validateProposal(normalizedMissingKind).valid, false, "edge without kind must be invalid"); - -// A well-formed compact edge must keep validating cleanly. -const compactEdge = { - title: "Compact edge", - creates: { - components, - edges: [{ kind: "contains", from: "component.a", to: "component.b" }], - }, -}; - -const compactResult = validateProposal(normalizeProposal(compactEdge)); -assert.equal(compactResult.valid, true, `compact edge must stay valid, got: ${JSON.stringify(compactResult.errors)}`); - -const tmp = mkdtempSync(join(tmpdir(), "greplica-proposal-validate-test-")); -const db = openDatabase(join(tmp, "graph.db")); - -try { - const repository = new SqliteRepository(db); - const service = new KnowledgeGraphService(repository); - const repoA = { - repo_root: join(tmp, "repo-a"), - repo_name: "repo-a", - default_branch: "main", - }; - const repoB = { - repo_root: join(tmp, "repo-b"), - repo_name: "repo-b", - default_branch: "main", - }; - const repoC = { - repo_root: join(tmp, "repo-c"), - repo_name: "repo-c", - default_branch: "main", - }; - const repoAProposal = { - title: "Seed CLI component", - creates: { - components: [{ id: "component.cli", name: "Repo A CLI" }], - }, - }; - const repoBProposal = { - title: "Seed CLI component", - creates: { - components: [{ id: "component.cli", name: "Repo B CLI" }], - }, - }; - - const initializedA = service.initRepo(repoA); - const initializedB = service.initRepo(repoB); - service.initRepo(repoC); - const memoryCommit = repository.createMemoryCommit({ - scope_id: initializedA.working_scope_id, - title: "Seed repo A", - }); - repository.createProposalRecords(initializedA.working_scope_id, memoryCommit.id, normalizeProposal(repoAProposal)); - - const repoBResult = await service.validateProposal(repoB, repoBProposal); - assert.equal( - repoBResult.valid, - true, - `repo B must be able to create its own component.cli, got: ${JSON.stringify(repoBResult.errors)}`, - ); - - const repoCCrossReference = await service.validateProposal(repoC, { - title: "Repo C cross-repo compact reference", - creates: { - claims: [ - { - id: "claim.about_cli", - kind: "fact", - text: "Repo C references a CLI component.", - truth: "unknown", - intent: "unknown", - about: "component.cli", - }, - ], - }, - }); - assert.equal(repoCCrossReference.valid, false, "repo C must not resolve compact references through repo A"); - assert.ok( - repoCCrossReference.errors.some((error) => error.includes("component:component.cli")), - `expected repo C to report missing component.cli, got: ${JSON.stringify(repoCCrossReference.errors)}`, - ); - - const repoBMemoryCommit = repository.createMemoryCommit({ - scope_id: initializedB.working_scope_id, - title: "Seed repo B", - }); - repository.createProposalRecords(initializedB.working_scope_id, repoBMemoryCommit.id, normalizeProposal(repoBProposal)); - - const repoBDuplicateResult = await service.validateProposal(repoB, repoBProposal); - assert.equal(repoBDuplicateResult.valid, false, "repo B must still reject duplicate IDs inside the same repo"); - assert.ok( - repoBDuplicateResult.errors.includes("component:component.cli already exists."), - `expected repo B duplicate error, got: ${JSON.stringify(repoBDuplicateResult.errors)}`, - ); - - const repoAGraph = service.readGraph(repoA); - const repoBGraph = service.readGraph(repoB); - assert.deepEqual(repoAGraph.components.map((component) => component.name), ["Repo A CLI"]); - assert.deepEqual(repoBGraph.components.map((component) => component.name), ["Repo B CLI"]); - assert.equal(initializedB.repo_id === initializedA.repo_id, false, "test repos must be distinct"); -} finally { - db.close(); -} - -console.log("check-proposal-validate: ok"); diff --git a/scripts/check-transcript-bundle.js b/scripts/check-transcript-bundle.js deleted file mode 100644 index b16333f..0000000 --- a/scripts/check-transcript-bundle.js +++ /dev/null @@ -1,272 +0,0 @@ -import assert from "node:assert/strict"; -import { execFileSync } from "node:child_process"; -import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; -import { join, dirname } from "node:path"; -import { tmpdir } from "node:os"; -import { fileURLToPath } from "node:url"; - -const root = new URL("..", import.meta.url); -const cliPath = fileURLToPath(new URL("dist/apps/cli/main.js", root)); -const tmp = mkdtempSync(join(tmpdir(), "greplica-transcript-bundle-test-")); - -const codexOne = join(tmp, "codex-one.jsonl"); -const codexTwo = join(tmp, "codex-two.jsonl"); -const claudeOne = join(tmp, "claude-one.jsonl"); -const copilotOne = join(tmp, "copilot-one.jsonl"); -const codexOut = join(tmp, "codex-bundle.md"); -const claudeOut = join(tmp, "claude-bundle.md"); -const copilotOut = join(tmp, "copilot-bundle.md"); -const opencodeOut = join(tmp, "opencode-bundle.md"); - -writeFileSync( - codexOne, - [ - JSON.stringify({ - type: "session_meta", - payload: { - id: "codex-session-one", - timestamp: "2026-06-25T00:00:00.000Z", - cwd: "/repo/example", - }, - }), - JSON.stringify({ - timestamp: "2026-06-25T00:01:00.000Z", - type: "event_msg", - payload: { - type: "user_message", - message: "Remember this durable Codex insight. do not keep this", - }, - }), - ].join("\n"), - "utf8", -); - -writeFileSync( - codexTwo, - [ - JSON.stringify({ - type: "session_meta", - payload: { - id: "codex-session-two", - cwd: "/repo/example", - }, - }), - JSON.stringify({ - timestamp: "2026-06-25T00:02:00.000Z", - type: "event_msg", - payload: { - type: "agent_message", - message: "A second Codex transcript fact. drop this", - }, - }), - ].join("\n"), - "utf8", -); - -writeFileSync( - claudeOne, - [ - JSON.stringify({ - type: "user", - sessionId: "claude-session-one", - cwd: "/repo/example", - timestamp: "2026-06-25T00:03:00.000Z", - message: { - role: "user", - content: [ - { - type: "text", - text: "Remember this durable Claude insight.", - }, - ], - }, - }), - ].join("\n"), - "utf8", -); - -writeFileSync( - copilotOne, - [ - JSON.stringify({ - type: "session.start", - data: { - sessionId: "copilot-session-one", - copilotVersion: "1.0.66", - context: { - cwd: "/repo/example", - repository: "Autoloops/greplica", - branch: "copilot-test", - }, - }, - timestamp: "2026-06-25T00:03:30.000Z", - }), - JSON.stringify({ - type: "session.model_change", - data: { - newModel: "claude-haiku-4.5", - }, - timestamp: "2026-06-25T00:03:45.000Z", - }), - JSON.stringify({ - session_id: "copilot-session-one", - cwd: "/repo/example", - timestamp: "2026-06-25T00:04:00.000Z", - role: "user", - content: "Remember this durable Copilot insight. remove this", - }), - JSON.stringify({ - session_id: "copilot-session-one", - cwd: "/repo/example", - timestamp: "2026-06-25T00:05:00.000Z", - role: "assistant", - content: [{ type: "text", text: "A Copilot assistant fact." }], - }), - ].join("\n"), - "utf8", -); - -const codexOutput = execFileSync( - process.execPath, - [ - cliPath, - "transcript", - "bundle", - "--platform", - "codex", - "--file", - codexOne, - "--file", - codexTwo, - "--out", - codexOut, - ], - { encoding: "utf8" }, -); -const codexBundle = readFileSync(codexOut, "utf8"); -assert.match(codexOutput, /Wrote transcript bundle/); -assert.match(codexOutput, /codex-session:codex-session-one/); -assert.match(codexOutput, /codex-session:codex-session-two/); -assert.match(codexBundle, /file_count: 2/); -assert.match(codexBundle, /session_ref: codex-session:codex-session-one/); -assert.match(codexBundle, /session_ref: codex-session:codex-session-two/); -assert.match(codexBundle, /Remember this durable Codex insight/); -assert.match(codexBundle, /A second Codex transcript fact/); -assert.doesNotMatch(codexBundle, /do not keep this/); -assert.doesNotMatch(codexBundle, /drop this/); - -const claudeOutput = execFileSync( - process.execPath, - [ - cliPath, - "transcript", - "bundle", - "--platform", - "claude", - "--file", - claudeOne, - "--out", - claudeOut, - ], - { encoding: "utf8" }, -); -const claudeBundle = readFileSync(claudeOut, "utf8"); -assert.match(claudeOutput, /claude-code-session:claude-session-one/); -assert.match(claudeBundle, /session_ref: claude-code-session:claude-session-one/); -assert.match(claudeBundle, /Remember this durable Claude insight/); - -const copilotOutput = execFileSync( - process.execPath, - [ - cliPath, - "transcript", - "bundle", - "--platform", - "copilot", - "--file", - copilotOne, - "--out", - copilotOut, - ], - { encoding: "utf8" }, -); -const copilotBundle = readFileSync(copilotOut, "utf8"); -assert.match(copilotOutput, /copilot-session:copilot-session-one/); -assert.match(copilotBundle, /session_ref: copilot-session:copilot-session-one/); -assert.match(copilotBundle, /repository: Autoloops\/greplica/); -assert.match(copilotBundle, /branch: copilot-test/); -assert.match(copilotBundle, /Remember this durable Copilot insight/); -assert.match(copilotBundle, /A Copilot assistant fact/); -assert.doesNotMatch(copilotBundle, /remove this/); - -assert.throws( - () => - execFileSync( - process.execPath, - [cliPath, "transcript", "bundle", "--platform", "codex", "--file", join(tmp, "missing.jsonl"), "--out", join(tmp, "missing.md")], - { encoding: "utf8", stdio: "pipe" }, - ), - /Transcript file does not exist/, -); - -const opencodeDataHome = join(tmp, "opencode-data"); -const opencodeSessionId = "opencode-session-one"; -const opencodeSessionFile = join(opencodeDataHome, "opencode", "storage", "session", `${opencodeSessionId}.json`); -mkdirSync(dirname(opencodeSessionFile), { recursive: true }); -mkdirSync(join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId), { recursive: true }); -writeFileSync( - opencodeSessionFile, - JSON.stringify({ - id: opencodeSessionId, - directory: "/repo/example", - }), - "utf8", -); -writeFileSync( - join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId, "msg-01.json"), - JSON.stringify({ - role: "user", - content: "Remember this durable OpenCode insight. remove this", - time: "2026-06-25T00:06:00.000Z", - }), - "utf8", -); -writeFileSync( - join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId, "msg-02.json"), - JSON.stringify({ - role: "assistant", - parts: [{ text: "An OpenCode assistant fact." }], - time: "2026-06-25T00:07:00.000Z", - }), - "utf8", -); - -const opencodeOutput = execFileSync( - process.execPath, - [ - cliPath, - "transcript", - "bundle", - "--platform", - "opencode", - "--file", - opencodeSessionFile, - "--out", - opencodeOut, - ], - { - encoding: "utf8", - env: { - ...process.env, - XDG_DATA_HOME: opencodeDataHome, - }, - }, -); -const opencodeBundle = readFileSync(opencodeOut, "utf8"); -assert.match(opencodeOutput, /opencode-session:opencode-session-one/); -assert.match(opencodeBundle, /session_ref: opencode-session:opencode-session-one/); -assert.match(opencodeBundle, /Remember this durable OpenCode insight/); -assert.match(opencodeBundle, /An OpenCode assistant fact/); -assert.doesNotMatch(opencodeBundle, /remove this/); - -console.log("Transcript bundle checks passed."); diff --git a/tests/integration/bm25-tokenizer.test.ts b/tests/integration/bm25-tokenizer.test.ts new file mode 100644 index 0000000..cc54b2f --- /dev/null +++ b/tests/integration/bm25-tokenizer.test.ts @@ -0,0 +1,53 @@ +import { describe, test, expect, beforeAll } from 'vitest'; + +let tokenize: (text: string) => string[]; +let scoreBm25: (query: string, documents: { key: string; text: string }[], config: unknown) => { id: string }[]; +let graphContextConfig: unknown; + +beforeAll(async () => { + const bm25 = await import(new URL('../../dist/libs/knowledge-graph/graph-context/bm25.js', import.meta.url).href); + const config = await import(new URL('../../dist/libs/knowledge-graph/graph-context/config.js', import.meta.url).href); + tokenize = bm25.tokenize; + scoreBm25 = bm25.scoreBm25; + graphContextConfig = config.graphContextConfig; +}); + +describe('BM25 tokenizer', () => { + test('camelCase emits sub-tokens and keeps full lowercased token', () => { + const tokens = tokenize('handleUserAuth'); + expect(tokens).toContain('user'); + expect(tokens).toContain('auth'); + expect(tokens).toContain('handle'); + expect(tokens).toContain('handleuserauth'); + }); + + test('PascalCase emits sub-tokens', () => { + expect(tokenize('HandleUserAuth')).toContain('user'); + }); + + test('snake_case emits sub-tokens and keeps original token', () => { + const tokens = tokenize('handle_user_auth'); + expect(tokens).toContain('user'); + expect(tokens).toContain('handle_user_auth'); + }); + + test('kebab-case emits sub-tokens', () => { + expect(tokenize('graph-context')).toContain('graph'); + expect(tokenize('graph-context')).toContain('context'); + }); + + test('letter-number boundary emits sub-tokens', () => { + expect(tokenize('user2FA')).toContain('user'); + expect(tokenize('user2FA')).toContain('fa'); + }); + + test('English stemming variants still apply', () => { + expect(tokenize('tokens')).toContain('token'); + }); + + test('BM25 matches camelCase doc tokens to spaced query terms', () => { + const documents = [{ key: 'doc:auth', text: 'The handleUserAuth function validates sessions.' }]; + const ranked = scoreBm25('user auth validation', documents, graphContextConfig); + expect(ranked[0]?.id).toBe('doc:auth'); + }); +}); diff --git a/tests/integration/install-options.test.ts b/tests/integration/install-options.test.ts index cd9827b..6c3bf6b 100644 --- a/tests/integration/install-options.test.ts +++ b/tests/integration/install-options.test.ts @@ -27,6 +27,7 @@ function installInTempRepo(tmp: string, name: string, flags: string[], platform const greplicaHome = join(tmp, name, 'greplica-home'); const codexHome = join(tmp, name, 'codex-home'); const copilotHome = join(tmp, name, 'copilot-home'); + const xdgConfigHome = join(tmp, name, 'xdg-config-home'); mkdirSync(repo, { recursive: true }); execFileSync('git', ['init', '--quiet'], { cwd: repo, encoding: 'utf8' }); @@ -35,7 +36,7 @@ function installInTempRepo(tmp: string, name: string, flags: string[], platform GREPLICA_HOME: greplicaHome, CODEX_HOME: codexHome, COPILOT_HOME: copilotHome, - XDG_CONFIG_HOME: join(tmp, name, 'xdg-config-home'), + XDG_CONFIG_HOME: xdgConfigHome, GREPLICA_INSTALL_SKIP_PREWARM: '1', }; @@ -46,7 +47,7 @@ function installInTempRepo(tmp: string, name: string, flags: string[], platform }); execFileSync(process.execPath, [cli, 'doctor'], { cwd: repo, encoding: 'utf8', env }); - return { repo, greplicaHome, codexHome, copilotHome, output, env }; + return { repo, greplicaHome, codexHome, copilotHome, xdgConfigHome, output, env }; } describe('install options', () => { @@ -98,13 +99,15 @@ describe('install options', () => { expect(readConfig(noHooks.greplicaHome).session.autoMemoryUpdates).toBe(false); }); - test('opencode does not support hooks but installs skills', () => { + test('opencode installs hooks and enables auto memory updates', () => { const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); - const unsupportedHooks = installInTempRepo(tmp, 'unsupported-hooks', ['--hooks', 'enabled', '--auto-memory', 'enabled'], 'opencode'); + const opencodeHooks = installInTempRepo(tmp, 'opencode-hooks', ['--hooks', 'enabled', '--auto-memory', 'enabled'], 'opencode'); - expect(unsupportedHooks.output).toMatch(/Hooks: not installed for this platform\./); - expect(unsupportedHooks.output).toMatch(/Automatic memory updates: disabled\./); - expect(readConfig(unsupportedHooks.greplicaHome).session.autoMemoryUpdates).toBe(false); + expect(opencodeHooks.output).toMatch(/Installed Greplica for OpenCode\./); + expect(opencodeHooks.output).toMatch(/Hooks: installed for UserPromptSubmit, Stop\./); + expect(opencodeHooks.output).toMatch(/Automatic memory updates: enabled\./); + expect(existsSync(join(opencodeHooks.xdgConfigHome, 'opencode', 'hooks.json'))).toBe(true); + expect(readConfig(opencodeHooks.greplicaHome).session.autoMemoryUpdates).toBe(true); }); test('copilot installs hooks and enables auto memory updates', () => { diff --git a/tests/integration/proposal-validate.test.ts b/tests/integration/proposal-validate.test.ts index a8da61a..68f015f 100644 --- a/tests/integration/proposal-validate.test.ts +++ b/tests/integration/proposal-validate.test.ts @@ -1,13 +1,25 @@ import { describe, test, expect, beforeAll } from 'vitest'; +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; let normalizeProposal: (input: unknown) => unknown; let validateProposal: (proposal: unknown) => { valid: boolean; errors: string[] }; +let KnowledgeGraphService: new (repository: unknown) => unknown; +let openDatabase: (path: string) => { close: () => void }; +let SqliteRepository: new (db: unknown) => unknown; beforeAll(async () => { const proposal = await import(new URL('../../dist/libs/knowledge-graph/proposal.js', import.meta.url).href); const validate = await import(new URL('../../dist/libs/knowledge-graph/validate-proposal.js', import.meta.url).href); + const service = await import(new URL('../../dist/libs/knowledge-graph/service.js', import.meta.url).href); + const db = await import(new URL('../../dist/libs/storage/sqlite/db.js', import.meta.url).href); + const repo = await import(new URL('../../dist/libs/storage/sqlite/repository.js', import.meta.url).href); normalizeProposal = proposal.normalizeProposal; validateProposal = validate.validateProposal; + KnowledgeGraphService = service.KnowledgeGraphService; + openDatabase = db.openDatabase; + SqliteRepository = repo.SqliteRepository; }); const components = [ @@ -65,3 +77,84 @@ describe('proposal validation', () => { expect(compactResult.valid).toBe(true); }); }); + +describe('repo-scoped graph objects', () => { + test('repos are distinct and cross-repo compact references are rejected', async () => { + const tmp = mkdtempSync(join(tmpdir(), 'greplica-proposal-validate-test-')); + const db = openDatabase(join(tmp, 'graph.db')); + + try { + const repository = new SqliteRepository(db); + const service = new KnowledgeGraphService(repository); + const repoA = { repo_root: join(tmp, 'repo-a'), repo_name: 'repo-a', default_branch: 'main' }; + const repoB = { repo_root: join(tmp, 'repo-b'), repo_name: 'repo-b', default_branch: 'main' }; + const repoC = { repo_root: join(tmp, 'repo-c'), repo_name: 'repo-c', default_branch: 'main' }; + const repoAProposal = { + title: 'Seed CLI component', + creates: { components: [{ id: 'component.cli', name: 'Repo A CLI' }] }, + }; + const repoBProposal = { + title: 'Seed CLI component', + creates: { components: [{ id: 'component.cli', name: 'Repo B CLI' }] }, + }; + + const initializedA = (service as any).initRepo(repoA); + const initializedB = (service as any).initRepo(repoB); + (service as any).initRepo(repoC); + const memoryCommit = (repository as any).createMemoryCommit({ + scope_id: initializedA.working_scope_id, + title: 'Seed repo A', + }); + (repository as any).createProposalRecords( + initializedA.working_scope_id, + memoryCommit.id, + normalizeProposal(repoAProposal), + ); + + const repoBResult = await (service as any).validateProposal(repoB, repoBProposal); + expect(repoBResult.valid).toBe(true); + + const repoCCrossReference = await (service as any).validateProposal(repoC, { + title: 'Repo C cross-repo compact reference', + creates: { + claims: [ + { + id: 'claim.about_cli', + kind: 'fact', + text: 'Repo C references a CLI component.', + truth: 'unknown', + intent: 'unknown', + about: 'component.cli', + }, + ], + }, + }); + expect(repoCCrossReference.valid).toBe(false); + expect( + repoCCrossReference.errors.some((error: string) => error.includes('component:component.cli')), + ).toBe(true); + + const repoBMemoryCommit = (repository as any).createMemoryCommit({ + scope_id: initializedB.working_scope_id, + title: 'Seed repo B', + }); + (repository as any).createProposalRecords( + initializedB.working_scope_id, + repoBMemoryCommit.id, + normalizeProposal(repoBProposal), + ); + + const repoBDuplicateResult = await (service as any).validateProposal(repoB, repoBProposal); + expect(repoBDuplicateResult.valid).toBe(false); + expect(repoBDuplicateResult.errors.includes('component:component.cli already exists.')).toBe(true); + + const repoAGraph = (service as any).readGraph(repoA); + const repoBGraph = (service as any).readGraph(repoB); + expect(repoAGraph.components.map((c: { name: string }) => c.name)).toEqual(['Repo A CLI']); + expect(repoBGraph.components.map((c: { name: string }) => c.name)).toEqual(['Repo B CLI']); + expect(initializedB.repo_id === initializedA.repo_id).toBe(false); + } finally { + db.close(); + } + }); +}); diff --git a/tests/integration/transcript-bundle.test.ts b/tests/integration/transcript-bundle.test.ts index 424b3d2..30074a0 100644 --- a/tests/integration/transcript-bundle.test.ts +++ b/tests/integration/transcript-bundle.test.ts @@ -1,8 +1,8 @@ import { describe, test, expect } from 'vitest'; import { execFileSync } from 'node:child_process'; -import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = fileURLToPath(new URL('../..', import.meta.url)); @@ -159,16 +159,52 @@ describe('transcript bundle', () => { ).toThrow(/Transcript file does not exist/); }); - test('errors on unsupported opencode platform', () => { + test('bundles opencode transcripts and strips system instructions', () => { const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); - const codexOne = join(tmp, 'opencode-one.jsonl'); - writeFileSync(codexOne, '{}', 'utf8'); - expect(() => - execFileSync( - process.execPath, - [cli, 'transcript', 'bundle', '--platform', 'opencode', '--file', codexOne, '--out', join(tmp, 'opencode-bundle.md')], - { encoding: 'utf8', stdio: 'pipe' }, - ), - ).toThrow(/OpenCode transcript projection is not supported yet/); + const opencodeDataHome = join(tmp, 'opencode-data'); + const opencodeSessionId = 'opencode-session-one'; + const opencodeSessionFile = join(opencodeDataHome, 'opencode', 'storage', 'session', `${opencodeSessionId}.json`); + const opencodeOut = join(tmp, 'opencode-bundle.md'); + + mkdirSync(dirname(opencodeSessionFile), { recursive: true }); + mkdirSync(join(opencodeDataHome, 'opencode', 'storage', 'message', opencodeSessionId), { recursive: true }); + + writeFileSync( + opencodeSessionFile, + JSON.stringify({ id: opencodeSessionId, directory: '/repo/example' }), + 'utf8', + ); + writeFileSync( + join(opencodeDataHome, 'opencode', 'storage', 'message', opencodeSessionId, 'msg-01.json'), + JSON.stringify({ + role: 'user', + content: 'Remember this durable OpenCode insight. remove this', + time: '2026-06-25T00:06:00.000Z', + }), + 'utf8', + ); + writeFileSync( + join(opencodeDataHome, 'opencode', 'storage', 'message', opencodeSessionId, 'msg-02.json'), + JSON.stringify({ + role: 'assistant', + parts: [{ text: 'An OpenCode assistant fact.' }], + time: '2026-06-25T00:07:00.000Z', + }), + 'utf8', + ); + + const output = execFileSync( + process.execPath, + [cli, 'transcript', 'bundle', '--platform', 'opencode', '--file', opencodeSessionFile, '--out', opencodeOut], + { encoding: 'utf8', env: { ...process.env, XDG_DATA_HOME: opencodeDataHome } }, + ); + + const bundle = readFileSync(opencodeOut, 'utf8'); + + expect(output).toMatch(/opencode-session:opencode-session-one/); + expect(bundle).toMatch(/session_ref: opencode-session:opencode-session-one/); + expect(bundle).toMatch(/Remember this durable OpenCode insight/); + expect(bundle).toMatch(/An OpenCode assistant fact/); + expect(bundle).not.toMatch(/remove this/); }); }); From 111645e83873b00b3911a4e660caa492a5d56c1c Mon Sep 17 00:00:00 2001 From: dawgdevv Date: Tue, 7 Jul 2026 01:07:54 +0530 Subject: [PATCH 4/6] feat(tests): add comprehensive checks for tokenizer, graph view, install options, proposal validation, repo context, and transcript bundling - Introduced new test scripts to validate BM25 tokenization for various naming conventions. - Added checks for graph view rendering and component initialization. - Implemented tests for install options, ensuring correct behavior for hooks and auto memory updates. - Validated proposal normalization and edge cases in proposal validation. - Established repo context detection tests for different repository setups. - Created transcript bundling tests for multiple platforms, ensuring proper handling of session data and instructions. --- scripts/check-bm25-tokenizer.js | 28 +++ scripts/check-graph-view.js | 48 +++++ scripts/check-install-options.js | 141 +++++++++++++++ scripts/check-proposal-validate.js | 160 +++++++++++++++++ scripts/check-repo-context.js | 58 ++++++ scripts/check-transcript-bundle.js | 272 +++++++++++++++++++++++++++++ 6 files changed, 707 insertions(+) create mode 100644 scripts/check-bm25-tokenizer.js create mode 100644 scripts/check-graph-view.js create mode 100644 scripts/check-install-options.js create mode 100644 scripts/check-proposal-validate.js create mode 100644 scripts/check-repo-context.js create mode 100644 scripts/check-transcript-bundle.js diff --git a/scripts/check-bm25-tokenizer.js b/scripts/check-bm25-tokenizer.js new file mode 100644 index 0000000..f880b45 --- /dev/null +++ b/scripts/check-bm25-tokenizer.js @@ -0,0 +1,28 @@ +import assert from "node:assert/strict"; + +const root = new URL("..", import.meta.url); +const { tokenize, scoreBm25 } = await import(new URL("dist/libs/knowledge-graph/graph-context/bm25.js", root)); +const { graphContextConfig } = await import(new URL("dist/libs/knowledge-graph/graph-context/config.js", root)); + +assert.ok(tokenize("handleUserAuth").includes("user"), "camelCase should emit sub-token user"); +assert.ok(tokenize("handleUserAuth").includes("auth"), "camelCase should emit sub-token auth"); +assert.ok(tokenize("handleUserAuth").includes("handle"), "camelCase should emit sub-token handle"); +assert.ok(tokenize("handleUserAuth").includes("handleuserauth"), "camelCase should keep full lowercased token"); + +assert.ok(tokenize("HandleUserAuth").includes("user"), "PascalCase should emit sub-token user"); +assert.ok(tokenize("handle_user_auth").includes("user"), "snake_case should emit sub-token user"); +assert.ok(tokenize("handle_user_auth").includes("handle_user_auth"), "snake_case should keep original token"); + +assert.ok(tokenize("graph-context").includes("graph"), "kebab-case should emit sub-token graph"); +assert.ok(tokenize("graph-context").includes("context"), "kebab-case should emit sub-token context"); + +assert.ok(tokenize("user2FA").includes("user"), "letter-number boundary should emit user"); +assert.ok(tokenize("user2FA").includes("fa"), "letter-number boundary should emit fa"); + +assert.ok(tokenize("tokens").includes("token"), "English stemming variants should still apply"); + +const documents = [{ key: "doc:auth", text: "The handleUserAuth function validates sessions." }]; +const ranked = scoreBm25("user auth validation", documents, graphContextConfig); +assert.equal(ranked[0]?.id, "doc:auth", "BM25 should match camelCase doc tokens to spaced query terms"); + +console.log("BM25 tokenizer checks passed."); diff --git a/scripts/check-graph-view.js b/scripts/check-graph-view.js new file mode 100644 index 0000000..11661ed --- /dev/null +++ b/scripts/check-graph-view.js @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const root = new URL("..", import.meta.url); +const { openDatabase } = await import(new URL("dist/libs/storage/sqlite/db.js", root)); +const { SqliteRepository } = await import(new URL("dist/libs/storage/sqlite/repository.js", root)); +const { KnowledgeGraphService } = await import(new URL("dist/libs/knowledge-graph/service.js", root)); + +const tmp = mkdtempSync(join(tmpdir(), "greplica-graph-view-test-")); +const db = openDatabase(join(tmp, "graph.db")); + +try { + const repository = new SqliteRepository(db); + const service = new KnowledgeGraphService(repository); + const repo = { + repo_root: join(tmp, "repo"), + repo_name: "graph-view-null-anchor", + default_branch: "main", + }; + + const initialized = service.initRepo(repo); + const memoryCommit = repository.createMemoryCommit({ + scope_id: initialized.working_scope_id, + title: "Seed null component anchor", + }); + + repository.createProposalRecords(initialized.working_scope_id, memoryCommit.id, { + title: "Seed null component anchor", + creates: { + components: [ + { + id: "component.no_anchor", + name: "Component Without Anchor", + }, + ], + }, + }); + + const html = service.buildGraphView(repo); + assert.match(html, /Component Without Anchor/); + assert.match(html, /Greplica graph view/); +} finally { + db.close(); +} + +console.log("Graph view checks passed."); diff --git a/scripts/check-install-options.js b/scripts/check-install-options.js new file mode 100644 index 0000000..ccb1654 --- /dev/null +++ b/scripts/check-install-options.js @@ -0,0 +1,141 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = new URL("..", import.meta.url); +const cliPath = fileURLToPath(new URL("dist/apps/cli/main.js", root)); +const { greplicaHookGuidance } = await import(new URL("dist/libs/hooks/guidance.js", root)); +const { shouldRunAutoMemoryUpdates } = await import(new URL("dist/libs/hooks/worker.js", root)); + +const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); + +const autoSave = installInTempRepo("auto-save", ["--hooks", "enabled", "--auto-memory", "enabled"]); +assert.match(autoSave.output, /Hooks: installed for UserPromptSubmit, Stop\./); +assert.match(autoSave.output, /Automatic memory updates: enabled\./); +assert.ok(existsSync(join(autoSave.codexHome, "hooks.json"))); +assert.equal(readConfig(autoSave.greplicaHome).session.autoMemoryUpdates, true); +assert.equal(shouldRunAutoMemoryUpdates(readConfig(autoSave.greplicaHome)), true); + +const guidanceOnly = installInTempRepo("guidance-only", ["--hooks", "enabled", "--auto-memory", "disabled"]); +assert.match(guidanceOnly.output, /Hooks: installed for UserPromptSubmit, Stop\./); +assert.match(guidanceOnly.output, /Automatic memory updates: disabled\./); +assert.ok(existsSync(join(guidanceOnly.codexHome, "hooks.json"))); +assert.equal(readConfig(guidanceOnly.greplicaHome).session.autoMemoryUpdates, false); +assert.equal(shouldRunAutoMemoryUpdates(readConfig(guidanceOnly.greplicaHome)), false); + +const hookOutput = execFileSync( + process.execPath, + [cliPath, "hook", "ingest", "--platform", "codex"], + { + cwd: guidanceOnly.repo, + encoding: "utf8", + input: JSON.stringify({ + hook_event_name: "UserPromptSubmit", + session_id: "guidance-only-session", + cwd: guidanceOnly.repo, + }), + env: guidanceOnly.env, + }, +); +assert.match(hookOutput, /Greplica hook guidance/); +assert.match(hookOutput, /greplica graph context/); + +const noHooks = installInTempRepo("no-hooks", ["--hooks", "disabled"]); +assert.match(noHooks.output, /Hooks: not installed\./); +assert.match(noHooks.output, /Automatic memory updates: disabled\./); +assert.match(noHooks.output, /To give future agents Greplica guidance without hooks/); +assert.ok(noHooks.output.includes(greplicaHookGuidance)); +assert.equal(existsSync(join(noHooks.codexHome, "hooks.json")), false); +assert.equal(readConfig(noHooks.greplicaHome).session.autoMemoryUpdates, false); + +const opencodeHooks = installInTempRepo("opencode-hooks", ["--hooks", "enabled", "--auto-memory", "enabled"], "opencode"); +assert.match(opencodeHooks.output, /Installed Greplica for OpenCode\./); +assert.match(opencodeHooks.output, /Hooks: installed for UserPromptSubmit, Stop\./); +assert.match(opencodeHooks.output, /Automatic memory updates: enabled\./); +assert.ok(existsSync(join(opencodeHooks.xdgConfigHome, "opencode", "hooks.json"))); +assert.equal(readConfig(opencodeHooks.greplicaHome).session.autoMemoryUpdates, true); + +const copilotHooks = installInTempRepo("copilot-hooks", ["--hooks", "enabled", "--auto-memory", "enabled"], "copilot"); +assert.match(copilotHooks.output, /Installed Greplica for GitHub Copilot CLI\./); +assert.match(copilotHooks.output, /Hooks: installed for SessionStart, Stop\./); +assert.match(copilotHooks.output, /Automatic memory updates: enabled\./); +assert.ok(existsSync(join(copilotHooks.copilotHome, "hooks", "greplica.json"))); +assert.equal(readConfig(copilotHooks.greplicaHome).session.autoMemoryUpdates, true); + +const invalid = spawnSync( + process.execPath, + [ + cliPath, + "install", + "--platform", + "codex", + "--embedding", + "local", + "--hooks", + "disabled", + "--auto-memory", + "enabled", + ], + { + cwd: noHooks.repo, + encoding: "utf8", + env: noHooks.env, + }, +); +assert.notEqual(invalid.status, 0); +assert.match(invalid.stderr, /--auto-memory enabled requires --hooks enabled/); + +const invalidValue = spawnSync( + process.execPath, + [cliPath, "install", "--platform", "codex", "--embedding", "local", "--hooks", "sometimes"], + { + cwd: noHooks.repo, + encoding: "utf8", + env: noHooks.env, + }, +); +assert.notEqual(invalidValue.status, 0); +assert.match(invalidValue.stderr, /expected enabled or disabled/); + +console.log("Install option checks passed."); + +function installInTempRepo(name, flags, platform = "codex") { + const repo = join(tmp, name, "repo"); + const greplicaHome = join(tmp, name, "greplica-home"); + const codexHome = join(tmp, name, "codex-home"); + const copilotHome = join(tmp, name, "copilot-home"); + const xdgConfigHome = join(tmp, name, "xdg-config-home"); + mkdirSync(repo, { recursive: true }); + execFileSync("git", ["init", "--quiet"], { cwd: repo, encoding: "utf8" }); + + const env = { + ...process.env, + GREPLICA_HOME: greplicaHome, + CODEX_HOME: codexHome, + COPILOT_HOME: copilotHome, + XDG_CONFIG_HOME: xdgConfigHome, + GREPLICA_INSTALL_SKIP_PREWARM: "1", + }; + const output = execFileSync( + process.execPath, + [cliPath, "install", "--platform", platform, "--embedding", "local", ...flags], + { + cwd: repo, + encoding: "utf8", + env, + }, + ); + execFileSync(process.execPath, [cliPath, "doctor"], { + cwd: repo, + encoding: "utf8", + env, + }); + return { repo, greplicaHome, codexHome, copilotHome, xdgConfigHome, output, env }; +} + +function readConfig(greplicaHome) { + return JSON.parse(readFileSync(join(greplicaHome, "config.json"), "utf8")); +} diff --git a/scripts/check-proposal-validate.js b/scripts/check-proposal-validate.js new file mode 100644 index 0000000..a1f6b85 --- /dev/null +++ b/scripts/check-proposal-validate.js @@ -0,0 +1,160 @@ +import assert from "node:assert/strict"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const root = new URL("..", import.meta.url); +const { normalizeProposal } = await import(new URL("dist/libs/knowledge-graph/proposal.js", root)); +const { validateProposal } = await import(new URL("dist/libs/knowledge-graph/validate-proposal.js", root)); +const { KnowledgeGraphService } = await import(new URL("dist/libs/knowledge-graph/service.js", root)); +const { openDatabase } = await import(new URL("dist/libs/storage/sqlite/db.js", root)); +const { SqliteRepository } = await import(new URL("dist/libs/storage/sqlite/repository.js", root)); + +const components = [ + { id: "component.a", name: "A" }, + { id: "component.b", name: "B" }, +]; + +// Edge written with canonical field names but no types (issue #82) must not crash +// normalization, and must surface as a validation error. +const malformedEdge = { + title: "Malformed edge repro", + creates: { + components, + edges: [{ from_id: "component.a", to_id: "component.b", kind: "contains" }], + }, +}; + +let normalized; +assert.doesNotThrow(() => { + normalized = normalizeProposal(malformedEdge); +}, "normalizeProposal must not throw on a malformed edge"); + +const malformedResult = validateProposal(normalized); +assert.equal(malformedResult.valid, false, "malformed edge proposal must be invalid"); +assert.ok( + malformedResult.errors.some((error) => error.includes("from'/'to")), + `expected a from/to guidance error, got: ${JSON.stringify(malformedResult.errors)}`, +); + +// Edge missing kind must also be reported, not crash. +const missingKindEdge = { + title: "Missing kind repro", + creates: { + components, + edges: [{ from: "component.a", to: "component.b" }], + }, +}; + +let normalizedMissingKind; +assert.doesNotThrow(() => { + normalizedMissingKind = normalizeProposal(missingKindEdge); +}, "normalizeProposal must not throw on an edge without kind"); +assert.equal(validateProposal(normalizedMissingKind).valid, false, "edge without kind must be invalid"); + +// A well-formed compact edge must keep validating cleanly. +const compactEdge = { + title: "Compact edge", + creates: { + components, + edges: [{ kind: "contains", from: "component.a", to: "component.b" }], + }, +}; + +const compactResult = validateProposal(normalizeProposal(compactEdge)); +assert.equal(compactResult.valid, true, `compact edge must stay valid, got: ${JSON.stringify(compactResult.errors)}`); + +const tmp = mkdtempSync(join(tmpdir(), "greplica-proposal-validate-test-")); +const db = openDatabase(join(tmp, "graph.db")); + +try { + const repository = new SqliteRepository(db); + const service = new KnowledgeGraphService(repository); + const repoA = { + repo_root: join(tmp, "repo-a"), + repo_name: "repo-a", + default_branch: "main", + }; + const repoB = { + repo_root: join(tmp, "repo-b"), + repo_name: "repo-b", + default_branch: "main", + }; + const repoC = { + repo_root: join(tmp, "repo-c"), + repo_name: "repo-c", + default_branch: "main", + }; + const repoAProposal = { + title: "Seed CLI component", + creates: { + components: [{ id: "component.cli", name: "Repo A CLI" }], + }, + }; + const repoBProposal = { + title: "Seed CLI component", + creates: { + components: [{ id: "component.cli", name: "Repo B CLI" }], + }, + }; + + const initializedA = service.initRepo(repoA); + const initializedB = service.initRepo(repoB); + service.initRepo(repoC); + const memoryCommit = repository.createMemoryCommit({ + scope_id: initializedA.working_scope_id, + title: "Seed repo A", + }); + repository.createProposalRecords(initializedA.working_scope_id, memoryCommit.id, normalizeProposal(repoAProposal)); + + const repoBResult = await service.validateProposal(repoB, repoBProposal); + assert.equal( + repoBResult.valid, + true, + `repo B must be able to create its own component.cli, got: ${JSON.stringify(repoBResult.errors)}`, + ); + + const repoCCrossReference = await service.validateProposal(repoC, { + title: "Repo C cross-repo compact reference", + creates: { + claims: [ + { + id: "claim.about_cli", + kind: "fact", + text: "Repo C references a CLI component.", + truth: "unknown", + intent: "unknown", + about: "component.cli", + }, + ], + }, + }); + assert.equal(repoCCrossReference.valid, false, "repo C must not resolve compact references through repo A"); + assert.ok( + repoCCrossReference.errors.some((error) => error.includes("component:component.cli")), + `expected repo C to report missing component.cli, got: ${JSON.stringify(repoCCrossReference.errors)}`, + ); + + const repoBMemoryCommit = repository.createMemoryCommit({ + scope_id: initializedB.working_scope_id, + title: "Seed repo B", + }); + repository.createProposalRecords(initializedB.working_scope_id, repoBMemoryCommit.id, normalizeProposal(repoBProposal)); + + const repoBDuplicateResult = await service.validateProposal(repoB, repoBProposal); + assert.equal(repoBDuplicateResult.valid, false, "repo B must still reject duplicate IDs inside the same repo"); + assert.ok( + repoBDuplicateResult.errors.includes("component:component.cli already exists."), + `expected repo B duplicate error, got: ${JSON.stringify(repoBDuplicateResult.errors)}`, + ); + + const repoAGraph = service.readGraph(repoA); + const repoBGraph = service.readGraph(repoB); + assert.deepEqual(repoAGraph.components.map((component) => component.name), ["Repo A CLI"]); + assert.deepEqual(repoBGraph.components.map((component) => component.name), ["Repo B CLI"]); + assert.equal(initializedB.repo_id === initializedA.repo_id, false, "test repos must be distinct"); +} finally { + db.close(); +} + +console.log("check-proposal-validate: ok"); diff --git a/scripts/check-repo-context.js b/scripts/check-repo-context.js new file mode 100644 index 0000000..410a9eb --- /dev/null +++ b/scripts/check-repo-context.js @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, realpathSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, join } from "node:path"; + +const root = new URL("..", import.meta.url); +const { detectRepoContext } = await import(new URL("dist/apps/cli/repo-context.js", root)); +const tmp = mkdtempSync(join(tmpdir(), "greplica-repo-context-test-")); + +const nonGitFolder = join(tmp, "plain-folder"); +mkdirSync(nonGitFolder); + +const fallbackContext = detectRepoContext(nonGitFolder); +assert.equal(fallbackContext.repo_root, realpathSync(nonGitFolder)); +assert.equal(fallbackContext.repo_name, basename(nonGitFolder)); +assert.equal(fallbackContext.default_branch, "main"); +assert.equal(fallbackContext.remote_url, undefined); + +const sshRepo = initRepo(join(tmp, "ssh-repo")); +git(sshRepo, "remote", "add", "origin", "git@github.com:Autoloops/greplica.git"); +const sshContext = detectRepoContext(sshRepo); +assert.equal(sshContext.repo_root, realpathSync(sshRepo)); +assert.equal(sshContext.remote_url, "git@github.com:Autoloops/greplica.git"); +assert.equal(sshContext.repo_name, "greplica"); + +const httpsRepo = initRepo(join(tmp, "https-repo")); +git(httpsRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); +const httpsContext = detectRepoContext(httpsRepo); +assert.equal(httpsContext.remote_url, "https://github.com/Autoloops/greplica.git"); +assert.equal(httpsContext.repo_name, "greplica"); + +const branchRepo = initRepo(join(tmp, "branch-repo")); +git(branchRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); +git(branchRepo, "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/trunk"); +const branchContext = detectRepoContext(branchRepo); +assert.equal(branchContext.default_branch, "trunk"); + +const noRemoteHeadRepo = initRepo(join(tmp, "no-remote-head-repo")); +git(noRemoteHeadRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); +const noRemoteHeadContext = detectRepoContext(noRemoteHeadRepo); +assert.equal(noRemoteHeadContext.default_branch, "main"); + +console.log("Repo context checks passed."); + +function initRepo(path) { + mkdirSync(path); + git(path, "init", "--quiet"); + return path; +} + +function git(cwd, ...args) { + return execFileSync("git", args, { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }).trim(); +} diff --git a/scripts/check-transcript-bundle.js b/scripts/check-transcript-bundle.js new file mode 100644 index 0000000..b16333f --- /dev/null +++ b/scripts/check-transcript-bundle.js @@ -0,0 +1,272 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { tmpdir } from "node:os"; +import { fileURLToPath } from "node:url"; + +const root = new URL("..", import.meta.url); +const cliPath = fileURLToPath(new URL("dist/apps/cli/main.js", root)); +const tmp = mkdtempSync(join(tmpdir(), "greplica-transcript-bundle-test-")); + +const codexOne = join(tmp, "codex-one.jsonl"); +const codexTwo = join(tmp, "codex-two.jsonl"); +const claudeOne = join(tmp, "claude-one.jsonl"); +const copilotOne = join(tmp, "copilot-one.jsonl"); +const codexOut = join(tmp, "codex-bundle.md"); +const claudeOut = join(tmp, "claude-bundle.md"); +const copilotOut = join(tmp, "copilot-bundle.md"); +const opencodeOut = join(tmp, "opencode-bundle.md"); + +writeFileSync( + codexOne, + [ + JSON.stringify({ + type: "session_meta", + payload: { + id: "codex-session-one", + timestamp: "2026-06-25T00:00:00.000Z", + cwd: "/repo/example", + }, + }), + JSON.stringify({ + timestamp: "2026-06-25T00:01:00.000Z", + type: "event_msg", + payload: { + type: "user_message", + message: "Remember this durable Codex insight. do not keep this", + }, + }), + ].join("\n"), + "utf8", +); + +writeFileSync( + codexTwo, + [ + JSON.stringify({ + type: "session_meta", + payload: { + id: "codex-session-two", + cwd: "/repo/example", + }, + }), + JSON.stringify({ + timestamp: "2026-06-25T00:02:00.000Z", + type: "event_msg", + payload: { + type: "agent_message", + message: "A second Codex transcript fact. drop this", + }, + }), + ].join("\n"), + "utf8", +); + +writeFileSync( + claudeOne, + [ + JSON.stringify({ + type: "user", + sessionId: "claude-session-one", + cwd: "/repo/example", + timestamp: "2026-06-25T00:03:00.000Z", + message: { + role: "user", + content: [ + { + type: "text", + text: "Remember this durable Claude insight.", + }, + ], + }, + }), + ].join("\n"), + "utf8", +); + +writeFileSync( + copilotOne, + [ + JSON.stringify({ + type: "session.start", + data: { + sessionId: "copilot-session-one", + copilotVersion: "1.0.66", + context: { + cwd: "/repo/example", + repository: "Autoloops/greplica", + branch: "copilot-test", + }, + }, + timestamp: "2026-06-25T00:03:30.000Z", + }), + JSON.stringify({ + type: "session.model_change", + data: { + newModel: "claude-haiku-4.5", + }, + timestamp: "2026-06-25T00:03:45.000Z", + }), + JSON.stringify({ + session_id: "copilot-session-one", + cwd: "/repo/example", + timestamp: "2026-06-25T00:04:00.000Z", + role: "user", + content: "Remember this durable Copilot insight. remove this", + }), + JSON.stringify({ + session_id: "copilot-session-one", + cwd: "/repo/example", + timestamp: "2026-06-25T00:05:00.000Z", + role: "assistant", + content: [{ type: "text", text: "A Copilot assistant fact." }], + }), + ].join("\n"), + "utf8", +); + +const codexOutput = execFileSync( + process.execPath, + [ + cliPath, + "transcript", + "bundle", + "--platform", + "codex", + "--file", + codexOne, + "--file", + codexTwo, + "--out", + codexOut, + ], + { encoding: "utf8" }, +); +const codexBundle = readFileSync(codexOut, "utf8"); +assert.match(codexOutput, /Wrote transcript bundle/); +assert.match(codexOutput, /codex-session:codex-session-one/); +assert.match(codexOutput, /codex-session:codex-session-two/); +assert.match(codexBundle, /file_count: 2/); +assert.match(codexBundle, /session_ref: codex-session:codex-session-one/); +assert.match(codexBundle, /session_ref: codex-session:codex-session-two/); +assert.match(codexBundle, /Remember this durable Codex insight/); +assert.match(codexBundle, /A second Codex transcript fact/); +assert.doesNotMatch(codexBundle, /do not keep this/); +assert.doesNotMatch(codexBundle, /drop this/); + +const claudeOutput = execFileSync( + process.execPath, + [ + cliPath, + "transcript", + "bundle", + "--platform", + "claude", + "--file", + claudeOne, + "--out", + claudeOut, + ], + { encoding: "utf8" }, +); +const claudeBundle = readFileSync(claudeOut, "utf8"); +assert.match(claudeOutput, /claude-code-session:claude-session-one/); +assert.match(claudeBundle, /session_ref: claude-code-session:claude-session-one/); +assert.match(claudeBundle, /Remember this durable Claude insight/); + +const copilotOutput = execFileSync( + process.execPath, + [ + cliPath, + "transcript", + "bundle", + "--platform", + "copilot", + "--file", + copilotOne, + "--out", + copilotOut, + ], + { encoding: "utf8" }, +); +const copilotBundle = readFileSync(copilotOut, "utf8"); +assert.match(copilotOutput, /copilot-session:copilot-session-one/); +assert.match(copilotBundle, /session_ref: copilot-session:copilot-session-one/); +assert.match(copilotBundle, /repository: Autoloops\/greplica/); +assert.match(copilotBundle, /branch: copilot-test/); +assert.match(copilotBundle, /Remember this durable Copilot insight/); +assert.match(copilotBundle, /A Copilot assistant fact/); +assert.doesNotMatch(copilotBundle, /remove this/); + +assert.throws( + () => + execFileSync( + process.execPath, + [cliPath, "transcript", "bundle", "--platform", "codex", "--file", join(tmp, "missing.jsonl"), "--out", join(tmp, "missing.md")], + { encoding: "utf8", stdio: "pipe" }, + ), + /Transcript file does not exist/, +); + +const opencodeDataHome = join(tmp, "opencode-data"); +const opencodeSessionId = "opencode-session-one"; +const opencodeSessionFile = join(opencodeDataHome, "opencode", "storage", "session", `${opencodeSessionId}.json`); +mkdirSync(dirname(opencodeSessionFile), { recursive: true }); +mkdirSync(join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId), { recursive: true }); +writeFileSync( + opencodeSessionFile, + JSON.stringify({ + id: opencodeSessionId, + directory: "/repo/example", + }), + "utf8", +); +writeFileSync( + join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId, "msg-01.json"), + JSON.stringify({ + role: "user", + content: "Remember this durable OpenCode insight. remove this", + time: "2026-06-25T00:06:00.000Z", + }), + "utf8", +); +writeFileSync( + join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId, "msg-02.json"), + JSON.stringify({ + role: "assistant", + parts: [{ text: "An OpenCode assistant fact." }], + time: "2026-06-25T00:07:00.000Z", + }), + "utf8", +); + +const opencodeOutput = execFileSync( + process.execPath, + [ + cliPath, + "transcript", + "bundle", + "--platform", + "opencode", + "--file", + opencodeSessionFile, + "--out", + opencodeOut, + ], + { + encoding: "utf8", + env: { + ...process.env, + XDG_DATA_HOME: opencodeDataHome, + }, + }, +); +const opencodeBundle = readFileSync(opencodeOut, "utf8"); +assert.match(opencodeOutput, /opencode-session:opencode-session-one/); +assert.match(opencodeBundle, /session_ref: opencode-session:opencode-session-one/); +assert.match(opencodeBundle, /Remember this durable OpenCode insight/); +assert.match(opencodeBundle, /An OpenCode assistant fact/); +assert.doesNotMatch(opencodeBundle, /remove this/); + +console.log("Transcript bundle checks passed."); From ae70780524e1f609fb4b51d1f6111199c0d8c231 Mon Sep 17 00:00:00 2001 From: dawgdevv Date: Fri, 10 Jul 2026 22:35:29 +0530 Subject: [PATCH 5/6] feat: enhance installation options and add anchor drift detection features --- package.json | 9 +- scripts/check-anchor-drift.js | 115 +++-- scripts/check-bm25-tokenizer.js | 78 ++-- scripts/check-graph-view.js | 85 ++-- scripts/check-install-options.js | 270 ++++++------ scripts/check-proposal-validate.js | 285 ++++++------ scripts/check-repo-context.js | 99 +++-- scripts/check-transcript-bundle.js | 462 +++++++++----------- tests/integration/bm25-tokenizer.test.ts | 53 --- tests/integration/graph-view.test.ts | 52 --- tests/integration/install-options.test.ts | 151 ------- tests/integration/proposal-validate.test.ts | 160 ------- tests/integration/repo-context.test.ts | 78 ---- tests/integration/transcript-bundle.test.ts | 210 --------- tsconfig.build.json | 3 +- tsconfig.json | 3 +- vitest.config.ts | 4 +- 17 files changed, 749 insertions(+), 1368 deletions(-) delete mode 100644 tests/integration/bm25-tokenizer.test.ts delete mode 100644 tests/integration/graph-view.test.ts delete mode 100644 tests/integration/install-options.test.ts delete mode 100644 tests/integration/proposal-validate.test.ts delete mode 100644 tests/integration/repo-context.test.ts delete mode 100644 tests/integration/transcript-bundle.test.ts diff --git a/package.json b/package.json index 3dbdbd4..03a4f06 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,12 @@ "smoke:openhands": "npm run build && node scripts/smoke-openhands-install.mjs", "smoke:copilot": "npm run build && node scripts/smoke-copilot-install.mjs", "smoke:opencode": "npm run build && node scripts/smoke-opencode-install.mjs", - "test": "npm run build && node scripts/check-transcript-bundle.js && node scripts/check-repo-context.js && node scripts/check-install-options.js && node scripts/check-graph-view.js && node scripts/check-proposal-validate.js && node scripts/check-bm25-tokenizer.js && node scripts/check-anchor-drift.js", - "test:transcript-bundle": "npm run build && node scripts/check-transcript-bundle.js", - "test:repo-context": "npm run build && node scripts/check-repo-context.js", + "test": "npm run build && vitest run", + "test:fast": "vitest run", + "test:watch": "vitest", + "test:coverage": "npm run build && vitest run --coverage", + "test:transcript-bundle": "npm run build && vitest run scripts/check-transcript-bundle.js", + "test:repo-context": "npm run build && vitest run scripts/check-repo-context.js", "eval:bootstrap-current": "npm run build && node dist/evals/cases/bootstrap-current-repo-at-8038fe8/run.js", "eval:coding-proposal-apply-atomicity": "npm run build && node dist/evals/cases/coding-proposal-apply-atomicity-at-d6ebf80/run.js", "eval:curated-doc-bootstrap-vercel-chat": "npm run build && node dist/evals/cases/curated-doc-bootstrap-vercel-chat-at-f3de128/run.js", diff --git a/scripts/check-anchor-drift.js b/scripts/check-anchor-drift.js index 09815db..26c2ba8 100644 --- a/scripts/check-anchor-drift.js +++ b/scripts/check-anchor-drift.js @@ -1,42 +1,101 @@ -import assert from "node:assert/strict"; +import { describe, test, expect, beforeAll } from "vitest"; import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const root = new URL("..", import.meta.url); -const { CodeAnchorResolver } = await import(new URL("dist/libs/knowledge-graph/code-anchors/resolver.js", root)); -const { fingerprintClaimAnchors } = await import(new URL("dist/libs/knowledge-graph/code-anchors/fingerprint.js", root)); -const { auditClaimCodeAnchors } = await import(new URL("dist/libs/knowledge-graph/code-anchors/audit.js", root)); +let CodeAnchorResolver; +let fingerprintClaimAnchors; +let auditClaimCodeAnchors; -const repo = mkdtempSync(join(tmpdir(), "greplica-anchor-drift-test-")); -const file = join(repo, "mod.py"); -const anchor = { file: "mod.py", symbol: "foo" }; -const claim = { id: "claim.foo", kind: "fact", text: "foo returns 3", truth: "code_verified", intent: "intended", code_anchors: [anchor] }; +beforeAll(async () => { + const resolver = await import(new URL("dist/libs/knowledge-graph/code-anchors/resolver.js", root)); + const fingerprint = await import(new URL("dist/libs/knowledge-graph/code-anchors/fingerprint.js", root)); + const audit = await import(new URL("dist/libs/knowledge-graph/code-anchors/audit.js", root)); + CodeAnchorResolver = resolver.CodeAnchorResolver; + fingerprintClaimAnchors = fingerprint.fingerprintClaimAnchors; + auditClaimCodeAnchors = audit.auditClaimCodeAnchors; +}); -// Baseline fingerprint captured when the fact was "written". -writeFileSync(file, "def foo():\n # returns the threshold\n return 3\n"); -const baseline = new Map([["claim.foo", await fingerprintClaimAnchors(repo, [anchor], new CodeAnchorResolver())]]); +describe("anchor drift", () => { + test("unchanged code does not drift", async () => { + const repo = mkdtempSync(join(tmpdir(), "greplica-anchor-drift-test-")); + const file = join(repo, "mod.py"); + const anchor = { file: "mod.py", symbol: "foo" }; + const claim = { id: "claim.foo", kind: "fact", text: "foo returns 3", truth: "code_verified", intent: "intended", code_anchors: [anchor] }; -async function driftedIds(variant) { - writeFileSync(file, variant); - const result = await auditClaimCodeAnchors(repo, [claim], new CodeAnchorResolver(), baseline); - return result.drifted.map((issue) => issue.claim_id); -} + writeFileSync(file, "def foo():\n # returns the threshold\n return 3\n"); + const baseline = new Map([["claim.foo", await fingerprintClaimAnchors(repo, [anchor], new CodeAnchorResolver())]]); -// Unchanged code does not drift. -assert.deepEqual(await driftedIds("def foo():\n # returns the threshold\n return 3\n"), []); + async function driftedIds(variant) { + writeFileSync(file, variant); + const result = await auditClaimCodeAnchors(repo, [claim], new CodeAnchorResolver(), baseline); + return result.drifted.map((issue) => issue.claim_id); + } -// A real value change (3 -> 8) drifts. -assert.deepEqual(await driftedIds("def foo():\n # returns the threshold\n return 8\n"), ["claim.foo"]); + expect(await driftedIds("def foo():\n # returns the threshold\n return 3\n")).toEqual([]); + }); -// Comment-only edits do not drift. -assert.deepEqual(await driftedIds("def foo():\n # returns the configured threshold value\n return 3\n"), []); + test("a real value change drifts", async () => { + const repo = mkdtempSync(join(tmpdir(), "greplica-anchor-drift-test-")); + const file = join(repo, "mod.py"); + const anchor = { file: "mod.py", symbol: "foo" }; + const claim = { id: "claim.foo", kind: "fact", text: "foo returns 3", truth: "code_verified", intent: "intended", code_anchors: [anchor] }; -// Whitespace-only edits do not drift. -assert.deepEqual(await driftedIds("def foo():\n\n # returns the threshold\n return 3\n\n"), []); + writeFileSync(file, "def foo():\n # returns the threshold\n return 3\n"); + const baseline = new Map([["claim.foo", await fingerprintClaimAnchors(repo, [anchor], new CodeAnchorResolver())]]); -// A claim with no stored baseline is treated as unknown, never drifted. -const noBaseline = await auditClaimCodeAnchors(repo, [claim], new CodeAnchorResolver()); -assert.deepEqual(noBaseline.drifted, []); + async function driftedIds(variant) { + writeFileSync(file, variant); + const result = await auditClaimCodeAnchors(repo, [claim], new CodeAnchorResolver(), baseline); + return result.drifted.map((issue) => issue.claim_id); + } -console.log("check-anchor-drift: ok"); + expect(await driftedIds("def foo():\n # returns the threshold\n return 8\n")).toEqual(["claim.foo"]); + }); + + test("comment-only edits do not drift", async () => { + const repo = mkdtempSync(join(tmpdir(), "greplica-anchor-drift-test-")); + const file = join(repo, "mod.py"); + const anchor = { file: "mod.py", symbol: "foo" }; + const claim = { id: "claim.foo", kind: "fact", text: "foo returns 3", truth: "code_verified", intent: "intended", code_anchors: [anchor] }; + + writeFileSync(file, "def foo():\n # returns the threshold\n return 3\n"); + const baseline = new Map([["claim.foo", await fingerprintClaimAnchors(repo, [anchor], new CodeAnchorResolver())]]); + + async function driftedIds(variant) { + writeFileSync(file, variant); + const result = await auditClaimCodeAnchors(repo, [claim], new CodeAnchorResolver(), baseline); + return result.drifted.map((issue) => issue.claim_id); + } + + expect(await driftedIds("def foo():\n # the configured threshold value\n return 3\n")).toEqual([]); + }); + + test("whitespace-only edits do not drift", async () => { + const repo = mkdtempSync(join(tmpdir(), "greplica-anchor-drift-test-")); + const file = join(repo, "mod.py"); + const anchor = { file: "mod.py", symbol: "foo" }; + const claim = { id: "claim.foo", kind: "fact", text: "foo returns 3", truth: "code_verified", intent: "intended", code_anchors: [anchor] }; + + writeFileSync(file, "def foo():\n # returns the threshold\n return 3\n"); + const baseline = new Map([["claim.foo", await fingerprintClaimAnchors(repo, [anchor], new CodeAnchorResolver())]]); + + async function driftedIds(variant) { + writeFileSync(file, variant); + const result = await auditClaimCodeAnchors(repo, [claim], new CodeAnchorResolver(), baseline); + return result.drifted.map((issue) => issue.claim_id); + } + + expect(await driftedIds("def foo():\n\n # returns the threshold\n return 3\n\n")).toEqual([]); + }); + + test("a claim with no stored baseline is treated as unknown, never drifted", async () => { + const repo = mkdtempSync(join(tmpdir(), "greplica-anchor-drift-test-")); + const anchor = { file: "mod.py", symbol: "foo" }; + const claim = { id: "claim.foo", kind: "fact", text: "foo returns 3", truth: "code_verified", intent: "intended", code_anchors: [anchor] }; + + const noBaseline = await auditClaimCodeAnchors(repo, [claim], new CodeAnchorResolver()); + expect(noBaseline.drifted).toEqual([]); + }); +}); diff --git a/scripts/check-bm25-tokenizer.js b/scripts/check-bm25-tokenizer.js index f880b45..5456ce0 100644 --- a/scripts/check-bm25-tokenizer.js +++ b/scripts/check-bm25-tokenizer.js @@ -1,28 +1,54 @@ -import assert from "node:assert/strict"; +import { describe, test, expect, beforeAll } from "vitest"; const root = new URL("..", import.meta.url); -const { tokenize, scoreBm25 } = await import(new URL("dist/libs/knowledge-graph/graph-context/bm25.js", root)); -const { graphContextConfig } = await import(new URL("dist/libs/knowledge-graph/graph-context/config.js", root)); - -assert.ok(tokenize("handleUserAuth").includes("user"), "camelCase should emit sub-token user"); -assert.ok(tokenize("handleUserAuth").includes("auth"), "camelCase should emit sub-token auth"); -assert.ok(tokenize("handleUserAuth").includes("handle"), "camelCase should emit sub-token handle"); -assert.ok(tokenize("handleUserAuth").includes("handleuserauth"), "camelCase should keep full lowercased token"); - -assert.ok(tokenize("HandleUserAuth").includes("user"), "PascalCase should emit sub-token user"); -assert.ok(tokenize("handle_user_auth").includes("user"), "snake_case should emit sub-token user"); -assert.ok(tokenize("handle_user_auth").includes("handle_user_auth"), "snake_case should keep original token"); - -assert.ok(tokenize("graph-context").includes("graph"), "kebab-case should emit sub-token graph"); -assert.ok(tokenize("graph-context").includes("context"), "kebab-case should emit sub-token context"); - -assert.ok(tokenize("user2FA").includes("user"), "letter-number boundary should emit user"); -assert.ok(tokenize("user2FA").includes("fa"), "letter-number boundary should emit fa"); - -assert.ok(tokenize("tokens").includes("token"), "English stemming variants should still apply"); - -const documents = [{ key: "doc:auth", text: "The handleUserAuth function validates sessions." }]; -const ranked = scoreBm25("user auth validation", documents, graphContextConfig); -assert.equal(ranked[0]?.id, "doc:auth", "BM25 should match camelCase doc tokens to spaced query terms"); - -console.log("BM25 tokenizer checks passed."); +let tokenize; +let scoreBm25; +let graphContextConfig; + +beforeAll(async () => { + const bm25 = await import(new URL("dist/libs/knowledge-graph/graph-context/bm25.js", root)); + const config = await import(new URL("dist/libs/knowledge-graph/graph-context/config.js", root)); + tokenize = bm25.tokenize; + scoreBm25 = bm25.scoreBm25; + graphContextConfig = config.graphContextConfig; +}); + +describe("BM25 tokenizer", () => { + test("camelCase emits sub-tokens and keeps full lowercased token", () => { + const tokens = tokenize("handleUserAuth"); + expect(tokens).toContain("user"); + expect(tokens).toContain("auth"); + expect(tokens).toContain("handle"); + expect(tokens).toContain("handleuserauth"); + }); + + test("PascalCase emits sub-tokens", () => { + expect(tokenize("HandleUserAuth")).toContain("user"); + }); + + test("snake_case emits sub-tokens and keeps original token", () => { + const tokens = tokenize("handle_user_auth"); + expect(tokens).toContain("user"); + expect(tokens).toContain("handle_user_auth"); + }); + + test("kebab-case emits sub-tokens", () => { + expect(tokenize("graph-context")).toContain("graph"); + expect(tokenize("graph-context")).toContain("context"); + }); + + test("letter-number boundary emits sub-tokens", () => { + expect(tokenize("user2FA")).toContain("user"); + expect(tokenize("user2FA")).toContain("fa"); + }); + + test("English stemming variants still apply", () => { + expect(tokenize("tokens")).toContain("token"); + }); + + test("BM25 matches camelCase doc tokens to spaced query terms", () => { + const documents = [{ key: "doc:auth", text: "The handleUserAuth function validates sessions." }]; + const ranked = scoreBm25("user auth validation", documents, graphContextConfig); + expect(ranked[0]?.id).toBe("doc:auth"); + }); +}); diff --git a/scripts/check-graph-view.js b/scripts/check-graph-view.js index 11661ed..3649c27 100644 --- a/scripts/check-graph-view.js +++ b/scripts/check-graph-view.js @@ -1,48 +1,59 @@ -import assert from "node:assert/strict"; +import { describe, test, expect, beforeAll } from "vitest"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const root = new URL("..", import.meta.url); -const { openDatabase } = await import(new URL("dist/libs/storage/sqlite/db.js", root)); -const { SqliteRepository } = await import(new URL("dist/libs/storage/sqlite/repository.js", root)); -const { KnowledgeGraphService } = await import(new URL("dist/libs/knowledge-graph/service.js", root)); +let openDatabase; +let SqliteRepository; +let KnowledgeGraphService; -const tmp = mkdtempSync(join(tmpdir(), "greplica-graph-view-test-")); -const db = openDatabase(join(tmp, "graph.db")); +beforeAll(async () => { + const db = await import(new URL("dist/libs/storage/sqlite/db.js", root)); + const repo = await import(new URL("dist/libs/storage/sqlite/repository.js", root)); + const service = await import(new URL("dist/libs/knowledge-graph/service.js", root)); + openDatabase = db.openDatabase; + SqliteRepository = repo.SqliteRepository; + KnowledgeGraphService = service.KnowledgeGraphService; +}); -try { - const repository = new SqliteRepository(db); - const service = new KnowledgeGraphService(repository); - const repo = { - repo_root: join(tmp, "repo"), - repo_name: "graph-view-null-anchor", - default_branch: "main", - }; +describe("graph view", () => { + test("renders components without code anchors", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-graph-view-test-")); + const db = openDatabase(join(tmp, "graph.db")); - const initialized = service.initRepo(repo); - const memoryCommit = repository.createMemoryCommit({ - scope_id: initialized.working_scope_id, - title: "Seed null component anchor", - }); + try { + const repository = new SqliteRepository(db); + const service = new KnowledgeGraphService(repository); + const repo = { + repo_root: join(tmp, "repo"), + repo_name: "graph-view-null-anchor", + default_branch: "main", + }; - repository.createProposalRecords(initialized.working_scope_id, memoryCommit.id, { - title: "Seed null component anchor", - creates: { - components: [ - { - id: "component.no_anchor", - name: "Component Without Anchor", - }, - ], - }, - }); + const initialized = service.initRepo(repo); + const memoryCommit = repository.createMemoryCommit({ + scope_id: initialized.working_scope_id, + title: "Seed null component anchor", + }); - const html = service.buildGraphView(repo); - assert.match(html, /Component Without Anchor/); - assert.match(html, /Greplica graph view/); -} finally { - db.close(); -} + repository.createProposalRecords(initialized.working_scope_id, memoryCommit.id, { + title: "Seed null component anchor", + creates: { + components: [ + { + id: "component.no_anchor", + name: "Component Without Anchor", + }, + ], + }, + }); -console.log("Graph view checks passed."); + const html = service.buildGraphView(repo); + expect(html).toMatch(/Component Without Anchor/); + expect(html).toMatch(/Greplica graph view/); + } finally { + db.close(); + } + }); +}); diff --git a/scripts/check-install-options.js b/scripts/check-install-options.js index 3de68aa..3e51dd0 100644 --- a/scripts/check-install-options.js +++ b/scripts/check-install-options.js @@ -1,4 +1,4 @@ -import assert from "node:assert/strict"; +import { describe, test, expect, beforeAll } from "vitest"; import { execFileSync, spawnSync } from "node:child_process"; import { existsSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; @@ -7,123 +7,27 @@ import { fileURLToPath } from "node:url"; const root = new URL("..", import.meta.url); const cliPath = fileURLToPath(new URL("dist/apps/cli/main.js", root)); -const { installCommandSuggestion, installPlatformUsage } = await import(new URL("dist/libs/install/paths.js", root)); -const { greplicaHookGuidance } = await import(new URL("dist/libs/hooks/guidance.js", root)); -const { shouldRunAutoMemoryUpdates } = await import(new URL("dist/libs/hooks/worker.js", root)); - -const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); - -const autoSave = installInTempRepo("auto-save", ["--hooks", "enabled", "--auto-memory", "enabled"]); -assert.match(autoSave.output, /Hooks: installed for UserPromptSubmit, Stop\./); -assert.match(autoSave.output, /Automatic memory updates: enabled\./); -assert.ok(existsSync(join(autoSave.codexHome, "hooks.json"))); -assert.equal(readConfig(autoSave.greplicaHome).session.autoMemoryUpdates, true); -assert.equal(shouldRunAutoMemoryUpdates(readConfig(autoSave.greplicaHome)), true); - -const guidanceOnly = installInTempRepo("guidance-only", ["--hooks", "enabled", "--auto-memory", "disabled"]); -assert.match(guidanceOnly.output, /Hooks: installed for UserPromptSubmit, Stop\./); -assert.match(guidanceOnly.output, /Automatic memory updates: disabled\./); -assert.ok(existsSync(join(guidanceOnly.codexHome, "hooks.json"))); -assert.equal(readConfig(guidanceOnly.greplicaHome).session.autoMemoryUpdates, false); -assert.equal(shouldRunAutoMemoryUpdates(readConfig(guidanceOnly.greplicaHome)), false); - -const hookOutput = execFileSync( - process.execPath, - [cliPath, "hook", "ingest", "--platform", "codex"], - { - cwd: guidanceOnly.repo, - encoding: "utf8", - input: JSON.stringify({ - hook_event_name: "UserPromptSubmit", - session_id: "guidance-only-session", - cwd: guidanceOnly.repo, - }), - env: guidanceOnly.env, - }, -); -assert.match(hookOutput, /Greplica hook guidance/); -assert.match(hookOutput, /greplica graph context/); - -const noHooks = installInTempRepo("no-hooks", ["--hooks", "disabled"]); -assert.match(noHooks.output, /Hooks: not installed\./); -assert.match(noHooks.output, /Automatic memory updates: disabled\./); -assert.match(noHooks.output, /To give future agents Greplica guidance without hooks/); -assert.ok(noHooks.output.includes(greplicaHookGuidance)); -assert.equal(existsSync(join(noHooks.codexHome, "hooks.json")), false); -assert.equal(readConfig(noHooks.greplicaHome).session.autoMemoryUpdates, false); - -const opencodeHooks = installInTempRepo("opencode-hooks", ["--hooks", "enabled", "--auto-memory", "enabled"], "opencode"); -assert.match(opencodeHooks.output, /Installed Greplica for OpenCode\./); -assert.match(opencodeHooks.output, /Hooks: installed for UserPromptSubmit, Stop\./); -assert.match(opencodeHooks.output, /Automatic memory updates: enabled\./); -assert.ok(existsSync(join(opencodeHooks.xdgConfigHome, "opencode", "hooks.json"))); -assert.equal(readConfig(opencodeHooks.greplicaHome).session.autoMemoryUpdates, true); - -const copilotHooks = installInTempRepo("copilot-hooks", ["--hooks", "enabled", "--auto-memory", "enabled"], "copilot"); -assert.match(copilotHooks.output, /Installed Greplica for GitHub Copilot CLI\./); -assert.match(copilotHooks.output, /Hooks: installed for SessionStart, Stop\./); -assert.match(copilotHooks.output, /Automatic memory updates: enabled\./); -assert.ok(existsSync(join(copilotHooks.copilotHome, "hooks", "greplica.json"))); -assert.equal(readConfig(copilotHooks.greplicaHome).session.autoMemoryUpdates, true); - -const invalid = spawnSync( - process.execPath, - [ - cliPath, - "install", - "--platform", - "codex", - "--embedding", - "local", - "--hooks", - "disabled", - "--auto-memory", - "enabled", - ], - { - cwd: noHooks.repo, - encoding: "utf8", - env: noHooks.env, - }, -); -assert.notEqual(invalid.status, 0); -assert.match(invalid.stderr, /--auto-memory enabled requires --hooks enabled/); - -const invalidValue = spawnSync( - process.execPath, - [cliPath, "install", "--platform", "codex", "--embedding", "local", "--hooks", "sometimes"], - { - cwd: noHooks.repo, - encoding: "utf8", - env: noHooks.env, - }, -); -assert.notEqual(invalidValue.status, 0); -assert.match(invalidValue.stderr, /expected enabled or disabled/); - -const notInstalledRepo = join(tmp, "not-installed", "repo"); -const notInstalledHome = join(tmp, "not-installed", "greplica-home"); -mkdirSync(notInstalledRepo, { recursive: true }); -execFileSync("git", ["init", "--quiet"], { cwd: notInstalledRepo, encoding: "utf8" }); -const notInstalled = spawnSync(process.execPath, [cliPath, "graph", "read"], { - cwd: notInstalledRepo, - encoding: "utf8", - env: { - ...process.env, - GREPLICA_HOME: notInstalledHome, - }, + +let installCommandSuggestion; +let installPlatformUsage; +let greplicaHookGuidance; +let shouldRunAutoMemoryUpdates; + +beforeAll(async () => { + const paths = await import(new URL("dist/libs/install/paths.js", root)); + const guidance = await import(new URL("dist/libs/hooks/guidance.js", root)); + const worker = await import(new URL("dist/libs/hooks/worker.js", root)); + installCommandSuggestion = paths.installCommandSuggestion; + installPlatformUsage = paths.installPlatformUsage; + greplicaHookGuidance = guidance.greplicaHookGuidance; + shouldRunAutoMemoryUpdates = worker.shouldRunAutoMemoryUpdates; }); -assert.notEqual(notInstalled.status, 0); -assert.match(notInstalled.stderr, /Greplica is not installed for this repo/); -assert.ok(notInstalled.stderr.includes(installCommandSuggestion), "not installed error should use shared install guidance"); -assert.ok(notInstalled.stderr.includes(installPlatformUsage), "not installed error should include every install platform"); -assert.match(notInstalled.stderr, /openhands/); -assert.match(notInstalled.stderr, /factory-droid/); -assert.match(notInstalled.stderr, /antigravity/); -console.log("Install option checks passed."); +function readConfig(greplicaHome) { + return JSON.parse(readFileSync(join(greplicaHome, "config.json"), "utf8")); +} -function installInTempRepo(name, flags, platform = "codex") { +function installInTempRepo(tmp, name, flags, platform = "codex") { const repo = join(tmp, name, "repo"); const greplicaHome = join(tmp, name, "greplica-home"); const codexHome = join(tmp, name, "codex-home"); @@ -143,20 +47,130 @@ function installInTempRepo(name, flags, platform = "codex") { const output = execFileSync( process.execPath, [cliPath, "install", "--platform", platform, "--embedding", "local", ...flags], - { - cwd: repo, - encoding: "utf8", - env, - }, + { cwd: repo, encoding: "utf8", env }, ); - execFileSync(process.execPath, [cliPath, "doctor"], { - cwd: repo, - encoding: "utf8", - env, - }); + execFileSync(process.execPath, [cliPath, "doctor"], { cwd: repo, encoding: "utf8", env }); return { repo, greplicaHome, codexHome, copilotHome, xdgConfigHome, output, env }; } -function readConfig(greplicaHome) { - return JSON.parse(readFileSync(join(greplicaHome, "config.json"), "utf8")); -} +describe("install options", () => { + test("auto-save mode installs hooks and enables auto memory updates", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); + const autoSave = installInTempRepo(tmp, "auto-save", ["--hooks", "enabled", "--auto-memory", "enabled"]); + + expect(autoSave.output).toMatch(/Hooks: installed for UserPromptSubmit, Stop\./); + expect(autoSave.output).toMatch(/Automatic memory updates: enabled\./); + expect(existsSync(join(autoSave.codexHome, "hooks.json"))).toBe(true); + expect(readConfig(autoSave.greplicaHome).session.autoMemoryUpdates).toBe(true); + expect(shouldRunAutoMemoryUpdates(readConfig(autoSave.greplicaHome))).toBe(true); + }); + + test("guidance-only mode installs hooks and injects guidance but disables auto memory updates", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); + const guidanceOnly = installInTempRepo(tmp, "guidance-only", ["--hooks", "enabled", "--auto-memory", "disabled"]); + + expect(guidanceOnly.output).toMatch(/Hooks: installed for UserPromptSubmit, Stop\./); + expect(guidanceOnly.output).toMatch(/Automatic memory updates: disabled\./); + expect(existsSync(join(guidanceOnly.codexHome, "hooks.json"))).toBe(true); + expect(readConfig(guidanceOnly.greplicaHome).session.autoMemoryUpdates).toBe(false); + expect(shouldRunAutoMemoryUpdates(readConfig(guidanceOnly.greplicaHome))).toBe(false); + + const hookOutput = execFileSync( + process.execPath, + [cliPath, "hook", "ingest", "--platform", "codex"], + { + cwd: guidanceOnly.repo, + encoding: "utf8", + input: JSON.stringify({ + hook_event_name: "UserPromptSubmit", + session_id: "guidance-only-session", + cwd: guidanceOnly.repo, + }), + env: guidanceOnly.env, + }, + ); + expect(hookOutput).toMatch(/Greplica hook guidance/); + expect(hookOutput).toMatch(/greplica graph context/); + }); + + test("no-hooks mode does not install hooks and disables auto memory updates", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); + const noHooks = installInTempRepo(tmp, "no-hooks", ["--hooks", "disabled"]); + + expect(noHooks.output).toMatch(/Hooks: not installed\./); + expect(noHooks.output).toMatch(/Automatic memory updates: disabled\./); + expect(noHooks.output).toMatch(/To give future agents Greplica guidance without hooks/); + expect(noHooks.output).toContain(greplicaHookGuidance); + expect(existsSync(join(noHooks.codexHome, "hooks.json"))).toBe(false); + expect(readConfig(noHooks.greplicaHome).session.autoMemoryUpdates).toBe(false); + }); + + test("opencode installs hooks and enables auto memory updates", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); + const opencodeHooks = installInTempRepo(tmp, "opencode-hooks", ["--hooks", "enabled", "--auto-memory", "enabled"], "opencode"); + + expect(opencodeHooks.output).toMatch(/Installed Greplica for OpenCode\./); + expect(opencodeHooks.output).toMatch(/Hooks: installed for UserPromptSubmit, Stop\./); + expect(opencodeHooks.output).toMatch(/Automatic memory updates: enabled\./); + expect(existsSync(join(opencodeHooks.xdgConfigHome, "opencode", "hooks.json"))).toBe(true); + expect(readConfig(opencodeHooks.greplicaHome).session.autoMemoryUpdates).toBe(true); + }); + + test("copilot installs hooks and enables auto memory updates", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); + const copilotHooks = installInTempRepo(tmp, "copilot-hooks", ["--hooks", "enabled", "--auto-memory", "enabled"], "copilot"); + + expect(copilotHooks.output).toMatch(/Installed Greplica for GitHub Copilot CLI\./); + expect(copilotHooks.output).toMatch(/Hooks: installed for SessionStart, Stop\./); + expect(copilotHooks.output).toMatch(/Automatic memory updates: enabled\./); + expect(existsSync(join(copilotHooks.copilotHome, "hooks", "greplica.json"))).toBe(true); + expect(readConfig(copilotHooks.greplicaHome).session.autoMemoryUpdates).toBe(true); + }); + + test("rejects auto-memory enabled without hooks enabled", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); + const noHooks = installInTempRepo(tmp, "invalid-combo", ["--hooks", "disabled"]); + + const invalid = spawnSync( + process.execPath, + [cliPath, "install", "--platform", "codex", "--embedding", "local", "--hooks", "disabled", "--auto-memory", "enabled"], + { cwd: noHooks.repo, encoding: "utf8", env: noHooks.env }, + ); + expect(invalid.status).not.toBe(0); + expect(invalid.stderr).toMatch(/--auto-memory enabled requires --hooks enabled/); + }); + + test("rejects invalid flag values", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); + const noHooks = installInTempRepo(tmp, "invalid-flag", ["--hooks", "disabled"]); + + const invalidValue = spawnSync( + process.execPath, + [cliPath, "install", "--platform", "codex", "--embedding", "local", "--hooks", "sometimes"], + { cwd: noHooks.repo, encoding: "utf8", env: noHooks.env }, + ); + expect(invalidValue.status).not.toBe(0); + expect(invalidValue.stderr).toMatch(/expected enabled or disabled/); + }); + + test("graph read on uninstalled repo reports not installed with install guidance", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-install-options-test-")); + const notInstalledRepo = join(tmp, "not-installed", "repo"); + const notInstalledHome = join(tmp, "not-installed", "greplica-home"); + mkdirSync(notInstalledRepo, { recursive: true }); + execFileSync("git", ["init", "--quiet"], { cwd: notInstalledRepo, encoding: "utf8" }); + + const notInstalled = spawnSync(process.execPath, [cliPath, "graph", "read"], { + cwd: notInstalledRepo, + encoding: "utf8", + env: { ...process.env, GREPLICA_HOME: notInstalledHome }, + }); + expect(notInstalled.status).not.toBe(0); + expect(notInstalled.stderr).toMatch(/Greplica is not installed for this repo/); + expect(notInstalled.stderr).toContain(installCommandSuggestion); + expect(notInstalled.stderr).toContain(installPlatformUsage); + expect(notInstalled.stderr).toMatch(/openhands/); + expect(notInstalled.stderr).toMatch(/factory-droid/); + expect(notInstalled.stderr).toMatch(/antigravity/); + }); +}); diff --git a/scripts/check-proposal-validate.js b/scripts/check-proposal-validate.js index a1f6b85..51d4efb 100644 --- a/scripts/check-proposal-validate.js +++ b/scripts/check-proposal-validate.js @@ -1,160 +1,153 @@ -import assert from "node:assert/strict"; +import { describe, test, expect, beforeAll } from "vitest"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; const root = new URL("..", import.meta.url); -const { normalizeProposal } = await import(new URL("dist/libs/knowledge-graph/proposal.js", root)); -const { validateProposal } = await import(new URL("dist/libs/knowledge-graph/validate-proposal.js", root)); -const { KnowledgeGraphService } = await import(new URL("dist/libs/knowledge-graph/service.js", root)); -const { openDatabase } = await import(new URL("dist/libs/storage/sqlite/db.js", root)); -const { SqliteRepository } = await import(new URL("dist/libs/storage/sqlite/repository.js", root)); +let normalizeProposal; +let validateProposal; +let KnowledgeGraphService; +let openDatabase; +let SqliteRepository; + +beforeAll(async () => { + const proposal = await import(new URL("dist/libs/knowledge-graph/proposal.js", root)); + const validate = await import(new URL("dist/libs/knowledge-graph/validate-proposal.js", root)); + const service = await import(new URL("dist/libs/knowledge-graph/service.js", root)); + const db = await import(new URL("dist/libs/storage/sqlite/db.js", root)); + const repo = await import(new URL("dist/libs/storage/sqlite/repository.js", root)); + normalizeProposal = proposal.normalizeProposal; + validateProposal = validate.validateProposal; + KnowledgeGraphService = service.KnowledgeGraphService; + openDatabase = db.openDatabase; + SqliteRepository = repo.SqliteRepository; +}); const components = [ { id: "component.a", name: "A" }, { id: "component.b", name: "B" }, ]; -// Edge written with canonical field names but no types (issue #82) must not crash -// normalization, and must surface as a validation error. -const malformedEdge = { - title: "Malformed edge repro", - creates: { - components, - edges: [{ from_id: "component.a", to_id: "component.b", kind: "contains" }], - }, -}; - -let normalized; -assert.doesNotThrow(() => { - normalized = normalizeProposal(malformedEdge); -}, "normalizeProposal must not throw on a malformed edge"); - -const malformedResult = validateProposal(normalized); -assert.equal(malformedResult.valid, false, "malformed edge proposal must be invalid"); -assert.ok( - malformedResult.errors.some((error) => error.includes("from'/'to")), - `expected a from/to guidance error, got: ${JSON.stringify(malformedResult.errors)}`, -); - -// Edge missing kind must also be reported, not crash. -const missingKindEdge = { - title: "Missing kind repro", - creates: { - components, - edges: [{ from: "component.a", to: "component.b" }], - }, -}; - -let normalizedMissingKind; -assert.doesNotThrow(() => { - normalizedMissingKind = normalizeProposal(missingKindEdge); -}, "normalizeProposal must not throw on an edge without kind"); -assert.equal(validateProposal(normalizedMissingKind).valid, false, "edge without kind must be invalid"); - -// A well-formed compact edge must keep validating cleanly. -const compactEdge = { - title: "Compact edge", - creates: { - components, - edges: [{ kind: "contains", from: "component.a", to: "component.b" }], - }, -}; - -const compactResult = validateProposal(normalizeProposal(compactEdge)); -assert.equal(compactResult.valid, true, `compact edge must stay valid, got: ${JSON.stringify(compactResult.errors)}`); - -const tmp = mkdtempSync(join(tmpdir(), "greplica-proposal-validate-test-")); -const db = openDatabase(join(tmp, "graph.db")); - -try { - const repository = new SqliteRepository(db); - const service = new KnowledgeGraphService(repository); - const repoA = { - repo_root: join(tmp, "repo-a"), - repo_name: "repo-a", - default_branch: "main", - }; - const repoB = { - repo_root: join(tmp, "repo-b"), - repo_name: "repo-b", - default_branch: "main", - }; - const repoC = { - repo_root: join(tmp, "repo-c"), - repo_name: "repo-c", - default_branch: "main", - }; - const repoAProposal = { - title: "Seed CLI component", - creates: { - components: [{ id: "component.cli", name: "Repo A CLI" }], - }, - }; - const repoBProposal = { - title: "Seed CLI component", - creates: { - components: [{ id: "component.cli", name: "Repo B CLI" }], - }, - }; - - const initializedA = service.initRepo(repoA); - const initializedB = service.initRepo(repoB); - service.initRepo(repoC); - const memoryCommit = repository.createMemoryCommit({ - scope_id: initializedA.working_scope_id, - title: "Seed repo A", +describe("proposal validation", () => { + test("does not crash on edges using from_id/to_id and reports a clear error", () => { + const malformedEdge = { + title: "Malformed edge repro", + creates: { + components, + edges: [{ from_id: "component.a", to_id: "component.b", kind: "contains" }], + }, + }; + + let normalized; + expect(() => { + normalized = normalizeProposal(malformedEdge); + }).not.toThrow(); + + const malformedResult = validateProposal(normalized); + expect(malformedResult.valid).toBe(false); + expect(malformedResult.errors.some((error) => error.includes("from'/'to"))).toBe(true); }); - repository.createProposalRecords(initializedA.working_scope_id, memoryCommit.id, normalizeProposal(repoAProposal)); - - const repoBResult = await service.validateProposal(repoB, repoBProposal); - assert.equal( - repoBResult.valid, - true, - `repo B must be able to create its own component.cli, got: ${JSON.stringify(repoBResult.errors)}`, - ); - - const repoCCrossReference = await service.validateProposal(repoC, { - title: "Repo C cross-repo compact reference", - creates: { - claims: [ - { - id: "claim.about_cli", - kind: "fact", - text: "Repo C references a CLI component.", - truth: "unknown", - intent: "unknown", - about: "component.cli", - }, - ], - }, + + test("does not crash on edges missing kind and reports them invalid", () => { + const missingKindEdge = { + title: "Missing kind repro", + creates: { + components, + edges: [{ from: "component.a", to: "component.b" }], + }, + }; + + let normalizedMissingKind; + expect(() => { + normalizedMissingKind = normalizeProposal(missingKindEdge); + }).not.toThrow(); + + expect(validateProposal(normalizedMissingKind).valid).toBe(false); }); - assert.equal(repoCCrossReference.valid, false, "repo C must not resolve compact references through repo A"); - assert.ok( - repoCCrossReference.errors.some((error) => error.includes("component:component.cli")), - `expected repo C to report missing component.cli, got: ${JSON.stringify(repoCCrossReference.errors)}`, - ); - - const repoBMemoryCommit = repository.createMemoryCommit({ - scope_id: initializedB.working_scope_id, - title: "Seed repo B", + + test("accepts well-formed compact edges", () => { + const compactEdge = { + title: "Compact edge", + creates: { + components, + edges: [{ kind: "contains", from: "component.a", to: "component.b" }], + }, + }; + + const compactResult = validateProposal(normalizeProposal(compactEdge)); + expect(compactResult.valid).toBe(true); + }); +}); + +describe("repo-scoped graph objects", () => { + test("repos are distinct and cross-repo compact references are rejected", async () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-proposal-validate-test-")); + const db = openDatabase(join(tmp, "graph.db")); + + try { + const repository = new SqliteRepository(db); + const service = new KnowledgeGraphService(repository); + const repoA = { repo_root: join(tmp, "repo-a"), repo_name: "repo-a", default_branch: "main" }; + const repoB = { repo_root: join(tmp, "repo-b"), repo_name: "repo-b", default_branch: "main" }; + const repoC = { repo_root: join(tmp, "repo-c"), repo_name: "repo-c", default_branch: "main" }; + const repoAProposal = { + title: "Seed CLI component", + creates: { components: [{ id: "component.cli", name: "Repo A CLI" }] }, + }; + const repoBProposal = { + title: "Seed CLI component", + creates: { components: [{ id: "component.cli", name: "Repo B CLI" }] }, + }; + + const initializedA = service.initRepo(repoA); + const initializedB = service.initRepo(repoB); + service.initRepo(repoC); + const memoryCommit = repository.createMemoryCommit({ + scope_id: initializedA.working_scope_id, + title: "Seed repo A", + }); + repository.createProposalRecords(initializedA.working_scope_id, memoryCommit.id, normalizeProposal(repoAProposal)); + + const repoBResult = await service.validateProposal(repoB, repoBProposal); + expect(repoBResult.valid).toBe(true); + + const repoCCrossReference = await service.validateProposal(repoC, { + title: "Repo C cross-repo compact reference", + creates: { + claims: [ + { + id: "claim.about_cli", + kind: "fact", + text: "Repo C references a CLI component.", + truth: "unknown", + intent: "unknown", + about: "component.cli", + }, + ], + }, + }); + expect(repoCCrossReference.valid).toBe(false); + expect( + repoCCrossReference.errors.some((error) => error.includes("component:component.cli")), + ).toBe(true); + + const repoBMemoryCommit = repository.createMemoryCommit({ + scope_id: initializedB.working_scope_id, + title: "Seed repo B", + }); + repository.createProposalRecords(initializedB.working_scope_id, repoBMemoryCommit.id, normalizeProposal(repoBProposal)); + + const repoBDuplicateResult = await service.validateProposal(repoB, repoBProposal); + expect(repoBDuplicateResult.valid).toBe(false); + expect(repoBDuplicateResult.errors.includes("component:component.cli already exists.")).toBe(true); + + const repoAGraph = service.readGraph(repoA); + const repoBGraph = service.readGraph(repoB); + expect(repoAGraph.components.map((component) => component.name)).toEqual(["Repo A CLI"]); + expect(repoBGraph.components.map((component) => component.name)).toEqual(["Repo B CLI"]); + expect(initializedB.repo_id === initializedA.repo_id).toBe(false); + } finally { + db.close(); + } }); - repository.createProposalRecords(initializedB.working_scope_id, repoBMemoryCommit.id, normalizeProposal(repoBProposal)); - - const repoBDuplicateResult = await service.validateProposal(repoB, repoBProposal); - assert.equal(repoBDuplicateResult.valid, false, "repo B must still reject duplicate IDs inside the same repo"); - assert.ok( - repoBDuplicateResult.errors.includes("component:component.cli already exists."), - `expected repo B duplicate error, got: ${JSON.stringify(repoBDuplicateResult.errors)}`, - ); - - const repoAGraph = service.readGraph(repoA); - const repoBGraph = service.readGraph(repoB); - assert.deepEqual(repoAGraph.components.map((component) => component.name), ["Repo A CLI"]); - assert.deepEqual(repoBGraph.components.map((component) => component.name), ["Repo B CLI"]); - assert.equal(initializedB.repo_id === initializedA.repo_id, false, "test repos must be distinct"); -} finally { - db.close(); -} - -console.log("check-proposal-validate: ok"); +}); diff --git a/scripts/check-repo-context.js b/scripts/check-repo-context.js index 410a9eb..7216bfe 100644 --- a/scripts/check-repo-context.js +++ b/scripts/check-repo-context.js @@ -1,47 +1,16 @@ -import assert from "node:assert/strict"; +import { describe, test, expect, beforeAll } from "vitest"; import { execFileSync } from "node:child_process"; import { mkdtempSync, mkdirSync, realpathSync } from "node:fs"; import { tmpdir } from "node:os"; import { basename, join } from "node:path"; const root = new URL("..", import.meta.url); -const { detectRepoContext } = await import(new URL("dist/apps/cli/repo-context.js", root)); -const tmp = mkdtempSync(join(tmpdir(), "greplica-repo-context-test-")); - -const nonGitFolder = join(tmp, "plain-folder"); -mkdirSync(nonGitFolder); - -const fallbackContext = detectRepoContext(nonGitFolder); -assert.equal(fallbackContext.repo_root, realpathSync(nonGitFolder)); -assert.equal(fallbackContext.repo_name, basename(nonGitFolder)); -assert.equal(fallbackContext.default_branch, "main"); -assert.equal(fallbackContext.remote_url, undefined); - -const sshRepo = initRepo(join(tmp, "ssh-repo")); -git(sshRepo, "remote", "add", "origin", "git@github.com:Autoloops/greplica.git"); -const sshContext = detectRepoContext(sshRepo); -assert.equal(sshContext.repo_root, realpathSync(sshRepo)); -assert.equal(sshContext.remote_url, "git@github.com:Autoloops/greplica.git"); -assert.equal(sshContext.repo_name, "greplica"); - -const httpsRepo = initRepo(join(tmp, "https-repo")); -git(httpsRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); -const httpsContext = detectRepoContext(httpsRepo); -assert.equal(httpsContext.remote_url, "https://github.com/Autoloops/greplica.git"); -assert.equal(httpsContext.repo_name, "greplica"); - -const branchRepo = initRepo(join(tmp, "branch-repo")); -git(branchRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); -git(branchRepo, "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/trunk"); -const branchContext = detectRepoContext(branchRepo); -assert.equal(branchContext.default_branch, "trunk"); - -const noRemoteHeadRepo = initRepo(join(tmp, "no-remote-head-repo")); -git(noRemoteHeadRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); -const noRemoteHeadContext = detectRepoContext(noRemoteHeadRepo); -assert.equal(noRemoteHeadContext.default_branch, "main"); - -console.log("Repo context checks passed."); +let detectRepoContext; + +beforeAll(async () => { + const repoContext = await import(new URL("dist/apps/cli/repo-context.js", root)); + detectRepoContext = repoContext.detectRepoContext; +}); function initRepo(path) { mkdirSync(path); @@ -56,3 +25,57 @@ function git(cwd, ...args) { stdio: ["ignore", "pipe", "pipe"], }).trim(); } + +describe("repo context", () => { + test("non-git folder falls back to folder name and main branch", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-repo-context-test-")); + const nonGitFolder = join(tmp, "plain-folder"); + mkdirSync(nonGitFolder); + + const fallbackContext = detectRepoContext(nonGitFolder); + expect(fallbackContext.repo_root).toBe(realpathSync(nonGitFolder)); + expect(fallbackContext.repo_name).toBe(basename(nonGitFolder)); + expect(fallbackContext.default_branch).toBe("main"); + expect(fallbackContext.remote_url).toBeUndefined(); + }); + + test("SSH remote is parsed correctly", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-repo-context-test-")); + const sshRepo = initRepo(join(tmp, "ssh-repo")); + git(sshRepo, "remote", "add", "origin", "git@github.com:Autoloops/greplica.git"); + + const sshContext = detectRepoContext(sshRepo); + expect(sshContext.repo_root).toBe(realpathSync(sshRepo)); + expect(sshContext.remote_url).toBe("git@github.com:Autoloops/greplica.git"); + expect(sshContext.repo_name).toBe("greplica"); + }); + + test("HTTPS remote is parsed correctly", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-repo-context-test-")); + const httpsRepo = initRepo(join(tmp, "https-repo")); + git(httpsRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); + + const httpsContext = detectRepoContext(httpsRepo); + expect(httpsContext.remote_url).toBe("https://github.com/Autoloops/greplica.git"); + expect(httpsContext.repo_name).toBe("greplica"); + }); + + test("custom default branch is detected from remote HEAD", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-repo-context-test-")); + const branchRepo = initRepo(join(tmp, "branch-repo")); + git(branchRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); + git(branchRepo, "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/trunk"); + + const branchContext = detectRepoContext(branchRepo); + expect(branchContext.default_branch).toBe("trunk"); + }); + + test("defaults to main when remote HEAD is not set", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-repo-context-test-")); + const noRemoteHeadRepo = initRepo(join(tmp, "no-remote-head-repo")); + git(noRemoteHeadRepo, "remote", "add", "origin", "https://github.com/Autoloops/greplica.git"); + + const noRemoteHeadContext = detectRepoContext(noRemoteHeadRepo); + expect(noRemoteHeadContext.default_branch).toBe("main"); + }); +}); diff --git a/scripts/check-transcript-bundle.js b/scripts/check-transcript-bundle.js index b16333f..08e8c4d 100644 --- a/scripts/check-transcript-bundle.js +++ b/scripts/check-transcript-bundle.js @@ -1,272 +1,230 @@ -import assert from "node:assert/strict"; +import { describe, test, expect } from "vitest"; import { execFileSync } from "node:child_process"; import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; -const root = new URL("..", import.meta.url); -const cliPath = fileURLToPath(new URL("dist/apps/cli/main.js", root)); -const tmp = mkdtempSync(join(tmpdir(), "greplica-transcript-bundle-test-")); +const root = fileURLToPath(new URL("..", import.meta.url)); +const cliPath = join(root, "dist", "apps", "cli", "main.js"); -const codexOne = join(tmp, "codex-one.jsonl"); -const codexTwo = join(tmp, "codex-two.jsonl"); -const claudeOne = join(tmp, "claude-one.jsonl"); -const copilotOne = join(tmp, "copilot-one.jsonl"); -const codexOut = join(tmp, "codex-bundle.md"); -const claudeOut = join(tmp, "claude-bundle.md"); -const copilotOut = join(tmp, "copilot-bundle.md"); -const opencodeOut = join(tmp, "opencode-bundle.md"); +describe("transcript bundle", () => { + test("bundles codex transcripts and strips system instructions", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-transcript-bundle-test-")); + const codexOne = join(tmp, "codex-one.jsonl"); + const codexTwo = join(tmp, "codex-two.jsonl"); + const codexOut = join(tmp, "codex-bundle.md"); -writeFileSync( - codexOne, - [ - JSON.stringify({ - type: "session_meta", - payload: { - id: "codex-session-one", - timestamp: "2026-06-25T00:00:00.000Z", - cwd: "/repo/example", - }, - }), - JSON.stringify({ - timestamp: "2026-06-25T00:01:00.000Z", - type: "event_msg", - payload: { - type: "user_message", - message: "Remember this durable Codex insight. do not keep this", - }, - }), - ].join("\n"), - "utf8", -); - -writeFileSync( - codexTwo, - [ - JSON.stringify({ - type: "session_meta", - payload: { - id: "codex-session-two", - cwd: "/repo/example", - }, - }), - JSON.stringify({ - timestamp: "2026-06-25T00:02:00.000Z", - type: "event_msg", - payload: { - type: "agent_message", - message: "A second Codex transcript fact. drop this", - }, - }), - ].join("\n"), - "utf8", -); + writeFileSync( + codexOne, + [ + JSON.stringify({ + type: "session_meta", + payload: { + id: "codex-session-one", + timestamp: "2026-06-25T00:00:00.000Z", + cwd: "/repo/example", + }, + }), + JSON.stringify({ + timestamp: "2026-06-25T00:01:00.000Z", + type: "event_msg", + payload: { + type: "user_message", + message: "Remember this durable Codex insight. do not keep this", + }, + }), + ].join("\n"), + "utf8", + ); -writeFileSync( - claudeOne, - [ - JSON.stringify({ - type: "user", - sessionId: "claude-session-one", - cwd: "/repo/example", - timestamp: "2026-06-25T00:03:00.000Z", - message: { - role: "user", - content: [ - { - type: "text", - text: "Remember this durable Claude insight.", + writeFileSync( + codexTwo, + [ + JSON.stringify({ + type: "session_meta", + payload: { + id: "codex-session-two", + cwd: "/repo/example", + }, + }), + JSON.stringify({ + timestamp: "2026-06-25T00:02:00.000Z", + type: "event_msg", + payload: { + type: "agent_message", + message: "A second Codex transcript fact. drop this", }, - ], - }, - }), - ].join("\n"), - "utf8", -); + }), + ].join("\n"), + "utf8", + ); -writeFileSync( - copilotOne, - [ - JSON.stringify({ - type: "session.start", - data: { - sessionId: "copilot-session-one", - copilotVersion: "1.0.66", - context: { + const output = execFileSync( + process.execPath, + [cliPath, "transcript", "bundle", "--platform", "codex", "--file", codexOne, "--file", codexTwo, "--out", codexOut], + { encoding: "utf8" }, + ); + const bundle = readFileSync(codexOut, "utf8"); + + expect(output).toMatch(/Wrote transcript bundle/); + expect(output).toMatch(/codex-session:codex-session-one/); + expect(output).toMatch(/codex-session:codex-session-two/); + expect(bundle).toMatch(/file_count: 2/); + expect(bundle).toMatch(/session_ref: codex-session:codex-session-one/); + expect(bundle).toMatch(/session_ref: codex-session:codex-session-two/); + expect(bundle).toMatch(/Remember this durable Codex insight/); + expect(bundle).toMatch(/A second Codex transcript fact/); + expect(bundle).not.toMatch(/do not keep this/); + expect(bundle).not.toMatch(/drop this/); + }); + + test("bundles claude transcript", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-transcript-bundle-test-")); + const claudeOne = join(tmp, "claude-one.jsonl"); + const claudeOut = join(tmp, "claude-bundle.md"); + + writeFileSync( + claudeOne, + [ + JSON.stringify({ + type: "user", + sessionId: "claude-session-one", cwd: "/repo/example", - repository: "Autoloops/greplica", - branch: "copilot-test", - }, - }, - timestamp: "2026-06-25T00:03:30.000Z", - }), - JSON.stringify({ - type: "session.model_change", - data: { - newModel: "claude-haiku-4.5", - }, - timestamp: "2026-06-25T00:03:45.000Z", - }), - JSON.stringify({ - session_id: "copilot-session-one", - cwd: "/repo/example", - timestamp: "2026-06-25T00:04:00.000Z", - role: "user", - content: "Remember this durable Copilot insight. remove this", - }), - JSON.stringify({ - session_id: "copilot-session-one", - cwd: "/repo/example", - timestamp: "2026-06-25T00:05:00.000Z", - role: "assistant", - content: [{ type: "text", text: "A Copilot assistant fact." }], - }), - ].join("\n"), - "utf8", -); + timestamp: "2026-06-25T00:03:00.000Z", + message: { + role: "user", + content: [{ type: "text", text: "Remember this durable Claude insight." }], + }, + }), + ].join("\n"), + "utf8", + ); -const codexOutput = execFileSync( - process.execPath, - [ - cliPath, - "transcript", - "bundle", - "--platform", - "codex", - "--file", - codexOne, - "--file", - codexTwo, - "--out", - codexOut, - ], - { encoding: "utf8" }, -); -const codexBundle = readFileSync(codexOut, "utf8"); -assert.match(codexOutput, /Wrote transcript bundle/); -assert.match(codexOutput, /codex-session:codex-session-one/); -assert.match(codexOutput, /codex-session:codex-session-two/); -assert.match(codexBundle, /file_count: 2/); -assert.match(codexBundle, /session_ref: codex-session:codex-session-one/); -assert.match(codexBundle, /session_ref: codex-session:codex-session-two/); -assert.match(codexBundle, /Remember this durable Codex insight/); -assert.match(codexBundle, /A second Codex transcript fact/); -assert.doesNotMatch(codexBundle, /do not keep this/); -assert.doesNotMatch(codexBundle, /drop this/); + const output = execFileSync( + process.execPath, + [cliPath, "transcript", "bundle", "--platform", "claude", "--file", claudeOne, "--out", claudeOut], + { encoding: "utf8" }, + ); + const bundle = readFileSync(claudeOut, "utf8"); -const claudeOutput = execFileSync( - process.execPath, - [ - cliPath, - "transcript", - "bundle", - "--platform", - "claude", - "--file", - claudeOne, - "--out", - claudeOut, - ], - { encoding: "utf8" }, -); -const claudeBundle = readFileSync(claudeOut, "utf8"); -assert.match(claudeOutput, /claude-code-session:claude-session-one/); -assert.match(claudeBundle, /session_ref: claude-code-session:claude-session-one/); -assert.match(claudeBundle, /Remember this durable Claude insight/); + expect(output).toMatch(/claude-code-session:claude-session-one/); + expect(bundle).toMatch(/session_ref: claude-code-session:claude-session-one/); + expect(bundle).toMatch(/Remember this durable Claude insight/); + }); -const copilotOutput = execFileSync( - process.execPath, - [ - cliPath, - "transcript", - "bundle", - "--platform", - "copilot", - "--file", - copilotOne, - "--out", - copilotOut, - ], - { encoding: "utf8" }, -); -const copilotBundle = readFileSync(copilotOut, "utf8"); -assert.match(copilotOutput, /copilot-session:copilot-session-one/); -assert.match(copilotBundle, /session_ref: copilot-session:copilot-session-one/); -assert.match(copilotBundle, /repository: Autoloops\/greplica/); -assert.match(copilotBundle, /branch: copilot-test/); -assert.match(copilotBundle, /Remember this durable Copilot insight/); -assert.match(copilotBundle, /A Copilot assistant fact/); -assert.doesNotMatch(copilotBundle, /remove this/); + test("bundles copilot transcript and strips system instructions", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-transcript-bundle-test-")); + const copilotOne = join(tmp, "copilot-one.jsonl"); + const copilotOut = join(tmp, "copilot-bundle.md"); -assert.throws( - () => - execFileSync( + writeFileSync( + copilotOne, + [ + JSON.stringify({ + type: "session.start", + data: { + sessionId: "copilot-session-one", + copilotVersion: "1.0.66", + context: { cwd: "/repo/example", repository: "Autoloops/greplica", branch: "copilot-test" }, + }, + timestamp: "2026-06-25T00:03:30.000Z", + }), + JSON.stringify({ + type: "session.model_change", + data: { newModel: "claude-haiku-4.5" }, + timestamp: "2026-06-25T00:03:45.000Z", + }), + JSON.stringify({ + session_id: "copilot-session-one", + cwd: "/repo/example", + timestamp: "2026-06-25T00:04:00.000Z", + role: "user", + content: "Remember this durable Copilot insight. remove this", + }), + JSON.stringify({ + session_id: "copilot-session-one", + cwd: "/repo/example", + timestamp: "2026-06-25T00:05:00.000Z", + role: "assistant", + content: [{ type: "text", text: "A Copilot assistant fact." }], + }), + ].join("\n"), + "utf8", + ); + + const output = execFileSync( process.execPath, - [cliPath, "transcript", "bundle", "--platform", "codex", "--file", join(tmp, "missing.jsonl"), "--out", join(tmp, "missing.md")], - { encoding: "utf8", stdio: "pipe" }, - ), - /Transcript file does not exist/, -); + [cliPath, "transcript", "bundle", "--platform", "copilot", "--file", copilotOne, "--out", copilotOut], + { encoding: "utf8" }, + ); + const bundle = readFileSync(copilotOut, "utf8"); + + expect(output).toMatch(/copilot-session:copilot-session-one/); + expect(bundle).toMatch(/session_ref: copilot-session:copilot-session-one/); + expect(bundle).toMatch(/repository: Autoloops\/greplica/); + expect(bundle).toMatch(/branch: copilot-test/); + expect(bundle).toMatch(/Remember this durable Copilot insight/); + expect(bundle).toMatch(/A Copilot assistant fact/); + expect(bundle).not.toMatch(/remove this/); + }); -const opencodeDataHome = join(tmp, "opencode-data"); -const opencodeSessionId = "opencode-session-one"; -const opencodeSessionFile = join(opencodeDataHome, "opencode", "storage", "session", `${opencodeSessionId}.json`); -mkdirSync(dirname(opencodeSessionFile), { recursive: true }); -mkdirSync(join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId), { recursive: true }); -writeFileSync( - opencodeSessionFile, - JSON.stringify({ - id: opencodeSessionId, - directory: "/repo/example", - }), - "utf8", -); -writeFileSync( - join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId, "msg-01.json"), - JSON.stringify({ - role: "user", - content: "Remember this durable OpenCode insight. remove this", - time: "2026-06-25T00:06:00.000Z", - }), - "utf8", -); -writeFileSync( - join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId, "msg-02.json"), - JSON.stringify({ - role: "assistant", - parts: [{ text: "An OpenCode assistant fact." }], - time: "2026-06-25T00:07:00.000Z", - }), - "utf8", -); + test("errors on missing transcript file", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-transcript-bundle-test-")); + expect(() => + execFileSync( + process.execPath, + [cliPath, "transcript", "bundle", "--platform", "codex", "--file", join(tmp, "missing.jsonl"), "--out", join(tmp, "missing.md")], + { encoding: "utf8", stdio: "pipe" }, + ), + ).toThrow(/Transcript file does not exist/); + }); -const opencodeOutput = execFileSync( - process.execPath, - [ - cliPath, - "transcript", - "bundle", - "--platform", - "opencode", - "--file", - opencodeSessionFile, - "--out", - opencodeOut, - ], - { - encoding: "utf8", - env: { - ...process.env, - XDG_DATA_HOME: opencodeDataHome, - }, - }, -); -const opencodeBundle = readFileSync(opencodeOut, "utf8"); -assert.match(opencodeOutput, /opencode-session:opencode-session-one/); -assert.match(opencodeBundle, /session_ref: opencode-session:opencode-session-one/); -assert.match(opencodeBundle, /Remember this durable OpenCode insight/); -assert.match(opencodeBundle, /An OpenCode assistant fact/); -assert.doesNotMatch(opencodeBundle, /remove this/); + test("bundles opencode transcripts and strips system instructions", () => { + const tmp = mkdtempSync(join(tmpdir(), "greplica-transcript-bundle-test-")); + const opencodeDataHome = join(tmp, "opencode-data"); + const opencodeSessionId = "opencode-session-one"; + const opencodeSessionFile = join(opencodeDataHome, "opencode", "storage", "session", `${opencodeSessionId}.json`); + const opencodeOut = join(tmp, "opencode-bundle.md"); + + mkdirSync(dirname(opencodeSessionFile), { recursive: true }); + mkdirSync(join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId), { recursive: true }); + + writeFileSync( + opencodeSessionFile, + JSON.stringify({ id: opencodeSessionId, directory: "/repo/example" }), + "utf8", + ); + writeFileSync( + join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId, "msg-01.json"), + JSON.stringify({ + role: "user", + content: "Remember this durable OpenCode insight. remove this", + time: "2026-06-25T00:06:00.000Z", + }), + "utf8", + ); + writeFileSync( + join(opencodeDataHome, "opencode", "storage", "message", opencodeSessionId, "msg-02.json"), + JSON.stringify({ + role: "assistant", + parts: [{ text: "An OpenCode assistant fact." }], + time: "2026-06-25T00:07:00.000Z", + }), + "utf8", + ); + + const output = execFileSync( + process.execPath, + [cliPath, "transcript", "bundle", "--platform", "opencode", "--file", opencodeSessionFile, "--out", opencodeOut], + { encoding: "utf8", env: { ...process.env, XDG_DATA_HOME: opencodeDataHome } }, + ); + const bundle = readFileSync(opencodeOut, "utf8"); -console.log("Transcript bundle checks passed."); + expect(output).toMatch(/opencode-session:opencode-session-one/); + expect(bundle).toMatch(/session_ref: opencode-session:opencode-session-one/); + expect(bundle).toMatch(/Remember this durable OpenCode insight/); + expect(bundle).toMatch(/An OpenCode assistant fact/); + expect(bundle).not.toMatch(/remove this/); + }); +}); diff --git a/tests/integration/bm25-tokenizer.test.ts b/tests/integration/bm25-tokenizer.test.ts deleted file mode 100644 index cc54b2f..0000000 --- a/tests/integration/bm25-tokenizer.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, test, expect, beforeAll } from 'vitest'; - -let tokenize: (text: string) => string[]; -let scoreBm25: (query: string, documents: { key: string; text: string }[], config: unknown) => { id: string }[]; -let graphContextConfig: unknown; - -beforeAll(async () => { - const bm25 = await import(new URL('../../dist/libs/knowledge-graph/graph-context/bm25.js', import.meta.url).href); - const config = await import(new URL('../../dist/libs/knowledge-graph/graph-context/config.js', import.meta.url).href); - tokenize = bm25.tokenize; - scoreBm25 = bm25.scoreBm25; - graphContextConfig = config.graphContextConfig; -}); - -describe('BM25 tokenizer', () => { - test('camelCase emits sub-tokens and keeps full lowercased token', () => { - const tokens = tokenize('handleUserAuth'); - expect(tokens).toContain('user'); - expect(tokens).toContain('auth'); - expect(tokens).toContain('handle'); - expect(tokens).toContain('handleuserauth'); - }); - - test('PascalCase emits sub-tokens', () => { - expect(tokenize('HandleUserAuth')).toContain('user'); - }); - - test('snake_case emits sub-tokens and keeps original token', () => { - const tokens = tokenize('handle_user_auth'); - expect(tokens).toContain('user'); - expect(tokens).toContain('handle_user_auth'); - }); - - test('kebab-case emits sub-tokens', () => { - expect(tokenize('graph-context')).toContain('graph'); - expect(tokenize('graph-context')).toContain('context'); - }); - - test('letter-number boundary emits sub-tokens', () => { - expect(tokenize('user2FA')).toContain('user'); - expect(tokenize('user2FA')).toContain('fa'); - }); - - test('English stemming variants still apply', () => { - expect(tokenize('tokens')).toContain('token'); - }); - - test('BM25 matches camelCase doc tokens to spaced query terms', () => { - const documents = [{ key: 'doc:auth', text: 'The handleUserAuth function validates sessions.' }]; - const ranked = scoreBm25('user auth validation', documents, graphContextConfig); - expect(ranked[0]?.id).toBe('doc:auth'); - }); -}); diff --git a/tests/integration/graph-view.test.ts b/tests/integration/graph-view.test.ts deleted file mode 100644 index d4a4cec..0000000 --- a/tests/integration/graph-view.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { describe, test, expect, beforeAll } from 'vitest'; -import { mkdtempSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -let openDatabase: (path: string) => import('better-sqlite3').Database; -let SqliteRepository: new (db: import('better-sqlite3').Database) => unknown; -let KnowledgeGraphService: new (repository: unknown) => { - initRepo: (input: { repo_root: string; repo_name: string; default_branch: string }) => { working_scope_id: string }; - buildGraphView: (input: { repo_name: string }) => string; -}; - -beforeAll(async () => { - const db = await import(new URL('../../dist/libs/storage/sqlite/db.js', import.meta.url).href); - const repo = await import(new URL('../../dist/libs/storage/sqlite/repository.js', import.meta.url).href); - const kg = await import(new URL('../../dist/libs/knowledge-graph/service.js', import.meta.url).href); - openDatabase = db.openDatabase; - SqliteRepository = repo.SqliteRepository; - KnowledgeGraphService = kg.KnowledgeGraphService; -}); - -describe('graph view', () => { - test('renders components without anchors', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-graph-view-test-')); - const db = openDatabase(join(tmp, 'graph.db')); - - try { - const repository = new (SqliteRepository as any)(db); - const service = new (KnowledgeGraphService as any)(repository); - const repo = { - repo_root: join(tmp, 'repo'), - repo_name: 'graph-view-null-anchor', - default_branch: 'main', - }; - - const initialized = service.initRepo(repo); - const memoryCommit = repository.createMemoryCommit({ scope_id: initialized.working_scope_id, title: 'Seed null component anchor' }); - - repository.createProposalRecords(initialized.working_scope_id, memoryCommit.id, { - title: 'Seed null component anchor', - creates: { components: [{ id: 'component.no_anchor', name: 'Component Without Anchor' }] }, - }); - - const html = service.buildGraphView(repo); - - expect(html).toMatch(/Component Without Anchor/); - expect(html).toMatch(/Greplica graph view/); - } finally { - db.close(); - } - }); -}); diff --git a/tests/integration/install-options.test.ts b/tests/integration/install-options.test.ts deleted file mode 100644 index 6c3bf6b..0000000 --- a/tests/integration/install-options.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { describe, test, expect, beforeAll } from 'vitest'; -import { execFileSync, spawnSync } from 'node:child_process'; -import { existsSync, mkdirSync, mkdtempSync, readFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const root = fileURLToPath(new URL('../..', import.meta.url)); -const cli = join(root, 'dist', 'apps', 'cli', 'main.js'); - -let greplicaHookGuidance: string; -let shouldRunAutoMemoryUpdates: (config: { session: { autoMemoryUpdates: boolean } }) => boolean; - -beforeAll(async () => { - const guidance = await import(new URL('../../dist/libs/hooks/guidance.js', import.meta.url).href); - const worker = await import(new URL('../../dist/libs/hooks/worker.js', import.meta.url).href); - greplicaHookGuidance = guidance.greplicaHookGuidance; - shouldRunAutoMemoryUpdates = worker.shouldRunAutoMemoryUpdates; -}); - -function readConfig(greplicaHome: string) { - return JSON.parse(readFileSync(join(greplicaHome, 'config.json'), 'utf8')); -} - -function installInTempRepo(tmp: string, name: string, flags: string[], platform = 'codex') { - const repo = join(tmp, name, 'repo'); - const greplicaHome = join(tmp, name, 'greplica-home'); - const codexHome = join(tmp, name, 'codex-home'); - const copilotHome = join(tmp, name, 'copilot-home'); - const xdgConfigHome = join(tmp, name, 'xdg-config-home'); - mkdirSync(repo, { recursive: true }); - execFileSync('git', ['init', '--quiet'], { cwd: repo, encoding: 'utf8' }); - - const env: Record = { - ...process.env as Record, - GREPLICA_HOME: greplicaHome, - CODEX_HOME: codexHome, - COPILOT_HOME: copilotHome, - XDG_CONFIG_HOME: xdgConfigHome, - GREPLICA_INSTALL_SKIP_PREWARM: '1', - }; - - const output = execFileSync(process.execPath, [cli, 'install', '--platform', platform, '--embedding', 'local', ...flags], { - cwd: repo, - encoding: 'utf8', - env, - }); - - execFileSync(process.execPath, [cli, 'doctor'], { cwd: repo, encoding: 'utf8', env }); - return { repo, greplicaHome, codexHome, copilotHome, xdgConfigHome, output, env }; -} - -describe('install options', () => { - test('auto-save mode installs hooks and enables auto memory updates', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); - const autoSave = installInTempRepo(tmp, 'auto-save', ['--hooks', 'enabled', '--auto-memory', 'enabled']); - - expect(autoSave.output).toMatch(/Hooks: installed for UserPromptSubmit, Stop\./); - expect(autoSave.output).toMatch(/Automatic memory updates: enabled\./); - expect(existsSync(join(autoSave.codexHome, 'hooks.json'))).toBe(true); - expect(readConfig(autoSave.greplicaHome).session.autoMemoryUpdates).toBe(true); - expect(shouldRunAutoMemoryUpdates(readConfig(autoSave.greplicaHome))).toBe(true); - }); - - test('guidance-only mode installs hooks and injects guidance but disables auto memory updates', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); - const guidanceOnly = installInTempRepo(tmp, 'guidance-only', ['--hooks', 'enabled', '--auto-memory', 'disabled']); - - expect(guidanceOnly.output).toMatch(/Hooks: installed for UserPromptSubmit, Stop\./); - expect(guidanceOnly.output).toMatch(/Automatic memory updates: disabled\./); - expect(existsSync(join(guidanceOnly.codexHome, 'hooks.json'))).toBe(true); - expect(readConfig(guidanceOnly.greplicaHome).session.autoMemoryUpdates).toBe(false); - expect(shouldRunAutoMemoryUpdates(readConfig(guidanceOnly.greplicaHome))).toBe(false); - - const hookOutput = execFileSync( - process.execPath, - [cli, 'hook', 'ingest', '--platform', 'codex'], - { - cwd: guidanceOnly.repo, - encoding: 'utf8', - input: JSON.stringify({ hook_event_name: 'UserPromptSubmit', session_id: 'guidance-only-session', cwd: guidanceOnly.repo }), - env: guidanceOnly.env, - }, - ); - - expect(hookOutput).toMatch(/Greplica hook guidance/); - expect(hookOutput).toMatch(/greplica graph context/); - }); - - test('no-hooks mode does not install hooks and disables auto memory updates', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); - const noHooks = installInTempRepo(tmp, 'no-hooks', ['--hooks', 'disabled']); - - expect(noHooks.output).toMatch(/Hooks: not installed\./); - expect(noHooks.output).toMatch(/Automatic memory updates: disabled\./); - expect(noHooks.output).toMatch(/To give future agents Greplica guidance without hooks/); - expect(noHooks.output).toContain(greplicaHookGuidance); - expect(existsSync(join(noHooks.codexHome, 'hooks.json'))).toBe(false); - expect(readConfig(noHooks.greplicaHome).session.autoMemoryUpdates).toBe(false); - }); - - test('opencode installs hooks and enables auto memory updates', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); - const opencodeHooks = installInTempRepo(tmp, 'opencode-hooks', ['--hooks', 'enabled', '--auto-memory', 'enabled'], 'opencode'); - - expect(opencodeHooks.output).toMatch(/Installed Greplica for OpenCode\./); - expect(opencodeHooks.output).toMatch(/Hooks: installed for UserPromptSubmit, Stop\./); - expect(opencodeHooks.output).toMatch(/Automatic memory updates: enabled\./); - expect(existsSync(join(opencodeHooks.xdgConfigHome, 'opencode', 'hooks.json'))).toBe(true); - expect(readConfig(opencodeHooks.greplicaHome).session.autoMemoryUpdates).toBe(true); - }); - - test('copilot installs hooks and enables auto memory updates', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); - const copilotHooks = installInTempRepo(tmp, 'copilot-hooks', ['--hooks', 'enabled', '--auto-memory', 'enabled'], 'copilot'); - - expect(copilotHooks.output).toMatch(/Installed Greplica for GitHub Copilot CLI\./); - expect(copilotHooks.output).toMatch(/Hooks: installed for SessionStart, Stop\./); - expect(copilotHooks.output).toMatch(/Automatic memory updates: enabled\./); - expect(existsSync(join(copilotHooks.copilotHome, 'hooks', 'greplica.json'))).toBe(true); - expect(readConfig(copilotHooks.greplicaHome).session.autoMemoryUpdates).toBe(true); - }); - - test('rejects auto-memory enabled without hooks enabled', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); - const noHooks = installInTempRepo(tmp, 'invalid-combo', ['--hooks', 'disabled']); - - const invalid = spawnSync( - process.execPath, - [cli, 'install', '--platform', 'codex', '--embedding', 'local', '--hooks', 'disabled', '--auto-memory', 'enabled'], - { cwd: noHooks.repo, encoding: 'utf8', env: noHooks.env }, - ); - - expect(invalid.status).not.toBe(0); - expect(invalid.stderr).toMatch(/--auto-memory enabled requires --hooks enabled/); - }); - - test('rejects invalid flag values', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-install-options-test-')); - const noHooks = installInTempRepo(tmp, 'invalid-flag', ['--hooks', 'disabled']); - - const invalidValue = spawnSync( - process.execPath, - [cli, 'install', '--platform', 'codex', '--embedding', 'local', '--hooks', 'sometimes'], - { cwd: noHooks.repo, encoding: 'utf8', env: noHooks.env }, - ); - - expect(invalidValue.status).not.toBe(0); - expect(invalidValue.stderr).toMatch(/expected enabled or disabled/); - }); -}); diff --git a/tests/integration/proposal-validate.test.ts b/tests/integration/proposal-validate.test.ts deleted file mode 100644 index 68f015f..0000000 --- a/tests/integration/proposal-validate.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { describe, test, expect, beforeAll } from 'vitest'; -import { mkdtempSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; - -let normalizeProposal: (input: unknown) => unknown; -let validateProposal: (proposal: unknown) => { valid: boolean; errors: string[] }; -let KnowledgeGraphService: new (repository: unknown) => unknown; -let openDatabase: (path: string) => { close: () => void }; -let SqliteRepository: new (db: unknown) => unknown; - -beforeAll(async () => { - const proposal = await import(new URL('../../dist/libs/knowledge-graph/proposal.js', import.meta.url).href); - const validate = await import(new URL('../../dist/libs/knowledge-graph/validate-proposal.js', import.meta.url).href); - const service = await import(new URL('../../dist/libs/knowledge-graph/service.js', import.meta.url).href); - const db = await import(new URL('../../dist/libs/storage/sqlite/db.js', import.meta.url).href); - const repo = await import(new URL('../../dist/libs/storage/sqlite/repository.js', import.meta.url).href); - normalizeProposal = proposal.normalizeProposal; - validateProposal = validate.validateProposal; - KnowledgeGraphService = service.KnowledgeGraphService; - openDatabase = db.openDatabase; - SqliteRepository = repo.SqliteRepository; -}); - -const components = [ - { id: 'component.a', name: 'A' }, - { id: 'component.b', name: 'B' }, -]; - -describe('proposal validation', () => { - test('does not crash on edges using from_id/to_id and reports a clear error', () => { - const malformedEdge = { - title: 'Malformed edge repro', - creates: { - components, - edges: [{ from_id: 'component.a', to_id: 'component.b', kind: 'contains' }], - }, - }; - - let normalized: unknown; - expect(() => { - normalized = normalizeProposal(malformedEdge); - }).not.toThrow(); - - const malformedResult = validateProposal(normalized!); - expect(malformedResult.valid).toBe(false); - expect(malformedResult.errors.some((error: string) => error.includes("from'/'to"))).toBe(true); - }); - - test('does not crash on edges missing kind and reports them invalid', () => { - const missingKindEdge = { - title: 'Missing kind repro', - creates: { - components, - edges: [{ from: 'component.a', to: 'component.b' }], - }, - }; - - let normalizedMissingKind: unknown; - expect(() => { - normalizedMissingKind = normalizeProposal(missingKindEdge); - }).not.toThrow(); - - expect(validateProposal(normalizedMissingKind!).valid).toBe(false); - }); - - test('accepts well-formed compact edges', () => { - const compactEdge = { - title: 'Compact edge', - creates: { - components, - edges: [{ kind: 'contains', from: 'component.a', to: 'component.b' }], - }, - }; - - const compactResult = validateProposal(normalizeProposal(compactEdge)); - expect(compactResult.valid).toBe(true); - }); -}); - -describe('repo-scoped graph objects', () => { - test('repos are distinct and cross-repo compact references are rejected', async () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-proposal-validate-test-')); - const db = openDatabase(join(tmp, 'graph.db')); - - try { - const repository = new SqliteRepository(db); - const service = new KnowledgeGraphService(repository); - const repoA = { repo_root: join(tmp, 'repo-a'), repo_name: 'repo-a', default_branch: 'main' }; - const repoB = { repo_root: join(tmp, 'repo-b'), repo_name: 'repo-b', default_branch: 'main' }; - const repoC = { repo_root: join(tmp, 'repo-c'), repo_name: 'repo-c', default_branch: 'main' }; - const repoAProposal = { - title: 'Seed CLI component', - creates: { components: [{ id: 'component.cli', name: 'Repo A CLI' }] }, - }; - const repoBProposal = { - title: 'Seed CLI component', - creates: { components: [{ id: 'component.cli', name: 'Repo B CLI' }] }, - }; - - const initializedA = (service as any).initRepo(repoA); - const initializedB = (service as any).initRepo(repoB); - (service as any).initRepo(repoC); - const memoryCommit = (repository as any).createMemoryCommit({ - scope_id: initializedA.working_scope_id, - title: 'Seed repo A', - }); - (repository as any).createProposalRecords( - initializedA.working_scope_id, - memoryCommit.id, - normalizeProposal(repoAProposal), - ); - - const repoBResult = await (service as any).validateProposal(repoB, repoBProposal); - expect(repoBResult.valid).toBe(true); - - const repoCCrossReference = await (service as any).validateProposal(repoC, { - title: 'Repo C cross-repo compact reference', - creates: { - claims: [ - { - id: 'claim.about_cli', - kind: 'fact', - text: 'Repo C references a CLI component.', - truth: 'unknown', - intent: 'unknown', - about: 'component.cli', - }, - ], - }, - }); - expect(repoCCrossReference.valid).toBe(false); - expect( - repoCCrossReference.errors.some((error: string) => error.includes('component:component.cli')), - ).toBe(true); - - const repoBMemoryCommit = (repository as any).createMemoryCommit({ - scope_id: initializedB.working_scope_id, - title: 'Seed repo B', - }); - (repository as any).createProposalRecords( - initializedB.working_scope_id, - repoBMemoryCommit.id, - normalizeProposal(repoBProposal), - ); - - const repoBDuplicateResult = await (service as any).validateProposal(repoB, repoBProposal); - expect(repoBDuplicateResult.valid).toBe(false); - expect(repoBDuplicateResult.errors.includes('component:component.cli already exists.')).toBe(true); - - const repoAGraph = (service as any).readGraph(repoA); - const repoBGraph = (service as any).readGraph(repoB); - expect(repoAGraph.components.map((c: { name: string }) => c.name)).toEqual(['Repo A CLI']); - expect(repoBGraph.components.map((c: { name: string }) => c.name)).toEqual(['Repo B CLI']); - expect(initializedB.repo_id === initializedA.repo_id).toBe(false); - } finally { - db.close(); - } - }); -}); diff --git a/tests/integration/repo-context.test.ts b/tests/integration/repo-context.test.ts deleted file mode 100644 index c4b105f..0000000 --- a/tests/integration/repo-context.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { describe, test, expect, beforeAll } from 'vitest'; -import { execFileSync } from 'node:child_process'; -import { mkdtempSync, mkdirSync, realpathSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { basename, join } from 'node:path'; - -const tmpRoot = mkdtempSync(join(tmpdir(), 'greplica-repo-context-test-')); - -let detectRepoContext: (cwd: string) => { repo_root: string; repo_name: string; default_branch: string; remote_url: string | undefined }; - -beforeAll(async () => { - const repoContext = await import(new URL('../../dist/apps/cli/repo-context.js', import.meta.url).href); - detectRepoContext = repoContext.detectRepoContext; -}); - -function initRepo(path: string) { - mkdirSync(path); - execFileSync('git', ['init', '--quiet'], { cwd: path, encoding: 'utf8' }); - return path; -} - -function git(cwd: string, ...args: string[]) { - return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); -} - -describe('repo context detection', () => { - test('falls back to folder name for non-git directories', () => { - const nonGitFolder = join(tmpRoot, 'plain-folder'); - mkdirSync(nonGitFolder); - - const fallbackContext = detectRepoContext(nonGitFolder); - - expect(fallbackContext.repo_root).toBe(realpathSync(nonGitFolder)); - expect(fallbackContext.repo_name).toBe(basename(nonGitFolder)); - expect(fallbackContext.default_branch).toBe('main'); - expect(fallbackContext.remote_url).toBeUndefined(); - }); - - test('parses ssh remote urls', () => { - const sshRepo = initRepo(join(tmpRoot, 'ssh-repo')); - git(sshRepo, 'remote', 'add', 'origin', 'git@github.com:Autoloops/greplica.git'); - - const sshContext = detectRepoContext(sshRepo); - - expect(sshContext.repo_root).toBe(realpathSync(sshRepo)); - expect(sshContext.remote_url).toBe('git@github.com:Autoloops/greplica.git'); - expect(sshContext.repo_name).toBe('greplica'); - }); - - test('parses https remote urls', () => { - const httpsRepo = initRepo(join(tmpRoot, 'https-repo')); - git(httpsRepo, 'remote', 'add', 'origin', 'https://github.com/Autoloops/greplica.git'); - - const httpsContext = detectRepoContext(httpsRepo); - - expect(httpsContext.remote_url).toBe('https://github.com/Autoloops/greplica.git'); - expect(httpsContext.repo_name).toBe('greplica'); - }); - - test('detects default branch from remote HEAD', () => { - const branchRepo = initRepo(join(tmpRoot, 'branch-repo')); - git(branchRepo, 'remote', 'add', 'origin', 'https://github.com/Autoloops/greplica.git'); - git(branchRepo, 'symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/trunk'); - - const branchContext = detectRepoContext(branchRepo); - - expect(branchContext.default_branch).toBe('trunk'); - }); - - test('falls back to main when remote HEAD is missing', () => { - const noRemoteHeadRepo = initRepo(join(tmpRoot, 'no-remote-head-repo')); - git(noRemoteHeadRepo, 'remote', 'add', 'origin', 'https://github.com/Autoloops/greplica.git'); - - const noRemoteHeadContext = detectRepoContext(noRemoteHeadRepo); - - expect(noRemoteHeadContext.default_branch).toBe('main'); - }); -}); diff --git a/tests/integration/transcript-bundle.test.ts b/tests/integration/transcript-bundle.test.ts deleted file mode 100644 index 30074a0..0000000 --- a/tests/integration/transcript-bundle.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { execFileSync } from 'node:child_process'; -import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const root = fileURLToPath(new URL('../..', import.meta.url)); -const cli = join(root, 'dist', 'apps', 'cli', 'main.js'); - -describe('transcript bundle', () => { - test('bundles codex transcripts and strips system instructions', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); - const codexOne = join(tmp, 'codex-one.jsonl'); - const codexTwo = join(tmp, 'codex-two.jsonl'); - const codexOut = join(tmp, 'codex-bundle.md'); - - writeFileSync( - codexOne, - [ - JSON.stringify({ - type: 'session_meta', - payload: { id: 'codex-session-one', timestamp: '2026-06-25T00:00:00.000Z', cwd: '/repo/example' }, - }), - JSON.stringify({ - timestamp: '2026-06-25T00:01:00.000Z', - type: 'event_msg', - payload: { type: 'user_message', message: 'Remember this durable Codex insight. do not keep this' }, - }), - ].join('\n'), - 'utf8', - ); - - writeFileSync( - codexTwo, - [ - JSON.stringify({ - type: 'session_meta', - payload: { id: 'codex-session-two', cwd: '/repo/example' }, - }), - JSON.stringify({ - timestamp: '2026-06-25T00:02:00.000Z', - type: 'event_msg', - payload: { type: 'agent_message', message: 'A second Codex transcript fact. drop this' }, - }), - ].join('\n'), - 'utf8', - ); - - const output = execFileSync( - process.execPath, - [cli, 'transcript', 'bundle', '--platform', 'codex', '--file', codexOne, '--file', codexTwo, '--out', codexOut], - { encoding: 'utf8' }, - ); - - const bundle = readFileSync(codexOut, 'utf8'); - - expect(output).toMatch(/Wrote transcript bundle/); - expect(output).toMatch(/codex-session:codex-session-one/); - expect(output).toMatch(/codex-session:codex-session-two/); - expect(bundle).toMatch(/file_count: 2/); - expect(bundle).toMatch(/session_ref: codex-session:codex-session-one/); - expect(bundle).toMatch(/session_ref: codex-session:codex-session-two/); - expect(bundle).toMatch(/Remember this durable Codex insight/); - expect(bundle).toMatch(/A second Codex transcript fact/); - expect(bundle).not.toMatch(/do not keep this/); - expect(bundle).not.toMatch(/drop this/); - }); - - test('bundles claude transcript', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); - const claudeOne = join(tmp, 'claude-one.jsonl'); - const claudeOut = join(tmp, 'claude-bundle.md'); - - writeFileSync( - claudeOne, - [ - JSON.stringify({ - type: 'user', - sessionId: 'claude-session-one', - cwd: '/repo/example', - timestamp: '2026-06-25T00:03:00.000Z', - message: { role: 'user', content: [{ type: 'text', text: 'Remember this durable Claude insight.' }] }, - }), - ].join('\n'), - 'utf8', - ); - - const output = execFileSync( - process.execPath, - [cli, 'transcript', 'bundle', '--platform', 'claude', '--file', claudeOne, '--out', claudeOut], - { encoding: 'utf8' }, - ); - - const bundle = readFileSync(claudeOut, 'utf8'); - - expect(output).toMatch(/claude-code-session:claude-session-one/); - expect(bundle).toMatch(/session_ref: claude-code-session:claude-session-one/); - expect(bundle).toMatch(/Remember this durable Claude insight/); - }); - - test('bundles copilot transcript and strips system instructions', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); - const copilotOne = join(tmp, 'copilot-one.jsonl'); - const copilotOut = join(tmp, 'copilot-bundle.md'); - - writeFileSync( - copilotOne, - [ - JSON.stringify({ - type: 'session.start', - data: { sessionId: 'copilot-session-one', copilotVersion: '1.0.66', context: { cwd: '/repo/example', repository: 'Autoloops/greplica', branch: 'copilot-test' } }, - timestamp: '2026-06-25T00:03:30.000Z', - }), - JSON.stringify({ type: 'session.model_change', data: { newModel: 'claude-haiku-4.5' }, timestamp: '2026-06-25T00:03:45.000Z' }), - JSON.stringify({ - session_id: 'copilot-session-one', - cwd: '/repo/example', - timestamp: '2026-06-25T00:04:00.000Z', - role: 'user', - content: 'Remember this durable Copilot insight. remove this', - }), - JSON.stringify({ - session_id: 'copilot-session-one', - cwd: '/repo/example', - timestamp: '2026-06-25T00:05:00.000Z', - role: 'assistant', - content: [{ type: 'text', text: 'A Copilot assistant fact.' }], - }), - ].join('\n'), - 'utf8', - ); - - const output = execFileSync( - process.execPath, - [cli, 'transcript', 'bundle', '--platform', 'copilot', '--file', copilotOne, '--out', copilotOut], - { encoding: 'utf8' }, - ); - - const bundle = readFileSync(copilotOut, 'utf8'); - - expect(output).toMatch(/copilot-session:copilot-session-one/); - expect(bundle).toMatch(/session_ref: copilot-session:copilot-session-one/); - expect(bundle).toMatch(/repository: Autoloops\/greplica/); - expect(bundle).toMatch(/branch: copilot-test/); - expect(bundle).toMatch(/Remember this durable Copilot insight/); - expect(bundle).toMatch(/A Copilot assistant fact/); - expect(bundle).not.toMatch(/remove this/); - }); - - test('errors on missing transcript file', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); - expect(() => - execFileSync( - process.execPath, - [cli, 'transcript', 'bundle', '--platform', 'codex', '--file', join(tmp, 'missing.jsonl'), '--out', join(tmp, 'missing.md')], - { encoding: 'utf8', stdio: 'pipe' }, - ), - ).toThrow(/Transcript file does not exist/); - }); - - test('bundles opencode transcripts and strips system instructions', () => { - const tmp = mkdtempSync(join(tmpdir(), 'greplica-transcript-bundle-test-')); - const opencodeDataHome = join(tmp, 'opencode-data'); - const opencodeSessionId = 'opencode-session-one'; - const opencodeSessionFile = join(opencodeDataHome, 'opencode', 'storage', 'session', `${opencodeSessionId}.json`); - const opencodeOut = join(tmp, 'opencode-bundle.md'); - - mkdirSync(dirname(opencodeSessionFile), { recursive: true }); - mkdirSync(join(opencodeDataHome, 'opencode', 'storage', 'message', opencodeSessionId), { recursive: true }); - - writeFileSync( - opencodeSessionFile, - JSON.stringify({ id: opencodeSessionId, directory: '/repo/example' }), - 'utf8', - ); - writeFileSync( - join(opencodeDataHome, 'opencode', 'storage', 'message', opencodeSessionId, 'msg-01.json'), - JSON.stringify({ - role: 'user', - content: 'Remember this durable OpenCode insight. remove this', - time: '2026-06-25T00:06:00.000Z', - }), - 'utf8', - ); - writeFileSync( - join(opencodeDataHome, 'opencode', 'storage', 'message', opencodeSessionId, 'msg-02.json'), - JSON.stringify({ - role: 'assistant', - parts: [{ text: 'An OpenCode assistant fact.' }], - time: '2026-06-25T00:07:00.000Z', - }), - 'utf8', - ); - - const output = execFileSync( - process.execPath, - [cli, 'transcript', 'bundle', '--platform', 'opencode', '--file', opencodeSessionFile, '--out', opencodeOut], - { encoding: 'utf8', env: { ...process.env, XDG_DATA_HOME: opencodeDataHome } }, - ); - - const bundle = readFileSync(opencodeOut, 'utf8'); - - expect(output).toMatch(/opencode-session:opencode-session-one/); - expect(bundle).toMatch(/session_ref: opencode-session:opencode-session-one/); - expect(bundle).toMatch(/Remember this durable OpenCode insight/); - expect(bundle).toMatch(/An OpenCode assistant fact/); - expect(bundle).not.toMatch(/remove this/); - }); -}); diff --git a/tsconfig.build.json b/tsconfig.build.json index 3ddd5a6..dea4dde 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -6,6 +6,5 @@ "rootDir": ".", "declaration": true, "sourceMap": true - }, - "exclude": ["tests", "node_modules"] + } } diff --git a/tsconfig.json b/tsconfig.json index 21324af..c4e8387 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ "apps/**/*.ts", "libs/**/*.ts", "evals/**/*.ts", - "scripts/**/*.ts", - "tests/**/*.ts" + "scripts/**/*.ts" ] } diff --git a/vitest.config.ts b/vitest.config.ts index 2dbe5cd..8afb5ec 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,8 +4,8 @@ export default defineConfig({ test: { globals: false, environment: 'node', - include: ['tests/**/*.test.ts'], - exclude: ['node_modules', 'dist', 'evals'], + include: ['scripts/check-*.js'], + exclude: ['node_modules', 'dist', 'evals', 'scripts/check-node-version.js'], }, resolve: { conditions: ['node'], From 02b73ef45a368a547d4ed116996c3fce886f2c5b Mon Sep 17 00:00:00 2001 From: dawgdevv Date: Fri, 10 Jul 2026 22:55:30 +0530 Subject: [PATCH 6/6] fix: update comment in drift test to reflect accurate description of threshold value --- scripts/check-anchor-drift.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-anchor-drift.js b/scripts/check-anchor-drift.js index 26c2ba8..a8c5957 100644 --- a/scripts/check-anchor-drift.js +++ b/scripts/check-anchor-drift.js @@ -69,7 +69,7 @@ describe("anchor drift", () => { return result.drifted.map((issue) => issue.claim_id); } - expect(await driftedIds("def foo():\n # the configured threshold value\n return 3\n")).toEqual([]); + expect(await driftedIds("def foo():\n # returns the configured threshold value\n return 3\n")).toEqual([]); }); test("whitespace-only edits do not drift", async () => {