diff --git a/scripts/internal/init-db.sh b/scripts/internal/init-db.sh index 940a63a3..c6c74566 100755 --- a/scripts/internal/init-db.sh +++ b/scripts/internal/init-db.sh @@ -35,4 +35,11 @@ CREATE TABLE IF NOT EXISTS messages ( CREATE INDEX IF NOT EXISTS idx_unread ON messages(team, to_agent, read_at) WHERE read_at IS NULL; CREATE INDEX IF NOT EXISTS idx_history ON messages(team, created_at DESC); + +CREATE TABLE IF NOT EXISTS claims ( + message_id INTEGER PRIMARY KEY, + owner TEXT NOT NULL, + expires_at TEXT NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_claims_expiry ON claims(expires_at); SQL diff --git a/scripts/lib/claims.sh b/scripts/lib/claims.sh new file mode 100644 index 00000000..fafa9616 --- /dev/null +++ b/scripts/lib/claims.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# delivery daemon 向け unread message claim API。 + +if ! type agmsg_db_path >/dev/null 2>&1; then + _agmsg_claims_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + source "$_agmsg_claims_dir/storage.sh" +fi + +_agmsg_claim_quote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"; } + +_agmsg_claim_init() { + local scripts_dir + scripts_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + bash "$scripts_dir/internal/init-db.sh" >/dev/null +} + +# stdout: id US from US body US created_at。空 output は claim 対象なし。 +agmsg_claim_next() { + local team="$1" agent="$2" owner="$3" ttl="${4:-30}" db + db="$(agmsg_db_path)"; _agmsg_claim_init + agmsg_sqlite "$db" </dev/null diff --git a/tests/test_claims.bats b/tests/test_claims.bats new file mode 100644 index 00000000..5890a85a --- /dev/null +++ b/tests/test_claims.bats @@ -0,0 +1,59 @@ +#!/usr/bin/env bats + +load test_helper + +setup() { + setup_test_env + source "$SCRIPTS/lib/claims.sh" + export TEST_TEAM=claims-team TEST_AGENT=grok-pane TEST_OWNER=daemon-a + "$SCRIPTS/send.sh" "$TEST_TEAM" sender "$TEST_AGENT" "claim payload" >/dev/null +} + +teardown() { teardown_test_env; } + +@test "claim is exclusive until release" { + run agmsg_claim_next "$TEST_TEAM" "$TEST_AGENT" "$TEST_OWNER" 60 + [ "$status" -eq 0 ] + [ -n "$output" ] + + run agmsg_claim_next "$TEST_TEAM" "$TEST_AGENT" daemon-b 60 + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "released claim can be claimed and acknowledged exactly once" { + first="$(agmsg_claim_next "$TEST_TEAM" "$TEST_AGENT" "$TEST_OWNER" 60)" + id="${first%%$'\x1f'*}" + agmsg_release_claim "$id" "$TEST_OWNER" + + second="$(agmsg_claim_next "$TEST_TEAM" "$TEST_AGENT" daemon-b 60)" + [ -n "$second" ] + id="${second%%$'\x1f'*}" + agmsg_ack_claim "$id" daemon-b + + run agmsg_claim_next "$TEST_TEAM" "$TEST_AGENT" daemon-c 60 + [ "$status" -eq 0 ] + [ -z "$output" ] +} + +@test "wrong owner cannot acknowledge a claim" { + first="$(agmsg_claim_next "$TEST_TEAM" "$TEST_AGENT" "$TEST_OWNER" 60)" + id="${first%%$'\x1f'*}" + agmsg_ack_claim "$id" wrong-owner + run agmsg_claim_next "$TEST_TEAM" "$TEST_AGENT" daemon-b 60 + [ -z "$output" ] +} + +@test "expired claim is reclaimed by another daemon" { + first="$(agmsg_claim_next "$TEST_TEAM" "$TEST_AGENT" "$TEST_OWNER" 0)" + [ -n "$first" ] + run agmsg_claim_next "$TEST_TEAM" "$TEST_AGENT" daemon-b 60 + [ "$status" -eq 0 ] + [ -n "$output" ] +} + +@test "claim preserves escaped body text" { + "$SCRIPTS/send.sh" "$TEST_TEAM" sender "$TEST_AGENT" $'line one\nline two\tend' >/dev/null + first="$(agmsg_claim_next "$TEST_TEAM" "$TEST_AGENT" "$TEST_OWNER" 60)" + [[ "$first" == *'line one\\nline two\\tend'* ]] +}