Skip to content
academia.sh

Lesson 18 / 22

Testing Each Tool Separately from the Loop

Counting how many of eleven fictional defects placed in five tools the tool-level test catches: the eleven-case test suite fails on all four value defects and all three contract defects, but passes clean on all four definition defects. The overall catch rate is 0.6364, and by class 1.0000 and 1.0000 against 0.0000. The mechanism of the unseen class is written down: the test suite calls the tool's function directly, while the loop goes through the field list the tool declares, and a definition defect lives only in that list. The payoff of the layer is independence from the run: on the same task set the task the loop fails shifts from G23 to G15 when the seed changes, while the set of failing cases of the test suite is empty and identical in both runs.

Published Updated

In this lesson

  • Builds tool-level testing as a layer separate from the loop and tests each tool by calling its function directly.
  • Divides defects into value, contract, and definition classes and computes the share of defects caught in each class.
  • Explains through the call path why tool-level testing cannot see definition defects at all.
  • Compares the independence of the test suite from the run with the run-to-run variability of the loop measure.
Contents

The previous lesson split an agent run into two columns: the correctness of the sequence and the correctness of the answer. Both assume the tools work correctly. The moment that assumption is lifted, the four-state table falls short: it says a task failed, it does not say which tool is broken.

This lesson pulls the tools out of the loop and tests them one by one. The test levels were classified in the Quality and Testing Fundamentals course of the Software Quality and Testing curriculum, and the anatomy of the unit test was built in the Unit Testing and Test-Driven Development course; the procedure is not repeated here. The question belongs to measurement: how many of the defects placed in five tools show up in this layer, and which class this layer cannot see at all.

  • AQ7. The defects are fictional and are placed in a copy of the tool table; the original table does not change.
  • AQ8. There are three defect classes. Value defect: the function gives a wrong result. Contract defect: the function does not report an error where it should. Definition defect: the function is correct, the declared field list is wrong.
  • AQ9. Tool-level test tests a tool outside the loop, by calling its function directly; the expected output is written by hand.
  • AQ10. A defect counts as caught if it fails at least one case of the test suite.
  • AQ11. The test suite uses no seed; the loop measure is run with an eight-step budget and two seeds.

Environment, Tool Definitions, and the Agent Loop

A tool consists of two parts: the function does the computation, the definition declares which fields that function will take, and the loop builds the call only from the declared fields.

# FICTIONAL. Subscriber records, document set, tool set, task set and agent
# loop are generated within the lesson; no real endpoint or tool server is called.
SEED, M32 = 20260218, 0xFFFFFFFF


def rng(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def next_value():
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return next_value


def pick(u, w):
    t = 0.0
    for i, x in enumerate(w):
        t += x
        if u < t:
            return i
    return len(w) - 1


def tokenize(m):
    b = []
    for s in str(m).lower().replace(",", " , ").split():
        while len(s) > 5:
            b.append(s[:5])
            s = s[5:]
        b.append(s)
    return b


def trace_id(m):
    h = 2166136261
    for c in str(m):
        h = ((h ^ ord(c)) * 16777619) & M32
    return h


REGION = ["east", "west", "north", "south", "central"]
TYPE = ["residential", "commercial"]
SUBSCRIBERS = {}
for i in range(60):
    r = rng(SEED + 31 * i)
    previous = 10000 + int(r() * 80000)
    SUBSCRIBERS[f"A{i + 1:03d}"] = {"region": REGION[int(r() * 5)], "type": TYPE[int(r() * 2)],
                                    "previous": previous, "current": previous + int(r() * 70)}
TIERS, ABOVE = ((10, 4), (25, 7), (40, 11)), 16
COMMERCIAL_FACTOR = 2
DOCUMENTS = {"TR01": "tarife basamaklari 10 25 40 metrekup ve ustu",
             "TR02": "isyeri abonesinde birim ucret iki katidir",
             "SY01": "endeks farki tuketimi verir sayac donusu 100000 eklenir",
             "AR01": "ariza kaydi acildiginda saha ekibi iki gun icinde gider",
             "KS01": "planli kesinti duyurusu bir hafta once yayimlanir"}

CALLS = [0]                              # tool call counter


def _charge(m3, type_):
    remaining, lower, total = m3, 0, 0
    for upper, unit in TIERS:
        band = min(remaining, upper - lower)
        if band <= 0:
            break
        total += band * unit
        remaining -= band
        lower = upper
    total += remaining * ABOVE
    return total * (COMMERCIAL_FACTOR if type_ == "commercial" else 1)


def t_read_meter(subscriber=None, **_):
    a = SUBSCRIBERS.get(subscriber)
    if a is None:
        return {"error": "no such subscriber"}
    return {"previous": a["previous"], "current": a["current"], "type": a["type"],
            "region": a["region"]}


def t_consumption(previous=None, current=None, **_):
    if previous is None or current is None:
        return {"error": "missing field"}
    f = current - previous
    return {"m3": f + 100000 if f < 0 else f}


def t_tariff(m3=None, type="residential", **_):
    if m3 is None:
        return {"error": "missing field"}
    return {"amount": _charge(m3, type)}


def t_document(query=None, **_):
    if not query:
        return {"error": "missing field"}
    q = set(tokenize(query))
    best, code = -1, None
    for k, m in DOCUMENTS.items():
        o = len(q & set(tokenize(m)))
        if o > best:
            best, code = o, k
    return {"code": code, "text": DOCUMENTS[code]} if best > 0 else {"error": "not found"}


def t_record(subscriber=None, column=None, value=None, **_):
    if not (subscriber and column and value is not None):
        return {"error": "missing field"}
    return {"written": True, "subscriber": subscriber, "column": column, "value": value}


TOOLS = {
    "read_meter":    {"function": t_read_meter, "fields": ("subscriber",), "side_effect": False},
    "consumption":   {"function": t_consumption, "fields": ("previous", "current"),
                      "side_effect": False},
    "tariff":        {"function": t_tariff, "fields": ("m3", "type"), "side_effect": False},
    "search_docs":   {"function": t_document, "fields": ("query",), "side_effect": False},
    "write_record":  {"function": t_record, "fields": ("subscriber", "column", "value"),
                      "side_effect": True},
}
TOOL_NAMES = list(TOOLS)


def call_tool(name, state):
    """Fields outside the schema are filtered out; a missing field is the tool's own error."""
    CALLS[0] += 1
    if name not in TOOLS:
        return {"error": "no such tool"}
    kw = {a: state[a] for a in TOOLS[name]["fields"] if a in state}
    return TOOLS[name]["function"](**kw)


TASKS = []
for i in range(24):
    r = rng(SEED + 9973 * i)
    sub = f"A{1 + int(r() * 60):03d}"
    t = pick(r(), [0.30, 0.22, 0.20, 0.28])
    if t == 0:
        TASKS.append({"name": f"G{i + 1:02d}", "kind": "charge", "subscriber": sub,
                      "request": f"{sub} abonesinin bu donem bedeli nedir",
                      "sequence": ("read_meter", "consumption", "tariff")})
    elif t == 1:
        TASKS.append({"name": f"G{i + 1:02d}", "kind": "consumption", "subscriber": sub,
                      "request": f"{sub} abonesinin bu donem tuketimi kac metrekup",
                      "sequence": ("read_meter", "consumption")})
    elif t == 2:
        TASKS.append({"name": f"G{i + 1:02d}", "kind": "document", "subscriber": sub,
                      "request": "sayac donusunde endeks farki nasil hesaplanir",
                      "sequence": ("search_docs",)})
    else:
        TASKS.append({"name": f"G{i + 1:02d}", "kind": "record", "subscriber": sub,
                      "request": f"{sub} abonesinin bedelini hesapla ve kayda yaz",
                      "sequence": ("read_meter", "consumption", "tariff", "write_record")})


def correct_answer(g):
    a = SUBSCRIBERS[g["subscriber"]]
    f = a["current"] - a["previous"]
    m3 = f + 100000 if f < 0 else f
    if g["kind"] == "consumption":
        return m3
    if g["kind"] == "document":
        return "SY01"
    return _charge(m3, a["type"])


def selection(d):
    return max(0.34, 0.80 - 0.09 * (d - 1))       # share of picking the right tool, falls with depth


def agent(g, max_steps=8, seed=SEED, self_critique=False, approval=False):
    """FICTIONAL agent loop. No real model is called; tool selection
    comes from a single draw that depends on the task's sequence and the depth."""
    r = rng(seed ^ trace_id(g["name"]))
    state = {"subscriber": g["subscriber"], "query": g["request"], "column": "charge"}
    trace, step, tok, errors = [], 0, 0, 0
    seq, k = g["sequence"], 0
    while step < max_steps:
        step += 1
        tok += 60 + 22 * len(trace)
        target = seq[k] if k < len(seq) else None
        if target is None:                         # the job is done , the agent should stop
            if r() < 0.88:
                return {"done": True, "steps": step, "trace": trace, "tokens": tok,
                        "errors": errors, "answer": state.get("result"),
                        "approvals": state.get("approvals", 0)}
            choice = TOOL_NAMES[int(r() * len(TOOL_NAMES))]
        else:
            choice = target if r() < selection(k + 1) else TOOL_NAMES[int(r() * len(TOOL_NAMES))]
        if TOOLS[choice]["side_effect"] and approval:
            state["approvals"] = state.get("approvals", 0) + 1
        if choice == "write_record":
            state["value"] = state.get("result")
        s = call_tool(choice, state)
        trace.append((choice, "error" in s))
        tok += 30
        if "error" in s:
            errors += 1
            if self_critique:
                tok += 40
            continue
        state.update({kk: vv for kk, vv in s.items() if kk != "error"})
        if choice == "consumption":
            state["result"] = s["m3"]
        if choice == "tariff":
            state["result"] = s["amount"]
        if choice == "search_docs":
            state["result"] = s["code"]
        if choice == target:
            k += 1
    return {"done": False, "steps": step, "trace": trace, "tokens": tok, "errors": errors,
            "answer": state.get("result"), "approvals": state.get("approvals", 0)}


print("tool         declared fields             side effect")
for name, v in TOOLS.items():
    print(f"{name:<12} {', '.join(v['fields']):<26}  {v['side_effect']}")
tool         declared fields             side effect
read_meter   subscriber                  False
consumption  previous, current           False
tariff       m3, type                    False
search_docs  query                       False
write_record subscriber, column, value   True

Defect Set and Test Suite

Eleven defects are spread over five tools. Value and contract defects change the function; definition defects leave the function alone and move only the declared field list. The test suite carries one ordinary call and one edge case for each tool.

DEFECTS = {                     # tool , class , description , defective function or field list
    "D1": ("read_meter", "value", "current index same as previous",
           lambda subscriber=None, **_: ({"error": "no such subscriber"}
                                         if subscriber not in SUBSCRIBERS
                                         else dict(SUBSCRIBERS[subscriber],
                                                   current=SUBSCRIBERS[subscriber]["previous"]))),
    "D2": ("consumption", "value", "adds 10000 on meter rollover",
           lambda previous=None, current=None, **_: (
               {"error": "missing field"} if previous is None or current is None
               else {"m3": current - previous + (10000 if current < previous else 0)})),
    "D3": ("tariff", "value", "upper tier unit 11 instead of 16",
           lambda m3=None, type="residential", **_: (
               {"error": "missing field"} if m3 is None else
               {"amount": _charge(m3, type) - max(m3 - 40, 0) * 5
                * (COMMERCIAL_FACTOR if type == "commercial" else 1)})),
    "D4": ("search_docs", "value", "first match instead of highest overlap",
           lambda query=None, **_: next(
               ({"code": k, "text": m} for k, m in DOCUMENTS.items()
                if set(tokenize(query or "")) & set(tokenize(m))),
               {"error": "missing field" if not query else "not found"})),
    "D5": ("consumption", "contract", "zero instead of error on missing field",
           lambda previous=None, current=None, **_: t_consumption(previous or 0, current or 0)),
    "D6": ("tariff", "contract", "zero instead of error on missing field",
           lambda m3=None, type="residential", **_: t_tariff(m3 or 0, type)),
    "D7": ("write_record", "contract", "says written when value is missing",
           lambda subscriber=None, column=None, value=None, **_: {
               "written": True, "subscriber": subscriber, "column": column, "value": value}),
    "D8": ("tariff", "definition", "definition does not declare the type field", ("m3",)),
    "D9": ("write_record", "definition", "definition does not declare the value field",
           ("subscriber", "column")),
    "D10": ("search_docs", "definition", "definition declares subscriber instead of query",
            ("subscriber",)),
    "D11": ("read_meter", "definition", "definition declares an unused field",
            ("subscriber", "column")),
}
CLASSES = ("value", "contract", "definition")


def defective(code):
    """Copy of the table with a defect: if a tuple is given the declared field list,
    if a function is given the tool's function is replaced."""
    t = {a: dict(v) for a, v in TOOLS.items()}
    name, defect = DEFECTS[code][0], DEFECTS[code][3]
    t[name]["fields" if isinstance(defect, tuple) else "function"] = defect
    return t


A1 = "A001"
TESTS = [
    ("read_meter", {"subscriber": A1}, {"previous": SUBSCRIBERS[A1]["previous"],
                                        "current": SUBSCRIBERS[A1]["current"]}),
    ("read_meter", {"subscriber": "A999"}, {"error": "no such subscriber"}),
    ("consumption", {"previous": 100, "current": 150}, {"m3": 50}),
    ("consumption", {"previous": 99980, "current": 30}, {"m3": 50}),
    ("consumption", {"current": 150}, {"error": "missing field"}),
    ("tariff", {"m3": 50, "type": "commercial"}, {"amount": 940}),
    ("tariff", {}, {"error": "missing field"}),
    ("search_docs", {"query": "isyeri abonesinde birim ucret"}, {"code": "TR02"}),
    ("search_docs", {"query": ""}, {"error": "missing field"}),
    ("write_record", {"subscriber": A1, "column": "charge", "value": 500}, {"written": True}),
    ("write_record", {"subscriber": A1, "column": "charge"}, {"error": "missing field"}),
]


def tool_test(table):
    """Tests each tool outside the loop, by calling its function directly."""
    failed = []
    for name, inp, expected in TESTS:
        c = table[name]["function"](**inp)
        if ("error" in c) != ("error" in expected) or any(
                c.get(k) != v for k, v in expected.items()):
            failed.append(name)
    return failed


def loop_success(table, seed=SEED):
    global TOOLS
    original, TOOLS = TOOLS, table
    d = [g["name"] for g in TASKS
         if agent(g, max_steps=8, seed=seed)["answer"] != correct_answer(g)]
    TOOLS = original
    return d


print(f"test suite {len(TESTS)} cases , failing on the defect-free table "
      f"{len(tool_test(TOOLS))}")
print(f"defect set {len(DEFECTS)} defects , {len(TOOLS)} tools , {len(CLASSES)} classes\n")
print(f"{'code':<5} {'tool':<12} {'class':<10} {'failed':>6}  description")
caught = {s: [0, 0] for s in CLASSES}
for code, (name, cls, desc, defect) in DEFECTS.items():
    d = tool_test(defective(code))
    caught[cls][1] += 1
    caught[cls][0] += 1 if d else 0
    print(f"{code:<5} {name:<12} {cls:<10} {len(d):>6}  {desc}")

print("\nclass      defects     caught   share")
for s in CLASSES:
    a, b = caught[s]
    print(f"{s:<10} {b:>7} {a:>10} {a / b:>7.4f}")
tp = sum(a for a, _ in caught.values())
print(f"{'total':<10} {len(DEFECTS):>7} {tp:>10} {tp / len(DEFECTS):>7.4f}")

print("\ntwo runs , defect-free table")
print(f"tool-level test , failing cases {tool_test(TOOLS)} and {tool_test(TOOLS)}")
for t in (SEED, 20260219):
    d = loop_success(TOOLS, t)
    print(f"loop seed {t} , success {1 - len(d) / 24:.4f} , failing task {d}")
test suite 11 cases , failing on the defect-free table 0
defect set 11 defects , 5 tools , 3 classes

code  tool         class      failed  description
D1    read_meter   value           1  current index same as previous
D2    consumption  value           1  adds 10000 on meter rollover
D3    tariff       value           1  upper tier unit 11 instead of 16
D4    search_docs  value           1  first match instead of highest overlap
D5    consumption  contract        1  zero instead of error on missing field
D6    tariff       contract        1  zero instead of error on missing field
D7    write_record contract        1  says written when value is missing
D8    tariff       definition      0  definition does not declare the type field
D9    write_record definition      0  definition does not declare the value field
D10   search_docs  definition      0  definition declares subscriber instead of query
D11   read_meter   definition      0  definition declares an unused field

class      defects     caught   share
value            4          4  1.0000
contract         3          3  1.0000
definition       4          0  0.0000
total           11          7  0.6364

two runs , defect-free table
tool-level test , failing cases [] and []
loop seed 20260218 , success 0.9583 , failing task ['G23']
loop seed 20260219 , success 0.9583 , failing task ['G15']

Seven Caught, Four Unseen

The strength of the layer is complete in the first two classes: all four value defects and all three contract defects fail a case in the test suite. That the number of failing cases is one each is also information; the defect shows in a single case written for its own tool, and the name of the failing case gives the broken tool directly. What the four-state table cannot say is said here in one line.

The unseen class is not chance; it is the result of the call path. The test suite calls the tool’s function directly and never looks at the declared field list. The loop, on the other hand, builds the call through call_tool and filters from the state only the declared fields. A definition defect sits exactly inside this filter: the tariff function applies the commercial factor correctly, but because its definition does not declare the type field, that field never passes in the loop and every commercial subscriber is computed from the residential tariff. A test that calls the function cannot see this, because the function is correct. For all four definition defects the result is 0.0000.

Total catch is 0.6364. This number is specific to this defect set; what carries over is not the rate but how the rate is divided. A layer that tests the function is blind to the mismatch between function and definition, and this blindness does not close by adding cases.

A Layer Independent of the Run

The payoff of the layer is in the last block. On the defect-free tool table the loop gives 0.9583 at both seeds, but the failing task changes: G23 in the first run, G15 in the second. The tool-level test was run twice on the same table, and the set of failing cases came out empty and identical both times, because this layer uses no seed.

The verdict of the tool-level test is repeatable: a defect either shows in every run or never shows. The verdict of the loop measure is banded: the difference between the two runs is a single task, and no sign below the band counts as a finding.

Summary

  • A tool is the combination of a function and a definition; the tool-level test calls the function directly and never uses the definition.
  • The eleven-case test suite catches all four value defects and all three contract defects; each defect shows in a single case, and the failing case names the broken tool.
  • None of the four definition defects is caught: the share is 0.0000, the total catch is 0.6364.
  • The cause of the blindness is the call path: the loop filters only the declared fields, and the test suite calls the function directly and skips that filter.
  • The payoff of the layer is independence from the run: while the failing task of the loop shifts from G23 to G15 when the seed changes, the set of failing cases of the test suite is empty in both runs.

Next Step

This lesson drew the limit of a layer with a number: the tool-level test says cheaply and exactly whether a tool keeps its own word, but it never sees how the tool is connected to the loop. The four defects that go unseen have to show up somewhere, because the agent writes a wrong charge to commercial subscribers. The next lesson runs scenarios end to end and asks two questions together: how many of the defects that the tool level missed does the flow test catch, and which defect do both layers fail to see.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close