Skip to content

Commit 4a0924b

Browse files
committed
test: retry transient external-service failures in snippet runners
The generative/hybrid search snippet tests and the Claude llms.txt fetch test intermittently red the suite on transient blips (gRPC UNAVAILABLE, deadline exceeded, read timeouts, 429/5xx) against the shared test cluster + LLM APIs. Add a narrow transient-error retry (utils.retry_on_transient) around the Python, TypeScript, and C# snippet runners, and retry the Claude fetch up to 3x. Real failures (assertion/logic/syntax errors) still fail immediately — only messages matching a small transient-marker set are retried.
1 parent a8f68e5 commit 4a0924b

5 files changed

Lines changed: 106 additions & 41 deletions

File tree

tests/test_csharp.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import pytest
33
import os
44

5+
import utils
6+
57

68
CSHARP_CSPROJ = "_includes/code/csharp/WeaviateProject.Tests.csproj"
79

@@ -14,9 +16,19 @@ def run_csharp_test(test_class, empty_weaviates):
1416
]
1517
env = dict(os.environ)
1618

19+
def _run():
20+
result = subprocess.run(command, env=env, capture_output=True, text=True)
21+
if result.stdout.strip():
22+
print(result.stdout)
23+
if result.returncode != 0:
24+
raise Exception(
25+
f"C# {test_class} failed (exit {result.returncode})\n"
26+
f"--- STDERR ---\n{result.stderr}\n--- STDOUT ---\n{result.stdout}"
27+
)
28+
1729
try:
18-
subprocess.check_call(command, env=env)
19-
except subprocess.CalledProcessError as error:
30+
utils.retry_on_transient(_run, label=test_class)
31+
except Exception as error:
2032
pytest.fail(f"C# {test_class} failed with error: {error}")
2133

2234

tests/test_docs_indexability.py

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -453,43 +453,53 @@ def test_claude_can_fetch_llms_txt():
453453
# The llms.txt file starts with "# Weaviate Documentation" and contains
454454
# section headings like "## agents", "## cloud", "## weaviate".
455455
# Ask Claude to quote specific content to prove it fetched the real file.
456-
response = client.messages.create(
457-
model="claude-haiku-4-5-20251001",
458-
max_tokens=2048,
459-
tools=[{
460-
"type": "web_fetch_20250910",
461-
"name": "web_fetch",
462-
"max_uses": 1,
463-
"allowed_domains": ["docs.weaviate.io", "weaviate.io"],
464-
}],
465-
messages=[{
466-
"role": "user",
467-
"content": (
468-
f"Fetch {url} and tell me: "
469-
"1) What is the first heading line of the file (copy it verbatim)? "
470-
"2) List ALL the top-level section headings (lines starting with '## '). "
471-
"3) Does it mention code examples in multiple languages? Which ones?"
472-
),
473-
}],
474-
)
456+
# LLM responses are non-deterministic, so retry a few times: pass as soon
457+
# as one attempt satisfies ALL conditions; only fail if every attempt does.
458+
required_sections = ["agents", "cloud", "weaviate"]
459+
last_text = ""
460+
for attempt in range(3):
461+
response = client.messages.create(
462+
model="claude-haiku-4-5-20251001",
463+
max_tokens=2048,
464+
tools=[{
465+
"type": "web_fetch_20250910",
466+
"name": "web_fetch",
467+
"max_uses": 1,
468+
"allowed_domains": ["docs.weaviate.io", "weaviate.io"],
469+
}],
470+
messages=[{
471+
"role": "user",
472+
"content": (
473+
f"Fetch {url} and tell me: "
474+
"1) What is the first heading line of the file (copy it verbatim)? "
475+
"2) List ALL the top-level section headings (lines starting with '## '). "
476+
"3) Does it mention code examples in multiple languages? Which ones?"
477+
),
478+
}],
479+
)
480+
last_text = _extract_text_from_response(response)
481+
tl = last_text.lower()
482+
if "weaviate" in tl and all(s in tl for s in required_sections) and "python" in tl:
483+
return
484+
time.sleep(5)
475485

476-
text = _extract_text_from_response(response)
477-
text_lower = text.lower()
486+
# All attempts fell short — surface the last response via the existing assertions
487+
tl = last_text.lower()
478488

479489
# Must identify Weaviate
480-
assert "weaviate" in text_lower, (
481-
f"Claude couldn't identify Weaviate in llms.txt. Response: {text[:500]}"
490+
assert "weaviate" in tl, (
491+
f"Claude couldn't identify Weaviate in llms.txt. Response: {last_text[:500]}"
482492
)
483493

484494
# Must find the key top-level sections from llms.txt
485-
for section in ["agents", "cloud", "weaviate"]:
486-
assert section in text_lower, (
487-
f"Claude didn't find '{section}' section in llms.txt. Response: {text[:1000]}"
495+
for section in required_sections:
496+
assert section in tl, (
497+
f"Claude didn't find '{section}' section in llms.txt. Response: {last_text[:1000]}"
488498
)
489499

490500
# Must identify multi-language code examples
491-
assert "python" in text_lower, (
492-
f"Claude didn't find Python mentioned in llms.txt. Response: {text[:500]}"
501+
assert "python" in tl, (
502+
f"Claude didn't find Python mentioned in llms.txt. Response: {last_text[:500]}"
493503
)
494504

495505

tests/test_python.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44

55

66
def run_py_script(script_loc, custom_replace_pairs=None):
7-
if custom_replace_pairs:
8-
temp_proc_script_loc = utils.load_and_prep_temp_file(
9-
script_loc, lang="py", custom_replace_pairs=custom_replace_pairs
10-
)
11-
utils.execute_py_script_as_module(
12-
temp_proc_script_loc.read_text(), Path(script_loc).stem
13-
)
14-
else:
15-
proc_script = utils.load_and_prep_script(script_loc)
16-
utils.execute_py_script_as_module(proc_script, Path(script_loc).stem)
7+
def _exec():
8+
if custom_replace_pairs:
9+
temp_proc_script_loc = utils.load_and_prep_temp_file(
10+
script_loc, lang="py", custom_replace_pairs=custom_replace_pairs
11+
)
12+
utils.execute_py_script_as_module(
13+
temp_proc_script_loc.read_text(), Path(script_loc).stem
14+
)
15+
else:
16+
proc_script = utils.load_and_prep_script(script_loc)
17+
utils.execute_py_script_as_module(proc_script, Path(script_loc).stem)
18+
19+
utils.retry_on_transient(_exec, label=str(script_loc))
1720

1821

1922
def run_pyv3_script(script_loc):

tests/test_typescript.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ def run_ts_script(script_loc, custom_replace_pairs=None):
99
command = ["npx", "tsx", temp_proc_script_loc]
1010

1111
try:
12-
utils.run_script(command, script_loc)
12+
utils.retry_on_transient(
13+
lambda: utils.run_script(command, script_loc), label=str(script_loc)
14+
)
1315
except Exception as e:
1416
pytest.fail(str(e))
1517

tests/utils.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,50 @@
22
import re
33
import subprocess
44
import tempfile
5+
import time
56
import runpy
67
from pathlib import Path
78
from dotenv import load_dotenv
89

910
load_dotenv()
1011

12+
13+
# Substrings (case-insensitive) that indicate a transient external-service
14+
# failure worth retrying (gRPC blips, timeouts, rate limits, 5xx) rather than a
15+
# real snippet/assertion bug. Kept deliberately narrow.
16+
TRANSIENT_MARKERS = (
17+
"unavailable", "deadline exceeded", "deadline_exceeded",
18+
"timed out", "timeout", "read operation timed out",
19+
"connection reset", "econnreset", "connection aborted",
20+
"502", "503", "504", "429", "too many requests",
21+
"rate limit", "overloaded", "temporarily unavailable",
22+
)
23+
24+
25+
def is_transient_error(err) -> bool:
26+
"""True if the error message looks like a transient external-service blip."""
27+
msg = str(err).lower()
28+
return any(marker in msg for marker in TRANSIENT_MARKERS)
29+
30+
31+
def retry_on_transient(fn, *, retries: int = 2, delay: int = 8, label: str = ""):
32+
"""Call fn(); on a TRANSIENT error, wait and retry up to `retries` times.
33+
Non-transient errors (assertion/logic/syntax) re-raise immediately."""
34+
for attempt in range(retries + 1):
35+
try:
36+
return fn()
37+
except Exception as err:
38+
if attempt < retries and is_transient_error(err):
39+
print(
40+
f"[retry] transient failure on {label} "
41+
f"(attempt {attempt + 1}/{retries + 1}), retrying in {delay}s: "
42+
f"{str(err)[:300]}"
43+
)
44+
time.sleep(delay)
45+
continue
46+
raise
47+
48+
1149
def load_script(script_path: str) -> str:
1250
with open(script_path, "r") as f:
1351
code_block = f.read()

0 commit comments

Comments
 (0)