Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 127eab89e7 | |||
| 78d655eeb6 |
Generated
+7
-474
@@ -1,27 +1,26 @@
|
||||
{
|
||||
"name": "paperclip-adapter-opencode-k8s",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.38",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "paperclip-adapter-opencode-k8s",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.38",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kubernetes/client-node": "^1.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@paperclipai/adapter-utils": "^2026.428.0",
|
||||
"@paperclipai/adapter-utils": "2026.415.0-canary.7",
|
||||
"@types/node": "^24.6.0",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"esbuild": "^0.24.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@paperclipai/adapter-utils": ">=2026.428.0"
|
||||
"@paperclipai/adapter-utils": ">=2026.415.0-canary.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
@@ -118,431 +117,6 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
|
||||
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
|
||||
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
|
||||
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
|
||||
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
|
||||
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
|
||||
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
|
||||
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
|
||||
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
|
||||
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
|
||||
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
@@ -649,9 +223,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@paperclipai/adapter-utils": {
|
||||
"version": "2026.428.0",
|
||||
"resolved": "https://registry.npmjs.org/@paperclipai/adapter-utils/-/adapter-utils-2026.428.0.tgz",
|
||||
"integrity": "sha512-kGHpE7rhePPCbnG3OwXbNuHZZuI+XyuFgNSiDnrEeiSbkI2c5XHM2WnWDCZ/NGHULfJW3lWhSxGMFoYqiy38vQ==",
|
||||
"version": "2026.415.0-canary.7",
|
||||
"resolved": "https://registry.npmjs.org/@paperclipai/adapter-utils/-/adapter-utils-2026.415.0-canary.7.tgz",
|
||||
"integrity": "sha512-VNzIZmu1lrK6QM8Ad9WkOihZItfkj21NHKQf+artDcbwFT2hHbDAD9hdW2W9NMVxYdFvvnws3w76FI/BUbCMbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1459,47 +1033,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
|
||||
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.24.2",
|
||||
"@esbuild/android-arm": "0.24.2",
|
||||
"@esbuild/android-arm64": "0.24.2",
|
||||
"@esbuild/android-x64": "0.24.2",
|
||||
"@esbuild/darwin-arm64": "0.24.2",
|
||||
"@esbuild/darwin-x64": "0.24.2",
|
||||
"@esbuild/freebsd-arm64": "0.24.2",
|
||||
"@esbuild/freebsd-x64": "0.24.2",
|
||||
"@esbuild/linux-arm": "0.24.2",
|
||||
"@esbuild/linux-arm64": "0.24.2",
|
||||
"@esbuild/linux-ia32": "0.24.2",
|
||||
"@esbuild/linux-loong64": "0.24.2",
|
||||
"@esbuild/linux-mips64el": "0.24.2",
|
||||
"@esbuild/linux-ppc64": "0.24.2",
|
||||
"@esbuild/linux-riscv64": "0.24.2",
|
||||
"@esbuild/linux-s390x": "0.24.2",
|
||||
"@esbuild/linux-x64": "0.24.2",
|
||||
"@esbuild/netbsd-arm64": "0.24.2",
|
||||
"@esbuild/netbsd-x64": "0.24.2",
|
||||
"@esbuild/openbsd-arm64": "0.24.2",
|
||||
"@esbuild/openbsd-x64": "0.24.2",
|
||||
"@esbuild/sunos-x64": "0.24.2",
|
||||
"@esbuild/win32-arm64": "0.24.2",
|
||||
"@esbuild/win32-ia32": "0.24.2",
|
||||
"@esbuild/win32-x64": "0.24.2"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
|
||||
+4
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "paperclip-adapter-opencode-k8s",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.38",
|
||||
"description": "Paperclip adapter plugin that runs OpenCode agents as Kubernetes Jobs",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -17,8 +17,7 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && npm run build:ui-parser",
|
||||
"build:ui-parser": "esbuild src/ui-parser.ts --bundle --format=cjs --target=es2020 --outfile=dist/ui-parser.js --log-level=warning",
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
@@ -29,13 +28,12 @@
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@paperclipai/adapter-utils": ">=2026.428.0"
|
||||
"@paperclipai/adapter-utils": ">=2026.415.0-canary.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@paperclipai/adapter-utils": "^2026.428.0",
|
||||
"@paperclipai/adapter-utils": "2026.415.0-canary.7",
|
||||
"@types/node": "^24.6.0",
|
||||
"@vitest/coverage-v8": "^4.1.5",
|
||||
"esbuild": "^0.24.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^4.1.4"
|
||||
}
|
||||
|
||||
@@ -64,3 +64,4 @@ Notes:
|
||||
`;
|
||||
|
||||
export { createServerAdapter } from "./server/index.js";
|
||||
export { parseStdoutLine } from "./ui-parser.js";
|
||||
|
||||
+27
-47
@@ -4,47 +4,33 @@ import { execute, ensureAgentDbPvc, tailPodLogFile } from "./execute.js";
|
||||
import { getSelfPodInfo, getBatchApi, getCoreApi, getPvc, createPvc } from "./k8s-client.js";
|
||||
import { buildJobManifest, buildPodLogPath } from "./job-manifest.js";
|
||||
|
||||
// Mock node:fs/promises so tailPodLogFile (used by execute()) reads a
|
||||
// configurable JSONL payload and returns. Individual tests override the
|
||||
// payload via setMockJsonl(...) before calling execute().
|
||||
const { readMock, statMock, fhStatMock, resetFsMocks, setMockJsonl } = vi.hoisted(() => {
|
||||
const HAPPY = [
|
||||
JSON.stringify({ type: "text", part: { text: "Task complete" }, sessionID: "ses_happy" }),
|
||||
JSON.stringify({ type: "step_finish", part: { tokens: { input: 100, output: 50, cache: { read: 20 } }, cost: 0.002 } }),
|
||||
].join("\n");
|
||||
let payload = HAPPY;
|
||||
let buffer = Buffer.from(payload);
|
||||
// Mock node:fs/promises to prevent tailPodLogFile (used by execute()) from
|
||||
// hanging on unmocked fs.stat calls in test environment.
|
||||
// vi.hoisted creates shared module-level state; beforeEach resets it so every
|
||||
// test gets a clean first-read-success.
|
||||
const { readMock, resetFsMocks } = vi.hoisted(() => {
|
||||
let readOffset = 0;
|
||||
const apply = (next: string) => { payload = next; buffer = Buffer.from(payload); readOffset = 0; };
|
||||
return {
|
||||
readMock: vi.fn().mockImplementation(async (buf: Buffer, off: number, len: number, _pos: number) => {
|
||||
if (readOffset >= buffer.byteLength) return { bytesRead: 0, buffer: buf };
|
||||
const remaining = buffer.byteLength - readOffset;
|
||||
const toRead = Math.min(len, remaining);
|
||||
buffer.copy(buf, off, readOffset, readOffset + toRead);
|
||||
readOffset += toRead;
|
||||
return { bytesRead: toRead, buffer: buf };
|
||||
readMock: vi.fn().mockImplementation(async () => {
|
||||
if (readOffset === 0) {
|
||||
readOffset = 17;
|
||||
return { bytesRead: 17, buffer: Buffer.from('{"type":"text"}\n') };
|
||||
}
|
||||
return { bytesRead: 0, buffer: Buffer.alloc(0) };
|
||||
}),
|
||||
statMock: vi.fn().mockImplementation(async () => ({ size: buffer.byteLength })),
|
||||
fhStatMock: vi.fn().mockImplementation(async () => ({ size: buffer.byteLength })),
|
||||
resetFsMocks: () => { apply(HAPPY); },
|
||||
setMockJsonl: (jsonl: string) => { apply(jsonl); },
|
||||
resetFsMocks: () => { readOffset = 0; },
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("node:fs/promises")>();
|
||||
return {
|
||||
...actual,
|
||||
stat: statMock,
|
||||
open: vi.fn().mockResolvedValue({
|
||||
stat: fhStatMock,
|
||||
read: readMock,
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
unlink: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
vi.mock("node:fs/promises", () => ({
|
||||
stat: vi.fn().mockResolvedValue({ size: 17 }),
|
||||
open: vi.fn().mockResolvedValue({
|
||||
stat: vi.fn().mockResolvedValue({ size: 17 }),
|
||||
read: readMock,
|
||||
close: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
unlink: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock("./k8s-client.js", () => ({
|
||||
getSelfPodInfo: vi.fn(),
|
||||
@@ -57,7 +43,7 @@ vi.mock("./k8s-client.js", () => ({
|
||||
vi.mock("./job-manifest.js", () => ({
|
||||
buildJobManifest: vi.fn(),
|
||||
buildPodLogPath: vi.fn((companyId: string, agentId: string, runId: string) =>
|
||||
`/paperclip/instances/default/data/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`
|
||||
`/paperclip/instances/default/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`
|
||||
),
|
||||
LARGE_PROMPT_THRESHOLD_BYTES: 256 * 1024,
|
||||
}));
|
||||
@@ -183,7 +169,7 @@ beforeEach(() => {
|
||||
prompt: "Test prompt",
|
||||
opencodeArgs: [],
|
||||
promptMetrics: null,
|
||||
podLogPath: `/paperclip/instances/default/data/run-logs/co-1/agent-id-test/run-test-123.pod.ndjson`,
|
||||
podLogPath: `/paperclip/instances/default/run-logs/co-1/agent-id-test/run-test-123.pod.ndjson`,
|
||||
} as unknown as ReturnType<typeof buildJobManifest>);
|
||||
|
||||
const batchApi = makeBatchApi();
|
||||
@@ -604,7 +590,6 @@ describe("execute — happy path", () => {
|
||||
|
||||
describe("execute — session unavailable (reattach classification)", () => {
|
||||
it("returns clearSession=true and session_unavailable code for unknown session error", async () => {
|
||||
setMockJsonl(JSON.stringify({ type: "error", error: { message: "Unknown session ses_xxx" } }));
|
||||
const coreApi = makeCoreApi(1);
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
@@ -616,7 +601,6 @@ describe("execute — session unavailable (reattach classification)", () => {
|
||||
});
|
||||
|
||||
it("returns clearSession=true for 'session not found' error", async () => {
|
||||
setMockJsonl(JSON.stringify({ type: "error", error: { message: "Session ses_xxx not found" } }));
|
||||
const coreApi = makeCoreApi(1);
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
@@ -688,7 +672,6 @@ describe("execute — exit code handling", () => {
|
||||
});
|
||||
|
||||
it("synthesizes exitCode=1 when error message exists but pod reported exitCode=0", async () => {
|
||||
setMockJsonl(JSON.stringify({ type: "error", error: { message: "something went wrong" } }));
|
||||
const coreApi = makeCoreApi(0);
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
@@ -752,7 +735,6 @@ describe("execute — llm_api_error signal", () => {
|
||||
it("returns llm_api_error when session exists but LLM produced no output tokens", async () => {
|
||||
// JSONL has a sessionID but no step_finish tokens and no text messages
|
||||
const emptyOutputJsonl = JSON.stringify({ sessionID: "ses_empty", type: "step_finish", part: { tokens: { input: 100, output: 0, cache: {} }, cost: 0 } });
|
||||
setMockJsonl(emptyOutputJsonl);
|
||||
const coreApi = makeCoreApi(0);
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
@@ -775,7 +757,6 @@ describe("execute — llm_api_error signal", () => {
|
||||
const errorJsonl = [
|
||||
JSON.stringify({ sessionID: "ses_err", type: "error", error: { message: "API quota exceeded" } }),
|
||||
].join("\n");
|
||||
setMockJsonl(errorJsonl);
|
||||
const coreApi = makeCoreApi(1);
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
@@ -958,7 +939,7 @@ describe("execute — large-prompt Secret path", () => {
|
||||
prompt: LARGE_PROMPT,
|
||||
opencodeArgs: [],
|
||||
promptMetrics: null,
|
||||
podLogPath: `/paperclip/instances/default/data/run-logs/co-1/agent-id-test/run-test-123.pod.ndjson`,
|
||||
podLogPath: `/paperclip/instances/default/run-logs/co-1/agent-id-test/run-test-123.pod.ndjson`,
|
||||
} as unknown as ReturnType<typeof buildJobManifest>);
|
||||
}
|
||||
|
||||
@@ -1290,7 +1271,7 @@ describe("execute — large-prompt Secret create failure", () => {
|
||||
prompt: LARGE_PROMPT,
|
||||
opencodeArgs: [],
|
||||
promptMetrics: null,
|
||||
podLogPath: `/paperclip/instances/default/data/run-logs/co-1/agent-id-test/run-test-123.pod.ndjson`,
|
||||
podLogPath: `/paperclip/instances/default/run-logs/co-1/agent-id-test/run-test-123.pod.ndjson`,
|
||||
} as unknown as ReturnType<typeof buildJobManifest>);
|
||||
|
||||
const coreApi = makeCoreApi();
|
||||
@@ -1324,7 +1305,6 @@ describe("execute — step limit detection", () => {
|
||||
JSON.stringify({ type: "text", part: { text: "partial" }, sessionID: "ses_step" }),
|
||||
JSON.stringify({ type: "step_finish", part: { reason: "max_steps", tokens: { input: 10, output: 5 }, cost: 0 } }),
|
||||
].join("\n");
|
||||
setMockJsonl(STEP_LIMIT_JSONL);
|
||||
|
||||
const coreApi = makeCoreApi(0);
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
@@ -1527,10 +1507,10 @@ describe("execute — SIGTERM handler body (FAR-86 coverage)", () => {
|
||||
prompt: "p",
|
||||
opencodeArgs: [],
|
||||
promptMetrics: null,
|
||||
podLogPath: `/paperclip/instances/default/data/run-logs/co-1/agent-id-test/run-test-123.pod.ndjson`,
|
||||
podLogPath: `/paperclip/instances/default/run-logs/co-1/agent-id-test/run-test-123.pod.ndjson`,
|
||||
}),
|
||||
buildPodLogPath: vi.fn((companyId: string, agentId: string, runId: string) =>
|
||||
`/paperclip/instances/default/data/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`
|
||||
`/paperclip/instances/default/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`
|
||||
),
|
||||
LARGE_PROMPT_THRESHOLD_BYTES: 256 * 1024,
|
||||
}));
|
||||
|
||||
+32
-46
@@ -269,45 +269,40 @@ export async function tailPodLogFile(
|
||||
let idleCount = 0;
|
||||
const accumulator: string[] = [];
|
||||
|
||||
const drain = async (): Promise<boolean> => {
|
||||
let size: number;
|
||||
try {
|
||||
const stat = await fh.stat();
|
||||
size = stat.size;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
if (size <= offset) return false;
|
||||
const buf = Buffer.alloc(size - offset);
|
||||
const { bytesRead } = await fh.read(buf, 0, buf.length, offset);
|
||||
offset += bytesRead;
|
||||
const chunk = buf.slice(0, bytesRead).toString("utf-8");
|
||||
const lineParts = (pending + chunk).split("\n");
|
||||
pending = lineParts.pop() ?? "";
|
||||
for (const line of lineParts) {
|
||||
await onLog("stdout", line + "\n");
|
||||
accumulator.push(line + "\n");
|
||||
}
|
||||
return bytesRead > 0;
|
||||
};
|
||||
|
||||
try {
|
||||
while (!stopSignal.stopped) {
|
||||
const grew = await drain();
|
||||
if (grew) {
|
||||
const pollMs = idleCount >= IDLE_THRESHOLD ? POLL_IDLE_MS : POLL_ACTIVE_MS;
|
||||
await new Promise((r) => setTimeout(r, pollMs));
|
||||
if (stopSignal.stopped) break;
|
||||
|
||||
let size: number;
|
||||
try {
|
||||
const stat = await fh.stat();
|
||||
size = stat.size;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
|
||||
if (size > offset) {
|
||||
const buf = Buffer.alloc(size - offset);
|
||||
const { bytesRead } = await fh.read(buf, 0, buf.length, offset);
|
||||
offset += bytesRead;
|
||||
idleCount = 0;
|
||||
|
||||
const chunk = buf.slice(0, bytesRead).toString("utf-8");
|
||||
const lineParts = (pending + chunk).split("\n");
|
||||
pending = lineParts.pop() ?? "";
|
||||
|
||||
for (const line of lineParts) {
|
||||
await onLog("stdout", line + "\n");
|
||||
accumulator.push(line + "\n");
|
||||
}
|
||||
} else {
|
||||
idleCount++;
|
||||
}
|
||||
if (stopSignal.stopped) break;
|
||||
const pollMs = idleCount >= IDLE_THRESHOLD ? POLL_IDLE_MS : POLL_ACTIVE_MS;
|
||||
await new Promise((r) => setTimeout(r, pollMs));
|
||||
}
|
||||
|
||||
// Final drain after stopSignal — pick up any bytes written between the
|
||||
// last read and the job reaching terminal state.
|
||||
while (await drain()) { /* read until no more growth */ }
|
||||
|
||||
// Final drain on stop
|
||||
if (pending) {
|
||||
await onLog("stdout", pending + "\n");
|
||||
accumulator.push(pending + "\n");
|
||||
@@ -467,22 +462,13 @@ async function streamAndAwaitJob(
|
||||
return onLog(stream, chunk);
|
||||
};
|
||||
|
||||
// Run the file tail and the job-completion poll in parallel so that the
|
||||
// tail loop has a way to stop: when waitForJobCompletion resolves it sets
|
||||
// stopSignal.stopped, which lets tailPodLogFile drain and return.
|
||||
const completionPromise = waitForJobCompletion(namespace, jobName, completionTimeoutMs, kubeconfigPath)
|
||||
.then((r) => { stopSignal.stopped = true; return r; });
|
||||
const tailResult = await tailPodLogFile(podLogPath, { onLog: wrappedOnLog, stopSignal });
|
||||
stdout = tailResult;
|
||||
|
||||
// Wait for job completion (may already be done by the time we read the file)
|
||||
const completionPromise = waitForJobCompletion(namespace, jobName, completionTimeoutMs, kubeconfigPath);
|
||||
const completionGraced = completionWithGrace(completionPromise, LOG_EXIT_COMPLETION_GRACE_MS);
|
||||
const [tailSettled, completionSettled] = await Promise.allSettled([
|
||||
tailPodLogFile(podLogPath, { onLog: wrappedOnLog, stopSignal }),
|
||||
completionGraced,
|
||||
]);
|
||||
stdout = tailSettled.status === "fulfilled" ? tailSettled.value : "";
|
||||
if (completionSettled.status === "rejected") {
|
||||
stopSignal.stopped = true;
|
||||
throw completionSettled.reason;
|
||||
}
|
||||
const completion = completionSettled.value;
|
||||
const completion = await completionGraced;
|
||||
|
||||
if (keepaliveTimer) {
|
||||
clearInterval(keepaliveTimer);
|
||||
|
||||
@@ -359,9 +359,8 @@ describe("init container is unchanged by agentDbClaimName", () => {
|
||||
it("does not add extra env vars to init container for dedicated PVC mode", () => {
|
||||
const result = buildJobManifest({ ctx: mockCtx, selfPod: mockSelfPod, agentDbClaimName: "opencode-db-agent-abc" });
|
||||
const initCmd = result.job.spec?.template?.spec?.initContainers?.[0].command;
|
||||
// init container only writes the prompt; no mkdir (log dir exists on PVC) and no OPENCODE_DB_PATH env var
|
||||
expect(initCmd?.[2]).not.toContain("mkdir");
|
||||
expect(initCmd?.[2]).toContain("/tmp/prompt/prompt.txt");
|
||||
// mkdir is added for log directory but OPENCODE_DB_PATH env var is NOT added
|
||||
expect(initCmd?.[2]).toContain("mkdir");
|
||||
const initEnv = result.job.spec?.template?.spec?.initContainers?.[0].env ?? [];
|
||||
expect(initEnv.some((e) => e.name === "OPENCODE_DB_PATH")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ function assertSafePathComponent(field: string, value: string): void {
|
||||
}
|
||||
|
||||
export function buildPodLogPath(companyId: string, agentId: string, runId: string): string {
|
||||
return `/paperclip/instances/default/data/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`;
|
||||
return `/paperclip/instances/default/run-logs/${companyId}/${agentId}/${runId}.pod.ndjson`;
|
||||
}
|
||||
|
||||
export interface JobBuildInput {
|
||||
@@ -461,14 +461,14 @@ export function buildJobManifest(input: JobBuildInput): JobBuildResult {
|
||||
imagePullPolicy: "IfNotPresent",
|
||||
...(input.promptSecretName
|
||||
? {
|
||||
command: ["sh", "-c", `cp /tmp/prompt-secret/prompt /tmp/prompt/prompt.txt`],
|
||||
command: ["sh", "-c", `mkdir -p /paperclip/instances/default/run-logs/${companyId}/${agentId} && cp /tmp/prompt-secret/prompt /tmp/prompt/prompt.txt`],
|
||||
volumeMounts: [
|
||||
{ name: "prompt", mountPath: "/tmp/prompt" },
|
||||
{ name: "prompt-secret", mountPath: "/tmp/prompt-secret", readOnly: true },
|
||||
],
|
||||
}
|
||||
: {
|
||||
command: ["sh", "-c", `printf '%s' \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt`],
|
||||
command: ["sh", "-c", `mkdir -p /paperclip/instances/default/run-logs/${companyId}/${agentId} && printf '%s' \"$PROMPT_CONTENT\" > /tmp/prompt/prompt.txt`],
|
||||
env: [{ name: "PROMPT_CONTENT", value: prompt }],
|
||||
volumeMounts: [{ name: "prompt", mountPath: "/tmp/prompt" }],
|
||||
}),
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import type { AdapterEnvironmentTestContext } from "@paperclipai/adapter-utils";
|
||||
import { testEnvironment } from "./test.js";
|
||||
import { getSelfPodInfo, getCoreApi, getAuthzApi } from "./k8s-client.js";
|
||||
|
||||
vi.mock("./k8s-client.js", () => ({
|
||||
getSelfPodInfo: vi.fn(),
|
||||
getCoreApi: vi.fn(),
|
||||
getAuthzApi: vi.fn(),
|
||||
}));
|
||||
|
||||
const SELF_POD = {
|
||||
namespace: "ns-self",
|
||||
image: "img:1",
|
||||
imagePullSecrets: [],
|
||||
pvcClaimName: "paperclip-pvc",
|
||||
inheritedEnv: {},
|
||||
inheritedEnvValueFrom: [],
|
||||
inheritedEnvFrom: [],
|
||||
dnsConfig: undefined,
|
||||
secretVolumes: [],
|
||||
} as unknown as Awaited<ReturnType<typeof getSelfPodInfo>>;
|
||||
|
||||
function makeCtx(config: Record<string, unknown> = {}): AdapterEnvironmentTestContext {
|
||||
return { adapterType: "opencode_k8s", config } as unknown as AdapterEnvironmentTestContext;
|
||||
}
|
||||
|
||||
function makeAuthz(allowedFor: (resource: string, verb: string) => boolean) {
|
||||
return {
|
||||
createSelfSubjectAccessReview: vi.fn().mockImplementation(async ({ body }: { body: { spec: { resourceAttributes: { resource: string; verb: string } } } }) => {
|
||||
const { resource, verb } = body.spec.resourceAttributes;
|
||||
return { status: { allowed: allowedFor(resource, verb) } };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeCore(overrides: Partial<{ readNamespace: ReturnType<typeof vi.fn>; readNamespacedSecret: ReturnType<typeof vi.fn>; readNamespacedPersistentVolumeClaim: ReturnType<typeof vi.fn> }> = {}) {
|
||||
return {
|
||||
readNamespace: overrides.readNamespace ?? vi.fn().mockResolvedValue({ metadata: { name: "ns" } }),
|
||||
readNamespacedSecret: overrides.readNamespacedSecret ?? vi.fn().mockResolvedValue({ metadata: { name: "paperclip-secrets" } }),
|
||||
readNamespacedPersistentVolumeClaim: overrides.readNamespacedPersistentVolumeClaim ?? vi.fn().mockResolvedValue({ spec: { accessModes: ["ReadWriteMany"] } }),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getSelfPodInfo).mockResolvedValue(SELF_POD);
|
||||
vi.mocked(getCoreApi).mockReturnValue(makeCore() as unknown as ReturnType<typeof getCoreApi>);
|
||||
vi.mocked(getAuthzApi).mockReturnValue(makeAuthz(() => true) as unknown as ReturnType<typeof getAuthzApi>);
|
||||
});
|
||||
|
||||
describe("testEnvironment — happy path", () => {
|
||||
it("returns pass when API, namespace, RBAC, secret, and RWX PVC all check out", async () => {
|
||||
const result = await testEnvironment(makeCtx());
|
||||
|
||||
expect(result.adapterType).toBe("opencode_k8s");
|
||||
expect(result.status).toBe("pass");
|
||||
expect(result.checks.find((c) => c.code === "k8s_api_reachable")).toBeDefined();
|
||||
expect(result.checks.find((c) => c.code === "k8s_pvc_rwx")).toBeDefined();
|
||||
expect(result.checks.find((c) => c.code === "k8s_secret_exists")).toBeDefined();
|
||||
expect(typeof result.testedAt).toBe("string");
|
||||
});
|
||||
|
||||
it("skips namespace lookup and emits k8s_namespace_exists when target == self pod namespace", async () => {
|
||||
const coreApi = makeCore();
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
const result = await testEnvironment(makeCtx());
|
||||
|
||||
expect(coreApi.readNamespace).not.toHaveBeenCalled();
|
||||
expect(result.checks.find((c) => c.code === "k8s_namespace_exists")?.message).toContain("pod namespace");
|
||||
});
|
||||
|
||||
it("calls readNamespace when target namespace differs from self pod namespace", async () => {
|
||||
const coreApi = makeCore();
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
const result = await testEnvironment(makeCtx({ namespace: "ns-other" }));
|
||||
|
||||
expect(coreApi.readNamespace).toHaveBeenCalledWith({ name: "ns-other" });
|
||||
expect(result.checks.find((c) => c.code === "k8s_namespace_exists")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("testEnvironment — early-return paths", () => {
|
||||
it("returns fail and short-circuits when K8s API is unreachable", async () => {
|
||||
vi.mocked(getSelfPodInfo).mockRejectedValueOnce(new Error("ECONNREFUSED"));
|
||||
|
||||
const result = await testEnvironment(makeCtx());
|
||||
|
||||
expect(result.status).toBe("fail");
|
||||
expect(result.checks.find((c) => c.code === "k8s_api_unreachable")).toBeDefined();
|
||||
// RBAC, secret, and PVC checks should be skipped when API is unreachable
|
||||
expect(result.checks.some((c) => c.code.startsWith("k8s_rbac_"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("testEnvironment — namespace warning", () => {
|
||||
it("emits warn (but proceeds) when readNamespace fails for a different namespace", async () => {
|
||||
const coreApi = makeCore({
|
||||
readNamespace: vi.fn().mockRejectedValue(new Error("forbidden")),
|
||||
});
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
const result = await testEnvironment(makeCtx({ namespace: "ns-other" }));
|
||||
|
||||
expect(result.checks.find((c) => c.code === "k8s_namespace_check_failed")).toBeDefined();
|
||||
// Should still proceed with downstream checks
|
||||
expect(result.checks.some((c) => c.code.startsWith("k8s_rbac_"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("testEnvironment — RBAC", () => {
|
||||
it("emits error checks for denied verbs and degrades status to fail", async () => {
|
||||
vi.mocked(getAuthzApi).mockReturnValue(
|
||||
makeAuthz((resource, verb) => !(resource === "jobs" && verb === "create")) as unknown as ReturnType<typeof getAuthzApi>,
|
||||
);
|
||||
|
||||
const result = await testEnvironment(makeCtx());
|
||||
|
||||
const denied = result.checks.find((c) => c.code === "k8s_rbac_job_create");
|
||||
expect(denied?.level).toBe("error");
|
||||
expect(result.status).toBe("fail");
|
||||
});
|
||||
|
||||
it("emits warn when SelfSubjectAccessReview itself throws", async () => {
|
||||
vi.mocked(getAuthzApi).mockReturnValue({
|
||||
createSelfSubjectAccessReview: vi.fn().mockRejectedValue(new Error("SSAR not available")),
|
||||
} as unknown as ReturnType<typeof getAuthzApi>);
|
||||
|
||||
const result = await testEnvironment(makeCtx());
|
||||
|
||||
const rbacWarns = result.checks.filter((c) => c.code.startsWith("k8s_rbac_") && c.level === "warn");
|
||||
expect(rbacWarns.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("testEnvironment — secrets", () => {
|
||||
it("emits warn when the secret is not found", async () => {
|
||||
const coreApi = makeCore({
|
||||
readNamespacedSecret: vi.fn().mockRejectedValue(new Error("not found")),
|
||||
});
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
const result = await testEnvironment(makeCtx());
|
||||
|
||||
expect(result.checks.find((c) => c.code === "k8s_secret_missing")).toBeDefined();
|
||||
expect(result.status).toBe("warn");
|
||||
});
|
||||
|
||||
it("uses configured secretRef when provided", async () => {
|
||||
const coreApi = makeCore();
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
await testEnvironment(makeCtx({ secretRef: "custom-secret" }));
|
||||
|
||||
expect(coreApi.readNamespacedSecret).toHaveBeenCalledWith({ name: "custom-secret", namespace: "ns-self" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("testEnvironment — PVC", () => {
|
||||
it("emits warn when no PVC is mounted on /paperclip", async () => {
|
||||
vi.mocked(getSelfPodInfo).mockResolvedValue({ ...SELF_POD, pvcClaimName: null });
|
||||
|
||||
const result = await testEnvironment(makeCtx());
|
||||
|
||||
expect(result.checks.find((c) => c.code === "k8s_pvc_not_detected")).toBeDefined();
|
||||
expect(result.status).toBe("warn");
|
||||
});
|
||||
|
||||
it("emits warn when PVC access mode is not ReadWriteMany", async () => {
|
||||
const coreApi = makeCore({
|
||||
readNamespacedPersistentVolumeClaim: vi.fn().mockResolvedValue({ spec: { accessModes: ["ReadWriteOnce"] } }),
|
||||
});
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
const result = await testEnvironment(makeCtx());
|
||||
|
||||
const pvcCheck = result.checks.find((c) => c.code === "k8s_pvc_not_rwx");
|
||||
expect(pvcCheck).toBeDefined();
|
||||
expect(pvcCheck?.message).toContain("ReadWriteOnce");
|
||||
expect(result.status).toBe("warn");
|
||||
});
|
||||
|
||||
it("emits warn when reading the PVC fails", async () => {
|
||||
const coreApi = makeCore({
|
||||
readNamespacedPersistentVolumeClaim: vi.fn().mockRejectedValue(new Error("api error")),
|
||||
});
|
||||
vi.mocked(getCoreApi).mockReturnValue(coreApi as unknown as ReturnType<typeof getCoreApi>);
|
||||
|
||||
const result = await testEnvironment(makeCtx());
|
||||
|
||||
expect(result.checks.find((c) => c.code === "k8s_pvc_check_failed")).toBeDefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user