Skip to content

fix(streams): enforce repository prompt-sharing policy - #76

Merged
sagnik11 merged 1 commit into
Autter-dev:mainfrom
rajeshaipython-stack:codex/enforce-stream-repository-privacy
Oct 24, 2026
Merged

sagnik11 merged 1 commit into
Autter-dev:mainfrom
rajeshaipython-stack:codex/enforce-stream-repository-privacy

Conversation

@rajeshaipython-stack

@rajeshaipython-stack rajeshaipython-stack commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Streamed session and trace content can currently bypass repository prompt-sharing rules, even though commit-time transcript capture checks those rules.

I reproduced this with the official Windows x64 v2.2.1 release and a dummy transcript, while logged out:

Repository policy Stream cursor Prompt rows in outgoing metrics queue
Allowed control 347 1
Matching prompt exclusion 347 1

This reproduction inspects the local outgoing queue; it does not establish a server-side upload.

The stream worker now checks all repository remotes against the existing capture filters and effective prompt-storage policy before creating content-bearing metrics. It reloads settings per batch, conservatively withholds streams whose repository context cannot satisfy configured restrictions, and advances the cursor for withheld content so enabling sharing later does not replay private history. Events use the worker's telemetry handle directly, making the actual queue path testable.

A separate one-time migration holds older session/trace metrics for explicit sync import consent, including installations that already completed the earlier backlog migration. It preserves their content and leaves ordinary metrics pending. The README documents the behavior and existing consent commands.

Regression coverage includes the real daemon with an allowed control and excluded repository, policy combinations and secondary remotes, and preservation/idempotence of the old-queue hold. Test database paths are pinned explicitly so Windows Known Folders cannot route fixtures into the user's profile.

Validation on Rust 1.93.0 / Linux: task lint, formatting, and task build passed; 20 focused tests passed (8 transcript/config/database unit tests, 1 migration-state unit test, 6 native-daemon privacy integration tests, and 5 upload-hold integration tests). Full validation run.

The Windows release reproduction is confirmed; candidate runtime tests completed on Linux. The temporary validation workflow lives only on a separate fork branch and is not part of this PR.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


View code changes stack in Autter

Summary

Summary generated by Autter.

Why this change

Stream sync could send prompt-bearing data (transcripts and metrics) to the Autter platform even when the repository was excluded from prompt sharing or local prompt storage was enabled. Backlogs queued by earlier versions may already contain such data.

What changes for users

Prompt-sharing policy is now enforced on the stream and telemetry paths, so excluded repositories and local-mode users no longer queue platform-only data. Legacy backlog items are held until the user explicitly imports or deletes them with autter sync import.

Implementation and review notes

  • src/config.rs: effective_prompt_storage is extended, and new unit tests cover the policy resolution (about 56 added test lines).
  • src/daemon/stream_worker.rs: session attributes are now set differently for shared versus non-shared streams. Reviewers should confirm that the non-shared branch cannot leak transcript content.
  • src/upload_hold.rs: hold_all, release_all and ensure_legacy_migration manage held backlog items, using HELD_RETRY_AT = i64::MAX so held items are never picked up for retry.
  • src/metrics/db.rs: hold_all moves queued events into metrics_held in a single transaction, then clears metrics. Check that the INSERT and DELETE stay atomic and scoped to the intended rows.
  • tests/integration/local_mode_privacy.rs and tests/integration/upload_hold.rs are new end-to-end checks against autter sync status --json. src/lib.rs is a high-churn hotspot touched indirectly through its dependents.

Changes

  • src/config.rs: extends effective_prompt_storage and adds unit tests for the prompt-sharing policy resolution.
  • src/daemon/stream_worker.rs: branches on shared-stream state when building session attributes, so non-shared streams follow the policy.
  • src/daemon/telemetry_worker.rs and src/metrics/db.rs: small additions to the telemetry submission path, plus hold_all, which moves queued metrics events into the metrics_held table.
  • src/upload_hold.rs: adds hold, release and one-time legacy migration handling (ensure_legacy_migration, hold_all, release_all) for backlog items queued by autter 2.2.0 and earlier.
  • Tests: new tests/integration/local_mode_privacy.rs cases, a new tests/integration/upload_hold.rs suite, and helper updates in tests/integration/repos/test_repo.rs.
  • README.md: four lines of documentation.

Acceptance Criteria

  • In local prompt-storage mode, capturing an agent session results in no transcript or metrics entries in autter sync status --json upload queues.
  • For a repository excluded from prompt sharing, no transcript data is queued for upload.
  • Existing backlog from autter 2.2.0 or earlier is held after upgrade and is not uploaded until autter sync import is run.
  • autter onboard --connect without --yes leaves the existing backlog held.
  • task test and task lint pass with no new failures.

Test Plan

  • Run task dev to install the local debug build.
  • Run autter onboard --local, capture an agent session, then check autter sync status --json for queued transcripts or metrics (expect none).
  • Mark a repository as excluded from prompt sharing, capture a session in it, and confirm no transcript is queued.
  • Seed a backlog from the pre-upgrade version, run autter onboard --connect without --yes, and confirm the backlog stays held.
  • Run autter sync import and confirm held items are released as expected.
  • Run autter doctor in local mode and confirm it reports that uploads are disabled.
  • Run task test and task lint before opening the PR.

Rollback Plan

  • Revert the commit for this PR, which restores the previous stream and telemetry behavior.
  • Held items are kept in metrics_held and the held queue tables rather than deleted, so reverting does not lose data. Use autter sync import or delete on the held items if they should not be kept.
  • If only the hold logic is at fault, revert the src/upload_hold.rs and src/metrics/db.rs changes and keep the config and stream gating.

Related Issues

No linked issue was identified in the branch name, PR title, or commit context.

Written for commit a7c1eb0. Summary will update on new commits.

@autter-dev

autter-dev Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Couldn't fully check this: a test suite this PR doesn't touch was already failing

Nothing Autter could check blocks merging, but some checks didn't run. See “Autter couldn't check everything” below.

  • 🔄 Re-run review

What we checked

✅ Code review: No problems found. 3 minor suggestions.
❌ Build and tests: 1 test covering code this change affects fails (tests/integration/local_mode_privacy.rs).
⚠️ Installed and used the package like a user: 12 of 12 simulated users failed the same check with and without this PR, so the check itself likely expects the wrong result. Not a problem in your code.
📸 See the failing step on the Walkthrough tab

⚙️ Autter couldn't check everything

These are problems with Autter's setup, not with your code.

  • Couldn't run the autter tests. The test output provided is truncated and does not show any failed tests; all visible tests passed successfully.
    👉 Nothing to do for this PR. That suite fails without your change too.

Plus 3 minor suggestions (style, conventions, hardening) in the sections below. They don't block merging.


🧭 Installed and used the package like a user: take a look

📸 See the failing step on the Walkthrough tab

12 of 12 simulated users failed the same check with and without this PR, so the check itself likely expects the wrong result. Not a problem in your code.

Revision: a7c1eb04e370

This change is a package or command-line tool, so Autter installed it the way a user receives it into a clean project and used it, rather than only running the repo's own tests.

Database for .: built from Autter's index of your schema (the repo has no migrate command), 21 tables; seeded 79 rows in 21 tables.

Simulated users: 12 simulated command-line users, compared with the code before this PR (344bc7f). Each one has a different habit: follows the docs, mistypes, double-clicks, and so on.

Goal With this PR Before this PR Result
A user checks what the autter CLI can do 0/6 0/6 The check likely expects the wrong result
A user who wants their authorship data to stay on the machine opens the… 0/6 0/6 The check likely expects the wrong result

A user checks what the autter CLI can do: The check likely expects the wrong result. Every simulated user failed the same check with and without this PR, without crashing (exit_code: exit code 1 (wanted 0)), so the goal's check likely expects the wrong result.

  • 5 of 6 simulated users: exit_code: exit code 1 (wanted 0) (Follows the docs, Runs it twice, Mistypes a command, Presses Ctrl-C halfway, Runs in CI; happened again on a retry; also before this PR).
  • 1 of 6 simulated users: exit_code: no copy of 2: exit code 1 (wanted 0) (Runs it twice at once; happened again on a retry; also before this PR).

A user who wants their authorship data to stay on the machine opens the…: The check likely expects the wrong result. Every simulated user failed the same check with and without this PR, without crashing (exit_code: exit code 1 (wanted 0)), so the goal's check likely expects the wrong result.

  • 6 of 6 simulated users: exit_code: exit code 1 (wanted 0) (Follows the docs, Runs it twice, Mistypes a command, Runs in CI, Reads --help first, Forgets an argument; happened again on a retry; also before this PR).

Package walk of autter: autter-test-git-shim looks like the project's own tooling (a test helper, shim or fixture that the README doesn't mention), so the walk didn't run it.

Steps Autter took (4)
  1. check “autter: packages with cargo package” — completed
  2. check “autter: a clean consumer crate builds against the packaged crate” — completed
  3. check “autter: the binaries build from the packaged crate” — completed
  4. check “autter: autter --help runs” — completed

Live view: streamed directly from the walk's runner.

Trying the change like a user doesn't block merging.

🧪 Test run: failed

Autter checked a7c1eb04.

150 tests ran: 149 passed, 1 failed.

  • ⚠️ Functional: couldn't run: a test suite this PR doesn't touch was already failing
  • ❌ Regression: 1 of 146 affected test files (of 228 in the repo) broke
  • ✅ Tests Autter wrote: 4 written by Autter for this change, all passed

What failed

  • ❌ the tests failed before any test reported a result (tests/integration/local_mode_privacy.rs): The test runner exited prematurely before completing the suite, as indicated by the truncation of the output mid-stream rather than a specific test failure. (Autter couldn't check whether it failed before this PR: the base commit's run failed without naming a failing test, so it can't say whether this failure is new.)

Autter couldn't run your tests because…

  • A test suite this PR doesn't touch was already failing (functional).
    👉 Nothing to do for this PR. That suite fails without your change too.

This is about Autter's setup, not your code.

✅ 149 passed

tests/db_autter.rs · tests/config_autter.rs · tests/telemetry_worker_autter.rs · tests/stream_worker_autter.rs

Full test report

This PR changes 8 source files. It ran the project's test command for the workspace this PR touched, because those suites are the fastest check that the change did not break existing behavior.

Targeted agent checks: Autter also checks the change itself: it executes the test-plan items the project's suites do not cover, and it writes a temporary test for each changed file that has no test. That session stopped before it reported anything, because it stopped with an error. Nothing from it is included below — only the project's own test commands are reported. As a result, 8 changed files and 7 test-plan items stayed unchecked.

Execution summary: 4 checks executed · 2 passed · 2 failed.

Why suites failed

  • autter — cargo test: the failing test names no file this PR changes, so it most probably fails on the base branch too.
    First error: test api::types::tests::test_cas_upload_result_with_error ... ok

Project test commands

Scope Command Result
autter cargo test ❌ failed · 85s
  • ❌ autter: 5 failing tests: git::authorship_traversal::tests::test_batch_read_blobs_with_oids_empty, git::authorship_traversal::tests::test_commits_have_authorship_notes_empty, git::authorship_traversal::tests::test_commits_have_authorship_notes_nonexistent, git::authorship_traversal::tests::test_load_ai_touched_files_empty_commits, git::authorship_traversal::tests::test_load_ai_touched_files_for_nonexistent_commit. Repeat with: cargo test

Code-level simulations

Revision a7c1eb04e370 · 1157.0s · up to 2 scenarios in parallel

  • Stream upload honors exclusions across every repository remote — failed (code_diff, code_diff, spec)
    Source: src/config.rs — self.is_allowed_repository_with_remotes(remotes) && self.effective_prompt_storage_with_remotes(remotes) != PromptStorageMode::Local
    Source: src/daemon/stream_worker.rs — let upload_allowed =
    config::Config::fresh().should_upload_transcript_with_remotes(remotes.as_ref());
    Source: PR description — The stream worker now checks all repository remotes against the existing capture filters and effective prompt-storage policy
    Ran: python3 '.autter/scratch/stream_upload_exclusions.py'
    ✅ The excluded fixture's cursor advances but its private marker is absent from queued metrics.: expected cursor_advanced=true, queue_observed=true, and marker_queued=false; observed cursor_advanced=True (watermark=397); queue_observed=True; marker_queued=False (matching_queue_rows=0) (code)
    ❌ The allowed control queues the marker, confirming the transcript reached the worker and the queue assertion is live.: expected marker_queued=true in the local outgoing metrics queue; observed queue_observed=True; marker_queued=False (matching_queue_rows=0); cursor_advanced=True (code)
    ✅ No assertion claims server-side upload; all observations are from the local outgoing queue.: expected all observations come from the isolated local outgoing queue; no server-side upload claim; observed observations queried only the local metrics SQLite queue; no server response or upload result was used (code)
    Root cause (confirmed): Confirmed harness defect: in .autter/scratch/stream_upload_exclusions.py:248, queue_observed is set true when metrics-db does not exist (queue_observed or not metrics_path.exists()). Thus the reported queue_observed=true does not establish that the outgoing queue was opened or queried; the cursor proves only that the stream reader advanced. No changed-code cause for the allowed control's missing marker is confirmed. The stream worker can withhold events at the new privacy gate, or the event can fail to reach durable storage / be uploaded by an authenticated client, and the supplied observations do not distinguish these paths.
    src/daemon/stream_worker.rs:947 — The worker resolves repository remotes from the checkpoint work directory; if lookup fails, remotes becomes None.
    src/config.rs:665 — The changed predicate permits upload only when platform sync is enabled and the remotes satisfy repository and effective prompt-storage policy; unknown remotes with configured restrictions are denied.
    src/daemon/stream_worker.rs:969 — The reader obtains a batch and applies the predicate before converting raw transcript events to MetricEvents.
    src/daemon/stream_worker.rs:1043 — Non-empty converted batches are handed directly to the daemon telemetry buffer; the watermark update at lines 1068-1075 happens regardless of whether upload was allowed, so cursor advancement is not evidence of queue insertion.
    src/daemon/telemetry_worker.rs:541 — Metric events are persisted locally when sync is enabled and no live authenticated upload succeeds; the branch depends on client authentication.
    src/daemon/telemetry_worker.rs:815 — Durable persistence serializes each MetricEvent and inserts it into the metrics table, which is the integration boundary the harness queries.
    .autter/scratch/stream_upload_exclusions.py:233 — The fixture checks for a matching SQLite row, but its reported queue_observed value is not a trustworthy indication that the DB was present or queried.
    .autter/scratch/stream_upload_exclusions.py:248 — The or not metrics_path.exists() term turns a missing metrics DB into queue_observed=true, invalidating the claimed live allowed control.
    Suggested fix: Make queue_observed true only after the metrics DB exists and the SELECT succeeds; report DB absence/query errors separately. For the allowed control, force or inject an unauthenticated API client so a successful live upload cannot consume events before the queue assertion, then assert that a transcript MetricEvent containing the marker reaches telemetry flush and is inserted. Add a diagnostic for the resolved remotes and upload_allowed decision so an empty queue can be attributed to policy versus producer/flush behavior.
    Regression: Identical harness and assertions failed on revision 344bc7f.
    Indexed blast radius: 1 direct / 2 transitive dependents (indexed 2026-10-19T03:12:06.473Z).
  • Unknown repository context is withheld without replay after policy changes — execution incomplete (code_diff, code_diff, spec, spec)
    Source: src/config.rs — if remotes.is_none()
    && (self.has_repository_filters()
    || !self.exclude_prompts_in_repositories.is_empty()
    || !self.include_prompts_in_repositories.is_empty())
    {
    return false;
    Source: src/daemon/stream_worker.rs — Still advance the cursor for withheld content so enabling sharing
    // later does not silently replay previously private conversations.
    Source: README.md — When repository restrictions are configured, streams without enough repository context to apply those rules are not uploaded.
    Source: PR description — It reloads settings per batch, conservatively withholds streams whose repository context cannot satisfy configured restrictions, and advances the cursor for withheld content so enabling sharing later does not replay private history.
    Ran: python3 '.autter/scratch/unknown_repository_privacy.py'
    Indexed blast radius: 1 direct / 2 transitive dependents (indexed 2026-10-19T03:12:06.473Z).
    Could not complete: Simulation did not emit exactly one completed assertion report.
  • Local prompt-storage mode queues neither streamed content nor submitted metrics — passed (code_diff, code_diff, code_diff, spec)
    Source: src/config.rs — if !self.platform_sync_enabled() {
    return false;
    }
    Source: src/daemon/stream_worker.rs — .filter(|_| upload_allowed)
    Source: tests/integration/local_mode_privacy.rs — local_mode_daemon_drops_metrics_instead_of_queueing_them
    Source: PR description — In local prompt-storage mode, capturing an agent session results in no transcript or metrics entries in autter sync status --json upload queues.
    Ran: python3 '.autter/scratch/local_prompt_storage_mode.py'
    ✅ The marker is absent from queued metrics and pending metrics remain zero in local mode.: expected sync status pending.metrics = 0 and queued metrics containing the transcript marker = 0; observed pending.metrics=0; queued_marker_rows=0; status_observed=True; metrics_queue_observed=True; unavailable=none (code)
    ✅ The session stream is consumed without placing platform-only content in the outgoing queue.: expected stream watermark reaches transcript byte length and queued metrics containing the marker = 0; observed watermark=344; transcript_bytes=344; queued_marker_rows=0; checkpoint=accepted; unavailable=none (code)
    Indexed blast radius: 1 direct / 2 transitive dependents (indexed 2026-10-19T03:12:06.473Z).
  • Legacy migration holds only session and trace metrics, preserving other rows — passed (code_diff, code_diff, code_diff, spec)
    Source: src/metrics/db.rs — WHERE CASE WHEN json_valid(event_json) THEN json_extract(event_json, '$.e') END IN (?1, ?2)
    Source: src/upload_hold.rs — let transcripts_pending =
    state.transcript_privacy_migration < TRANSCRIPT_PRIVACY_MIGRATION_VERSION;
    Source: tests/integration/upload_hold.rs — transcript_backlog_from_completed_legacy_migration_requires_consent
    Source: PR description — A separate one-time migration holds older session/trace metrics for explicit sync import consent, including installations that already completed the earlier backlog migration.
    Ran: python3 '.autter/scratch/upload_hold_harness.py'
    ✅ Only the e=5 and e=6 rows move to the held queue; their event JSON content is preserved.: expected held event_json rows exactly ['{"t":2,"e":5,"v":{"payload":"session-original"},"a":{}}', '{"t":3,"e":6,"v":{"payload":"trace-original"},"a":{}}']; held count/status count 2; observed held rows=['{"t":2,"e":5,"v":{"payload":"session-original"},"a":{}}', '{"t":3,"e":6,"v":{"payload":"trace-original"},"a":{}}'], held_count=2, status_held=2, import_exit=0, db_error=None (code)
    ✅ Ordinary valid rows and malformed legacy JSON remain pending and do not block the migration.: expected pending event_json rows exactly ['{"t":1,"e":1,"v":{"payload":"ordinary"},"a":{}}', '{"t":4,"e":7,"v":{"payload":"other"},"a":{}}', '{malformed legacy json']; pending count/status count 3; observed pending rows=['{"t":1,"e":1,"v":{"payload":"ordinary"},"a":{}}', '{malformed legacy json', '{"t":4,"e":7,"v":{"payload":"other"},"a":{}}'], pending_count=3, status_pending=3, status_json_found=True, import_exit=0, import_output='Held for your consent: 2 telemetry events, 0 authorship notes, 0 commit summaries, 0 transcripts, 0 file-change records.\nThese were queued before you connected this machine, or by an older autter\nversion, and may include transcripts recorded in local mode or from\nrepositories excluded from prompt sharing.\n\nUpload them: autter sync import --yes\nDelete them: autter sync purge --force', db_error=None (code)
    ✅ A second migration attempt does not hold the newly added event or move the previously pending rows.: expected held rows remain ['{"t":2,"e":5,"v":{"payload":"session-original"},"a":{}}', '{"t":3,"e":6,"v":{"payload":"trace-original"},"a":{}}']; pending rows become ['{"t":1,"e":1,"v":{"payload":"ordinary"},"a":{}}', '{"t":4,"e":7,"v":{"payload":"other"},"a":{}}', '{"t":5,"e":5,"v":{"payload":"session-after-migration"},"a":{}}', '{malformed legacy json']; held/pending counts and status are 2/4; observed held rows=['{"t":2,"e":5,"v":{"payload":"session-original"},"a":{}}', '{"t":3,"e":6,"v":{"payload":"trace-original"},"a":{}}'], pending rows=['{"t":1,"e":1,"v":{"payload":"ordinary"},"a":{}}', '{malformed legacy json', '{"t":4,"e":7,"v":{"payload":"other"},"a":{}}', '{"t":5,"e":5,"v":{"payload":"session-after-migration"},"a":{}}'], held/pending counts=2/4, status held/pending=2/4, status_json_found=True, import_exit=0, db_error=None (code)
    Indexed blast radius: 1 direct / 2 transitive dependents (indexed 2026-10-19T03:12:06.473Z).
    Source unavailable: Production traffic data was not available for this run.
    Coverage limitation: No production runtime patterns or production IDs were supplied, so scenarios rely on the checked-out code and the PR description only.
    Coverage limitation: No disposable Postgres is required: the changed paths under test persist these upload queues in local SQLite and use isolated daemon/repository fixtures.
    Coverage limitation: Queue assertions intentionally do not verify server receipt; the PR description explicitly states that the reported reproduction observes only the local outgoing queue.
    Coverage limitation: Scenario omitted because its source evidence could not be verified: Explicit sync import consent releases preserved transcript backlog
    Coverage limitation: Scenario omitted because its source evidence could not be verified: Non-interactive connect without yes keeps legacy session content held

Declared tests: 4 test file(s) found — 0 ran, 4 not observed in suite output, 0 did not run.

"Not observed" means a test command ran but never named the file in its output. A runner that stops at the first failure, shards its files, or prints only a summary produces this. Autter cannot confirm these tests ran, so it does not count them as passed. The autter command failed here, so the files after the failure most probably never started.

⚠️ Test cases that did not run
  • ❔ agent-support/vscode/src/test/extension.test.ts (1 case) — its suite (autter) failed before it reported this file
  • ❔ agent-support/vscode/src/test/semver.test.ts (5 cases) — its suite (autter) failed before it reported this file
  • ❔ agent-support/vscode/src/test/vscode-hooks.test.ts (2 cases) — its suite (autter) failed before it reported this file
  • ❔ npm/install.test.js (3 cases) — its suite (autter) failed before it reported this file

Tests for this change

Autter wrote no test for this change. The session that writes them stopped before it reported anything, because it stopped with an error. 8 changed source files therefore have no execution-backed check in this run.

Changed files with no execution-backed check
  • src/config.rs
  • src/daemon/stream_worker.rs
  • src/daemon/telemetry_worker.rs
  • src/metrics/db.rs
  • src/upload_hold.rs
  • tests/integration/local_mode_privacy.rs
  • tests/integration/repos/test_repo.rs
  • tests/integration/upload_hold.rs

Change coverage (is each changed file's behavior verified by a test?)

Changed file Related test Result
src/config.rs src/config.rs ⬜ not verifiable
src/daemon/stream_worker.rs tests/integration/local_mode_privacy.rs ⬜ not verifiable
src/daemon/telemetry_worker.rs tests/integration/local_mode_privacy.rs ⬜ not verifiable
src/metrics/db.rs src/metrics/db.rs ⬜ not verifiable
src/upload_hold.rs tests/integration/upload_hold.rs ⬜ not verifiable
tests/integration/local_mode_privacy.rs tests/integration/local_mode_privacy.rs ⬜ not verifiable
tests/integration/repos/test_repo.rs tests/integration/local_mode_privacy.rs ⬜ not verifiable
tests/integration/upload_hold.rs tests/integration/upload_hold.rs ⬜ not verifiable
🤖 Coverage-check evidence

src/config.rs

src/daemon/stream_worker.rs

src/daemon/telemetry_worker.rs

src/metrics/db.rs

src/upload_hold.rs

tests/integration/local_mode_privacy.rs

tests/integration/repos/test_repo.rs

tests/integration/upload_hold.rs

Temporary tests are written under .autter/scratch/ for verification only — they are never committed to the repository.

Test plan (from the PR description)

  • ⬜ Run task dev to install the local debug build. — needs manual verification
  • ⬜ Run autter onboard --local, capture an agent session, then check autter sync status --json for queued transcripts or metrics (expect none). — needs manual verification
  • ⬜ Mark a repository as excluded from prompt sharing, capture a session in it, and confirm no transcript is queued. — No run covers repository exclusion or session capture steps.
  • ⬜ Seed a backlog from the pre-upgrade version, run autter onboard --connect without --yes, and confirm the backlog stays held. — needs manual verification
  • ⬜ Run autter sync import and confirm held items are released as expected. — needs manual verification
  • ⬜ Run autter doctor in local mode and confirm it reports that uploads are disabled. — needs manual verification
  • ⬜ Run task test and task lint before opening the PR. — Run used cargo test, not task test or task lint.

⬜ items could not be verified automatically and still need a manual check.

💡 More findings (1): suggestions and ones Autter couldn't confirm
💡 1 minor suggestion: conventions, hardening and tidiness (not bugs)

Comments on the code are kept for real bugs. These are things worth knowing that the change doesn't prove wrong: a missing safety layer, a convention the repo usually follows, a file that usually changes together with one you touched. Take what fits and ignore the rest. They don't block merging.

  • 🟠 Unhandled state / input (risk 69/100) — src/daemon/stream_worker.rs:949

🔎 3 low-signal finding(s) (test-file conventions, hardening advice on internal routes, packages the registry knows) were kept off this PR — see the Autter dashboard.

🚦 Pre-merge checks: 2 worth a look, 163 passed
Check Status Explanation
PII stored without application-level encryption ⚠️ Worth a look 1 potential issue(s) detected (max risk 100/100): tests/integration/local_mode_privacy.rs:186.
Overbroad try/catch swallowing all exceptions ⚠️ Worth a look 2 potential issue(s) detected (max risk 39/100): tests/integration/local_mode_privacy.rs:208, tests/integration/upload_hold.rs:82.
✅ 163 passed

Too many files changed · Too many lines changed · Too many unrelated chapters · Generated files hiding real changes · Missing PR context · Mixed concerns (refactor + behavior change) · Migration + app logic + UI combined in one PR · Sensitive data in logs · Log injection · Missing audit logging · Removed observability · Rate limiting not detected · Rate limiting removed · Batch size limit not detected · Pagination not detected · Merge-blocking marker left in the change · Known-defect marker shipped in code · Untracked TODO without an issue reference · Test disabled or left pending · Redundant alias / duplicate import · Redundant type construct · Unnecessary type assertion · Module smell · Dead export (no callers) · Code duplication / DRY violation · Silent exception swallowing · Unhandled promise rejection · Circuit breaker not detected · Stack trace leakage · Multi-write without detected transaction · Possible TOCTOU in critical path · Idempotency key not detected · Possible non-atomic read-modify-write · Optimistic locking not detected · Prompt injection risk · LLM output used in a dangerous sink · Sensitive data in prompt or system-prompt leakage · Over-privileged LLM tool / excessive agency · Missing validation on an LLM-driven decision · Unbounded LLM usage (denial-of-wallet) · Table exposed without row-level security · Over-broad row-level security policy · Code path that bypasses row-level security · Privileged database credential reachable from the client · Privileged query without row-level scoping · Row-level security enabled with no policy · Template-default gradient styling · Interchangeable AI marketing copy · Placeholder content shipped to users · Emoji standing in for an icon system · Call-to-action that goes nowhere · Templated page composition · Color outside the brand palette · Hardcoded style bypassing design tokens · Typography outside the brand type system · One-off UI instead of the shared component · Copy that does not match brand voice · Screen does not match the rest of the product · PII in logs · PII or internals leaked in error response · User data stored without retention controls · PII sent to external / cross-border destination · Lockfile resolution / integrity tampered · Dependency runs install-time lifecycle script · Possible dependency-confusion attack · Lockfile resolves a dependency the manifest does not declare · Checked-in build artefact modified without source change · Dockerfile build-step is insecure · External artefact pulled in without integrity pinning · Changed export, importer not updated · Missing linked tracker issue · Missing CODEOWNERS reviewer approval · Missing security-team review on sensitive path · Source changes without matching tests · Migration missing rollback / down step · Frontend importing database client directly · Route handler bypassing service layer · Backend service importing UI module · Cross-context internals import · Workspace package rule violation · Inconsistent logging pattern · Inconsistent error handling · Endpoint missing input validation · Multi-write without transaction wrapper · New feature shipped without feature flag · Module placed in the wrong workspace package · Hallucinated import (package not installed) · Nonexistent package (not found in registry) · Call to function that does not exist · Generic placeholder identifier in production logic · Repetitive boilerplate (duplicated block) · TODO / FIXME on critical path · Comment contradicts or fabricates code behaviour · Abstraction defined but never used · Code style differs from rest of codebase · Established pattern ignored · Unhandled edge case (null / empty / zero / boundary) · Insecure training-data / doc-example pattern · Dead code (defined but never referenced) · Deprecated API call · API pattern from wrong library version · API endpoint removed · HTTP method changed (GET ↔ POST etc.) · New required field added to request · Field removed from response schema · Response field type changed · HTTP status code changed · Auth requirement added / removed / changed · Error response shape changed · Pagination behaviour changed · Outbound webhook payload schema changed · GraphQL field removed without deprecation · GraphQL enum value removed · Connected repositories · Vendor API consumer · SQL injection · Cross-site scripting (XSS) · Path traversal · Command injection · Insecure deserialization · Weak cryptography · Hardcoded secret · Insecure randomness for security material · Unsafe file upload · Missing input validation · Unsafe CORS configuration · Unsafe / open redirect · Missing CSRF protection · Unsafe cookie / session settings · Sensitive data exposure · Invisible Unicode / Trojan Source characters · API key in source · Access token in source · Private key in source · Database connection URL with embedded credentials · Cloud credential in source · Webhook signing secret in source · OAuth client secret in source · JWT signing secret in source · Hardcoded password · Auth middleware removed from route · Route protection changed (protected → public) · Permission / RBAC check removed · Required role weakened · Admin-only route exposed to lower privilege · Token validation skipped in middleware chain · JWT verification weakened or changed · Session expiration / TTL changed · Password reset flow changed · OAuth callback / redirect handling changed · Webhook endpoint missing signature verification · Public route touches private/PII data · Code correctness issue · Runtime error risk · Resource leak risk · Data integrity risk · Maintainability issue · Co-change coupling · Scanner suppressions added · Scanner settings changed · Complexity Guard · Release Notes Curator · Intent vs. implementation

☑️ Task list (0)

No concrete follow-up tasks were generated for this PR.

Generated from PR diff, blast radius, and context.

Issues found

No unresolved code findings remain for this revision — 17 finding(s) dismissed as likely false positives by verification. Dismissed findings stay listed with their verdicts in the Autter review dashboard.

4 additional non-blocking finding(s) are available in the Autter review.

🤖 Release Notes Curator

Release impact: minor. The PR is titled as a fix, but it adds user-visible behavior: session and trace events queued by older versions are held once for consent after upgrade, and streams without repository context are skipped when repository sharing rules are configured. I applied the existing release: minor label.

Changelog draft:

Repository prompt-sharing rules now apply to streamed sessions

On connected machines, streamed session content now follows the same repository prompt-sharing rules as commit-time capture. Repositories matched by exclude_prompts_in_repositories no longer have their session content uploaded, and streams that can't be matched to a repository are skipped when repository rules are configured. After upgrading, session and trace history queued by earlier versions is held once for your review. Run autter sync status to see it, then autter sync import --yes to upload it or autter sync purge --force to discard the backlog.

Custom agent · runs after review · configured in Autter

📄 What this PR changes

Prompt-sharing policy is now enforced on the stream and telemetry paths, so excluded repositories and local-mode users no longer queue platform-only data. Legacy backlog items are held until the user explicitly imports or deletes them with autter sync import.

  • src/config.rs: effective_prompt_storage is extended, and new unit tests cover the policy resolution (about 56 added test lines).
  • src/daemon/stream_worker.rs: session attributes are now set differently for shared versus non-shared streams. Reviewers should confirm that the non-shared branch cannot leak transcript content.
  • src/upload_hold.rs: hold_all, release_all and ensure_legacy_migration manage held backlog items, using HELD_RETRY_AT = i64::MAX so held items are never picked up for retry.
  • src/metrics/db.rs: hold_all moves queued events into metrics_held in a single transaction, then clears metrics. Check that the INSERT and DELETE stay atomic and scoped to the intended rows.
  • tests/integration/local_mode_privacy.rs and tests/integration/upload_hold.rs are new end-to-end checks against autter sync status --json. src/lib.rs is a high-churn hotspot touched indirectly through its dependents.
⚙️ Details: what Autter couldn't run
Tests failed: autter (Cargo.toml)

🟡 [deterministic] Tests failed: autter — Risk: 40/100

Autter couldn't check the autter test suite (cargo test in .). This is a problem with Autter's setup, not with this PR.

What happened: The test output provided is truncated and does not show any failed tests; all visible tests passed successfully.

How to fix: Check if the test run was interrupted or if the full output contains failures further down. Verify the CI logs for a complete test summary.

Show details (test output)
ls_with_mock ... ok
test auth::credentials::tests::test_legacy_database_credential_is_sanitized_and_refresh_preserved ... ok
test auth::credentials::tests::test_load_nonexistent_with_mock ... ok
test auth::credentials::tests::test_load_error_handling ... ok
test auth::credentials::tests::test_login_store_clears_the_refusal ... ok
test auth::credentials::tests::test_file_backend_sets_unix_permissions ... ok
test auth::credentials::tests::test_file_backend_store_load_clear ... ok
test auth::credentials::tests::test_overwrite_credentials_with_mock ... ok
test auth::credentials::tests::test_mark_refresh_failed_marks_only_the_refused_token ... ok
test auth::credentials::tests::test_mark_refresh_failed_keeps_the_first_refusal ... ok
test auth::credentials::tests::test_file_backend_creates_directory ... ok
---
test auth::notice::tests::notice_interval_parse_roundtrip ... ok
test auth::notice::tests::pending_upload_remains_distinct_from_an_earlier_receipt ... ok
test auth::notice::tests::upload_failure_names_the_queue_and_error ... ok
test auth::types::tests::test_access_token_exactly_at_boundary ... ok
test auth::types::tests::test_access_token_expired ... ok
test auth::types::tests::tes…
🛠 AI fix prompt (copy & paste into your coding agent)
The autter test suite (`cargo test` in `.`) fails. The test output provided is truncated and does not show any failed tests; all visible tests passed successfully. Fix: Check if the test run was interrupted or if the full output contains failures further down. Verify the CI logs for a complete test summary.

Flagged by Autter security & observability checks.

📖 What these words mean
  • Must fix: Blocks merging. Autter is confident this is a real problem in this PR.
  • Should fix: Doesn't block merging, but is likely a real problem worth fixing.
  • Minor: Style, conventions or hardening ideas. Fine to skip.
  • Simulated users: Autter runs your app, package or CLI the way people would, each with a different habit (follows the docs, mistypes a command, …).
  • Already broken before this PR: The same failure happens on the code before this PR, so this PR didn't cause it.
  • We couldn't tell: Some runs failed, but not clearly enough to blame this PR.
  • Doesn't block merging: Shown for your information; the merge check ignores it.

View this PR in Autter · Checked commit a7c1eb0 · Tick Re-run review to check this commit again

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autter left 1 comment on the code below (🟡 1). The autter/review-gate check says whether anything blocks merging. Each comment has a prompt you can paste into your coding agent.

Comment thread README.md

Installation lets you choose **local-only** mode or **connect to the Autter platform**, and the choice is reversible at any time with `autter onboard`. Local-only attribution, blame, and stats run entirely on your machine — no code, prompts, or usage data ever leave it.

On connected machines, repository prompt-sharing rules also apply to streamed session content. For example, `autter config --add exclude_prompts_in_repositories "https://github.com/acme/private-*"` keeps matching repositories' session content out of the upload queue. When repository restrictions are configured, streams without enough repository context to apply those rules are not uploaded.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [deterministic] markdownlint: MD013 — Risk: 30/100

Line length: Expected: 80; Actual: 393

🛠 AI fix prompt (copy & paste into your coding agent)
Fix the markdownlint `MD013` issue at README.md:69: Line length: Expected: 80; Actual: 393

Flagged by Autter security & observability checks.

@autter-dev

autter-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

🧪 Tests Autter wrote for this change

Autter wrote 6 test files for the changed code that had no test, ran them on the base commit and on this PR, and kept 4: 4 files passed.

The tests

tests/db_autter.rs — 5 cases, ✅ passes · covers what this PR adds

Runs with cargo test --quiet --test db_autter.

use autter::metrics::db::MetricsDatabase;
use autter::metrics::types::MetricEventId;
use std::sync::{MutexGuard, Once};

static INIT: Once = Once::new();

/// Pin every database path to a per-process temp directory before the
/// global singleton is first created, so no test touches the real profile.
fn init_test_paths() {
    INIT.call_once(|| {
        let dir = std::env::temp_dir().join(format!(
            "autter-db-hold-test-{}",
            std::process::id()
        ));
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).expect("failed to create test db dir");
        let db_path = dir.join("metrics-db");
        // SAFETY: runs once, before any other thread reads these variables
        // through the global database initializer.
        unsafe {
            std::env::set_var("AUTTER_TEST_METRICS_DB_PATH", &db_path);
            std::env::set_var("HOME", &dir);
            std::env::set_var("USERPROFILE", &dir);
        }
    });
}

/// Lock the shared database and reset it so each test starts empty.
fn fresh_db() -> MutexGuard<'static, MetricsDatabase> {
    init_test_paths();
    let mutex = MetricsDatabase::global().expect("failed to open metrics database");
    let mut guard = mutex.lock().unwrap_or_else(|e| e.into_inner());
    guard.delete_all().expect("failed to reset metrics database");
    guard
}

fn session_json(seq: u32) -> String {
    format!(
        r#"{{"e":{},"seq":{}}}"#,
        MetricEventId::SessionEvent as u16,
        seq
    )
}

fn trace_json(seq: u32) -> String {
    format!(
        r#"{{"e":{},"seq":{}}}"#,
        MetricEventId::OtelTrace as u16,
        seq
    )
}

fn ordinary_json(seq: u32) -> String {
    format!(r#"{{"e":9999,"seq":{}}}"#, seq)
}

#[test]
fn new_hold_transcript_events_moves_only_session_and_trace_rows() {
    let mut db = fresh_db();
    let session = session_json(1);
    let trace = trace_json(2);
    let ordinary = ordinary_json(3);
    let invalid = "not json".to_string();
    db.insert_events(&[
        session.clone(),
        ordinary.clone(),
        trace.clone(),
        invalid.clone(),
    ])
    .unwrap();

    let held = db.hold_transcript_events().unwrap();

    assert_eq!(held, 2);
    assert_eq!(db.count_held().unwrap(), 2);
    assert_eq!(db.count().unwrap(), 2);
    let remaining: Vec<String> = db
        .get_batch(10)
        .unwrap()
        .into_iter()
        .map(|r| r.event_json)
        .collect();
    assert_eq!(remaining, vec![ordinary.clone(), invalid.clone()]);

    let released = db.release_held().unwrap();
    assert_eq!(released, 2);
    let after_release: Vec<String> = db
        .get_batch(10)
        .unwrap()
        .into_iter()
        .map(|r| r.event_json)
        .collect();
    assert_eq!(after_release, vec![ordinary, invalid, session, trace]);
}

#[test]
fn new_hold_transcript_events_is_idempotent() {
    let mut db = fresh_db();
    db.insert_events(&[session_json(1), ordinary_json(2)])
        .unwrap();

    assert_eq!(db.hold_transcript_events().unwrap(), 1);
    assert_eq!(db.hold_transcript_events().unwrap(), 0);

    assert_eq!(db.count_held().unwrap(), 1);
    assert_eq!(db.count().unwrap(), 1);
}

#[test]
fn new_hold_transcript_events_empty_queue_returns_zero() {
    let mut db = fresh_db();

    assert_eq!(db.hold_transcript_events().unwrap(), 0);

    assert_eq!(db.count().unwrap(), 0);
    assert_eq!(db.count_held().unwrap(), 0);
}

#[test]
fn new_hold_transcript_events_leaves_ordinary_only_queue_untouched() {
    let mut db = fresh_db();
    let ordinary = ordinary_json(1);
    db.insert_events(&[ordinary.clone()]).unwrap();

    assert_eq!(db.hold_transcript_events().unwrap(), 0);

    assert_eq!(db.count().unwrap(), 1);
    assert_eq!(db.count_held().unwrap(), 0);
    let records = db.get_batch(10).unwrap();
    assert_eq!(records.len(), 1);
    assert_eq!(records[0].event_json, ordinary);
}

#[test]
fn new_hold_transcript_events_preserves_json_without_event_key() {
    let mut db = fresh_db();
    let no_event_key = r#"{"x":1}"#.to_string();
    let array_json = "[1,2]".to_string();
    db.insert_events(&[no_event_key.clone(), array_json.clone(), session_json(7)])
        .unwrap();

    assert_eq!(db.hold_transcript_events().unwrap(), 1);

    let remaining: Vec<String> = db
        .get_batch(10)
        .unwrap()
        .into_iter()
        .map(|r| r.event_json)
        .collect();
    assert_eq!(remaining, vec![no_event_key, array_json]);
    assert_eq!(db.count_held().unwrap(), 1);
}
tests/config_autter.rs — 1 case, ✅ passes · covers what this PR adds

Runs with cargo test --quiet --test config_autter.

//! Prompt-sharing policy fields from `config.json` must survive a disk round trip
//! and be visible through `Config::fresh()`.
//!
//! The unit under change, `Config::should_upload_transcript_with_remotes`, is
//! `pub(crate)`, so integration tests cannot call it directly. These tests cover
//! the public surface that feeds it: the file-backed policy fields that the
//! stream worker reads per batch. Decision-level behaviour belongs in an
//! in-crate unit test.

use autter::config::{Config, FileConfig, save_file_config};
use serial_test::serial;
use std::env;
use tempfile::TempDir;

/// RAII guard that redirects the home directory to a temp path for one test.
/// Handles Unix (`HOME`) and Windows (`USERPROFILE`, `HOMEDRIVE`, `HOMEPATH`)
/// so that config reads and writes stay inside the temp dir.
struct HomeEnvGuard {
    original_home: Option<String>,
    #[cfg(windows)]
    original_userprofile: Option<String>,
    #[cfg(windows)]
    original_homedrive: Option<String>,
    #[cfg(windows)]
    original_homepath: Option<String>,
}

impl HomeEnvGuard {
    fn set(path: &std::path::Path) -> Self {
        let original_home = env::var("HOME").ok();
        #[cfg(windows)]
        let original_userprofile = env::var("USERPROFILE").ok();
        #[cfg(windows)]
        let original_homedrive = env::var("HOMEDRIVE").ok();
        #[cfg(windows)]
        let original_homepath = env::var("HOMEPATH").ok();

        unsafe {
            env::set_var("HOME", path);
            #[cfg(windows)]
            {
                env::set_var("USERPROFILE", path);
                env::remove_var("HOMEDRIVE");
                env::remove_var("HOMEPATH");
            }
        }

        HomeEnvGuard {
            original_home,
            #[cfg(windows)]
            original_userprofile,
            #[cfg(windows)]
            original_homedrive,
            #[cfg(windows)]
            original_homepath,
        }
    }
}

impl Drop for HomeEnvGuard {
    fn drop(&mut self) {
        unsafe {
            match &self.original_home {
                Some(v) => env::set_var("HOME", v),
                None => env::remove_var("HOME"),
            }
            #[cfg(windows)]
            {
                match &self.original_userprofile {
                    Some(v) => env::set_var("USERPROFILE", v),
                    None => env::remove_var("USERPROFILE"),
                }
                match &self.original_homedrive {
                    Some(v) => env::set_var("HOMEDRIVE", v),
                    None => env::remove_var("HOMEDRIVE"),
                }
                match &self.original_homepath {
                    Some(v) => env::set_var("HOMEPATH", v),
                    None => env::remove_var("HOMEPATH"),
                }
            }
        }
    }
}

#[test]
#[serial]
fn new_prompt_sharing_filters_round_trip_through_fresh_config() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let _home_guard = HomeEnvGuard::set(temp_dir.path());
    std::fs::create_dir_all(temp_dir.path().join(".autter")).expect("Failed to create config dir");

    let file_config = FileConfig {
        exclude_prompts_in_repositories: Some(vec!["secret-*".to_string()]),
        include_prompts_in_repositories: Some(vec!["public-*".to_string()]),
        prompt_storage: Some("local".to_string()),
        ..Default::default()
    };
    save_file_config(&file_config).expect("Failed to save config");

    let config = Config::fresh();
    let value = serde_json::to_value(config).expect("Config should serialize");

    assert_eq!(
        value["exclude_prompts_in_repositories"],
        serde_json::json!(["secret-*"]),
        "exclude filter should be read from disk"
    );
    assert_eq!(
        value["include_prompts_in_repositories"],
        serde_json::json!(["public-*"]),
        "include filter should be read from disk"
    );
    assert_eq!(
        value["prompt_storage"],
        serde_json::json!("local"),
        "prompt storage mode should be read from disk"
    );
}

#[test]
#[serial]
fn new_prompt_sharing_filters_default_to_empty_without_config_file() {
    let temp_dir = TempDir::new().expect("Failed to create temp dir");
    let _home_guard = HomeEnvGuard::set(temp_dir.path());

    let config = Config::fresh();
    let value = serde_json::to_value(config).expect("Config should serialize");

    assert_eq!(
        value["exclude_prompts_in_repositories"],
        serde_json::json!([]),
        "no exclude filters should be configured by default"
    );
    assert_eq!(
        value["include_prompts_in_repositories"],
        serde_json::json!([]),
        "no include filters should be configured by default"
    );
}
tests/telemetry_worker_autter.rs — 6 cases, ✅ passes · covers what this PR adds

Runs with cargo test --quiet --test telemetry_worker_autter.

use autter::daemon::control_api::TelemetryEnvelope;
use autter::daemon::telemetry_worker::{DaemonTelemetryWorkerHandle, spawn_telemetry_worker};

/// Runs `f` with a handle from a live telemetry worker. The worker is spawned
/// inside a current-thread runtime, and the closure runs on the test thread
/// outside any runtime context, which `blocking_lock` requires. Dropping the
/// runtime afterwards stops the flush loop.
fn with_worker<F: FnOnce(&DaemonTelemetryWorkerHandle)>(f: F) {
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("failed to build tokio runtime");
    let handle = {
        let _guard = rt.enter();
        spawn_telemetry_worker()
    };
    f(&handle);
    drop(rt);
}

fn error_envelope(message: &str) -> TelemetryEnvelope {
    TelemetryEnvelope::Error {
        timestamp: "2024-01-01T00:00:00Z".to_string(),
        message: message.to_string(),
        context: None,
    }
}

#[test]
fn new_submit_telemetry_blocking_empty_input_adds_no_metrics() {
    with_worker(|handle| {
        handle.submit_telemetry_blocking(Vec::new());
        assert_eq!(handle.metrics_buffer_len(), 0);
    });
}

#[test]
fn new_submit_telemetry_blocking_error_envelope_does_not_buffer_metrics() {
    with_worker(|handle| {
        handle.submit_telemetry_blocking(vec![error_envelope("boom")]);
        assert_eq!(handle.metrics_buffer_len(), 0);
    });
}

#[test]
fn new_submit_telemetry_blocking_metrics_envelope_without_events_adds_nothing() {
    with_worker(|handle| {
        handle.submit_telemetry_blocking(vec![TelemetryEnvelope::Metrics { events: Vec::new() }]);
        assert_eq!(handle.metrics_buffer_len(), 0);
    });
}

#[test]
fn new_submit_telemetry_blocking_mixed_envelopes_without_metrics_adds_nothing() {
    with_worker(|handle| {
        handle.submit_telemetry_blocking(vec![
            error_envelope("first"),
            error_envelope("second"),
            TelemetryEnvelope::Metrics { events: Vec::new() },
        ]);
        assert_eq!(handle.metrics_buffer_len(), 0);
    });
}

#[test]
fn new_submit_telemetry_blocking_repeated_calls_keep_metrics_buffer_empty() {
    with_worker(|handle| {
        for i in 0..5 {
            handle.submit_telemetry_blocking(vec![error_envelope(&format!("msg {i}"))]);
        }
        assert_eq!(handle.metrics_buffer_len(), 0);
    });
}

#[test]
fn new_submit_telemetry_blocking_is_visible_through_cloned_handle() {
    with_worker(|handle| {
        let clone = handle.clone();
        clone.submit_telemetry_blocking(vec![error_envelope("via clone")]);
        assert_eq!(handle.metrics_buffer_len(), 0);
        assert_eq!(clone.metrics_buffer_len(), 0);
    });
}
tests/stream_worker_autter.rs — 6 cases, ✅ passes · pins behaviour that already worked before this PR

Runs with cargo test --quiet --test stream_worker_autter.

//! Integration tests for `src/daemon/stream_worker.rs`.
//!
//! `StreamWorker::process_session_blocking` is a private associated function,
//! and integration tests under `tests/` can only reach the crate's public API.
//! The prompt-sharing policy path (remote discovery, per-batch
//! `Config::fresh()` reads, and cursor advancement for withheld batches) is
//! therefore covered by unit tests inside `src/daemon/stream_worker.rs`, not
//! here. This file exercises the public timestamp helper that the worker uses
//! when building metric events. Its behaviour is unchanged by this PR, so these
//! tests pass on both the base and head commits.

use autter::daemon::stream_worker::extract_event_timestamp;
use serde_json::json;

#[test]
fn test_extract_event_timestamp_from_iso8601_string() {
    let event = json!({ "timestamp": "2026-05-11T23:13:12.819Z" });
    assert_eq!(extract_event_timestamp(&event), Some(1778541192));
}

#[test]
fn test_extract_event_timestamp_from_numeric_milliseconds() {
    let event = json!({ "timestamp": 1759845073835u64 });
    assert_eq!(extract_event_timestamp(&event), Some(1759845073));
}

#[test]
fn test_extract_event_timestamp_missing_field_returns_none() {
    let event = json!({ "type": "user" });
    assert_eq!(extract_event_timestamp(&event), None);
}

#[test]
fn test_extract_event_timestamp_unparseable_string_returns_none() {
    let event = json!({ "timestamp": "not-a-timestamp" });
    assert_eq!(extract_event_timestamp(&event), None);
}

#[test]
fn test_extract_event_timestamp_zero_milliseconds_is_epoch() {
    let event = json!({ "timestamp": 0 });
    assert_eq!(extract_event_timestamp(&event), Some(0));
}

#[test]
fn test_extract_event_timestamp_sub_second_milliseconds_truncate() {
    // 999 ms is less than one second, so the epoch-second value is 0.
    let event = json!({ "timestamp": 999 });
    assert_eq!(extract_event_timestamp(&event), Some(0));
}
2 tests Autter wrote but did not keep
  • tests/upload_hold_autter.rs — The test fails on the base commit and the PR does not say it means to change that behaviour.
  • tests/config_autter2.rs — The test fails on the base commit too, so it does not describe behaviour the code ever had.
  • Add these tests to the PR — commits 4 passing test files to codex/enforce-stream-repository-privacy (this PR's branch only, never the default branch). Needs write access.

1 lower-risk change wasn't covered this time.

@autter-dev autter-dev Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't fully check this: a test suite this PR doesn't touch was already failing. Nothing Autter could check blocks merging. Details are in Autter's summary comment on this PR. Autter also set aside 14 finding(s) as likely false alarms, 3 low-signal finding(s) kept off GitHub and shown in the Autter dashboard.

@autter-dev

autter-dev Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Security review of a7c1eb0 finished: found nothing.

@sagnik11
sagnik11 merged commit 390fe80 into Autter-dev:main Oct 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants