summaryrefslogtreecommitdiff
path: root/9agents/test/e2e.sh
diff options
context:
space:
mode:
Diffstat (limited to '9agents/test/e2e.sh')
-rwxr-xr-x9agents/test/e2e.sh74
1 files changed, 74 insertions, 0 deletions
diff --git a/9agents/test/e2e.sh b/9agents/test/e2e.sh
index fa41bef..3413285 100755
--- a/9agents/test/e2e.sh
+++ b/9agents/test/e2e.sh
@@ -242,6 +242,19 @@ AGENT_START=$(sed 's/.*) //' "/proc/$AGENT/stat" | awk '{print $20}')
AGENT_CWD=$(readlink "/proc/$AGENT/cwd")
printf '{"pid":%d,"sessionId":"sess-e2e","cwd":"%s","name":"e2e-agent","status":"idle","procStart":"%s"}\n' \
"$AGENT" "$AGENT_CWD" "$AGENT_START" > "$ACT/home/.claude/sessions/$AGENT.json"
+# Its transcript, for the chat. Claude Code's store is named after the cwd,
+# every byte that is not a letter or a digit dashed; it has to exist before
+# the scan that finds the agent, as it does for a real one.
+SLUG=$(printf %s "$AGENT_CWD" | sed 's/[^A-Za-z0-9]/-/g')
+CT="$ACT/home/.claude/projects/$SLUG/sess-e2e.jsonl"
+mkdir -p "$(dirname "$CT")"
+cat > "$CT" <<'CEOF'
+{"type":"user","message":{"role":"user","content":"list the files"},"timestamp":"2026-09-24T12:51:26.691Z"}
+{"type":"assistant","message":{"content":[{"type":"tool_use","id":"t1","name":"Bash","input":{"command":"ls"}}]}}
+{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"t1","content":"a\nb"}]}}
+{"type":"attachment","attachment":{"type":"reminder"}}
+{"type":"assistant","message":{"content":[{"type":"text","text":"two files: \"a\" and \"b\""}]}}
+CEOF
act_roots() {
echo --root "claude=$ACT/home/.claude" --root "codex=$ACT/home/.codex" \
@@ -257,6 +270,67 @@ expect_eq "its session comes from the harness's own record" "sess-e2e" "$(p9 "$A
expect_eq "the route taken is named" "registry" "$(p9 "$ACT/ro.sock" read "/active/claude/$AGENT/via")"
expect_eq "the harness's own name is served" "e2e-agent" "$(p9 "$ACT/ro.sock" read "/active/claude/$AGENT/name")"
expect_eq "the harness's own status is served" "idle" "$(p9 "$ACT/ro.sock" read "/active/claude/$AGENT/status")"
+
+# The chat: the transcript as one file per message, named <id>-<kind>.
+CHAT="/active/claude/$AGENT/chat"
+expect_contains "the agent has a chat" "chat" "$(p9 "$ACT/ro.sock" ls "/active/claude/$AGENT")"
+expect_eq "the chat lists its messages by id and kind, tools by name" "00000000-user 00000001-bash 00000002-bash-result 00000003-assistant" \
+ "$(p9 "$ACT/ro.sock" ls "$CHAT" | tr '\n' ' ' | sed 's/ $//')"
+expect_eq "a message reads as its text" "list the files" "$(p9 "$ACT/ro.sock" read "$CHAT/00000000-user")"
+expect_eq "a tool call reads as its name, then its input" 'Bash
+{"command":"ls"}' "$(p9 "$ACT/ro.sock" read "$CHAT/00000001-bash")"
+expect_eq "escapes are undone" 'two files: "a" and "b"' "$(p9 "$ACT/ro.sock" read "$CHAT/00000003-assistant")"
+# The length a stat reports must be what a read returns, or a kernel
+# mount would cut the file short.
+expect_eq "a message reads its whole length" "4" "$(p9 "$ACT/ro.sock" read "$CHAT/00000002-bash-result" | wc -c)"
+expect_eq "a message's length is what reading it returns" "4" \
+ "$(p9 "$ACT/ro.sock" ls -l "$CHAT/00000002-bash-result" | awk '{print $6}')"
+p9 "$ACT/ro.sock" read "$CHAT/00000000-assistant" >/dev/null 2>&1 \
+ && fail "a name with the wrong kind is no file" || pass "a name with the wrong kind is no file"
+echo '{"type":"user","message":{"content":"thanks"}}' >> "$CT"
+expect_eq "a message written later takes the next id" "00000004-user" "$(p9 "$ACT/ro.sock" ls "$CHAT" | tail -1)"
+expect_eq "and reads at once" "thanks" "$(p9 "$ACT/ro.sock" read "$CHAT/00000004-user")"
+
+# Through the kernel: a mount holds a fid for every inode it caches, and a
+# long chat is thousands of them. Every message must stat, read and match.
+if [ -n "${NS:-}" ]; then
+ for i in $(seq 1 1000); do printf '{"type":"user","message":{"content":"m%d"}}\n' "$i"; done >> "$CT"
+ KOUT=$("$NS" --unix "$ACT/ro.sock" --name a -- sh -c '
+ C=$NINE_MOUNT/active/claude/'"$AGENT"'/chat
+ echo "files=$(ls "$C" | wc -l)"
+ ls -l "$C" >/dev/null 2>&1 && echo "lsl=ok" || echo "lsl=failed"
+ echo "read=$(cat "$C"/* 2>/dev/null | wc -c)"
+ echo "stat=$(stat -c %s "$C"/* | paste -sd+ | bc)"' 2>&1)
+ expect_contains "a 1005-message chat lists through a kernel mount" "files=1005" "$KOUT"
+ expect_contains "and every message stats there" "lsl=ok" "$KOUT"
+ expect_eq "and reads there exactly as long as it stats" \
+ "$(echo "$KOUT" | sed -n 's/^stat=//p')" "$(echo "$KOUT" | sed -n 's/^read=//p')"
+else
+ echo "SKIP: kernel-mount chat check needs 9ns"
+fi
+
+# A codex agent: found by the rollout it holds open, which the fake holds
+# as its stdin.
+RO="$ACT/home/.codex/sessions/2026/09/21/rollout-2026-09-21T10-00-00-01a0bcd2-5653-7cc0-aa36-676f6467a72a.jsonl"
+mkdir -p "$(dirname "$RO")"
+cat > "$RO" <<'REOF'
+{"timestamp":"2026-09-21T10:00:00.000Z","type":"session_meta","payload":{"id":"01a0bcd2-5653-7cc0-aa36-676f6467a72a"}}
+{"timestamp":"2026-09-21T10:00:01.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"hi codex"}]}}
+{"timestamp":"2026-09-21T10:00:01.000Z","type":"event_msg","payload":{"type":"user_message","message":"hi codex"}}
+{"timestamp":"2026-09-21T10:00:02.000Z","type":"response_item","payload":{"type":"function_call","name":"shell","arguments":"{\"command\":[\"ls\"]}","call_id":"c1"}}
+{"timestamp":"2026-09-21T10:00:03.000Z","type":"response_item","payload":{"type":"function_call_output","call_id":"c1","output":"x\n"}}
+REOF
+cp /bin/sleep "$ACT/bin/codex"
+"$ACT/bin/codex" 600 < "$RO" &
+CODEX=$!
+PIDS+=("$CODEX")
+sleep 0.3
+expect_eq "a codex agent's chat comes from the rollout it holds" "00000000-user 00000001-shell 00000002-shell-result" \
+ "$(p9 "$ACT/ro.sock" ls "/active/codex/$CODEX/chat" | tr '\n' ' ' | sed 's/ $//')"
+expect_eq "its tool call reads as name and arguments" 'shell
+{"command":["ls"]}' "$(p9 "$ACT/ro.sock" read "/active/codex/$CODEX/chat/00000001-shell")"
+kill "$CODEX" 2>/dev/null
+
if echo "nope" | p9 "$ACT/ro.sock" write "/active/claude/$AGENT/zmx" 2>/dev/null; then
fail "without --allow-move the tree stays read-only" "the write was accepted"
else