feat(hooks): Hooks Comprehensive Integration Testing (#9112)

This commit is contained in:
Edilmo Palencia
2025-11-26 21:38:35 -08:00
committed by GitHub
parent 6a43b31218
commit 7a4280a482
12 changed files with 1028 additions and 0 deletions

View File

@@ -0,0 +1 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Addressing the Inquiry**\n\nI've grasped the core of the user's question and identified that no tools are needed. My focus is now on crafting a straightforward, direct response that fully addresses their query without any unnecessary complexity. The goal is to provide a clear and concise answer.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12777,"totalTokenCount":12802,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12777}],"thoughtsTokenCount":25}},{"candidates":[{"content":{"parts":[{"text":"4","thoughtSignature":"CiQBcsjafBFqw6veocEvtOGGuQcsyHdcNrXDIn19n9ImwBBwcYQKdgFyyNp8g7o8Ji++OXoqml4gbLPIB2DQbXcaRQfRuYefF8RxMEpzJSITZBlT1VpJQoeYmQcb9c8dg/POmo5d3ZcuLbpVJpbjMIV1SoUI4KEn3zqz7a8BFuyq3zY4VEliRWMZO21JMd8qp59M9m64hX7W1YPyzu8KPwFyyNp8aNCD7P1NJDG3csQkiMW/0jWdPkh+7+XxT7i3ku/lYH4yTEShdicPcmnzoPGhEWTUDr/4Lx+A0DnVGQ=="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12777,"totalTokenCount":12802,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12777}],"thoughtsTokenCount":25}}]}

View File

@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Analyzing File Access**\n\nI've realized the `read_file` tool is perfect for accessing the contents of `test-file.txt`. My next step is to call this tool and set the `file_path` parameter to `test-file.txt`.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12785,"totalTokenCount":12841,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12785}],"thoughtsTokenCount":56}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"read_file","args":{"file_path":"test-file.txt"}},"thoughtSignature":"CiQBcsjafE9D7iAF+V3wpXP81/VmxiMeSFA6afML/lAB76U6QFQKXgFyyNp8i/vhxpkTQ5Cq81QTeEJDDMaYihzSTFMqO4Vj0+CLNtoy+SC/LmqA+WaXh4tm6UCNFTzB2fpVW13YOU1oVYhLpVpeck746YExu1MOSTAq7AC9Yz8ZoelXdecKdwFyyNp8q0PejiY9K1osdOJ02tOHAzAb8ZCSFHtHamEPxRB93krGMNvuIYC1jM1JnC/fzpH8gYV+0/xkoPJMHpF/aSzWq4kZ/j5cUhMYaqKJTulY8ZZGfawnXG7z0spmmr06gwfgILa+HK++xQhhTphMQCobX5hyCjUBcsjafHY6eJfVNitYmfruLV1mnoYnNViHuAOOOni9jIz4VMIjLbClKkb2rpVfHIjx+vZSHA=="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12785,"candidatesTokenCount":20,"totalTokenCount":12861,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12785}],"thoughtsTokenCount":56}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"This"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12889,"candidatesTokenCount":1,"totalTokenCount":12890,"cachedContentTokenCount":12206,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12889}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12206}]}},{"candidates":[{"content":{"parts":[{"text":" is test content"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12889,"candidatesTokenCount":4,"totalTokenCount":12893,"cachedContentTokenCount":12206,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12889}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12206}]}}]}

View File

@@ -0,0 +1,4 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Formulating the Write Operation**\n\nOkay, I'm now clear on the user's intent: they want a file called `approved.txt`, containing the words \"Approved content.\" I've decided to leverage the `write_file` tool. The specific parameter assignments seem straightforward; `file_path` will be \"approved.txt\", and the file's `content` will precisely mirror the desired output string.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12778,"totalTokenCount":12838,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12778}],"thoughtsTokenCount":60}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"content":"Approved content","file_path":"approved.txt"}},"thoughtSignature":"CiQBcsjafF4NswdygCBTU7cA/yXVRcUI3XHwV+E8BDg/hRr1MaoKZQFyyNp8HRY1qEvivtg0LpYPo1022IfTY3QIeigqGvSoRVospxT5MBggc9nRbwH2vrdhZ772IdqOCrpjNHs3wc+h0AF4JzjlBet6+yC2m7TdenVOkzVAtqnNDMQAIS1gDZyKs8w/CngBcsjafOeuyDQtxuK7JCafKjtfvPvoKOkVxzDetQtHesBkPtv1Xng9dkP77jLH44hn9rrg7yA+za6vssiFZUjC/FU25pCWQgIhM+K7nt3wbAgoOZRqra2gRr3od2D3osV/UpYhy8MoloykqrWvHDOzT/0KScpHarwKXQFyyNp8qabyDYlfElywQBjqQT4f6My7+Ln9AbKZQz4NaEe90ESg4jr4jjANxyd/WKzRheaBq7BYxTHQSeShgQbVjk2D0tZO4hAN+CToMtQwJl95Ss4ZEov6gAwMNA=="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12778,"candidatesTokenCount":24,"totalTokenCount":12862,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12778}],"thoughtsTokenCount":60}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12939,"totalTokenCount":12939,"cachedContentTokenCount":12203,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12939}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12203}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12939,"totalTokenCount":12939,"cachedContentTokenCount":12203,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12939}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12203}]}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Confirming File Creation**\n\nI've successfully created the file `approved.txt`, and I've verified that it contains the intended content, \"Approved content\". Moving on.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12887,"totalTokenCount":12932,"cachedContentTokenCount":12198,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12887}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12198}],"thoughtsTokenCount":45}},{"candidates":[{"content":{"parts":[{"text":"**Assessing File Contents**\n\nI'm now checking the content of `approved.txt`. I used `cat` to display its contents, and it confirms the initial content of \"Approved content\" is present. My next step will be based on this verification.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12887,"totalTokenCount":12946,"cachedContentTokenCount":12198,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12887}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12198}],"thoughtsTokenCount":59}},{"candidates":[{"content":{"parts":[{"text":"I have created the file. What would you like me to do next?","thoughtSignature":"CiQBcsjafEAq9BWRwBqUousKwXME0A2Wh1tJI5cJC9ROpr9Cix8KagFyyNp81VagWC/YxtY8zCAiThU3BHMVh5wZIsGIWv1NNIXqACLQLoSeLhWEneb6CBkKdbKBugy6g9+jP5phYt+Vz5oYuO1Op2kM1qWjFmEQyr71TUISNtZ9zrOHNQKKW7K9ukUi0paw85YKoAEBcsjafF6QLINjBWwQPZh6EPVNGk4wojTKglNp7xy5vclYBbq58A6A8AtZUHKYA2cV32SLb2TGcPnkE4iKunvPf6sZy9Uc7gKA+x/OgSl7i5m0wSpMOh9fLpGt4CNtieigpxHkNAdxdZ5qzGvCkBFWYhaZAWGbj7+1YibIKJFNjX9yEz1T5dOQmVmceu80dFyz+fwl7RiOXSGR5xK4J7DeClYBcsjafPUccUubdSVLFmRohU4bBtQzLvXxw25mqm5TKANLKINQoloZ+xfXzfe8xw/WZL/mg30AqQErBXPNnLk5vIWLK7suuFAZ7oXdisTCj3MRa1HQmQ=="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12887,"candidatesTokenCount":13,"totalTokenCount":12959,"cachedContentTokenCount":12198,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12887}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12198}],"thoughtsTokenCount":59}}]}

View File

@@ -0,0 +1 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Responding Truthfully**\n\nI've determined the user's question is straightforward and doesn't necessitate tools. My primary focus now is ensuring a truthful and direct response, without getting caught up in unnecessary complexity. The goal is clarity and accuracy in my reply.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12795,"totalTokenCount":12818,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12795}],"thoughtsTokenCount":23}},{"candidates":[{"content":{"parts":[{"text":"Hello! I'm doing well, thank you. I'm ready to help you with your software engineering tasks. All","thoughtSignature":"CiQBcsjafEYrS7SUZE2xuCgUZ7+hs+NrTRZFywSgq09wuKUzD5gKbQFyyNp8snmh8vfDLLCmnKl2shxGR5McWLmRDIQx+gvyW9ipB+5v5R3tvYgBY0yYGxuB8XPHJDP8unxCqg2koazS050HLU5NZaF74m9KDAWrnWPqQ2hDPc9suJRZpcTse5R+nepMu+oXWEsD03UKOwFyyNp82dmgHDF2DLELc6ly78JDLDmb4kM4qkXmuT8OP7Nu5z2o8kkHiKD4HTx0srjLi6u6dN4ufA0o"}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12795,"candidatesTokenCount":24,"totalTokenCount":12842,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12795}],"thoughtsTokenCount":23}},{"candidates":[{"content":{"parts":[{"text":" our interactions are logged for security and compliance purposes. How can I assist you today?"}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12795,"candidatesTokenCount":41,"totalTokenCount":12859,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12795}],"thoughtsTokenCount":23}}]}

View File

@@ -0,0 +1 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Initiating String Output**\n\nI'm now fully focused on directly outputting the specified string. The process has been simplified to its core objective, eliminating extraneous steps. All systems are go for immediate execution of the requested string output.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12419,"totalTokenCount":12439,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12419}],"thoughtsTokenCount":20}},{"candidates":[{"content":{"parts":[{"text":"The security hook modified this request successfully.","thoughtSignature":"CiQBcsjafAsmW87n4ndCW3YiNIqK6jp0zaTwTjz12vWiwbCFNAUKdQFyyNp808SX5BqCBNZt+dlgsPf74u9W6ofevKGwkTTHQZWJEQiJR2j4uRfESTazuawuWfzKfNJq5Zml6fokNR9jzmQM+Jf4FHw95Jd4lneap+YGO9x5nZMNDI1cHRx0vs4BYW9GWY7lBIM8xKtaEkPrwqc88goiAXLI2nx5o6VrBpXs6jzf5maZIauSYw42zlnkqdDEMI20rg=="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12419,"candidatesTokenCount":7,"totalTokenCount":12446,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12419}],"thoughtsTokenCount":20}}]}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Constructing File Parameters**\n\nI'm currently focused on the parameters needed for creating `test.txt`. The `write_file` tool seems ideal. I've settled on the `file_path` being \"test.txt\" and the `content` parameter being \"Hello World\". This should result in the desired file creation.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12779,"totalTokenCount":12843,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12779}],"thoughtsTokenCount":64}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"file_path":"test.txt","content":"Hello World"}},"thoughtSignature":"CiQBcsjafGeGML0hnm03md4ExPwk5i2rcaNDqetrrKnEoEFjxRcKYQFyyNp8rs78myvJfPMuC2AWyTHEoWUps7GWpGu/2VU1BB3ekI32yO0q9KSKkmGX28Palht22I77ac5HsFTuutPBDWIqSkrERkzOh3HKJE2MXzsVJJGHX3jVBirJ+Y8F1OAKcQFyyNp8pMKA4E8M3PhbuhDzOv3c9tVEgCQ4W6kzmZHBQeUQNuHVLw1cZfx/aichP6fJeZEJPCXROa7WEWPbwY9evB+ofTqjiifUXo0l4smudNHAerr7UrspQVDwGRGnWBkKiy9a6V5q6XkEhYci+2tBCnIBcsjafNb1jWT0qNMJcPcb8Ngu9xVLsMxb3DEftWMblDwnwv+tMaaQWeXVav8HgSYyg/P40pfOgOtASYZAHZGDhkwfbYY8J1Br8Y71kpEzoImbeQwALV1LMsr1uHQjq2nekTjmOXbIFr68Ef44BzFSBuI="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12779,"candidatesTokenCount":24,"totalTokenCount":12867,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12779}],"thoughtsTokenCount":64}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Acknowledge the Block**\n\nI've hit a snag. The file creation was blocked, as you saw. Rather than persisting, I'm waiting on your next instructions. I'm ready to proceed, but need your input on how to adjust course.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12886,"totalTokenCount":12925,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12886}],"thoughtsTokenCount":39}},{"candidates":[{"content":{"parts":[{"text":"I am sorry, but I was unable to create the file as the request was blocked by the security policy.","thoughtSignature":"CikBcsjafKIxdnJr+6NHip/3cc9MiaqA+EfxMy6T2dArrEw3Ga3y69q7fgppAXLI2nxri+Z6NSOI3wvN+xmipbnuKNWfZKMWHN/amQSZcMqIoFNpTyt1t8B/MIGtIVpQ8CfQQAypanAIGa4G+tEhKxOXIdFNktSLB/Yrr0HQQAkD6t12s8S0KTdH5P4xmAII4gdwvSXzCnIBcsjafO06UBT/9dUO7OcS6UItneCNaFUEwAzcn5nGO0kUfu50lTPEwPTGNgrOc8FB303GEcIjZpTXc5X737oyAxqgRk7fY10vwBGOJk+PuiVIQeZQKTazZ7Q90W2d2MRTIAatQYi572oiw4m6vqLJg7o="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12886,"candidatesTokenCount":21,"totalTokenCount":12946,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12886}],"thoughtsTokenCount":39}}]}

View File

@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Defining File Parameters**\n\nI've decided on the filename: `error-test.txt`. I'll populate it with the text \"testing error handling\". The `write_file` tool seems ideal for this, given its clear functionality. I'm focusing on assigning the values of `file_path` as `error-test.txt` and `content` as \"testing error handling\" to initiate the tool.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12786,"totalTokenCount":12852,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12786}],"thoughtsTokenCount":66}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"file_path":"error-test.txt","content":"testing error handling"}},"thoughtSignature":"CiQBcsjafMeH+OLl4BHZIH0Hg2b339mDmV+8hSLTIZ8rBtABe/sKZwFyyNp8Gh1K04S9kcEId8vXyr9F9ium+5Hpc2KjkW6gfIcXRYrwYA9kvwQT9i7xz/0Dtr39FNkcqJil59sI1MrRKI+SfMtAOxo85PPV5Dd5oWaFEgufexxZIjJoJrxocUw0TMwU1SMKeAFyyNp8D36DcvOYdJEs4SbdRH/WP+abiCnPTKHuV1lFxuZXcyig/HEv2+uGN3XgdRu5kKLto0DbkaRRrjb5Z9w9MytOzQzg0ffZnvUyE1uyCJInBV+kSnosrNi81+WSlKnCPhQO67i7y3H0zPmoQSSIw2e1VadZdAprAXLI2nwchfIb/xiTeWb2cnNDPj98A31b/i80QyRXEnQp2DAlwvPSp/CLs+J82tzps+lFFcKXT3QRID+/Y7D3wTxxKiET3/dwobW4y9hrHP+DhzU5h1GC5fOcvximpOl9KUp98viPrOAaMqs="}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12786,"candidatesTokenCount":27,"totalTokenCount":12879,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12786}],"thoughtsTokenCount":66}},{"candidates":[{"content":{"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12786,"candidatesTokenCount":27,"totalTokenCount":12879,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12786}],"thoughtsTokenCount":66}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"OK."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12951,"candidatesTokenCount":2,"totalTokenCount":12953,"cachedContentTokenCount":12203,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12951}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12203}]}}]}

View File

@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Defining File Creation**\n\nI'm thinking about the user's intent to generate a file named \"input-test.txt\" with the content \" test\". I've determined that the `write_file` tool is suitable. I've parsed `file_path` as \"input-test.txt\" and `content` as \" test\". This should accomplish the user's need.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12778,"totalTokenCount":12840,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12778}],"thoughtsTokenCount":62}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"file_path":"input-test.txt","content":"test"}},"thoughtSignature":"CiQBcsjafCO/Ifs3Lj/Gtzy2ylSYoGB3GXjJby4F3R8FxWp+hP0KZwFyyNp8oD7KvcSYXDimGOiqAdxtdOJpc2tFJbHm2Jw7ahiuKLtoKZWE+1bBZEWVKxC0dCQIeIcxZ0SaLn7tDbfc2qPzhyUA46d/T1+e314SFLWW1asIOBkQ4T0sFDAFPZ4m9bFm3UkKbAFyyNp8EAnclI0wYCGwpg0AOOV52F5J9Hc2EeaXkGsc6hCnba7aNhPucWYIn2Da8FK2IJAWUWaNvGNGoNUZETaG+iL9+6KRJgN3Ql/wQzQ2pHUvTGHC3RkfMGTQ+YCQKvlOReilps5lDmMnhQpTAXLI2nzcl9Aqd0Nb/w934w+tqz1Jth7GlQVMYktHOl7Hgkoykfh3NzM67SEAilxjowfBL6MY7UBUP3YGwi1CXVVa4d0wHnMD9BJYp2w8ztZch8I="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12778,"candidatesTokenCount":25,"totalTokenCount":12865,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12778}],"thoughtsTokenCount":62}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**File Creation Achieved**\n\nI've successfully created the file as requested. Now, I'm ready to move on to the next instruction whenever it arrives. I am now awaiting the next task.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12940,"totalTokenCount":12965,"cachedContentTokenCount":12203,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12940}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12203}],"thoughtsTokenCount":25}},{"candidates":[{"content":{"parts":[{"text":"Done.","thoughtSignature":"CiQBcsjafEwfH5zTnAjEjloMcDDflS/MmoH03HXVl8HoQ04vmVIKcQFyyNp8/6HrBz8vokXB1Ms1zW51p32T3Ni3HEbgSFPHMGZt9LHFtLkLzuFrxym66z1Tcb5tqj+7jAdpM/dIUb6ecrKj9FWqMB+QR4BSxdAiJSiL8Rp+Pc5ckCtT1nrv4C5w3/fhCNE4WvZzeyGPt+PACjsBcsjafNWzUJcHxgKp6MYWQ8RW0QrGerM51nkgXHBafxY5KwTznX4B/ETccGnXX3zSciaJiZR1FfudVw=="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12940,"candidatesTokenCount":1,"totalTokenCount":12966,"cachedContentTokenCount":12203,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12940}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12203}],"thoughtsTokenCount":25}}]}

View File

@@ -0,0 +1,2 @@
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"**Initializing File Creation**\n\nI've decided on the `write_file` tool to create the telemetry file. I'll pass \"telemetry-test.txt\" as the file path, and an empty string for the content, as the user didn't specify anything to include. This is the initial setup; the file should now exist.\n\n\n","thought":true}],"role":"model"},"index":0}],"usageMetadata":{"promptTokenCount":12779,"totalTokenCount":12850,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12779}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}],"thoughtsTokenCount":71}},{"candidates":[{"content":{"parts":[{"functionCall":{"name":"write_file","args":{"content":"","file_path":"telemetry-test.txt"}},"thoughtSignature":"CiQBcsjafG+JDSqtKOK+ZvSjZQZmS91c1Gz0YyTiirI2u5+rhEIKZAFyyNp8DNXb+xHILTC+FVlEifqEHdrmfFNBLKojci1UIBhcZpQ4UXCMkxUXYKO34IjTlyLgSsjVbbXWEFXatb/z/RtTDcf51uc3YOEwlDScGempkJxfFgcPfIiD7bhuHBqdQfUKfAFyyNp8wZ71h+QjdfVw12PwDXWgGZ0Xed1GuyJXuqAwpWnwxDIvsDaPwDFYyLR1XDiIZZk4AvFCGt6HGMSLRuPh4K3i9CVnDc5hcjyvMIde0idAFMrgs2Mq5SARfCPrWkqyq2f0Q0WonUl2n7yr/sDQ78rx2E6qXyUJ8XMKfAFyyNp8DdTYLttyI0jknqAeZDxdFmHtpJUI8UKP5YHzpQc8Qn80OJcwhZSRH4HRKCqoC7Sukq/A5vJ5T468WqgjOoLlPLq02bYRTf/q6LC1ogEhdLHrcFv2jDeCdXJJ8NHv3O4DZAUAk1W5Gd0428zMFOxH3AgkWwEGuow="}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12779,"candidatesTokenCount":24,"totalTokenCount":12874,"cachedContentTokenCount":12204,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12779}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12204}],"thoughtsTokenCount":71}}]}
{"method":"generateContentStream","response":[{"candidates":[{"content":{"parts":[{"text":"OK."}],"role":"model"},"finishReason":"STOP","index":0}],"usageMetadata":{"promptTokenCount":12951,"candidatesTokenCount":2,"totalTokenCount":12953,"cachedContentTokenCount":12202,"promptTokensDetails":[{"modality":"TEXT","tokenCount":12951}],"cacheTokensDetails":[{"modality":"TEXT","tokenCount":12202}]}}]}

View File

@@ -0,0 +1,923 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from './test-helper.js';
import { join } from 'node:path';
import { writeFileSync } from 'node:fs';
describe('Hooks System Integration', () => {
let rig: TestRig;
beforeEach(() => {
rig = new TestRig();
});
afterEach(async () => {
if (rig) {
await rig.cleanup();
}
});
describe('Command Hooks - Blocking Behavior', () => {
it('should block tool execution when hook returns block decision', async () => {
await rig.setup(
'should block tool execution when hook returns block decision',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.block-tool.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
BeforeTool: [
{
matcher: 'write_file',
sequential: true,
hooks: [
{
type: 'command',
command:
'echo "{\\"decision\\": \\"block\\", \\"reason\\": \\"File writing blocked by security policy\\"}"',
timeout: 5000,
},
],
},
],
},
},
},
);
const prompt = 'Create a file called test.txt with content "Hello World"';
const result = await rig.run(prompt);
// The hook should block the write_file tool
const toolLogs = rig.readToolLogs();
const writeFileCalls = toolLogs.filter(
(t) =>
t.toolRequest.name === 'write_file' && t.toolRequest.success === true,
);
// Tool should not be called due to blocking hook
expect(writeFileCalls).toHaveLength(0);
// Result should mention the blocking reason
expect(result).toContain('File writing blocked by security policy');
// Should generate hook telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
});
it('should allow tool execution when hook returns allow decision', async () => {
await rig.setup(
'should allow tool execution when hook returns allow decision',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.allow-tool.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command:
'echo "{\\"decision\\": \\"allow\\", \\"reason\\": \\"File writing approved\\"}"',
timeout: 5000,
},
],
},
],
},
},
},
);
const prompt =
'Create a file called approved.txt with content "Approved content"';
await rig.run(prompt);
// The hook should allow the write_file tool
const foundWriteFile = await rig.waitForToolCall('write_file');
expect(foundWriteFile).toBeTruthy();
// File should be created
const fileContent = rig.readFile('approved.txt');
expect(fileContent).toContain('Approved content');
// Should generate hook telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
});
});
describe('Command Hooks - Additional Context', () => {
it('should add additional context from AfterTool hooks', async () => {
const command =
'echo "{\\"hookSpecificOutput\\": {\\"hookEventName\\": \\"AfterTool\\", \\"additionalContext\\": \\"Security scan: File content appears safe\\"}}"';
await rig.setup('should add additional context from AfterTool hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.after-tool-context.responses',
),
settings: {
tools: {
enableHooks: true,
},
hooks: {
AfterTool: [
{
matcher: 'read_file',
hooks: [
{
type: 'command',
command: command,
timeout: 5000,
},
],
},
],
},
},
});
// Create a test file to read
rig.createFile('test-file.txt', 'This is test content');
const prompt =
'Read the contents of test-file.txt and tell me what it contains';
await rig.run(prompt);
// Should find read_file tool call
const foundReadFile = await rig.waitForToolCall('read_file');
expect(foundReadFile).toBeTruthy();
// Should generate hook telemetry
const hookTelemetryFound = rig.readHookLogs();
expect(hookTelemetryFound.length).toBeGreaterThan(0);
expect(hookTelemetryFound[0].hookCall.hook_event_name).toBe('AfterTool');
expect(hookTelemetryFound[0].hookCall.hook_name).toBe(command);
expect(hookTelemetryFound[0].hookCall.hook_input).toBeDefined();
expect(hookTelemetryFound[0].hookCall.hook_output).toBeDefined();
expect(hookTelemetryFound[0].hookCall.exit_code).toBe(0);
expect(hookTelemetryFound[0].hookCall.stdout).toBeDefined();
expect(hookTelemetryFound[0].hookCall.stderr).toBeDefined();
});
});
describe('BeforeModel Hooks - LLM Request Modification', () => {
it('should modify LLM requests with BeforeModel hooks', async () => {
// Create a hook script that replaces the LLM request with a modified version
// Note: Providing messages in the hook output REPLACES the entire conversation
await rig.setup('should modify LLM requests with BeforeModel hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.before-model.responses',
),
});
const hookScript = `#!/bin/bash
echo '{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "BeforeModel",
"llm_request": {
"messages": [
{
"role": "user",
"content": "Please respond with exactly: The security hook modified this request successfully."
}
]
}
}
}'`;
const scriptPath = join(rig.testDir!, 'before_model_hook.sh');
writeFileSync(scriptPath, hookScript);
// Make executable
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should modify LLM requests with BeforeModel hooks', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
BeforeModel: [
{
hooks: [
{
type: 'command',
command: scriptPath,
timeout: 5000,
},
],
},
],
},
},
});
const prompt = 'Tell me a story';
const result = await rig.run(prompt);
// The hook should have replaced the request entirely
// Verify that the model responded to the modified request, not the original
expect(result).toBeDefined();
expect(result.length).toBeGreaterThan(0);
// The response should contain the expected text from the modified request
expect(result.toLowerCase()).toContain('security hook modified');
// Should generate hook telemetry
// Should generate hook telemetry
const hookTelemetryFound = rig.readHookLogs();
expect(hookTelemetryFound.length).toBeGreaterThan(0);
expect(hookTelemetryFound[0].hookCall.hook_event_name).toBe(
'BeforeModel',
);
expect(hookTelemetryFound[0].hookCall.hook_name).toBe(scriptPath);
expect(hookTelemetryFound[0].hookCall.hook_input).toBeDefined();
expect(hookTelemetryFound[0].hookCall.hook_output).toBeDefined();
expect(hookTelemetryFound[0].hookCall.exit_code).toBe(0);
expect(hookTelemetryFound[0].hookCall.stdout).toBeDefined();
expect(hookTelemetryFound[0].hookCall.stderr).toBeDefined();
});
});
describe('AfterModel Hooks - LLM Response Modification', () => {
it('should modify LLM responses with AfterModel hooks', async () => {
await rig.setup('should modify LLM responses with AfterModel hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.after-model.responses',
),
});
// Create a hook script that modifies the LLM response
const hookScript = `#!/bin/bash
echo '{
"hookSpecificOutput": {
"hookEventName": "AfterModel",
"llm_response": {
"candidates": [
{
"content": {
"role": "model",
"parts": [
"[FILTERED] Response has been filtered for security compliance."
]
},
"finishReason": "STOP"
}
]
}
}
}'`;
const scriptPath = join(rig.testDir!, 'after_model_hook.sh');
writeFileSync(scriptPath, hookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should modify LLM responses with AfterModel hooks', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
AfterModel: [
{
hooks: [
{
type: 'command',
command: scriptPath,
timeout: 5000,
},
],
},
],
},
},
});
const prompt = 'What is 2 + 2?';
const result = await rig.run(prompt);
// The hook should have replaced the model response
expect(result).toContain(
'[FILTERED] Response has been filtered for security compliance',
);
// Should generate hook telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
});
});
describe('BeforeToolSelection Hooks - Tool Configuration', () => {
it('should modify tool selection with BeforeToolSelection hooks', async () => {
await rig.setup(
'should modify tool selection with BeforeToolSelection hooks',
{
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.before-tool-selection.responses',
),
},
);
// Create a hook script that restricts available tools
const hookScript = `#!/bin/bash
echo '{
"hookSpecificOutput": {
"hookEventName": "BeforeToolSelection",
"toolConfig": {
"mode": "ANY",
"allowedFunctionNames": ["read_file", "run_shell_command"]
}
}
}'`;
const scriptPath = join(rig.testDir!, 'before_tool_selection_hook.sh');
writeFileSync(scriptPath, hookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup(
'should modify tool selection with BeforeToolSelection hooks',
{
settings: {
debugMode: true,
tools: {
enableHooks: true,
},
hooks: {
BeforeToolSelection: [
{
hooks: [
{
type: 'command',
command: scriptPath,
timeout: 5000,
},
],
},
],
},
},
},
);
// Create a test file
rig.createFile('new_file_data.txt', 'test data');
const prompt =
'Check the content of new_file_data.txt, after that run echo command to see the content';
await rig.run(prompt);
// Should use read_file (allowed) but not run_shell_command (not in allowed list)
const foundReadFile = await rig.waitForToolCall('read_file');
expect(foundReadFile).toBeTruthy();
// Should generate hook telemetry indicating the hook was called
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
// Verify the hook was called for BeforeToolSelection event
const hookLogs = rig.readHookLogs();
const beforeToolSelectionHook = hookLogs.find(
(log) => log.hookCall.hook_event_name === 'BeforeToolSelection',
);
expect(beforeToolSelectionHook).toBeDefined();
expect(beforeToolSelectionHook?.hookCall.success).toBe(true);
});
});
describe('BeforeAgent Hooks - Prompt Augmentation', () => {
it('should augment prompts with BeforeAgent hooks', async () => {
await rig.setup('should augment prompts with BeforeAgent hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.before-agent.responses',
),
});
// Create a hook script that adds context to the prompt
const hookScript = `#!/bin/bash
echo '{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "BeforeAgent",
"additionalContext": "SYSTEM INSTRUCTION: You are in a secure environment. Always mention security compliance in your responses."
}
}'`;
const scriptPath = join(rig.testDir!, 'before_agent_hook.sh');
writeFileSync(scriptPath, hookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should augment prompts with BeforeAgent hooks', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
BeforeAgent: [
{
hooks: [
{
type: 'command',
command: scriptPath,
timeout: 5000,
},
],
},
],
},
},
});
const prompt = 'Hello, how are you?';
const result = await rig.run(prompt);
// The hook should have added security context, which should influence the response
expect(result).toContain('security');
// Should generate hook telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
});
});
describe.skip('Notification Hooks - Permission Handling', () => {
it('should handle notification hooks for tool permissions', async () => {
await rig.setup('should handle notification hooks for tool permissions');
// Create a hook script that logs notification events
const hookScript = `#!/bin/bash
echo '{
"suppressOutput": false,
"systemMessage": "Permission request logged by security hook"
}'`;
const scriptPath = join(rig.testDir!, 'notification_hook.sh');
writeFileSync(scriptPath, hookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should handle notification hooks for tool permissions', {
settings: {
// Configure tools to enable hooks and require confirmation to trigger notifications
tools: {
enableHooks: true,
confirmationRequired: ['run_shell_command'],
},
hooks: {
Notification: [
{
matcher: 'ToolPermission',
hooks: [
{
type: 'command',
command: scriptPath,
timeout: 5000,
},
],
},
],
},
},
});
const prompt =
'Run the command "echo test" (this should trigger a permission prompt)';
// Use stdin to automatically approve the permission
await rig.run({
prompt,
stdin: 'y\n', // Approve the permission
});
// Should find the shell command execution
const foundShellCommand = await rig.waitForToolCall('run_shell_command');
expect(foundShellCommand).toBeTruthy();
// Should generate hook telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
});
});
describe('Sequential Hook Execution', () => {
// Note: This test checks telemetry for hook context in API requests,
// which behaves differently with mocked responses. Keeping real LLM calls.
it('should execute hooks sequentially when configured', async () => {
await rig.setup('should execute hooks sequentially when configured');
// Create two hooks that modify the input sequentially
const hook1Script = `#!/bin/bash
echo '{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "BeforeAgent",
"additionalContext": "Step 1: Initial validation passed."
}
}'`;
const hook2Script = `#!/bin/bash
echo '{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "BeforeAgent",
"additionalContext": "Step 2: Security check completed."
}
}'`;
const script1Path = join(rig.testDir!, 'sequential_hook1.sh');
const script2Path = join(rig.testDir!, 'sequential_hook2.sh');
writeFileSync(script1Path, hook1Script);
writeFileSync(script2Path, hook2Script);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${script1Path}"`);
execSync(`chmod +x "${script2Path}"`);
await rig.setup('should execute hooks sequentially when configured', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
BeforeAgent: [
{
sequential: true,
hooks: [
{
type: 'command',
command: script1Path,
timeout: 5000,
},
{
type: 'command',
command: script2Path,
timeout: 5000,
},
],
},
],
},
},
});
const prompt = 'Hello, please help me with a task';
await rig.run(prompt);
// Should generate hook telemetry
let hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
hookTelemetryFound = await rig.waitForTelemetryEvent('api_request');
const apiRequests = rig.readAllApiRequest();
const apiRequestsTexts = apiRequests
?.filter(
(request) =>
'attributes' in request &&
typeof request['attributes'] === 'object' &&
request['attributes'] !== null &&
'request_text' in request['attributes'] &&
typeof request['attributes']['request_text'] === 'string',
)
.map((request) => request['attributes']['request_text']);
expect(apiRequestsTexts).toBeDefined();
let hasBeforeAgentHookContext = false;
let hasAfterToolHookContext = false;
for (const requestText of apiRequestsTexts) {
if (requestText.includes('Step 1: Initial validation passed')) {
hasBeforeAgentHookContext = true;
}
if (requestText.includes('Step 2: Security check completed')) {
hasAfterToolHookContext = true;
}
}
expect(hasBeforeAgentHookContext).toBeTruthy();
expect(hasAfterToolHookContext).toBeTruthy();
});
});
describe('Hook Input/Output Validation', () => {
it('should provide correct input format to hooks', async () => {
await rig.setup('should provide correct input format to hooks', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.input-validation.responses',
),
});
// Create a hook script that validates the input format
const hookScript = `#!/bin/bash
# Read JSON input from stdin
input=$(cat)
# Check for required fields
if echo "$input" | jq -e '.session_id and .cwd and .hook_event_name and .timestamp and .tool_name and .tool_input' > /dev/null 2>&1; then
echo '{"decision": "allow", "reason": "Input format is correct"}'
exit 0
else
echo '{"decision": "block", "reason": "Input format is invalid"}'
exit 0
fi`;
const scriptPath = join(rig.testDir!, 'input_validation_hook.sh');
writeFileSync(scriptPath, hookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should provide correct input format to hooks', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
BeforeTool: [
{
hooks: [
{
type: 'command',
command: scriptPath,
timeout: 5000,
},
],
},
],
},
},
});
const prompt = 'Create a file called input-test.txt with content "test"';
await rig.run(prompt);
// Hook should validate input format successfully
const foundWriteFile = await rig.waitForToolCall('write_file');
expect(foundWriteFile).toBeTruthy();
// Check that the file was created (hook allowed it)
const fileContent = rig.readFile('input-test.txt');
expect(fileContent).toContain('test');
// Should generate hook telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
});
});
describe('Multiple Event Types', () => {
// Note: This test checks telemetry for hook context in API requests,
// which behaves differently with mocked responses. Keeping real LLM calls.
it('should handle hooks for all major event types', async () => {
await rig.setup('should handle hooks for all major event types');
// Create hook scripts for different events
const beforeToolScript = `#!/bin/bash
echo '{"decision": "allow", "systemMessage": "BeforeTool: File operation logged"}'`;
const afterToolScript = `#!/bin/bash
echo '{"hookSpecificOutput": {"hookEventName": "AfterTool", "additionalContext": "AfterTool: Operation completed successfully"}}'`;
const beforeAgentScript = `#!/bin/bash
echo '{"decision": "allow", "hookSpecificOutput": {"hookEventName": "BeforeAgent", "additionalContext": "BeforeAgent: User request processed"}}'`;
const beforeToolPath = join(rig.testDir!, 'before_tool.sh');
const afterToolPath = join(rig.testDir!, 'after_tool.sh');
const beforeAgentPath = join(rig.testDir!, 'before_agent.sh');
writeFileSync(beforeToolPath, beforeToolScript);
writeFileSync(afterToolPath, afterToolScript);
writeFileSync(beforeAgentPath, beforeAgentScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${beforeToolPath}"`);
execSync(`chmod +x "${afterToolPath}"`);
execSync(`chmod +x "${beforeAgentPath}"`);
await rig.setup('should handle hooks for all major event types', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
BeforeAgent: [
{
hooks: [
{
type: 'command',
command: beforeAgentPath,
timeout: 5000,
},
],
},
],
BeforeTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: beforeToolPath,
timeout: 5000,
},
],
},
],
AfterTool: [
{
matcher: 'write_file',
hooks: [
{
type: 'command',
command: afterToolPath,
timeout: 5000,
},
],
},
],
},
},
});
const prompt =
'Create a file called multi-event-test.txt with content ' +
'"testing multiple events", and then please reply with ' +
'everything I say just after this:"';
const result = await rig.run(prompt);
// Should execute write_file tool
const foundWriteFile = await rig.waitForToolCall('write_file');
expect(foundWriteFile).toBeTruthy();
// File should be created
const fileContent = rig.readFile('multi-event-test.txt');
expect(fileContent).toContain('testing multiple events');
// Result should contain context from all hooks
expect(result).toContain('BeforeTool: File operation logged');
// Should generate hook telemetry
let hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
hookTelemetryFound = await rig.waitForTelemetryEvent('api_request');
const apiRequests = rig.readAllApiRequest();
const apiRequestsTexts = apiRequests
?.filter(
(request) =>
'attributes' in request &&
typeof request['attributes'] === 'object' &&
request['attributes'] !== null &&
'request_text' in request['attributes'] &&
typeof request['attributes']['request_text'] === 'string',
)
.map((request) => request['attributes']['request_text']);
expect(apiRequestsTexts).toBeDefined();
let hasBeforeAgentHookContext = false;
let hasAfterToolHookContext = false;
for (const requestText of apiRequestsTexts) {
if (requestText.includes('BeforeAgent: User request processed')) {
hasBeforeAgentHookContext = true;
}
if (
requestText.includes('AfterTool: Operation completed successfully')
) {
hasAfterToolHookContext = true;
}
}
expect(hasBeforeAgentHookContext).toBeTruthy();
expect(hasAfterToolHookContext).toBeTruthy();
});
});
describe('Hook Error Handling', () => {
it('should handle hook failures gracefully', async () => {
await rig.setup('should handle hook failures gracefully', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.error-handling.responses',
),
});
// Create a hook script that fails
const failingHookScript = `#!/bin/bash
echo "Hook encountered an error" >&2
exit 1`;
const workingHookScript = `#!/bin/bash
echo '{"decision": "allow", "reason": "Working hook succeeded"}'`;
const failingPath = join(rig.testDir!, 'failing_hook.sh');
const workingPath = join(rig.testDir!, 'working_hook.sh');
writeFileSync(failingPath, failingHookScript);
writeFileSync(workingPath, workingHookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${failingPath}"`);
execSync(`chmod +x "${workingPath}"`);
await rig.setup('should handle hook failures gracefully', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
BeforeTool: [
{
hooks: [
{
type: 'command',
command: failingPath,
timeout: 5000,
},
{
type: 'command',
command: workingPath,
timeout: 5000,
},
],
},
],
},
},
});
const prompt =
'Create a file called error-test.txt with content "testing error handling"';
await rig.run(prompt);
// Despite one hook failing, the working hook should still allow the operation
const foundWriteFile = await rig.waitForToolCall('write_file');
expect(foundWriteFile).toBeTruthy();
// File should be created
const fileContent = rig.readFile('error-test.txt');
expect(fileContent).toContain('testing error handling');
// Should generate hook telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
});
});
describe('Hook Telemetry and Observability', () => {
it('should generate telemetry events for hook executions', async () => {
await rig.setup('should generate telemetry events for hook executions', {
fakeResponsesPath: join(
import.meta.dirname,
'hooks-system.telemetry.responses',
),
});
const hookScript = `#!/bin/bash
echo '{"decision": "allow", "reason": "Telemetry test hook"}'`;
const scriptPath = join(rig.testDir!, 'telemetry_hook.sh');
writeFileSync(scriptPath, hookScript);
const { execSync } = await import('node:child_process');
execSync(`chmod +x "${scriptPath}"`);
await rig.setup('should generate telemetry events for hook executions', {
settings: {
tools: {
enableHooks: true,
},
hooks: {
BeforeTool: [
{
hooks: [
{
type: 'command',
command: scriptPath,
timeout: 5000,
},
],
},
],
},
},
});
const prompt = 'Create a file called telemetry-test.txt';
await rig.run(prompt);
// Should execute the tool
const foundWriteFile = await rig.waitForToolCall('write_file');
expect(foundWriteFile).toBeTruthy();
// Should generate hook telemetry
const hookTelemetryFound = await rig.waitForTelemetryEvent('hook_call');
expect(hookTelemetryFound).toBeTruthy();
});
});
});

View File

@@ -952,6 +952,16 @@ export class TestRig {
return logs; return logs;
} }
readAllApiRequest(): ParsedLog[] {
const logs = this._readAndParseTelemetryLog();
const apiRequests = logs.filter(
(logData) =>
logData.attributes &&
logData.attributes['event.name'] === 'gemini_cli.api_request',
);
return apiRequests;
}
readLastApiRequest(): ParsedLog | null { readLastApiRequest(): ParsedLog | null {
const logs = this._readAndParseTelemetryLog(); const logs = this._readAndParseTelemetryLog();
const apiRequests = logs.filter( const apiRequests = logs.filter(
@@ -1028,4 +1038,47 @@ export class TestRig {
await run.expectText(' Type your message or @path/to/file', 30000); await run.expectText(' Type your message or @path/to/file', 30000);
return run; return run;
} }
readHookLogs() {
const parsedLogs = this._readAndParseTelemetryLog();
const logs: {
hookCall: {
hook_event_name: string;
hook_name: string;
hook_input: Record<string, unknown>;
hook_output: Record<string, unknown>;
exit_code: number;
stdout: string;
stderr: string;
duration_ms: number;
success: boolean;
error: string;
};
}[] = [];
for (const logData of parsedLogs) {
// Look for tool call logs
if (
logData.attributes &&
logData.attributes['event.name'] === 'gemini_cli.hook_call'
) {
logs.push({
hookCall: {
hook_event_name: logData.attributes.hook_event_name ?? '',
hook_name: logData.attributes.hook_name ?? '',
hook_input: logData.attributes.hook_input ?? {},
hook_output: logData.attributes.hook_output ?? {},
exit_code: logData.attributes.exit_code ?? 0,
stdout: logData.attributes.stdout ?? '',
stderr: logData.attributes.stderr ?? '',
duration_ms: logData.attributes.duration_ms ?? 0,
success: logData.attributes.success ?? false,
error: logData.attributes.error ?? '',
},
});
}
}
return logs;
}
} }