Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions scripts/api.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ set -euo pipefail
# Usage:
# api.sh get teams
# api.sh get teams <team> members
# api.sh get teams <team> messages [--agent <name>] [--limit N] [--before-id <id>]
# api.sh get teams <team> messages [--agent <name>] [--limit N]
# [--before-id <id> | --after-id <id>]
#
# Output is always JSONL — one JSON object per line, UTF-8, no
# pretty-printing — for every resource, including `teams` (a uniform
Expand Down Expand Up @@ -95,19 +96,25 @@ get_members() {
get_messages() {
local team="$1"
shift
local agent="" limit=30 before_id=""
local agent="" limit=30 before_id="" after_id=""
while [ $# -gt 0 ]; do
case "$1" in
--agent) agent="${2:?--agent needs a value}"; shift 2 ;;
--limit) limit="${2:?--limit needs a value}"; shift 2 ;;
--before-id) before_id="${2:?--before-id needs a value}"; shift 2 ;;
--after-id) after_id="${2:?--after-id needs a value}"; shift 2 ;;
*) echo "Unknown option: $1" >&2; exit 1 ;;
esac
done
# Non-numeric values would otherwise land straight in the SQL text below —
# same guard history.sh uses for LIMIT.
case "$limit" in ''|*[!0-9]*) limit=30 ;; esac
case "$before_id" in ''|*[!0-9]*) before_id="" ;; esac
case "$after_id" in ''|*[!0-9]*) after_id="" ;; esac
if [ -n "$before_id" ] && [ -n "$after_id" ]; then
echo "--before-id and --after-id are mutually exclusive" >&2
exit 1
fi

local db; db="$(agmsg_db_path)"
if [ ! -f "$db" ]; then
Expand All @@ -123,6 +130,9 @@ get_messages() {
if [ -n "$before_id" ]; then
where="$where AND id<$before_id"
fi
if [ -n "$after_id" ]; then
where="$where AND id>$after_id"
fi

# Inner query takes the most recent `limit` by id DESC, outer re-sorts
# ASC — oldest-first output, same ordering contract §2.1 of the driver
Expand All @@ -133,6 +143,14 @@ get_messages() {
# a decimal STRING (not a JSON number) so a future UUIDv7/Redis-stream-id
# driver doesn't change this field's JSON type — a consumer parsing id as
# a string today needs no change once that lands.
# Forward polling must take the OLDEST rows after the cursor. Taking the
# most recent rows would silently skip messages whenever more than `limit`
# arrive between polls. Backward/history queries keep the existing newest-N
# behavior, with both paths emitting oldest-first JSONL.
local page_order="ORDER BY id DESC LIMIT $limit"
if [ -n "$after_id" ]; then
page_order="ORDER BY id ASC LIMIT $limit"
fi
agmsg_sqlite "$db" "
SELECT json_object(
'type', 'message_sent',
Expand All @@ -143,7 +161,7 @@ get_messages() {
'body', body,
'at', created_at
) FROM (
SELECT * FROM messages WHERE $where ORDER BY id DESC LIMIT $limit
SELECT * FROM messages WHERE $where $page_order
) ORDER BY id ASC;
"
}
Expand Down
21 changes: 21 additions & 0 deletions tests/test_api.bats
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,27 @@ json_valid_line() {
[ "$(json_field "$output" body)" = "one" ]
}

@test "api: get teams <team> messages --after-id pages forward without skipping" {
bash "$SCRIPTS/send.sh" testteam alice bob "one"
local cursor
cursor="$(json_field "$(bash "$SCRIPTS/api.sh" get teams testteam messages --limit 1)" id)"
bash "$SCRIPTS/send.sh" testteam alice bob "two"
bash "$SCRIPTS/send.sh" testteam alice bob "three"
bash "$SCRIPTS/send.sh" testteam alice bob "four"

run bash "$SCRIPTS/api.sh" get teams testteam messages --after-id "$cursor" --limit 2
[ "$status" -eq 0 ]
[ "$(echo "$output" | wc -l | tr -d ' ')" -eq 2 ]
[ "$(json_field "$(echo "$output" | sed -n 1p)" body)" = "two" ]
[ "$(json_field "$(echo "$output" | sed -n 2p)" body)" = "three" ]
}

@test "api: get teams <team> messages rejects before and after cursors together" {
run bash "$SCRIPTS/api.sh" get teams testteam messages --before-id 3 --after-id 1
[ "$status" -ne 0 ]
[[ "$output" =~ "mutually exclusive" ]]
}

@test "api: get teams <team> messages emits valid JSON for a body containing a quote" {
bash "$SCRIPTS/send.sh" testteam alice bob "she said \"hi\""
run bash "$SCRIPTS/api.sh" get teams testteam messages
Expand Down