mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
test(mcp): add rapid-init integration test for poll/getline buffering fix
This commit is contained in:
1 parent
b8b2dd356b
commit
16552befdd
2 files changed
+155
No files matched your search
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Integration test for the poll/getline FILE* buffering fix.
|
||||
|
||||
Spawns the MCP server binary, sends initialize + notifications/initialized +
|
||||
tools/list all at once (no delays), and asserts that the tools/list response
|
||||
arrives within 5 seconds.
|
||||
|
||||
Usage:
|
||||
python3 scripts/test_mcp_rapid_init.py [/path/to/binary]
|
||||
|
||||
Exit codes:
|
||||
0 - PASS
|
||||
1 - FAIL
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
TIMEOUT_S = 5
|
||||
|
||||
MESSAGES = (
|
||||
b'{"jsonrpc":"2.0","id":1,"method":"initialize",'
|
||||
b'"params":{"protocolVersion":"2025-11-25","capabilities":{}}}\n'
|
||||
b'{"jsonrpc":"2.0","method":"notifications/initialized"}\n'
|
||||
b'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}\n'
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) >= 2:
|
||||
binary = sys.argv[1]
|
||||
else:
|
||||
# Default: look for build artifact relative to this script's directory
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
repo_root = os.path.dirname(script_dir)
|
||||
binary = os.path.join(repo_root, "build", "c", "codebase-memory-mcp")
|
||||
|
||||
if not os.path.isfile(binary):
|
||||
print(f"FAIL: binary not found at {binary}")
|
||||
sys.exit(1)
|
||||
|
||||
if not os.access(binary, os.X_OK):
|
||||
print(f"FAIL: binary not executable: {binary}")
|
||||
sys.exit(1)
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[binary],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
try:
|
||||
# Write all 3 messages in one call and close stdin to signal EOF
|
||||
stdout_data, _ = proc.communicate(input=MESSAGES, timeout=TIMEOUT_S)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
print(
|
||||
f"FAIL: server did not respond within {TIMEOUT_S}s "
|
||||
f"(poll/getline buffering bug not fixed)"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
output = stdout_data.decode("utf-8", errors="replace")
|
||||
|
||||
if "tools" not in output:
|
||||
print("FAIL: tools/list response not found in server output")
|
||||
print(f"Server output was:\n{output!r}")
|
||||
sys.exit(1)
|
||||
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1215,6 +1215,78 @@ TEST(snippet_include_neighbors_enabled) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* POLL/GETLINE FILE* BUFFERING FIX
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
|
||||
/* Signal handler used by alarm() to abort the test if it hangs */
|
||||
static void alarm_handler(int sig) {
|
||||
(void)sig;
|
||||
/* Writing to stderr is async-signal-safe */
|
||||
const char msg[] = "FAIL: mcp_server_run_rapid_messages timed out (>5s)\n";
|
||||
write(STDERR_FILENO, msg, sizeof(msg) - 1);
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
TEST(mcp_server_run_rapid_messages) {
|
||||
/* Simulate a client sending initialize + notifications/initialized +
|
||||
* tools/list all at once (no delays), which exercises the FILE*
|
||||
* buffering fix: the first getline() over-reads kernel data into the
|
||||
* libc buffer; without the fix, subsequent poll() calls block for 60s.
|
||||
*
|
||||
* We use alarm(5) to abort the test process if the server hangs. */
|
||||
int fds[2];
|
||||
ASSERT_EQ(pipe(fds), 0);
|
||||
|
||||
/* Write all 3 messages to the write end in one shot */
|
||||
const char *msgs =
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\","
|
||||
"\"params\":{\"protocolVersion\":\"2025-11-25\",\"capabilities\":{}}}\n"
|
||||
"{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}\n"
|
||||
"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\",\"params\":{}}\n";
|
||||
ssize_t written = write(fds[1], msgs, strlen(msgs));
|
||||
ASSERT_TRUE(written > 0);
|
||||
close(fds[1]); /* EOF signals end of input to the server */
|
||||
|
||||
FILE *in_fp = fdopen(fds[0], "r");
|
||||
ASSERT_NOT_NULL(in_fp);
|
||||
|
||||
FILE *out_fp = tmpfile();
|
||||
ASSERT_NOT_NULL(out_fp);
|
||||
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
|
||||
/* Install alarm to fail the test if cbm_mcp_server_run blocks */
|
||||
signal(SIGALRM, alarm_handler);
|
||||
alarm(5);
|
||||
|
||||
int rc = cbm_mcp_server_run(srv, in_fp, out_fp);
|
||||
|
||||
alarm(0); /* cancel alarm */
|
||||
signal(SIGALRM, SIG_DFL);
|
||||
|
||||
ASSERT_EQ(rc, 0);
|
||||
|
||||
/* Verify that tools/list response is present in output */
|
||||
rewind(out_fp);
|
||||
char buf[4096] = {0};
|
||||
size_t nread = fread(buf, 1, sizeof(buf) - 1, out_fp);
|
||||
ASSERT_TRUE(nread > 0);
|
||||
ASSERT_NOT_NULL(strstr(buf, "tools"));
|
||||
|
||||
cbm_mcp_server_free(srv);
|
||||
fclose(out_fp);
|
||||
/* in_fp already EOF; fclose cleans up */
|
||||
fclose(in_fp);
|
||||
PASS();
|
||||
}
|
||||
#endif /* !_WIN32 */
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* SUITE
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
@@ -1287,6 +1359,11 @@ SUITE(mcp) {
|
||||
RUN_TEST(parse_file_uri_windows);
|
||||
RUN_TEST(parse_file_uri_invalid);
|
||||
|
||||
/* Poll/getline FILE* buffering fix */
|
||||
#ifndef _WIN32
|
||||
RUN_TEST(mcp_server_run_rapid_messages);
|
||||
#endif
|
||||
|
||||
/* Snippet resolution (port of snippet_test.go) */
|
||||
RUN_TEST(snippet_exact_qn);
|
||||
RUN_TEST(snippet_qn_suffix);
|
||||
|
||||
Reference in new issue
Block a user