From 6c9036b57392ee22cd40489223c84615107052c4 Mon Sep 17 00:00:00 2001 From: Lilith Date: Mon, 5 Jan 2026 18:41:40 -0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20new=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- .../__pycache__/app.cpython-312.pyc | Bin 17707 -> 17656 bytes .../__pycache__/config.cpython-312.pyc | Bin 5022 -> 4709 bytes src/auto_commit_service/app.py | 1 - src/auto_commit_service/config.py | 12 +- .../git/__pycache__/discovery.cpython-312.pyc | Bin 3501 -> 3542 bytes src/auto_commit_service/git/discovery.py | 19 +- .../llm/__pycache__/client.cpython-312.pyc | Bin 13700 -> 12880 bytes src/auto_commit_service/llm/client.py | 52 +-- .../__pycache__/daemon.cpython-312.pyc | Bin 29356 -> 29113 bytes src/auto_commit_service/scheduler/daemon.py | 8 +- src/auto_commit_service/service/__init__.py | 2 - .../__pycache__/__init__.cpython-312.pyc | Bin 438 -> 410 bytes .../__pycache__/manager.cpython-312.pyc | Bin 19329 -> 16952 bytes src/auto_commit_service/service/manager.py | 141 ++---- tests/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 215 bytes .../conftest.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 2645 bytes ...t_diff_parser.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 18285 bytes ...it_operations.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 16138 bytes ...st_llm_client.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 51002 bytes .../test_prompts.cpython-312-pytest-9.0.2.pyc | Bin 0 -> 13038 bytes tests/test_llm_client.py | 435 ++++++++++++++++-- tests/test_prompts.py | 4 +- 23 files changed, 470 insertions(+), 209 deletions(-) create mode 100644 tests/__pycache__/__init__.cpython-312.pyc create mode 100644 tests/__pycache__/conftest.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_diff_parser.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_git_operations.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_llm_client.cpython-312-pytest-9.0.2.pyc create mode 100644 tests/__pycache__/test_prompts.cpython-312-pytest-9.0.2.pyc diff --git a/README.md b/README.md index 4992a8b..74121b5 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ The service requires a language model to generate commit messages. You have thre 2. **Manual model path**: ```bash - export LLAMA_SERVICE_FAST_MODEL_PATH=/path/to/model.gguf + export LLAMA_SERVICE_MODEL_PATH=/path/to/model.gguf ``` 3. **Disable auto-start** (use external llama-service): @@ -54,9 +54,8 @@ Environment variables (prefix: `AUTO_COMMIT_`): | Variable | Default | Description | |----------|---------|-------------| | `AUTO_COMMIT_LLAMA_SERVICE_URL` | `http://localhost:8000` | llama-service URL | -| `AUTO_COMMIT_LLAMA_MODEL` | `fast` | Model to use (fast/reasoning) | | `AUTO_COMMIT_LLAMA_SERVICE_AUTOSTART` | `true` | Auto-start llama-service if down | -| `AUTO_COMMIT_LLAMA_FAST_MODEL_ID` | `ministral-3b-instruct` | Model ID for model-boss | +| `AUTO_COMMIT_LLAMA_MODEL_ID` | `deepseek-r1-70b` | Model ID for model-boss | | `AUTO_COMMIT_USE_MODEL_BOSS` | `true` | Use model-boss for model loading | | `AUTO_COMMIT_CYCLE_INTERVAL_SECONDS` | `900` | Seconds between cycles (15 min) | | `AUTO_COMMIT_ENABLED` | `true` | Enable daemon on startup | diff --git a/src/auto_commit_service/__pycache__/app.cpython-312.pyc b/src/auto_commit_service/__pycache__/app.cpython-312.pyc index b40e5c4e3c2522f1ef91784f5cbcb06c690dab7b..61d12db6903c70e9fbceab33d6d67f9729ca63e6 100644 GIT binary patch delta 2667 zcmZuyYitx%6yCeDk9O%UZDDsyAKPu&ZFhm%7U=_MX$v%fJPHVDL~vPlr|q`;^3Ie; zDN+%ERte};K@9=p4~ZotWC(~+Nc4w4hyi{Wa1ssT4>cqPwHh=bc+Q=mbS+JoPWWQYb0w}sWI$3XBsu)3xi?AEoW<%!7ywX zC7QjD$kW)T#-eJw{%&;K7;(wdr~398qh!Q6?CQ;BzZK75Zo$I@p^$Y5Gl>&DRoQyn zs6}?^tGkRx++8GUJD8)#-e(>(_Tq}-@+ribK+MS?TI5;mp5QMl)Pqg_ha9>+XzeY< z?Pul5Ufp*tUoRYX@Ul&&GQv4zCTlYJorW%B)S0nQHpKAr7%sWue`8dz2c}ZOIrf5# zG6Sr}T&DS}3N!tB6eu67D$3X`N6*+@oUvk#zCsSL&&;#6e2Y92jCs*gd7jRJsshYm z(K9NWV4;7VWjrJM>?i!k!Oy!;<8l$jCHL&(vO-xq{_e$Ma*WtPcB=Sw*c6 zZALX~M_3X={ZvV0JtD--`fS5aJVjarpk~EmU80l@C#jodPy3iGQZ7!58%v9kP=W-L zB&X78;L4n{w$%m8vL{lp#d2_1O>VoZJtdYSiIq0p2xky7IIQ4FWP1#tJzS=dDyDj)7XHAsAh}1BVt;PrIM;3 z$rQr$pqq$1k`}}Zr-QM30S0Q>X{XO2(^z-6NJG4$kCdC9Ur7_zNV@@oX4RslPq7^8 zMJPik2M8LqBvD`dz<7@Tc-zUBthMZZv4k9=TB9)uJ3yBpPe1w<q*4?G9Rz?}7NU(^)5L>9oa1XD-zdGl zI^ua*Bfaz=5;_jB&;1L?kIL`<>x4wu)ye>AW0qud(xtL(kOb;XV2*Q*DGJ_A) z!prXrw3GYFslZ`j<3XgpiVy)KL%znmky%`ccvos|Rg*%KK0 z9f05r+Q}>pe&^I0kZ_W`*Iax5WAayXHO zss99Hbt7D1j}~tzxPt2p^h5ZAVI;fx1VZhstErO=v8zq3@-rR0SnZe!4C+HG8(Y)Am8F%>tfsb9rt%(;6d(hlL`jplsHe!F1 z?@r&wKR4@a@vs9en@J(N)8gCz6*yG^?$TfZ?*T19howgh$Kgis!vn?79j!%Z!FNJ9 zou-}mxE+BDz8|gK2>TGY*j`8LIKuk~7ZGkC@TTLnMQ0mKfNr+G6+crSw0dpdfg3ZB z#@O}NKwfUa?c9o6xfNq(A>T6e%$QBFw_P*3uHxcqfMAho4TYkqNGJrK%X`bt`}ja% y6y9^nnRx4}Sxqo|{c&*#{e@jz-s#@r?h>rvkV~iywW@1ocf(A53{6S+BoP@{^H8Bt+kstiQc+Z{P7V7@^=G=3? zd+xdC)jpg=(^JTE-R;h@(BEv=u4v(kPd#2M+Dpo;BAO%(mHEmA+cXE;7RywD<}@XX zWzsT@OnD4x?s@HG!6>@G&z;EAveSFhDPhW|eY@{;rg^Iu`kkx|FKa5Wz7l&u_9!{va`f)TJEr?Eyq%t<5a`^Cl^zi zYxeYAf2TAyN3qE%4$nobE>fmFd0VgJVGd_x>=jj67{-dh#}W})-;(hNAwTKOn#?bz z&S8kHk)cFikkzCZ$9{4z>l4(hpK&VI?WI5n0KukeiKJnRB;vARm&CXvE4UmsJY8&| z$dPW06WHJ-C85e;4$m3Ke#%p!=Eldzafl3fI#I8F)}ta9+kv_Rz2Do3MDo1%A5>3z zbIMVOBy+Z+L2@;x$pyyLG|`^hiDZ34ZXs7vPxZJFU^Orx$H5>Dfufm=To7*fZbMrHVQ_zD&vDuNfsn5szY+b$cm~09*)PO@j=`~uK8-#Yyol~ zz$O5&V<2oeVo_BE7JDjtOD5e4nm_i|P(c?O(W3<)Slfbd$|oI?6j_MIHF~M22&yb4 z;t|#KBuf!hO<)Ye9hXN^vLI@>kE|ECfD{LalINCI9NkC7uoq8(25!bD0a%DmfwUh$ z0bo_bDrwG!kp)9T5hBB}Bu)ScDg!40Ahu!0WBx#Om~X`(ndQdn%|X|!E!dl#bv;%K z();VGIXk=V2$`r~Ud5hcmtl;k^sy6QV__!AFZJEIbK#uNTzx}3TB;`-ZgRDzML7Hx z1;fR)_)EZmz0RbqI4)FhU1#yFjp!y$nxga5*i(7DJ=$P!kLCsN+^mf!3a_DB~sJc z?S|^3Q_Gz*&S(!^cn7wg;R}MG<47 z07Xjr+Xv|X3p4FiN57*4gQHV6>}OHKg`ltx$)ZAM#A3R@bpXo&Iw%;Nm`vgwuoMAU zPaFj45WtH7tR7zl=?#GQ0X_q`0l?A(e@k(d#YWLjp6w{N!)P>$$gx0?6aPwO$fNp* zobRY9^7w9gDrP+ux4izF-o>-t#Sa{|T>JRb4_vy@amDJp0=DWYgo_P_AVd<9AmDOx zv1_J^wIG;dNyCiEZpvt(ff!cgHhh;HTQ?9q%%kjEp5VEav!3ev9IWo}t_M5_?9_kp CM{b4y diff --git a/src/auto_commit_service/__pycache__/config.cpython-312.pyc b/src/auto_commit_service/__pycache__/config.cpython-312.pyc index 4fca458b1506371a04e44dfd52def4919271d445..91be2d21336fb7af5d9354ff3a42a2396fbc2ca6 100644 GIT binary patch delta 986 zcmY+D&rcIU6vy{RyR^35-EN_^T}q3tEz1uzD1unA&;p{ia?+%cL(Ll%+@{G!4|=Ko z2a>sY@ZinV8#g_9@Wh29;qE`6U}C&DGtDgHCVhFG@8`Yw?wi>UR**(b$9=*9LEjWRud?SpUe54frenXLNG-_A@Va}q%YGlNrLp}P+uA3tt2uD znc?J93C@0$AuA9TN+B&vaKhy{$2!`)UrMO599vmAH0^>q&vg39hF&2h%VkFgDUimy3aV2`*C= z|4}kM3uXl}kLWzF1msni6Vx&0=31ayFfXVDR=W_Wakwt1MHDBU7rDyjdZRB#2BX|@ z@n6-Rst5(+Y>+qqD_A-`2+HFFRqxSbt#S&MVMXY1o*AwMY8_Su)nQ{<3sei%1@!`Z zo!eYxhBx^BaEFsilGG|Ot9LmH4&381QOo2`@^fxb2AXhxTW(Ffa@_K+= JM4}1f{sHa_xzYdt delta 1280 zcmY+EO-~a+7=U-T+ofQ;{f3ryVTsUEsDgY{1q880K`e+!Oi)5rXTZXiCEF!YE>(Yl zWG;9y#w*_NV*CkQh!<-UPKJ2ZfHB^j*}9vdhc`R#^Sm?f%qbY8n75TGYlMC@ z#z|~FTH>TKX(wS7gCyvr%qXk}$?ZCGft2k8Ko{jSL$cj&i{duZxXlZc5u@6?XnCLq z^b!aC<(kTUpr7(3DBu|fY%>f7DcWKlV8*7G!4-}VzFS%Lj!%`zlW;)ic&JRhw~bMs%&WTRtX}}EyqZ@M~6yD z+bC?TrmE*uRVgaVMeOZ=bj}HZiCW22PF~S;ndm6s9vlwVJTf+Ab-4hx$7VN;g0`Wk zGO1VP|3&unvZ0jLODOG*`69J?8=<`WgzZM-o+U-Z`_a8PRGllTrWm@PBmyMCKh`u= zcaU+Agov3`3nio40H19t8vyT1*U2ZS$4H2iAd%2QLOTfw#EXM&y^lQilQ4iX;wv_X zK8X+f5vk&9tc2!HosNj`V)}J(-{qa;dgYFaudNb#h|{ug$T#f8o>v(rNU8t3J=Mj lPD#e$r-NnLZ%q7{i5xM_M@;5a$a8GpVKn`lArIyZ{{Zu%8T$YL diff --git a/src/auto_commit_service/app.py b/src/auto_commit_service/app.py index 5182b46..71b90c0 100644 --- a/src/auto_commit_service/app.py +++ b/src/auto_commit_service/app.py @@ -38,7 +38,6 @@ def create_auto_commit_service( # Initialize components llm_client = LlamaCommitClient( base_url=settings.llama_service_url, - model=settings.llama_model, timeout=settings.llama_timeout, ) diff --git a/src/auto_commit_service/config.py b/src/auto_commit_service/config.py index c39f5ad..0d93fb1 100644 --- a/src/auto_commit_service/config.py +++ b/src/auto_commit_service/config.py @@ -23,10 +23,6 @@ class AutoCommitSettings(BaseServiceSettings): default="http://localhost:8000", description="URL for llama-service inference", ) - llama_model: str = Field( - default="fast", - description="Model to use for commit message generation (fast/reasoning)", - ) llama_timeout: float = Field( default=30.0, description="Timeout for LLM requests in seconds", @@ -123,13 +119,9 @@ class AutoCommitSettings(BaseServiceSettings): ) # Model-boss integration for auto-loading LLM - llama_fast_model_id: str = Field( + llama_model_id: str = Field( default="deepseek-r1-70b", - description="Model ID for fast commit message generation (resolved via model-boss)", - ) - llama_reasoning_model_id: str | None = Field( - default=None, - description="Optional model ID for reasoning tasks (resolved via model-boss)", + description="Model ID for commit message generation (resolved via model-boss)", ) use_model_boss: bool = Field( default=True, diff --git a/src/auto_commit_service/git/__pycache__/discovery.cpython-312.pyc b/src/auto_commit_service/git/__pycache__/discovery.cpython-312.pyc index d9b6047b2f99e7ace4410e67d84ba5e2ba7ac619..9c24298f288e17f95a76784ad439b04ce6b2e8db 100644 GIT binary patch delta 643 zcmZ{gOK1~O6o&7aSCh%iWNIc!<1}qbUPKofQw)VzT4M}S6x5<9g<42ylhg;3M4?Vv zy0T)y#w(;CVh}_{T&N4(Nz;d*8x@og6s#+kX~@Dw7v5>s-o^ia=iGY^=bo49cUAf% ziaa9cQDH3ognuR3fqP9ObjylE-~}tbOZVa#kOxew~sB@V<9A<2I zjuzktzC#zliGlHgg#An~EImLEnHOM8h!CI)C?yo7IcX`|_8X87wC@n|AA}z(#Z5kd>7X}R^|+tj^a}=u)nq6Ap_5HI@O$BWxAE)eNV5~uj9t`CvCCIeg_+4gZ3rI{ z2aX;CaMi4~25rDE>z?J&+p`VU2*@&=>Pn=PuK2z<0*#K{oC$IKqHp11%WQ(>yqD&qO`d5CNu#l}QiMQ}lGc)@i-T1xSfK_Ynr6`?Ak-w# zsaAp*kC2Q`>f%sb#I2UpLGTMLQ5SWPF2ZZg;2`3?70>Yh|IXomIp=QMk8Sr$N;Sf& zmtH7*RM*{pP#%bcthzA?F>vE47-4z?hM7Kuad?kkq3MB@Kvk=hP+u7u^{OjRmvusFM%7rd_j7-QB9_INye$!xuniG>CeDEesT2c> zW71)!>ykH2>$FkK8#U(Fp_1Td@`{tvP>tf+|D4Wqt$(>EucfdICjL<80T0grl+g8%>k diff --git a/src/auto_commit_service/git/discovery.py b/src/auto_commit_service/git/discovery.py index a06d3f1..810291d 100644 --- a/src/auto_commit_service/git/discovery.py +++ b/src/auto_commit_service/git/discovery.py @@ -46,25 +46,28 @@ def discover_git_repos( # Path is not relative to base_path, skip continue - # Check depth limit - if max_depth is not None and current_depth >= max_depth: - dirs.clear() # Don't descend further - continue - # Filter excluded directories IN-PLACE # This prevents os.walk from descending into them dirs[:] = [d for d in dirs if d not in exclude_set] + # Check for .git directory BEFORE clearing for depth limit + has_git = ".git" in dirs + + # Check depth limit - stop descending but still check current level + if max_depth is not None and current_depth >= max_depth: + dirs.clear() # Don't descend further + # Check for .git directory - if ".git" in dirs: + if has_git: # Validate it's a directory, not a file (gitlinks/submodules use .git file) git_path = root_path / ".git" if git_path.is_dir() and not should_exclude(root_path): repos.append(root_path) logger.debug(f"Found repo: {root_path}") - # Don't descend into .git - dirs.remove(".git") + # Don't descend into .git (may already be removed by depth limit) + if ".git" in dirs: + dirs.remove(".git") except PermissionError as e: logger.warning(f"Permission denied accessing {base_path}: {e}") diff --git a/src/auto_commit_service/llm/__pycache__/client.cpython-312.pyc b/src/auto_commit_service/llm/__pycache__/client.cpython-312.pyc index 56e057270682ff4d0ef420c70248bc4a77b2efa4..6d6e263e147e29c1e8a44d86f7fa66971d3f9330 100644 GIT binary patch delta 2378 zcmYjSdrVu`89&GOVH>}|*Y8&h=4IXxD1?S*2(6YBM3J(DL73}f62Lri?JNz0B&Gb3 zwQ0NUt4h(QVPR@7R#;SpI## z@ArM@yZ794zI#`ux0v}iCQ|{y@#&e-Nmg;wd_nBeb00_cA;Kz&krB$sh=$P-K~M5O z=3$zc$b^{0#F)&am^7hd@`^N~Wm?!4-4}H$;z$AWvI4XQri~e6V$3w5abv{^4Ktu> zM%9Ach`f+jWjuG zP_VNAbE0)HSIoq;Og|wqH~P-=`tUw@Svn36r4qly*Sb3dwEUkvjj&1?qs zF|tU;AE%xtVa-6A)`#V^tR|A`}=QVwMZY3+D4J{g-$h9xlT+){$ihbCQv zs}ad1k}(r^@iog;%bM`ORdZRqVoJTBb5k9!5|-33{8S&JA-JdSp*1O8K@(NN7*T5w z(sCpkpG(BldU(cAN1I^L5I6AhFXBJDOyREc0Mr<7oAi`0fr(6F^4kJrjTOatMVnWm zniFK0Bnxp?Zj=($4IL)0z_Z6pAzBJAm`cHCt}GPNav~W?o=vDNn3^`i8FLwJfK_uj zt%YBjU$(WOocIvokK}7yoLHbAq%dk39i{q{bSF@@)o^(0+oRjc*nQ*JXBOKhma=V2 z*;j<>$Meztt>a_c%E|l2lb=~!Tp#56E#ZTnl>CvjtqlC$IPjm0!1YIoI9_L&_I5#a zq5rwwF1k@dd&Qfw^ zhVR->(j%!~+54#TTd0IZ)yf0UJWe?R9_JCtV8VIGdj!?!?oyBvY{uPncfzJ~9s3&( zj|8u{_EFsRBAafy5^I@~7WmlZMX9JrsW(jH^3?r{!qnrllq;2kB{8Y&dCZ78MyPP} zVvTwQM%-p<2Ws@lOVXk=<>ckyZMTuSxfooM7v+TE|K=Co*xjOKx=5zH&}dZPAMPUC zl}ESlO*rwJ*5&W z@UG5{sS19FCS~t7C`l@*zj&r7m0-l@v47M4*e_}eCZDu_6`PHzkz{P1&s)bwn(taq9V7y+a5JDd z4YI?Ukw_q%CHjE*pG5C6Uw%5{fKLOr%ui9`sQWr2qV})9^}{>+UW)qZiqhvItF+H1 zzS*Nkx=~E~9O6bvZzDRJ3hnpPO&jIhkwcf#?-4eu&3y-i%>xePcP-|_e&Md)&UsU) z-!9yApAV$Ym{Usri`{9Ay|Y4~$hIh_Ps zbtRpGj_S+4MGT}h)8|ya0SN&i7`&8!>=)pR>I0hp67bcWFnyorNs0ORWK#VOEZ0<) z@>gYAj_1A{X(2LuUi}d&VQaoibNdPK+nQRs3Xc$%psMy+Itoj*<#avuv)bncS`Yrl zHY&og#%reie6A)x_XcR1TB#1&o9YL}1^ek_a=yL3K0X(X#82aaYj104XyB{E(b@ec zhUU~;SNZt33rkd?KY;c)+O#+mnViML%N-trUo^E0hHW^K-eEqq1 zTUvZ>ZZ57K=05) z8Vu4CpIhx232jCuR%b@bnU2_989iqO#No~Itc*vLeAin(2{mtrnzIC*tWlJL8KY4e zq}Th_l(p!Z>)nAY;eN&^NYA30CER8T;o<%h%9B#(^=O8mS&L@5$t=LJ=41F}TW{`_ c$N0^5z~7r|=xr!z8Me#{)cY0TsM<01KVN$<{r~^~ delta 3113 zcmZ`*du&tJ8NbKA_uBD0e#UkZ-`I&`ha^r0A>cx2LU^q-8=z(-?MRK!g*fpeJNE`i z8VF-lf)$B&XDTp|NdH=?Dvq zPUPSDzTfva_nz;Z?`9sKAF+I7HXAq?dtN=9`qze^SkCafj7*2ITm-nn$=m?K+<*q7 z0c{1h`iK2kBlB1bKVBBFAd6U(g<}S+J1-3Au%3Rzd#5VS^8*H4jg2rHv0gUGylg(E z@j~8X8f*gA0<0BeGsrfU8`ofqT#2oa*9Jem%pcR^O2}ykTP?N&$83xju>*JqcCP3= zc0m>=cCYF|>;b}s1GoxiH}=YAtjC69JobU^88<{F`kL^fAY^h_rk@D)Nc@iI)SoE5 z_9UW-h?BO7&rVGWliZ*HY-wN(XyU3iu8C?Bal~=^vMDu{NT()bNga}`5R+0xULXVDk~{+GMB~5I{!@K)S#Lhk+TXc zlsJ{jjAdXcrb|<>^b{{e(z%l|k+w^j9FZj~ zt1`)?vMEJPC7(QS0mb^Fx`Id)m83>V5H^>D7}PVM(;{ypVGs+t!ScRFMfzjWYb=op ztA#{A7TW?_Ksw76gPgbSruCY2M!Q(me1X4Y&bxdt8D?ENFVeG|QGqnk&H50krvv(K z)H!=Y|0U#T0t;yYD2Rzm4O%c%89f( zBq=Ivsv_Z3Qk71ofEba-p)O^*Sz@IhEFFr$bS{Bq-2CJetmD_JMK_uvEqp;NXCRy5 zNeY$!m8RfcJ4}Og#MFS=>6@l7YNfYLuRC@^j4}`KC+>6DW0SYSLceByc}lFXJoMG& zy{-9%rhHTEQH9{NJmv(m-9b1hjL7OV}AxmB;&|MK##+JEbPr~*_3Y5z;WWVC(Ri8cFUd7} zCO24n;@Uf#5R*Fu<`Wl~yh96e60Y*v;6W;t+=l37%aI^K zgS#1vn14r8_Bh1-p8(yH0BC|w2e2Hz^*K7ZOPO2RlPFp-v4w`Lu`Wd_SH886bizWV zitaA(tfYk9f$i1jjQhYqzRch`itR@bnf-;m7kQosCdv~#GkkT;rH|G+pF^+HVdqYN zAF#=l5`f5Y>?2nyWk0>|yag?sF1TE!J1EjuT>)gF*IoW+DoefqxB}~cXjm=cIW(yq zwyfHo%-A>ryH7TP)lMI|Tvper4Vzd5b-7vQ4tE{$&`P6^KP^lO^p&8KUUZw0pHb3j zaZ*%_>XS&@J1a*JR^^6k=z_c6^ai@5hcocg5U1ACJ)RJ%q#;kpSi8F1aF8DGc#(^a zctS1CRTf%}265*&m9@Om048!;r?OsJ8pBcgfhT}mv!8f|QHNO2XJkc59G8`XSs7QL zj}MNLTxL`)cjE?#DfeM$ByhemQAD=UG4BD?Pk-sX7Icwz@F?)3IWU)+xQ<1H7?fk_ zX`ffz#`HM7;j6Z_GI2YDE(Sa3eP6AS)s8&NpqoDOxr6;o>;X{p04twUiLlos>?BP1 z>0HD?xB2yzSD4p(uz%$y<%`i5{GIJJbvFmD4djF2@5kPb-kj-tB(#N=QJ>m9qp+Pj^WU4r(W;0F1g-?A&Jy%%*cxhu50 zPWxe<0P=@nEyR-kS-Iv9B6?|de|;YL2z$jb1}X!E0sHTSoT6RffN_F};|xyHq3{`~ z!XLv2G=4sGed$nvs))6+qDfNYdHzwttiUEV5M-M)b4DVe=X0Cn%%oluC}!uSX;G ztjiR{bm?ne&?d6u_iOf*;Y}wRVEl6^qpf6Rsgu9gW9g>0C0A9)hD6-_q9BMs`5} zeL3c<*#JTxguVoGfzKpTS=eW$0JnkCSVv#!6vlulonOM1D)8*%OV~@78fSq&n#-lh zUZ%7E1j$zz9Av^>lAg1BiWN*UaIUJNkx3@b?O$g8#Wr53p6*x*ZCVIzTIN7mHt~W~ zGzA39bnlE~COK2}cHc4wYOx2I1jFX(-et}NfkFCy+hNBxp=&x> dict[str, Any]: - """Check if llama-service is available. - - Returns: - Health status dict with keys: status, fast_model_loaded, reasoning_model_loaded - """ + """Check if llama-service is available.""" try: client = await self._get_client() response = await client.get(f"{self.base_url}/health") response.raise_for_status() return response.json() except httpx.ConnectError: - return { - "status": "error", - "error": "Service unreachable", - "fast_model_loaded": False, - "reasoning_model_loaded": False, - } + return {"status": "error", "error": "Service unreachable", "model_loaded": False} except httpx.TimeoutException: - return { - "status": "error", - "error": "Health check timeout", - "fast_model_loaded": False, - "reasoning_model_loaded": False, - } + return {"status": "error", "error": "Health check timeout", "model_loaded": False} except Exception as e: - return { - "status": "error", - "error": str(e), - "fast_model_loaded": False, - "reasoning_model_loaded": False, - } + return {"status": "error", "error": str(e), "model_loaded": False} async def is_available(self) -> bool: - """Check if the service is available and has models loaded.""" + """Check if the service is available and has model loaded.""" health = await self.health_check() - if health.get("status") == "error": - return False - - # Check if required model is loaded - if self.model == "fast": - return health.get("fast_model_loaded", False) - elif self.model == "reasoning": - return health.get("reasoning_model_loaded", False) - - return health.get("fast_model_loaded", False) + return health.get("status") == "ok" and health.get("model_loaded", False) async def generate_commit_message( self, @@ -169,7 +130,6 @@ class LlamaCommitClient: json={ "messages": [{"role": "user", "content": prompt}], "system_prompt": COMMIT_SYSTEM_PROMPT, - "model": self.model, "max_tokens": self.max_tokens, "temperature": self.temperature, "stream": False, diff --git a/src/auto_commit_service/scheduler/__pycache__/daemon.cpython-312.pyc b/src/auto_commit_service/scheduler/__pycache__/daemon.cpython-312.pyc index c480810d10cb7804e3d02e0b8eee2b8ae0c1eb6f..838292108c6471fc74a5bb7ba0e5b819abbcb8e9 100644 GIT binary patch delta 3642 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 RQ)^5+45aEa6(XO@{{WAxe1-r3 diff --git a/src/auto_commit_service/scheduler/daemon.py b/src/auto_commit_service/scheduler/daemon.py index abfd504..0113ffb 100644 --- a/src/auto_commit_service/scheduler/daemon.py +++ b/src/auto_commit_service/scheduler/daemon.py @@ -48,8 +48,7 @@ class CommitDaemon: lock_file=settings.llama_service_lock_file, startup_timeout=settings.llama_service_startup_timeout, health_check_timeout=5.0, - fast_model_id=settings.llama_fast_model_id, - reasoning_model_id=settings.llama_reasoning_model_id, + model_id=settings.llama_model_id, use_model_boss=settings.use_model_boss, ) else: @@ -400,11 +399,6 @@ class CommitDaemon: logger.error("Failed to start llama service") return False - if health == ServiceHealth.DEGRADED: - logger.warning("Llama service is degraded") - # Fail-fast: degraded service is not acceptable - return False - # Service is healthy self._service_crashed = False return True diff --git a/src/auto_commit_service/service/__init__.py b/src/auto_commit_service/service/__init__.py index 4c02b17..f50258f 100644 --- a/src/auto_commit_service/service/__init__.py +++ b/src/auto_commit_service/service/__init__.py @@ -5,7 +5,6 @@ from .manager import ( ServiceHealth, ServiceManagerError, ServiceStartError, - ServiceCrashError, ) __all__ = [ @@ -13,5 +12,4 @@ __all__ = [ "ServiceHealth", "ServiceManagerError", "ServiceStartError", - "ServiceCrashError", ] diff --git a/src/auto_commit_service/service/__pycache__/__init__.cpython-312.pyc b/src/auto_commit_service/service/__pycache__/__init__.cpython-312.pyc index 30ee7a91c9cd5a787971cf1e7d0f5b123f08b3a0..17086a0564b6efdab381eaed171f246380848946 100644 GIT binary patch delta 100 zcmdnSJd2t4G%qg~0}%W#ipk`i$ScXHF;QKfi;*FnA%$rXV-(xO5KTswiFH2AewyqP xAE-^{W^B;r1*&HR;$lf4@qw9qXkdl5HK764?*6qx`3 delta 108 zcmbQmyp5UnG%qg~0}#C46`g52kynyYXQH}1UoKk|8zVzHLkiO(#whlQ0h)}g6RUj0 zZV3jb7L{ctr#crU7H7B?73CNCX>v?Fsm4^qKJiU6Bj4l#MllgKMy9U}K;n}C8#hxU Jdl3)NcmOyQ9CQEx diff --git a/src/auto_commit_service/service/__pycache__/manager.cpython-312.pyc b/src/auto_commit_service/service/__pycache__/manager.cpython-312.pyc index 9f656b5d6c2abffe9b38935343f799a3d440e215..b6ca02f80c3d9b2478492152d00b586c2fb4cf05 100644 GIT binary patch delta 5028 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 diff --git a/src/auto_commit_service/service/manager.py b/src/auto_commit_service/service/manager.py index f17588d..887f566 100644 --- a/src/auto_commit_service/service/manager.py +++ b/src/auto_commit_service/service/manager.py @@ -23,15 +23,10 @@ class ServiceStartError(ServiceManagerError): """Failed to start service.""" -class ServiceCrashError(ServiceManagerError): - """Service crashed unexpectedly.""" - - class ServiceHealth(str, Enum): """Service health status.""" HEALTHY = "healthy" - DEGRADED = "degraded" CRASHED = "crashed" UNREACHABLE = "unreachable" @@ -46,23 +41,19 @@ class LlamaServiceManager: lock_file: Path | None = None, startup_timeout: float = 30.0, health_check_timeout: float = 5.0, - fast_model_id: str | None = None, - reasoning_model_id: str | None = None, - use_model_boss: bool = True, + model_id: str | None = None, + use_model_boss: bool = False, ): - """Initialize service manager.""" self.service_url = service_url self._pid_file = pid_file or Path.home() / ".config/commits/llama-service.pid" self._lock_file = lock_file or Path.home() / ".config/commits/llama-service.lock" self._startup_timeout = startup_timeout self._health_check_timeout = health_check_timeout - self._fast_model_id = fast_model_id - self._reasoning_model_id = reasoning_model_id + self._model_id = model_id self._use_model_boss = use_model_boss self._spawned_pid: int | None = None self._lock_fd: int | None = None - self._resolved_fast_model_path: str | None = None - self._resolved_reasoning_model_path: str | None = None + self._resolved_model_path: str | None = None async def ensure_service_available(self) -> bool: """Ensure service is available, starting if necessary.""" @@ -70,52 +61,33 @@ class LlamaServiceManager: if health == ServiceHealth.HEALTHY: return True - if health == ServiceHealth.DEGRADED: - # Degraded but running - acceptable for commits - return True if health == ServiceHealth.CRASHED: - # Stale PID file from previous session - clean up and restart - logger.info("Detected crashed service (stale PID file), cleaning up...") + logger.info("Detected crashed service (stale PID), cleaning up...") self._cleanup_pid_file() - # Fall through to restart logic logger.info("Llama service unreachable, attempting to start...") try: - # Resolve model paths via model-boss before starting - if self._use_model_boss and self._fast_model_id: - await self._resolve_model_paths() - + if self._use_model_boss and self._model_id: + await self._resolve_model_path() return await self.start_service() except ServiceStartError as e: logger.error(f"Failed to start llama-service: {e}") return False - async def _resolve_model_paths(self) -> None: - """Resolve model IDs to paths via model-boss. - - Raises: - ServiceStartError: If model resolution fails - """ + async def _resolve_model_path(self) -> None: + """Resolve model ID to path via model-boss.""" try: from lilith_model_boss import ensure_model - if self._fast_model_id and not self._resolved_fast_model_path: - logger.info(f"Resolving fast model via model-boss: {self._fast_model_id}") - self._resolved_fast_model_path = ensure_model(self._fast_model_id) - logger.info(f"Resolved fast model path: {self._resolved_fast_model_path}") - - if self._reasoning_model_id and not self._resolved_reasoning_model_path: - logger.info(f"Resolving reasoning model via model-boss: {self._reasoning_model_id}") - self._resolved_reasoning_model_path = ensure_model(self._reasoning_model_id) - logger.info(f"Resolved reasoning model path: {self._resolved_reasoning_model_path}") - + if self._model_id and not self._resolved_model_path: + logger.info(f"Resolving model via model-boss: {self._model_id}") + self._resolved_model_path = ensure_model(self._model_id) + logger.info(f"Resolved model path: {self._resolved_model_path}") except ImportError: - raise ServiceStartError( - "model-boss not installed. Install with: pip install auto-commit-service[model-boss]" - ) + raise ServiceStartError("model-boss not installed") except Exception as e: - raise ServiceStartError(f"Failed to resolve model paths: {e}") + raise ServiceStartError(f"Failed to resolve model path: {e}") async def start_service(self) -> bool: """Start llama service subprocess.""" @@ -148,7 +120,6 @@ class LlamaServiceManager: else: logger.error(f"✗ Service failed to start within {self._startup_timeout}s") return False - except Exception as e: logger.exception(f"Failed to start llama service: {e}") return False @@ -156,7 +127,7 @@ class LlamaServiceManager: self._release_lock() async def check_health(self) -> ServiceHealth: - """Check service health and detect crashes.""" + """Check service health.""" pid = self._read_pid_file() if pid and not self._is_process_alive(pid): return ServiceHealth.CRASHED @@ -166,7 +137,7 @@ class LlamaServiceManager: response = await client.get(f"{self.service_url}/health") if response.status_code == 200: data = response.json() - return ServiceHealth.HEALTHY if data.get("status") == "ok" else ServiceHealth.DEGRADED + return ServiceHealth.HEALTHY if data.get("status") == "ok" else ServiceHealth.UNREACHABLE return ServiceHealth.UNREACHABLE except (httpx.ConnectError, httpx.TimeoutException): return ServiceHealth.UNREACHABLE @@ -194,7 +165,6 @@ class LlamaServiceManager: logger.exception(f"Error stopping service: {e}") def _acquire_lock(self) -> bool: - """Acquire exclusive lock.""" self._lock_file.parent.mkdir(parents=True, exist_ok=True) try: self._lock_fd = os.open(self._lock_file, os.O_CREAT | os.O_WRONLY) @@ -204,7 +174,6 @@ class LlamaServiceManager: return False def _release_lock(self) -> None: - """Release lock.""" if self._lock_fd is not None: try: fcntl.flock(self._lock_fd, fcntl.LOCK_UN) @@ -214,7 +183,6 @@ class LlamaServiceManager: pass def _read_pid_file(self) -> int | None: - """Read PID from file.""" if not self._pid_file.exists(): return None try: @@ -224,12 +192,10 @@ class LlamaServiceManager: return None def _write_pid_file(self, pid: int) -> None: - """Write PID to file.""" self._pid_file.parent.mkdir(parents=True, exist_ok=True) self._pid_file.write_text(str(pid)) def _cleanup_pid_file(self) -> None: - """Remove PID file.""" try: if self._pid_file.exists(): self._pid_file.unlink() @@ -237,7 +203,6 @@ class LlamaServiceManager: pass def _is_process_alive(self, pid: int) -> bool: - """Check if process is alive.""" try: os.kill(pid, 0) return True @@ -250,79 +215,48 @@ class LlamaServiceManager: if not cache_dir.exists(): return None - # Preferred models for commit message generation (fast, small) - preferred_models = [ + preferred = [ "qwen2.5-1.5b-instruct-q4_k_m.gguf", "Ministral-3-3B-Instruct-2512-Q8_0.gguf", - "ministral-3b-instruct", ] - # Check for preferred models first - for model_name in preferred_models: - model_path = cache_dir / model_name - if model_path.exists(): - return str(model_path) + for name in preferred: + path = cache_dir / name + if path.exists(): + return str(path) - # Fall back to any small GGUF file (< 5GB) - for gguf_file in cache_dir.glob("*.gguf"): - if gguf_file.stat().st_size < 5 * 1024 * 1024 * 1024: # < 5GB - return str(gguf_file) + for gguf in cache_dir.glob("*.gguf"): + if gguf.stat().st_size < 5 * 1024 * 1024 * 1024: + return str(gguf) return None async def _spawn_service(self) -> asyncio.subprocess.Process: - """Spawn service as background subprocess. - - Raises: - ServiceStartError: If no model paths are configured - """ + """Spawn service subprocess.""" cmd = [sys.executable, "-m", "lilith_llama_service"] env = os.environ.copy() - # Use resolved model paths from model-boss if available - has_model_paths = False + model_path = self._resolved_model_path or env.get("LLAMA_SERVICE_MODEL_PATH") - if self._resolved_fast_model_path: - env["LLAMA_SERVICE_FAST_MODEL_PATH"] = self._resolved_fast_model_path - has_model_paths = True - logger.info(f"Using fast model: {self._resolved_fast_model_path}") + if not model_path: + model_path = self._find_default_model() - if self._resolved_reasoning_model_path: - env["LLAMA_SERVICE_REASONING_MODEL_PATH"] = self._resolved_reasoning_model_path - has_model_paths = True - logger.info(f"Using reasoning model: {self._resolved_reasoning_model_path}") - - # Fall back to environment variables if set - if not has_model_paths: - if "LLAMA_SERVICE_FAST_MODEL_PATH" in env or "LLAMA_SERVICE_REASONING_MODEL_PATH" in env: - has_model_paths = True - logger.info("Using model paths from environment variables") - - # Fall back to auto-discovered model in cache - if not has_model_paths: - default_model = self._find_default_model() - if default_model: - env["LLAMA_SERVICE_FAST_MODEL_PATH"] = default_model - has_model_paths = True - logger.info(f"Using auto-discovered model: {default_model}") - - # Fail if no models are configured - do not fall back to mock mode - if not has_model_paths: + if not model_path: raise ServiceStartError( - "No model paths configured and no models found in ~/.cache/models/. Either:\n" - " 1. Install model-boss: pip install auto-commit-service[model-boss]\n" - " 2. Set LLAMA_SERVICE_FAST_MODEL_PATH environment variable\n" - " 3. Place a GGUF model in ~/.cache/models/\n" - " 4. Disable llama_service_autostart in config" + "No model found. Either:\n" + " 1. Set LLAMA_SERVICE_MODEL_PATH\n" + " 2. Place a GGUF model in ~/.cache/models/" ) + env["LLAMA_SERVICE_MODEL_PATH"] = model_path + logger.info(f"Using model: {model_path}") + log_file = self._pid_file.parent / "llama-service.log" log_file.parent.mkdir(parents=True, exist_ok=True) with open(log_file, "a") as log: log.write(f"\n=== Service started at {time.ctime()} ===\n") - log.write(f"Fast model: {env.get('LLAMA_SERVICE_FAST_MODEL_PATH', 'not set')}\n") - log.write(f"Reasoning model: {env.get('LLAMA_SERVICE_REASONING_MODEL_PATH', 'not set')}\n") + log.write(f"Model: {model_path}\n") process = await asyncio.create_subprocess_exec( *cmd, @@ -334,7 +268,6 @@ class LlamaServiceManager: return process async def _wait_for_healthy(self, timeout: float) -> bool: - """Wait for service to become healthy.""" start = time.time() while time.time() - start < timeout: try: diff --git a/tests/__pycache__/__init__.cpython-312.pyc b/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..793da0875be8e95f087fd941ae000551ad1acb99 GIT binary patch literal 215 zcmX@j%ge<81Z#LAGgX1~V-N=h7@>^M96-i&h7^Vy+}z9(-Qv`uvdrXEJwHvxTkP@iDf!9q@hcfVgADoQsb7{@q@R(Wo2s9anUh(P zq3@iZlB(}ekeHmEn4Vg!?~t3LkHs+k5|CB;@$s2?nI-Y@dIgoYIBatBQ%ZAE?TXld XCWG8q407KGW=2NFTMQ~iEIERg6drrM-d$%mB!Q3uAy`C^tisxx5>iSl2!Ab6)qqIas(Y~y^+e7@p%;VIja| z-ZHSvqyptJvt=UBQQ@2(&cc{^6T6t`p+{&sbbQSRG8G91Wv;=+ma{>GuOGhE5U6h< z4q#mb=xS{UUE&tGx%IM49$L#(4uy#(1)({iHG4h+V2K%ikuwETxRYFtCH`&K3^K(} z;pG3lE4A4!(357q%%z^&)tZ@^<=DIbV3V|&_J8&tJZal(*CO{ZcZ1_lHTzj&fy?J= z%1MIt5X(B;rsI1=CsldI3T1+QFR{RmhG1#`gyQM$|stKBy`iHoU-mr7PB?J;MCg3 z!B$JR5Njd680LABWY%Yx(@`)7JgjB; zk8yQgC+e){VAZy4E1XqNdL~xKeBHR9&)`5Eb8K~!#8H{Mfy&T`9QUgfV1P^kKs9y> z>45_va!~L&bibhAE~AEsI(OWBZKY%X0d&IMASloMWn6<#87qLSNy6xy>1~{u8A+o!!I8k zIW+o;qUh6xiOVzcaIrW#G9ncUg$;Y9p`oFT`^U!Q!f^4xs5}Jya8ZUeD3BpO-NF13 zNoLI_mKzT4J%erA+nbk$7_DDEZkn=-7vwVJ5X5_v90!tS>Y+Xuiw62UhN}%Kb{Mp1 zvC31Pi$Nn(2wBI9q+FDwzd2qBEjxJbdTS{~$6ElR1F_fgxrs=z*%T#)CW1mW@x1C* zY%wpTegdq`mJuol;C;&w+S6m~sJQpaj=XwxGw-I3v z)hfGrvK>0w7DU&qF`vjTpcLQ>SXE?k-JKQ4PMB|q7RHRCWe_It+On4xIou_9BzX2n zB%nkmP0!REs9TW(KUL z6PnV4s%uyt=>nT*iv14Q)-++)OU1D2!xa%$6sXwTLsRjeM^pxBA_z$oHIZ{sY(-W# z{ZGFRabL0VQ8EH57%;&{&@~bq$9<1Ren#jTy4wB&>U@Y^dWd=-p&V3FK7GCKN?(KT zbRK_tax6`^KIy(rN%{{&#gsagO4 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_diff_parser.cpython-312-pytest-9.0.2.pyc b/tests/__pycache__/test_diff_parser.cpython-312-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af6eae93e7e7fe8ae17ee32ca72253d603ca8632 GIT binary patch literal 18285 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 V#L None: """Test health check when service is unavailable.""" - # No mock server, should fail health = await client.health_check() assert health["status"] == "error" - assert not health["fast_model_loaded"] + assert not health["model_loaded"] async def test_is_available_when_down(self, client: LlamaCommitClient) -> None: """Test is_available when service is down.""" available = await client.is_available() assert not available - def test_clean_response_removes_quotes(self, client: LlamaCommitClient) -> None: - """Test that clean_response removes surrounding quotes.""" - result = client._clean_response('"✨ Add new feature"') - assert result == "✨ Add new feature" - def test_clean_response_removes_code_blocks(self, client: LlamaCommitClient) -> None: - """Test that clean_response removes markdown code blocks.""" - result = client._clean_response("```\n✨ Add new feature\n```") - assert result == "✨ Add new feature" +class TestCleanResponseNormalization: + """Tests proving the _clean_response method normalizes sloppy LLM output. - def test_clean_response_takes_first_line(self, client: LlamaCommitClient) -> None: - """Test that clean_response takes only first line.""" - result = client._clean_response("✨ Add feature\nThis is extra text") - assert result == "✨ Add feature" + The LLM may produce commit messages in various formats. These tests + verify that all variations get normalized to: type(scope): emoji description + """ - def test_clean_response_adds_emoji_if_missing(self, client: LlamaCommitClient) -> None: - """Test that clean_response adds emoji for known types.""" - result = client._clean_response("Add new authentication") - assert result.startswith("✨") + @pytest.fixture + def client(self) -> LlamaCommitClient: + return LlamaCommitClient() - def test_clean_response_preserves_valid_emoji(self, client: LlamaCommitClient) -> None: - """Test that valid emoji prefixes are preserved.""" - result = client._clean_response("🔧 Update dependencies") - assert result == "🔧 Update dependencies" + # ========================================================================== + # Already correct format - should pass through + # ========================================================================== + + def test_correct_format_passes_through(self, client: LlamaCommitClient) -> None: + """Correctly formatted messages pass through unchanged.""" + result = client._clean_response("feat(auth): ✨ add login endpoint") + assert result == "feat(auth): ✨ add login endpoint" + + def test_correct_format_with_different_types(self, client: LlamaCommitClient) -> None: + """All valid types with correct format pass through (emojis may be normalized).""" + test_cases = [ + ("fix(api): 🐛 resolve null pointer in handler", "fix(api):"), + ("refactor(core): ♻️ extract validation logic", "refactor(core):"), + ("chore(deps): 🔧 update eslint to v9", "chore(deps):"), + ("docs(readme): 📝 add installation section", "docs(readme):"), + ("build(ci): ⬆️ upgrade node to v20", "build(ci):"), + ("test(auth): ✅ add unit tests for login", "test(auth):"), + ("perf(query): ⚡ optimize database lookup", "perf(query):"), + ] + for msg, expected_prefix in test_cases: + result = client._clean_response(msg) + assert result.startswith(expected_prefix), f"Expected {result} to start with {expected_prefix}" + # Verify description is preserved + assert "resolve null pointer" in result or "extract validation" in result or \ + "update eslint" in result or "add installation" in result or \ + "upgrade node" in result or "add unit tests" in result or \ + "optimize database" in result + + # ========================================================================== + # Markdown formatting removal + # ========================================================================== + + def test_removes_markdown_code_blocks(self, client: LlamaCommitClient) -> None: + """Strips markdown code block wrapper.""" + result = client._clean_response("```\nfeat(ui): ✨ add button component\n```") + assert result == "feat(ui): ✨ add button component" + + def test_removes_language_tagged_code_blocks(self, client: LlamaCommitClient) -> None: + """Strips code blocks with language tags.""" + result = client._clean_response("```text\nfix(api): 🐛 fix timeout\n```") + assert result == "fix(api): 🐛 fix timeout" + + def test_removes_surrounding_quotes(self, client: LlamaCommitClient) -> None: + """Strips surrounding double quotes.""" + result = client._clean_response('"feat(auth): ✨ add oauth"') + assert result == "feat(auth): ✨ add oauth" + + def test_removes_single_quotes(self, client: LlamaCommitClient) -> None: + """Strips surrounding single quotes.""" + result = client._clean_response("'chore(config): 🔧 update settings'") + assert result == "chore(config): 🔧 update settings" + + def test_takes_only_first_line(self, client: LlamaCommitClient) -> None: + """When LLM produces multiple lines, only first is used.""" + sloppy = """feat(api): ✨ add user endpoint +This adds a new REST endpoint for user management. +It supports CRUD operations.""" + result = client._clean_response(sloppy) + assert result == "feat(api): ✨ add user endpoint" + + # ========================================================================== + # Emoji position correction + # ========================================================================== + + def test_moves_emoji_from_start_to_after_colon(self, client: LlamaCommitClient) -> None: + """When emoji comes before type, move it after the colon.""" + result = client._clean_response("✨ feat(ui): add new component") + assert result == "feat(ui): ✨ add new component" + + def test_fixes_emoji_before_type_with_fix(self, client: LlamaCommitClient) -> None: + """Bug fix emoji at start gets repositioned.""" + result = client._clean_response("🐛 fix(auth): resolve login bug") + assert result == "fix(auth): 🐛 resolve login bug" + + # ========================================================================== + # Missing emoji addition + # ========================================================================== + + def test_adds_emoji_for_feat_type(self, client: LlamaCommitClient) -> None: + """Adds ✨ emoji for feat type when missing.""" + result = client._clean_response("feat(api): add health endpoint") + assert result == "feat(api): ✨ add health endpoint" + + def test_adds_emoji_for_fix_type(self, client: LlamaCommitClient) -> None: + """Adds 🐛 emoji for fix type when missing.""" + result = client._clean_response("fix(auth): resolve timeout issue") + assert result == "fix(auth): 🐛 resolve timeout issue" + + def test_adds_emoji_for_refactor_type(self, client: LlamaCommitClient) -> None: + """Adds ♻️ emoji for refactor type when missing.""" + result = client._clean_response("refactor(core): extract shared logic") + assert result == "refactor(core): ♻️ extract shared logic" + + def test_adds_emoji_for_chore_type(self, client: LlamaCommitClient) -> None: + """Adds 🔧 emoji for chore type when missing.""" + result = client._clean_response("chore(deps): update dependencies") + assert result == "chore(deps): 🔧 update dependencies" + + def test_adds_emoji_for_docs_type(self, client: LlamaCommitClient) -> None: + """Adds 📝 emoji for docs type when missing.""" + result = client._clean_response("docs(readme): update installation guide") + assert result == "docs(readme): 📝 update installation guide" + + # ========================================================================== + # Fallback to chore(shared) for emoji-only messages + # ========================================================================== + + def test_wraps_emoji_only_message(self, client: LlamaCommitClient) -> None: + """Bare emoji + description gets wrapped in chore(shared).""" + result = client._clean_response("✨ add new feature") + assert result == "chore(shared): ✨ add new feature" + + def test_wraps_wrench_emoji_message(self, client: LlamaCommitClient) -> None: + """Wrench emoji messages become chore(shared).""" + result = client._clean_response("🔧 update config") + assert result == "chore(shared): 🔧 update config" + + # ========================================================================== + # Keyword inference for unstructured messages + # ========================================================================== + + def test_infers_feat_from_add_keyword(self, client: LlamaCommitClient) -> None: + """'add' keyword triggers feat type inference.""" + result = client._clean_response("add new authentication module") + assert result.startswith("feat(shared): ✨") + assert "add new authentication module" in result + + def test_infers_fix_from_fix_keyword(self, client: LlamaCommitClient) -> None: + """'fix' keyword triggers fix type inference.""" + result = client._clean_response("fix null pointer in user handler") + assert result.startswith("fix(shared): 🐛") + + def test_infers_refactor_from_refactor_keyword(self, client: LlamaCommitClient) -> None: + """'refactor' keyword triggers refactor type inference.""" + result = client._clean_response("refactor the database layer") + assert result.startswith("refactor(shared): ♻️") + + def test_infers_chore_from_update_keyword(self, client: LlamaCommitClient) -> None: + """'update' keyword triggers chore type inference.""" + result = client._clean_response("update eslint configuration") + assert result.startswith("chore(shared): 🔧") + + def test_fallback_to_chore_for_unknown(self, client: LlamaCommitClient) -> None: + """Unknown messages default to chore(shared): 🔧.""" + result = client._clean_response("miscellaneous changes to codebase") + assert result.startswith("chore(shared): 🔧") + + +class TestCommitMessageGeneration: + """End-to-end tests for commit message generation with mocked LLM.""" + + @pytest.fixture + def client(self) -> LlamaCommitClient: + return LlamaCommitClient(base_url="http://test:8000") + + @pytest.fixture + def mock_httpx_response(self): + """Factory to create mock httpx responses.""" + def _create(content: str, status_code: int = 200): + response = MagicMock(spec=httpx.Response) + response.status_code = status_code + response.json.return_value = {"content": content} + response.text = content + return response + return _create + + async def test_generate_from_diff_cleans_output( + self, client: LlamaCommitClient, mock_httpx_response + ) -> None: + """Full flow: diff -> prompt -> mock LLM -> cleaned message.""" + # Simulate LLM returning a sloppy message + mock_response = mock_httpx_response('"✨ feat(api): add new endpoint"') + + with patch.object(client, "_get_client") as mock_get_client: + mock_http_client = AsyncMock() + mock_http_client.post.return_value = mock_response + mock_get_client.return_value = mock_http_client + + result = await client.generate_from_diff( + diff="diff --git a/api.py b/api.py\n+def new_endpoint(): pass", + repo_name="test-repo", + ) + + # Quotes should be stripped, message cleaned + assert result == "feat(api): ✨ add new endpoint" + + async def test_generate_commit_message_with_summary( + self, client: LlamaCommitClient, mock_httpx_response + ) -> None: + """Generate from DiffSummary produces cleaned message.""" + summary = DiffSummary( + files_modified=2, + files_added=1, + additions=50, + deletions=10, + file_types={".py": 2, ".md": 1}, + key_files=["src/api.py", "src/utils.py", "README.md"], + diff_excerpt="@@ -1,5 +1,10 @@ ...", + ) + + # Simulate LLM returning message without emoji + mock_response = mock_httpx_response("feat(api): add authentication module") + + with patch.object(client, "_get_client") as mock_get_client: + mock_http_client = AsyncMock() + mock_http_client.post.return_value = mock_response + mock_get_client.return_value = mock_http_client + + result = await client.generate_commit_message( + diff_summary=summary, + repo_name="auth-service", + branch="main", + ) + + # Emoji should be added automatically + assert result == "feat(api): ✨ add authentication module" + + async def test_handles_service_unavailable( + self, client: LlamaCommitClient + ) -> None: + """Raises LlamaServiceUnavailable when service is down.""" + with patch.object(client, "_get_client") as mock_get_client: + mock_http_client = AsyncMock() + mock_http_client.post.side_effect = httpx.ConnectError("Connection refused") + mock_get_client.return_value = mock_http_client + + with pytest.raises(LlamaServiceUnavailable): + await client.generate_from_diff("diff content", "repo") + + async def test_handles_503_service_unavailable( + self, client: LlamaCommitClient, mock_httpx_response + ) -> None: + """Raises LlamaServiceUnavailable on 503 response.""" + mock_response = mock_httpx_response("Service Unavailable", status_code=503) + + with patch.object(client, "_get_client") as mock_get_client: + mock_http_client = AsyncMock() + mock_http_client.post.return_value = mock_response + mock_get_client.return_value = mock_http_client + + with pytest.raises(LlamaServiceUnavailable): + await client.generate_from_diff("diff content", "repo") + + +class TestDiffToMessageIntegration: + """Integration tests: diff parsing -> prompt building -> message cleaning. + + These tests demonstrate the full pipeline from raw git diff to final + cleaned commit message, using mocked LLM responses. + """ + + @pytest.fixture + def sample_feature_diff(self) -> str: + """Diff showing a new feature being added.""" + return """diff --git a/src/auth/login.py b/src/auth/login.py +new file mode 100644 +index 0000000..abc1234 +--- /dev/null ++++ b/src/auth/login.py +@@ -0,0 +1,25 @@ ++from flask import request, jsonify ++ ++def login_endpoint(): ++ username = request.json.get('username') ++ password = request.json.get('password') ++ if authenticate(username, password): ++ return jsonify({'token': generate_token(username)}) ++ return jsonify({'error': 'Invalid credentials'}), 401 +""" + + @pytest.fixture + def sample_bugfix_diff(self) -> str: + """Diff showing a bug being fixed.""" + return """diff --git a/src/api/handler.py b/src/api/handler.py +index 1234567..abcdefg 100644 +--- a/src/api/handler.py ++++ b/src/api/handler.py +@@ -15,7 +15,9 @@ def process_request(data): +- result = data.get('value') ++ result = data.get('value') ++ if result is None: ++ raise ValueError("Missing required field: value") + return transform(result) +""" + + @pytest.fixture + def sample_chore_diff(self) -> str: + """Diff showing a config/maintenance change.""" + return """diff --git a/pyproject.toml b/pyproject.toml +index 1234567..abcdefg 100644 +--- a/pyproject.toml ++++ b/pyproject.toml +@@ -10,7 +10,7 @@ dependencies = [ +- "httpx>=0.24.0", ++ "httpx>=0.25.0", + "pydantic>=2.0.0", +] +""" + + def test_parse_feature_diff(self, sample_feature_diff: str) -> None: + """Feature diff is correctly parsed.""" + summary = summarize_diff(sample_feature_diff) + + assert summary.files_added == 1 + assert summary.files_modified == 0 + assert ".py" in summary.file_types + assert "src/auth/login.py" in summary.key_files + + def test_parse_bugfix_diff(self, sample_bugfix_diff: str) -> None: + """Bugfix diff is correctly parsed.""" + summary = summarize_diff(sample_bugfix_diff) + + assert summary.files_modified == 1 + assert summary.additions >= 2 # Added lines + assert "src/api/handler.py" in summary.key_files + + def test_parse_chore_diff(self, sample_chore_diff: str) -> None: + """Chore/config diff is correctly parsed.""" + summary = summarize_diff(sample_chore_diff) + + assert summary.files_modified == 1 + assert ".toml" in summary.file_types + assert "pyproject.toml" in summary.key_files + + async def test_full_pipeline_feature(self, sample_feature_diff: str) -> None: + """Full pipeline for feature: diff -> summary -> prompt -> clean message.""" + summary = summarize_diff(sample_feature_diff) + client = LlamaCommitClient() + + # Simulate sloppy LLM response + sloppy_response = "✨ feat(auth): add login endpoint" + + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"content": sloppy_response} + + with patch.object(client, "_get_client") as mock_get_client: + mock_http_client = AsyncMock() + mock_http_client.post.return_value = mock_response + mock_get_client.return_value = mock_http_client + + result = await client.generate_commit_message(summary, "auth-service") + + # Emoji moved to correct position + assert result == "feat(auth): ✨ add login endpoint" + + async def test_full_pipeline_bugfix(self, sample_bugfix_diff: str) -> None: + """Full pipeline for bugfix: diff -> summary -> prompt -> clean message.""" + summary = summarize_diff(sample_bugfix_diff) + client = LlamaCommitClient() + + # LLM returns message without emoji + sloppy_response = "fix(api): handle missing value field" + + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"content": sloppy_response} + + with patch.object(client, "_get_client") as mock_get_client: + mock_http_client = AsyncMock() + mock_http_client.post.return_value = mock_response + mock_get_client.return_value = mock_http_client + + result = await client.generate_commit_message(summary, "api-service") + + # Emoji should be added + assert result == "fix(api): 🐛 handle missing value field" + + async def test_full_pipeline_chore(self, sample_chore_diff: str) -> None: + """Full pipeline for chore: diff -> summary -> prompt -> clean message.""" + summary = summarize_diff(sample_chore_diff) + client = LlamaCommitClient() + + # LLM returns bare emoji message + sloppy_response = "⬆️ update httpx dependency" + + mock_response = MagicMock(spec=httpx.Response) + mock_response.status_code = 200 + mock_response.json.return_value = {"content": sloppy_response} + + with patch.object(client, "_get_client") as mock_get_client: + mock_http_client = AsyncMock() + mock_http_client.post.return_value = mock_response + mock_get_client.return_value = mock_http_client + + result = await client.generate_commit_message(summary, "project") + + # Should be wrapped in chore(shared) + assert result == "chore(shared): ⬆️ update httpx dependency" class TestClientContextManager: @@ -68,6 +449,8 @@ class TestClientContextManager: async def test_context_manager(self) -> None: """Test using client as async context manager.""" async with LlamaCommitClient() as client: + # Trigger client creation by calling a method + await client.health_check() assert client._client is not None # After exit, client should be closed diff --git a/tests/test_prompts.py b/tests/test_prompts.py index 3eaf2f3..16c5590 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -22,8 +22,8 @@ class TestCommitSystemPrompt: def test_includes_format_rules(self) -> None: """Test that system prompt includes formatting rules.""" - assert "50 characters" in COMMIT_SYSTEM_PROMPT or "under 50" in COMMIT_SYSTEM_PROMPT - assert "imperative" in COMMIT_SYSTEM_PROMPT.lower() + assert "50" in COMMIT_SYSTEM_PROMPT # Length limit + assert "lowercase" in COMMIT_SYSTEM_PROMPT.lower() # Case requirement class TestBuildCommitPrompt: