Skip to content

test: cover the Copilot CLI session source#32

Draft
ohad6k wants to merge 1 commit into
mainfrom
agents/copilot-source-tests
Draft

test: cover the Copilot CLI session source#32
ohad6k wants to merge 1 commit into
mainfrom
agents/copilot-source-tests

Conversation

@ohad6k

@ohad6k ohad6k commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Every session source in emulo has a test file named after it except one. test_codex_control_envelopes.py, test_claude_human_turns.py, test_opencode_source.py, test_antigravity_source.py — and nothing anywhere in the suite touched the Copilot CLI adapter. This adds tests/test_copilot_source.py. No production code changes.

What the adapter does

  • SOURCES["copilot"] points at ~/.copilot/session-state. Copilot CLI writes one events.jsonl per session under session-state/<session-id>/, next to non-transcript state (workspace.yaml, checkpoints/, files/, research/).
  • discover_files() globs **/*.jsonl and then drops any path with a subagents component.
  • user_messages() gates on the raw-line marker "type":"user.message", then keeps data.content and skips events whose data.source == "system" (harness steering). The date comes from timestamp[:10].
  • session_label() prefixes the parent directory, because every Copilot transcript is named events.jsonl and the bare filename would collapse every session block header to the same string.

Fixtures are built from the real shape on disk: compact JSON lines, ISO-8601 timestamp, and data carrying content, transformedContent, attachments, delivery, agentMode.

What each test protects

Discovery

  • test_discovers_one_events_file_per_session_and_ignores_sidecars — two sessions produce exactly two entries; workspace.yaml and checkpoints/cp1.json stay out.
  • test_subagents_transcripts_are_dropped_by_path_component — an agent-to-agent transcript under subagents/ is dropped, while a directory named my-subagents-notes/ still mines. Catches a substring-vs-component regression in the filter.
  • test_missing_or_empty_root_is_quiet — a missing or empty root returns [] instead of raising.
  • test_registered_in_sources_under_dot_copilot — the root stays ~/.copilot/session-state; source_kind() routes on that path component, so moving it silently reclassifies every session as custom.

Extraction

  • test_mines_typed_content_not_the_harness_expansion — asserts the mined text is data.content, not data.transformedContent. On real logs transformedContent is the model-facing string: the typed prose wrapped in <current_datetime> and <system_reminder> blocks (734 chars of typed text became 1135 chars in one real record). Switching fields would pour harness injections into the corpus.
  • test_compact_and_spaced_serialization_both_mine — the line-marker fast path matches raw text before json.loads, so both "type":"user.message" (what Copilot actually writes) and the spaced form must survive it. Guards USER_LINE_MARKERS.
  • test_system_source_events_are_dropped_and_typed_turns_keptsource: "system" is dropped; source: "user" and an absent source are both kept. The absent case matters: on the Copilot CLI build I checked locally, user.message records carry no source key at all, so an over-eager filter (e.g. requiring source == "user") would mine zero turns from current logs.
  • test_non_user_event_types_are_ignoredsession.info, assistant.message, tool.execution_start, system.message, subagent.started all carry data.content-ish payloads and must not be mined.
  • test_malformed_line_is_skipped_and_later_turns_survive — a torn write, a blank line and a non-JSON line are skipped per line, and a turn written after them is still mined.
  • test_empty_session_yields_nothing_and_is_not_counted — an empty file and a session with no user turns both yield [], and mine_files() counts 0 sessions / 0 messages rather than emitting an empty block.
  • test_injected_context_and_pasted_logs_are_not_mined — a # AGENTS.md instructions prompt, a pasted traceback and a whitespace-only turn drop; the real turn survives.

Identity and pipeline

  • test_source_kind_and_per_session_labelsource_kind is copilot, the label is <session-id>/events.jsonl, and two sessions get distinct labels and distinct stable_session_ids.
  • test_mine_files_tags_records_as_copilot_with_real_dates — end to end: the record is tagged source:copilot, first/last dates come from the event timestamps, and the system-injected turn is absent from the block text.
  • test_redaction_runs_on_copilot_turns — a ghp_ token in a Copilot turn is redacted and counted.

Verification

python -m unittest discover -s tests
Ran 410 tests in 38.015s
OK (skipped=3)

14 tests added by this file, 410 total, all green. The 3 skips are pre-existing.

Found, not fixed

user_messages() wraps the whole file read in one try/except Exception: pass, so a single record with an unexpected field type does not skip that record — it silently ends mining for the rest of the session. Malformed JSON is handled correctly per line (continue), but a parseable record with a bad type is not. Reproduced against the current code with a three-turn file that returned only the first turn, no warning:

  • {"type":"user.message","data":null,...}o.get("data", {}) returns None because the key exists, .get raises AttributeError, and every later turn in that file is lost.
  • data.content as a list — .strip() on a list raises, same truncation.
  • a numeric timestamp (epoch ms instead of ISO) — [:10] on an int raises, same truncation.

None of these shapes appear in the Copilot logs I have locally, so this is latent rather than active, but the same handler covers every source, so any producer that changes a field type would quietly halve someone's corpus. I did not write tests asserting the current truncation behavior, since that would pin a bug in place. A fix would be a per-record try/except inside the loop plus isinstance guards on data, content and timestamp.

🤖 Generated with Claude Code

The Copilot CLI adapter was the only session source with no dedicated
test file. Add tests/test_copilot_source.py covering the three places it
can regress: discovery of ~/.copilot/session-state/<id>/events.jsonl,
extraction of data.content from user.message events, and the
data.source == "system" filter.

Fixtures mirror the real on-disk shape (compact JSON lines, ISO
timestamps, transformedContent alongside content, workspace.yaml and
checkpoints/ sidecars).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ditto Ready Ready Preview, Comment Jul 23, 2026 1:24pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant