Skip to content

AOJ-459: Add event trigger path - #76

Merged
AojdevStudio merged 3 commits into
mainfrom
feature/aoj-459
Jun 5, 2026
Merged

AojdevStudio merged 3 commits into
mainfrom
feature/aoj-459

Conversation

@AojdevStudio

@AojdevStudio AojdevStudio commented Jun 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Adds a sync-app event trigger path with duplicate suppression.
  • Passes sanitized trigger context into the existing smoke audit trail.
  • Adds focused TypeScript and Python coverage.
  • Related issue(s): AOJ-459.

Test Plan

  • bun run typecheck in apps/simplefin-sync
  • bun test in apps/simplefin-sync
  • uv run ruff check .
  • uv run ruff format --check .
  • uv run mypy src/
  • uv run mypy buy_ticket_agent
  • uv run pytest tests/python/test_buy_ticket_agent_smoke.py -q --no-cov
  • uv run pytest -q -m "not integration"
  • uv run python -m buy_ticket_agent.main --smoke

Review Checklist

  • No secrets or PII committed
  • Documentation updated if applicable
  • Test coverage maintained or improved
  • Local CodeRabbit CLI review: 0 findings
  • Privacy review completed for public metadata and staged diff

Summary by CodeRabbit

  • New Features

    • Automated deposit detection that triggers ticket creation for deposits above a configurable threshold.
    • Persistent tracking of seen transactions with failure capping to avoid repeated failed attempts; run output and timeouts are recorded.
    • Trigger context is sanitized and included in ticket drafts and run logs; CLI polling/watch behavior improved with scrubbed error messages.
  • Tests

    • Added end-to-end tests covering the deposit trigger workflow, persistence/seen behavior, failure capping, and inclusion of sanitized trigger context in smoke runs.

@linear

linear Bot commented Jun 5, 2026

Copy link
Copy Markdown

AOJ-459

@coderabbitai

coderabbitai Bot commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d63b8519-2218-4faf-a85b-3da6e0e88350

📥 Commits

Reviewing files that changed from the base of the PR and between b29cd5c and 2e42d54.

📒 Files selected for processing (2)
  • apps/simplefin-sync/src/trigger.test.ts
  • apps/simplefin-sync/src/trigger.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/simplefin-sync/src/trigger.ts

📝 Walkthrough

Walkthrough

Adds a TypeScript polling pipeline that detects SimpleFIN deposit transactions above a threshold, deduplicates via in-memory and file-backed stores, spawns a Python buy-ticket subprocess per detection with a sanitized BUY_TICKET_* env, logs runs, and propagates trigger context into Python ticket payloads and logs.

Changes

Deposit Trigger Pipeline

Layer / File(s) Summary
Contracts and config
apps/simplefin-sync/src/trigger.ts
Defines exported interfaces and constants for DepositDetection, TriggerResult, SeenTransactionStore, PollOptions, and PollResult.
Seen-store implementations
apps/simplefin-sync/src/trigger.ts
Adds in-memory SeenTransactionStore and a lazy-loading file-backed store that persists seen keys and per-key failure counts.
Amount parsing & detection
apps/simplefin-sync/src/trigger.ts
Parses decimal thresholds into scaled bigints, generates stable keys, and constructs DepositDetection objects while deduplicating.
Subprocess command & env
apps/simplefin-sync/src/trigger.ts
Builds Python argv, constructs sanitized child env (denylisting SIMPLEFIN_), injects BUY_TICKET_TRIGGER_ fields, and returns the argv/env.
Subprocess runner
apps/simplefin-sync/src/trigger.ts
Spawns the buy-ticket subprocess, counts stdout/stderr characters, enforces timeout (kills on expiry), and maps exit codes to TriggerResult.
Run logging & failure helpers
apps/simplefin-sync/src/trigger.ts
Writes timestamped JSON run logs under notebooks/auto-tickets/runs, exposes helpers to read/increment failure counts, and sanitizes errors for logs.
Polling orchestration
apps/simplefin-sync/src/trigger.ts
Implements pollDepositTriggers: computes lookback, fetches pending accounts, finds detections, runs triggers, marks successes, increments failures, caps repeated failures (marks seen when cap reached), and returns aggregated results.
Error scrubbing & CLI
apps/simplefin-sync/src/trigger.ts
Scrubs sensitive parts of error messages, implements CLI runner validating env, constructs file-backed seen store, supports interval and --watch retry behavior, and adds module entrypoint.
TypeScript tests
apps/simplefin-sync/src/trigger.test.ts
Adds fixture builders and tests validating detection thresholding, sanitized keys, duplicate suppression, file-backed persistence, failure capping, run-log write-failure handling, and buy-ticket command/env outputs.
Python trigger-context plumbing
buy_ticket_agent/pipeline.py
Adds _trigger_context_from_env(), threads trigger_context into _make_ticket_payload, and includes trigger in success/failure run-log payloads and the draft ticket when present.
Python smoke test
tests/python/test_buy_ticket_agent_smoke.py
New test that sets BUY_TICKET_TRIGGER_* env vars, runs run_smoke(), and asserts draft and run-log JSON include expected sanitized trigger object.
Package scripts
apps/simplefin-sync/package.json
Reorders/updates npm scripts to include trigger, test, and typecheck alongside existing entries.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • AojdevStudio/Finance-Guru#75: Related previous work on the buy_ticket_agent smoke scaffold that this PR extends by adding trigger-context propagation and tests.

Poem

🐰 I sniff the deposits in the stream,
I hash the keys and keep them clean,
I spawn a tiny Python run,
I write a log for every one,
Safe env, no raw ids seen.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'AOJ-459: Add event trigger path' clearly and specifically describes the main feature being added—an event trigger path for the sync app with duplicate suppression, as detailed in the raw summary.
Description check ✅ Passed The PR description matches the required template structure with all sections completed: Summary with checked items and issue reference, comprehensive Test Plan with all tests checked, and complete Review Checklist with all items addressed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/aoj-459

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fbf1e52d30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/simplefin-sync/src/trigger.ts Outdated

const projectRoot = path.resolve(import.meta.dir, "..", "..", "..");
const client = createClient(accessUrl);
const seenStore = createMemorySeenStore();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Persist seen transaction keys across CLI runs

When bun run trigger is used as a scheduled one-shot command, or the --watch process restarts, this in-memory store starts empty while each poll fetches the last 7 days of transactions. Any deposit already processed in that lookback will therefore trigger another buy-ticket smoke run and notification because the prior log/state files are not consulted. Use a persisted seen store keyed by transactionKey so duplicate suppression survives process boundaries.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 5, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR introduces a trigger.ts module in apps/simplefin-sync that polls SimpleFIN for deposits over a configurable threshold, deduplicates them via hashed transaction keys (memory or file-backed store), caps repeated failures, and spawns buy_ticket_agent with a sanitized environment. It also threads the trigger context through pipeline.py so both the ticket draft and the run log capture the originating deposit metadata.

  • trigger.ts: Full pipeline from SimpleFIN polling → deposit detection → subprocess execution → audit log write, with credential scrubbing (SIMPLEFIN_* vars stripped from child env), atomic file-store persistence via temp-rename, and an in-process failure cap that prevents runaway retries.
  • pipeline.py: _trigger_context_from_env() reads the four BUY_TICKET_TRIGGER_* env vars and injects the resulting dict into both the ticket payload and the SmokeResult log.
  • Tests: TypeScript and Python coverage spans threshold detection, cross-poll deduplication, file-store restart persistence, failure capping, and the log-write-failure atomicity guarantee.

Confidence Score: 5/5

Safe to merge — all previously identified concerns are addressed and no new correctness issues were found.

The four issues flagged in the prior review round (credential leakage to child process, unbounded failure retries, log-write atomicity with deduplication mark, and in-memory state loss) are all resolved in this revision. The credential stripping via CHILD_ENV_DENYLIST_PREFIXES, the maxFailures cap, the try/catch reordering that ensures seenStore.mark() fires before re-throwing a log write error, and the new createFileSeenStore for persistence are all verified by the test suite. No new logic defects were identified.

No files require special attention. trigger.ts is the most complex new addition but its behavior is thoroughly exercised by trigger.test.ts.

Important Files Changed

Filename Overview
apps/simplefin-sync/src/trigger.ts New file implementing deposit detection, deduplication (memory + file stores), failure capping, credential scrubbing from child env, and audit log writing; all previously flagged issues are addressed.
apps/simplefin-sync/src/trigger.test.ts New test file with comprehensive coverage: detection threshold, intra-poll deduplication, cross-store-instance persistence, failure capping, log-write-failure atomicity, and env sanitization.
buy_ticket_agent/pipeline.py Adds _trigger_context_from_env() to read sanitized trigger metadata from env, and propagates it into both the ticket payload and the run log result dict.
tests/python/test_buy_ticket_agent_smoke.py Adds a test verifying that trigger context env vars appear in both the draft ticket and the run log when run_smoke is called with trigger env set.
apps/simplefin-sync/package.json Adds trigger, test, and typecheck scripts consistent with Bun tooling conventions.

Sequence Diagram

sequenceDiagram
    participant CLI as trigger.ts CLI
    participant Poll as pollDepositTriggers
    participant SFIN as SimpleFIN API
    participant Store as SeenTransactionStore
    participant Proc as buy_ticket_agent
    participant Log as Audit Log
    participant Py as pipeline.py

    CLI->>Poll: call with client, seenStore, projectRoot
    Poll->>SFIN: "fetchAccounts startDate, pending=true"
    SFIN-->>Poll: SfinAccountSet
    Poll->>Store: has(transactionKey) for each deposit over threshold
    Store-->>Poll: seen or unseen
    Poll->>Proc: Bun.spawn uv run python -m buy_ticket_agent.main --smoke
    Note over Proc,Py: BUY_TICKET_TRIGGER_* env vars injected, SIMPLEFIN_* stripped
    Proc->>Py: run_smoke()
    Py->>Py: _trigger_context_from_env()
    Py-->>Proc: SmokeResult with trigger context in draft and log
    Proc-->>Poll: exitCode, stdoutChars, stderrChars
    Poll->>Log: writeTriggerLog JSON
    Poll->>Store: mark on success, markFailure on non-zero exit
    Poll-->>CLI: PollResult triggered, failed, cappedFailures, skippedDuplicates
Loading

Fix All in Claude Code Fix All in Codex Fix All in Cursor

Reviews (3): Last reviewed commit: "fix(aoj-459): settle trigger review find..." | Re-trigger Greptile

Comment thread apps/simplefin-sync/src/trigger.ts
Comment thread apps/simplefin-sync/src/trigger.ts
Comment thread apps/simplefin-sync/src/trigger.ts

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/simplefin-sync/src/trigger.test.ts (1)

1-3: ⚡ Quick win

Use Bun.file for log reads in this TS test.

Please replace readFile usage with Bun.file(...).text() to match the repo’s Bun-first file I/O guideline.

Suggested patch
-import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
+import { mkdtemp, readdir, rm } from "node:fs/promises";
@@
-      const log = JSON.parse(await readFile(path.join(logDir, logFile), "utf8"));
+      const log = JSON.parse(await Bun.file(path.join(logDir, logFile)).text());

As per coding guidelines, "**/*.{ts,tsx,js,jsx}: Prefer Bun.file over node:fs's readFile/writeFile".

Also applies to: 138-138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/simplefin-sync/src/trigger.test.ts` around lines 1 - 3, Tests import and
use node:fs/promises.readFile; replace any readFile(...) calls in this test (and
the occurrence around line 138) with Bun.file(pathToFile).text() to follow the
Bun-first I/O guideline—locate uses of readFile in this file (e.g., references
in trigger.test.ts) and change them to call Bun.file(<same path>).text(),
removing the readFile import if no longer used and keeping mkdtemp/readdir/rm
imports as needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/simplefin-sync/src/trigger.ts`:
- Around line 280-289: Wrap the call to pollDepositTriggers(...) inside the
do/while loop in a try/catch so transient exceptions don't kill the process
during watch mode: call pollDepositTriggers with client, seenStore and
projectRoot inside try; on error, if watch is true log the error (including
context) and await Bun.sleep(intervalMs) before continuing the loop; if watch is
false rethrow the error so non-watch behavior stays the same. Ensure the catch
still awaits Bun.sleep(intervalMs) to avoid a tight retry loop and reference
pollDepositTriggers, seenStore, client, projectRoot, watch, and intervalMs when
locating the change.
- Around line 171-186: The runBuyTicketSubprocess function currently spawns a
child process without a timeout; add an AbortController-based timeout and pass
its signal to Bun.spawn (or otherwise ensure the process is killed) so the
subprocess can't hang forever. Concretely: create an AbortController and set a
timer (e.g., configurable constant) that calls controller.abort() and
proc.kill() on timeout, pass controller.signal into Bun.spawn options, and
ensure the timer is cleared once proc.exited resolves; also handle the abort
path so you still await countStreamChars(proc.stdout/stderr) and proc.exited (or
return a failure TriggerResult) when the process is aborted. Reference
runBuyTicketSubprocess, buildBuyTicketCommand, buildBuyTicketEnv,
countStreamChars and proc.exited/proc.kill when making the change.

---

Nitpick comments:
In `@apps/simplefin-sync/src/trigger.test.ts`:
- Around line 1-3: Tests import and use node:fs/promises.readFile; replace any
readFile(...) calls in this test (and the occurrence around line 138) with
Bun.file(pathToFile).text() to follow the Bun-first I/O guideline—locate uses of
readFile in this file (e.g., references in trigger.test.ts) and change them to
call Bun.file(<same path>).text(), removing the readFile import if no longer
used and keeping mkdtemp/readdir/rm imports as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4c06ad8c-42d7-434d-a3c3-307c841fb918

📥 Commits

Reviewing files that changed from the base of the PR and between e427ecf and fbf1e52.

📒 Files selected for processing (5)
  • apps/simplefin-sync/package.json
  • apps/simplefin-sync/src/trigger.test.ts
  • apps/simplefin-sync/src/trigger.ts
  • buy_ticket_agent/pipeline.py
  • tests/python/test_buy_ticket_agent_smoke.py

Comment thread apps/simplefin-sync/src/trigger.ts Outdated
Comment thread apps/simplefin-sync/src/trigger.ts
Comment thread apps/simplefin-sync/src/trigger.ts

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/simplefin-sync/src/trigger.ts`:
- Around line 35-39: The SeenTransactionStore interface allows optional
failureCount/markFailure which lets custom stores silently ignore maxFailures;
fix by adding a fallback internal failure tracker when failureCount or
markFailure are missing: in the code paths that check failures (where you
currently call store.failureCount(key) and store.markFailure(key)), detect if
those methods are undefined on the provided SeenTransactionStore and, if so, use
an internal Map/WeakMap to track per-key failure counts and increment/read from
that map so the configured maxFailures is honored; update the logic that decides
whether to requeue/retry to consult this fallback tracker exactly the same way
it would consult store.failureCount/markFailure so behavior is consistent.
- Around line 96-125: The persist() function currently writes directly to
statePath which can leave a truncated JSON on crash; change persist() to write
atomically by writing the JSON to a temp file in the same directory (e.g.,
statePath + ".tmp" or using a unique suffix), flush/fsync the temp file, then
rename/move the temp file to statePath (atomic rename) and remove the temp on
errors; keep the existing JSON payload built from seen and failures, ensure
mkdir(path.dirname(statePath), { recursive: true }) runs before creating the
temp, and handle/propagate I/O errors so load() can continue to assume a valid
JSON file.
- Around line 449-460: When handling errors from pollDepositTriggers, scrub the
error using pollErrorPayload before exiting in non-watch mode; replace the
current direct rethrow with first logging
console.error(JSON.stringify(pollErrorPayload(error))) and then rethrow the
original error (or throw a new Error if you prefer) so the CLI path uses the
scrubbed payload, while preserving the existing watch behavior that logs the
scrubbed payload, awaits Bun.sleep(intervalMs), and continues; update the catch
block around pollDepositTriggers to call pollErrorPayload(error) in both
branches (watch and non-watch) and then either rethrow or continue as
appropriate.
- Around line 378-389: writeTriggerLog is currently awaited before any
seen/failure bookkeeping so if it throws the already-run trigger will be
requeued; wrap the writeTriggerLog(options.projectRoot, detection, result, now)
call in a try/catch that logs the write error (but does not rethrow) and allows
execution to continue to the existing handling of result (including
options.seenStore.mark and markFailure). Ensure the catch references the same
detection/result variables so behavior is unchanged on success and that failures
to write the log are only recorded/logged and do not block seenStore.mark or
failed/cappedFailures increments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 37a6c8be-04bb-4204-967f-f7c815ac9cfd

📥 Commits

Reviewing files that changed from the base of the PR and between fbf1e52 and b29cd5c.

📒 Files selected for processing (2)
  • apps/simplefin-sync/src/trigger.test.ts
  • apps/simplefin-sync/src/trigger.ts

Comment thread apps/simplefin-sync/src/trigger.ts Outdated
Comment thread apps/simplefin-sync/src/trigger.ts
Comment on lines +378 to +389
const result = await trigger(detection);
await writeTriggerLog(options.projectRoot, detection, result, now);
if (result.status === "triggered") {
await options.seenStore.mark(detection.transactionKey);
triggered += 1;
} else {
const failures = await markFailure(options.seenStore, detection.transactionKey);
if (maxFailures > 0 && failures >= maxFailures) {
await options.seenStore.mark(detection.transactionKey);
cappedFailures += 1;
}
failed += 1;

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't let log-write failures requeue an already-run trigger.

writeTriggerLog(...) runs before any seen/failure bookkeeping. If the subprocess already completed and the log write fails, this transaction stays eligible and the next poll will run the same trigger again; on the failure path it also prevents maxFailures from ever advancing.

Suggested direction
   for (const detection of detections) {
@@
     const result = await trigger(detection);
-    await writeTriggerLog(options.projectRoot, detection, result, now);
     if (result.status === "triggered") {
       await options.seenStore.mark(detection.transactionKey);
       triggered += 1;
     } else {
       const failures = await markFailure(options.seenStore, detection.transactionKey);
       if (maxFailures > 0 && failures >= maxFailures) {
         await options.seenStore.mark(detection.transactionKey);
         cappedFailures += 1;
       }
       failed += 1;
     }
+    await writeTriggerLog(options.projectRoot, detection, result, now);
   }

If the run log is best-effort rather than part of the dedupe contract, catching/logging the write failure is even safer than letting it abort the poll after the subprocess has already run.

Based on learnings, log-before-state ordering is only required when the persisted state stores log_path; this seen-store does not persist the run-log path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/simplefin-sync/src/trigger.ts` around lines 378 - 389, writeTriggerLog
is currently awaited before any seen/failure bookkeeping so if it throws the
already-run trigger will be requeued; wrap the
writeTriggerLog(options.projectRoot, detection, result, now) call in a try/catch
that logs the write error (but does not rethrow) and allows execution to
continue to the existing handling of result (including options.seenStore.mark
and markFailure). Ensure the catch references the same detection/result
variables so behavior is unchanged on success and that failures to write the log
are only recorded/logged and do not block seenStore.mark or
failed/cappedFailures increments.

Comment thread apps/simplefin-sync/src/trigger.ts
@AojdevStudio
AojdevStudio merged commit 88655c3 into main Jun 5, 2026
13 checks passed
@AojdevStudio
AojdevStudio deleted the feature/aoj-459 branch June 5, 2026 17:27
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