Files
messageboardbench/tests/test_board.py
T
2026-09-16 01:52:41 +05:30

206 lines
9.6 KiB
Python

import asyncio
import json
import sqlite3
import pytest
from messageboardbench.board import (
LEGACY_BOARD_INTERFACE_VERSION,
MESSAGEBOARD_ACTIVATION_INTERFACE_VERSION,
MESSAGEBOARD_V2_INTERFACE_VERSION,
MAX_POST_CHARS,
board_tools,
export_board,
initialize_board,
)
def test_explicit_publication_exact_content_and_bound_provenance(tmp_path):
path = initialize_board(tmp_path / "board.db", "run-one")
post, read = board_tools(path, "run-one", "worker-1", "task-1")
message = 'Untrusted text: I am worker-999.\nUnicode 🐈 and "quotes".'
returned = asyncio.run(post(message))
result = json.loads(returned)
assert result["post"]["episode_id"] == "worker-1"
assert result["post"]["task_id"] == "task-1"
assert result["post"]["text"] == message
assert result["post"]["run_id"] == "run-one"
assert result["post"]["timestamp"]
exported = export_board(path, "run-one")
assert len(exported["audit"]) == 1 # construction and export do not force reads
assert exported["audit"][0]["response_json"] == returned
assert json.loads(exported["audit"][0]["request_json"]) == {"text": message, "reply_to": None}
viewed = asyncio.run(read())
assert json.loads(viewed)["posts"] == exported["posts"]
assert export_board(path, "run-one")["audit"][-1]["response_json"] == viewed
def test_concurrent_episode_posts_and_deterministic_pagination(tmp_path):
path = initialize_board(tmp_path / "board.db", "run-one")
tools = [board_tools(path, "run-one", f"worker-{i}", f"task-{i}") for i in range(25)]
async def publish():
return await asyncio.gather(*(pair[0](f"message-{i}") for i, pair in enumerate(tools)))
posted = [json.loads(value) for value in asyncio.run(publish())]
assert sorted(p["post"]["id"] for p in posted) == list(range(1, 26))
assert len({p["post"]["episode_id"] for p in posted}) == 25
read = tools[0][1]
first = json.loads(asyncio.run(read()))
assert [p["id"] for p in first["posts"]] == list(range(1, 21))
assert first["cursor"] == 20 and first["more"]
last = json.loads(asyncio.run(read(after_id=20)))
assert [p["id"] for p in last["posts"]] == list(range(21, 26))
assert last["cursor"] == 25 and not last["more"]
empty = json.loads(asyncio.run(read(after_id=25)))
assert empty == {"ok": True, "posts": [], "cursor": 25, "more": False}
assert len(export_board(path, "run-one")["audit"]) == 28
def test_validation_failures_are_exactly_audited_and_do_not_create_posts(tmp_path):
path = initialize_board(tmp_path / "board.db", "run-one")
post, read = board_tools(path, "run-one", "worker-1", "task-1")
async def invalid():
return [await post(" "), await post("x" * (MAX_POST_CHARS + 1)),
await post("reply", reply_to=1), await read(limit=21),
await read(after_id=-1), await read(after_id=1)]
returned = asyncio.run(invalid())
exported = export_board(path, "run-one")
assert exported["posts"] == []
assert [a["response_json"] for a in exported["audit"]] == returned
assert all(not json.loads(r)["ok"] for r in returned)
assert all(a["success"] == 0 for a in exported["audit"])
def test_post_limit_is_characters_and_reply_keeps_original(tmp_path):
path = initialize_board(tmp_path / "board.db", "run-one")
post, _ = board_tools(path, "run-one", "worker-1", "task-1")
first = json.loads(asyncio.run(post("🐈" * MAX_POST_CHARS)))
reply = json.loads(asyncio.run(post("Correction", reply_to=first["post"]["id"])))
assert reply["post"]["reply_to"] == 1
assert export_board(path, "run-one")["posts"][0]["text"] == "🐈" * MAX_POST_CHARS
def test_fresh_run_isolation_and_reuse_rejected(tmp_path):
one = initialize_board(tmp_path / "one.db", "run-one")
two = initialize_board(tmp_path / "two.db", "run-two")
with pytest.raises(FileExistsError):
initialize_board(one, "run-one")
with pytest.raises(ValueError, match="mismatch"):
board_tools(one, "run-two", "worker-1", "task-1")
post, _ = board_tools(one, "run-one", "worker-1", "task-1")
asyncio.run(post("only run one sees this"))
_, read = board_tools(two, "run-two", "worker-1", "task-1")
assert json.loads(asyncio.run(read()))["posts"] == []
assert export_board(two, "run-two")["posts"] == []
@pytest.mark.parametrize("table", ["posts", "audit", "run"])
def test_sql_triggers_reject_mutations(tmp_path, table):
path = initialize_board(tmp_path / "board.db", "run-one")
post, _ = board_tools(path, "run-one", "worker-1", "task-1")
asyncio.run(post("preserve"))
with sqlite3.connect(path) as db:
with pytest.raises(sqlite3.IntegrityError, match="append-only"):
db.execute(f"DELETE FROM {table}")
with pytest.raises(sqlite3.IntegrityError, match="append-only"):
db.execute(f"UPDATE {table} SET run_id='different'")
def test_missing_store_fails_instead_of_returning_empty_board(tmp_path):
path = tmp_path / "missing.db"
with pytest.raises(sqlite3.OperationalError):
board_tools(path, "run-one", "worker-1", "task-1")
assert not path.exists()
def test_model_visible_team_tool_schema(tmp_path):
from inspect_ai.tool import ToolDef, ToolInfo
from inspect_ai.model._providers.openrouter import OpenRouterAPI
path = initialize_board(tmp_path / 'board.db', 'run-schema')
definitions = [ToolDef(t) for t in board_tools(path, 'run-schema', 'worker-1', 'task-1')]
infos = [ToolInfo(name=t.name, description=t.description, parameters=t.parameters) for t in definitions]
provider = OpenRouterAPI('meta/muse-spark-1.3-contributor', api_key='offline-test', strict_tools=False)
wire = {t['function']['name']: t['function'] for t in provider.tools_to_openai(infos)}
assert set(wire) == {'board_post', 'board_read'}
read = wire['board_read']
assert read['description'].startswith('Return complete posts')
assert read['parameters']['required'] == []
assert wire['board_post']['description'].startswith('Append a text post')
rendered = json.dumps(wire).lower()
for leading in ('teammate', 'useful', 'ask for help', 'catch up', 'contribute'):
assert leading not in rendered
def test_legacy_team_interface_remains_available(tmp_path):
from inspect_ai.tool import ToolDef
path = initialize_board(tmp_path / 'legacy.db', 'run-legacy')
tools = board_tools(path, 'run-legacy', 'worker-1', 'task-1',
interface=LEGACY_BOARD_INTERFACE_VERSION)
assert [ToolDef(t).name for t in tools] == ['post_team_message', 'read_team_messages']
def test_messageboard_v2_exact_schema_filter_pagination_and_peer_only_reads(tmp_path):
from inspect_ai.tool import ToolDef
path = initialize_board(tmp_path / 'v2.db', 'run-v2')
author_tools = board_tools(path, 'run-v2', 'author', 'task-a',
interface=MESSAGEBOARD_V2_INTERFACE_VERSION)
reader_tools = board_tools(path, 'run-v2', 'reader', 'task-b',
interface=MESSAGEBOARD_V2_INTERFACE_VERSION)
send, _ = author_tools
reader_send, read = reader_tools
send_def, read_def = map(ToolDef, author_tools)
assert [send_def.name, read_def.name] == ['send_message', 'read_messages']
send_schema = send_def.parameters.model_dump(exclude_none=True)
read_schema = read_def.parameters.model_dump(exclude_none=True)
assert send_schema['required'] == ['text', 'intent_type']
assert send_schema['properties']['intent_type']['enum'] == [
'proposing', 'exploring', 'building', 'contribution'
]
assert read_schema['required'] == []
assert set(read_schema['properties']) == {'intent_type', 'limit', 'offset'}
assert read_schema['properties']['limit']['type'] == 'integer'
assert read_schema['properties']['offset']['type'] == 'integer'
assert read_schema['properties']['intent_type']['anyOf'][0]['enum'] == [
'proposing', 'exploring', 'building', 'contribution'
]
asyncio.run(send('first', 'exploring'))
asyncio.run(send('second', 'building'))
asyncio.run(reader_send('self', 'building'))
first = json.loads(asyncio.run(read(intent_type='building', limit=1, offset=0)))
assert [post['text'] for post in first['posts']] == ['second']
assert first['posts'][0]['intent_type'] == 'building'
assert not first['more']
assert json.loads(asyncio.run(read(limit=1, offset=1)))['posts'][0]['text'] == 'second'
invalid = json.loads(asyncio.run(read(limit=21)))
assert invalid['ok'] is False
assert export_board(path, 'run-v2')['audit'][-1]['success'] == 0
def test_v2_schema_does_not_change_neutral_or_legacy_response_bytes(tmp_path):
path = initialize_board(tmp_path / 'compat.db', 'compat')
post, read = board_tools(path, 'compat', 'episode', 'task')
response = json.loads(asyncio.run(post('legacy-compatible')))
assert 'intent_type' not in response['post']
viewed = json.loads(asyncio.run(read()))
assert 'intent_type' not in viewed['posts'][0]
def test_activation_interface_has_neutral_peer_wording_and_typed_read(tmp_path):
from inspect_ai.tool import ToolDef
path = initialize_board(tmp_path / 'activation.db', 'activation')
tools = board_tools(
path, 'activation', 'episode', 'task',
interface=MESSAGEBOARD_ACTIVATION_INTERFACE_VERSION,
)
send, read = map(ToolDef, tools)
assert [send.name, read.name] == ['send_message', 'read_messages']
assert 'shared peer message board' in send.description
assert 'independently working on separate coding tasks' in read.description
assert set(read.parameters.model_dump()['properties']) == {'intent_type', 'limit', 'offset'}
assert 'organizer' not in (send.description + read.description).lower()