Skip to content

Match AT final result codes exactly to avoid truncating SMS bodies #13

Description

@Justinabox

Summary

ATCommandExecutor._collect_response() treats an expected result as successful when the expected token appears anywhere in a response line. For the common expect=["OK"] path, an SMS body or modem data line containing OK can terminate collection before the real final OK arrives. A line starting with ERROR can similarly be treated as a modem failure even when it is user message content.

This can silently drop valid inbound SMS content and leave unread response lines to poison the next command.

Evidence

Affected code:

  • callstack/protocol/executor.py:290-292 uses if any(e in line for e in expect) for success matching.
  • callstack/protocol/executor.py:294-296 treats line.startswith(e) for final errors, so message bodies beginning with ERROR are also ambiguous.
  • callstack/sms/service.py:277-282 calls AT+CMGR with expect=["OK"] and parses only resp.lines.
  • callstack/sms/service.py:332-345 expects the line after +CMGR to be the message body.

Reproduction on current main (a687aece24a1):

PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pyserial-asyncio python -c 'import asyncio
from callstack.events.bus import EventBus
from callstack.protocol.urc import URCDispatcher
from callstack.protocol.executor import ATCommandExecutor
from callstack.transport.mock import MockTransport
from callstack.sms.service import SMSService
async def main():
    bus=EventBus(); t=MockTransport(); ex=ATCommandExecutor(t, URCDispatcher(bus)); svc=SMSService(ex,bus)
    t.feed("+CMGR: \\\"REC UNREAD\\\",\\\"+155****1234\\\",\\\"\\\",\\\"24/12/25,14:30:00+04\\\"", "OK computer", "OK")
    sms=await svc.read_message(0)
    print({"sms_is_none": sms is None, "body": None if sms is None else sms.body})
asyncio.run(main())'
# {'sms_is_none': True, 'body': None}

Repository health checks from this scout run:

git diff --check
# exit 0

PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
# 291 passed in 3.94s

Duplicate checks:

gh issue list --repo Justinabox/Callstack --state all --search 'executor OK substring SMS body truncates in:title,body'
# no matches

gh issue list --repo Justinabox/Callstack --state all --search 'ATResponse expect in line contains OK in:title,body'
# no matches

Expected behavior

The executor should collect data lines until a real final result code is received. User-controlled SMS bodies such as OK computer, OK, or ERROR: door open should be returned as data, not interpreted as modem final results.

Actual behavior

A data line containing OK satisfies the success expectation and returns early. In the AT+CMGR path above, the body line is mistaken for the final result, so _parse_single_message() sees no body line after the header and returns None.

Suggested fix direction

  • Separate final-result matching from prompt/intermediate matching.
  • For standard final codes, require exact OK, exact ERROR, or prefixed +CME ERROR / +CMS ERROR according to AT result-code syntax.
  • Keep prompt handling (expect=[">"]) and intermediate send references (+CMGS:) explicit so SMS send still works.
  • Add regression tests at the executor level and through SMSService.read_message().

Acceptance criteria

  • AT+CMGR responses with bodies containing OK are parsed as normal messages.
  • AT+CMGR responses with bodies beginning with ERROR are parsed as normal messages when followed by final OK.
  • Standard command success still terminates on exact final OK.
  • Standard modem failures still return success=False on exact ERROR, +CME ERROR..., or +CMS ERROR....
  • SMS prompt handling for expect=[">"] remains green.

Verification gates

git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/test_executor.py tests/test_sms_service.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions