mirror of
https://github.com/priyanshujain/messageboardbench.git
synced 2026-10-02 11:07:07 +00:00
206 lines
9.6 KiB
Python
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()
|