Skip to content

Fix integration event local previews#159

Merged
khaliqgant merged 1 commit into
mainfrom
fix/integration-event-local-preview-cleanup
Jun 8, 2026
Merged

Fix integration event local previews#159
khaliqgant merged 1 commit into
mainfrom
fix/integration-event-local-preview-cleanup

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

Summary

  • populate current local mount paths before live integration event reconciliation so local context fallback can read materialized Slack DM/thread files
  • add confirmed Slack writeback draft cleanup on Relayfile writeback.succeeded events for bounded command roots
  • add regressions for Slack user-message local roots, local DM preview fallback, and confirmed-only writeback draft cleanup

Verification

  • npm test
  • npx vitest run src/main/integrations.test.ts

Ops cleanup

  • Deleted 57 delivered outbound Slack command files under .integrations/slack/channels/C0B8ZL2L9GC__pear-pty-investigation/messages.
  • Verified matching reply-*.json / notice-*.json / announce-*.json count is now 0.

@codeant-ai
Copy link
Copy Markdown

codeant-ai Bot commented Jun 8, 2026

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 8, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds Slack writeback draft cleanup triggered on confirmed writebacks, exports a previously internal subscriptionSpecsFor helper for public reuse, threads localMountPaths through the integration manager into the event bridge, and adds comprehensive tests covering cleanup behavior and local context materialization from Slack user-message mounts.

Changes

Slack Writeback Cleanup and LocalMountPaths Threading

Layer / File(s) Summary
Export subscriptionSpecsFor and add filesystem cleanup support
src/main/integration-event-bridge.ts
subscriptionSpecsFor is exported from the event bridge, and rm is added to filesystem promise imports to enable local file deletion.
Implement Slack writeback draft cleanup
src/main/integration-event-bridge.ts
New cleanupConfirmedSlackWritebackDraft helper conditionally deletes confirmed Slack writeback draft files from local mount roots when writeback.succeeded events arrive, wired into the remote subscription callback with aggregated error logging.
Thread localMountPaths through integration manager
src/main/integrations.ts
New withCurrentLocalMountPaths helper computes local paths for the current workspace; withLocalMountPaths refactored to use it; syncEventSubscriptions passes enriched integrations (including computed localMountPaths) into event bridge reconcile.
Test cleanup and subscriptionSpecsFor behavior
src/main/__tests__/integration-event-bridge.test.ts
Test utilities added (stat import, waitForPathMissing helper); subscriptionSpecsFor imported and tested; tests verify confirmed writeback success removes local draft files and pending drafts wait for dispatcher confirmation before cleanup.
Test local Slack context materialization and integration
src/main/__tests__/integration-event-bridge.test.ts, src/main/integrations.test.ts
Tests verify bridge reads local Slack user-message context from localMountPaths when remote preview is unavailable; refreshAgentState test confirms localMountPaths are computed and passed into reconcile alongside mountPaths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • AgentWorkforce/pear#155: Both PRs center on Slack local-disk fallback behavior driven by subscriptionSpecsFor/localMountRoots—the retrieved PR extends the bridge to resolve and read targeted suffixed contexts from concrete local mount roots, while the main PR exports subscriptionSpecsFor and adds tests verifying Slack DM preview/context materializes from local Slack mount files via localMountRoots.
  • AgentWorkforce/pear#154: Both PRs modify integration-event-bridge's Slack event context/injection flow to resolve Slack content from specific mounted/local Slack paths (via mount roots/subscription specs and context-read targeting), with the main PR additionally exporting subscriptionSpecsFor and adding Slack writeback draft cleanup.

Poem

🐰 A bridge cleaned of drafts, now sturdy and bright,
Local mounts materialized—slack paths done right!
Subscription specs exported for all to behold,
Context flows local when previews grow cold.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix integration event local previews' directly relates to the main objective of populating local mount paths for integration event reconciliation and enabling local context fallback for Slack previews.
Description check ✅ Passed The description is directly related to the changeset, outlining the three key changes: populating local mount paths, adding writeback cleanup, and adding regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 fix/integration-event-local-preview-cleanup

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.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces local draft cleanup for confirmed Slack writeback events and ensures current local mount paths are passed during integration event reconciliation. Feedback on these changes highlights two issues in the draft cleanup logic: first, the check isSlackWritebackCommandRoot(root.remoteRoot) can incorrectly bypass cleanup when downloadHistoricalData is enabled; second, returning early after the first successful deletion prevents the cleanup of duplicate draft files across other matching local mount roots.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1884 to +1885
if (!isSlackWritebackCommandRoot(root.remoteRoot)) continue
if (!pathIsInsideMount(remotePath, root.remoteRoot)) continue
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

When downloadHistoricalData is enabled (true), the root.remoteRoot is a broader history root (e.g., /slack/channels/C123) rather than the narrow command root (e.g., /slack/channels/C123/messages). This causes isSlackWritebackCommandRoot(root.remoteRoot) to return false, completely bypassing the cleanup of confirmed writeback drafts for users with historical download enabled.\n\nSince isLikelyLocalWritebackCommandPath(remotePath) and pathIsInsideMount(remotePath, root.remoteRoot) already ensure the path is a valid writeback command file within the mount, this extra check is unnecessary and buggy.

Suggested change
if (!isSlackWritebackCommandRoot(root.remoteRoot)) continue
if (!pathIsInsideMount(remotePath, root.remoteRoot)) continue
if (!pathIsInsideMount(remotePath, root.remoteRoot)) continue

Comment on lines +1891 to +1897
logIntegrationEvent('confirmed Slack writeback draft cleaned', {
projectId,
eventId: event.id,
remotePath,
localRoot: root.localRoot
})
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Returning early after the first successful deletion prevents the cleanup of duplicate draft files in other matching local mount roots (for example, if the same channel is mounted in multiple local workspaces or roots). Removing the return statement allows the loop to continue and clean up all matching local draft files.

      logIntegrationEvent('confirmed Slack writeback draft cleaned', {\n        projectId,\n        eventId: event.id,\n        remotePath,\n        localRoot: root.localRoot\n      })

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@src/main/__tests__/integration-event-bridge.test.ts`:
- Around line 2117-2177: Add a duplicate/replay check to the "confirmed Slack
writeback success removes the local draft command file" test: after the first
harness.emit of the writeback.succeeded ChangeEvent (remoteDraftPath) and
waitForPathMissing, emit the identical writeback.succeeded event again via
harness.emit (same ChangeEvent payload) and assert the local draft still does
not exist (waitForPathMissing or stat check) and harness.sent remains [] to
ensure idempotent cleanup; update assertions around localDraftPath,
remoteDraftPath, harness.emit and waitForPathMissing to reflect the replayed
event case.
- Around line 154-162: The helper waitForPathMissing currently swallows all
errors from stat (catch(() => null)), which treats permission/IO errors as
“missing”; change the catch to only treat ENOENT as missing and rethrow any
other errors: in waitForPathMissing wrap the stat call with a catch that checks
err.code === 'ENOENT' then returns null, otherwise rethrow the error so tests
fail on non-ENOENT issues; reference the waitForPathMissing function and the
stat call to locate the change.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 26c5476e-ddea-4f82-b9b5-cd54c52a2184

📥 Commits

Reviewing files that changed from the base of the PR and between ee1e6c8 and 41f73f4.

📒 Files selected for processing (4)
  • src/main/__tests__/integration-event-bridge.test.ts
  • src/main/integration-event-bridge.ts
  • src/main/integrations.test.ts
  • src/main/integrations.ts

Comment on lines +154 to +162
async function waitForPathMissing(path: string, timeoutMs = 2_000): Promise<void> {
const deadline = Date.now() + timeoutMs
while (Date.now() <= deadline) {
const stats = await stat(path).catch(() => null)
if (!stats) return
await new Promise((resolve) => setTimeout(resolve, 10))
}
assert.equal(await stat(path).then(() => true).catch(() => false), false)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid swallowing non-ENOENT errors in waitForPathMissing.

On Line 157, catch(() => null) converts every stat failure into “missing,” which can falsely pass tests on permission/IO errors.

Suggested fix
 async function waitForPathMissing(path: string, timeoutMs = 2_000): Promise<void> {
   const deadline = Date.now() + timeoutMs
   while (Date.now() <= deadline) {
-    const stats = await stat(path).catch(() => null)
+    const stats = await stat(path).catch((error: NodeJS.ErrnoException) => {
+      if (error?.code === 'ENOENT') return null
+      throw error
+    })
     if (!stats) return
     await new Promise((resolve) => setTimeout(resolve, 10))
   }
-  assert.equal(await stat(path).then(() => true).catch(() => false), false)
+  const exists = await stat(path)
+    .then(() => true)
+    .catch((error: NodeJS.ErrnoException) => {
+      if (error?.code === 'ENOENT') return false
+      throw error
+    })
+  assert.equal(exists, false)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function waitForPathMissing(path: string, timeoutMs = 2_000): Promise<void> {
const deadline = Date.now() + timeoutMs
while (Date.now() <= deadline) {
const stats = await stat(path).catch(() => null)
if (!stats) return
await new Promise((resolve) => setTimeout(resolve, 10))
}
assert.equal(await stat(path).then(() => true).catch(() => false), false)
}
async function waitForPathMissing(path: string, timeoutMs = 2_000): Promise<void> {
const deadline = Date.now() + timeoutMs
while (Date.now() <= deadline) {
const stats = await stat(path).catch((error: NodeJS.ErrnoException) => {
if (error?.code === 'ENOENT') return null
throw error
})
if (!stats) return
await new Promise((resolve) => setTimeout(resolve, 10))
}
const exists = await stat(path)
.then(() => true)
.catch((error: NodeJS.ErrnoException) => {
if (error?.code === 'ENOENT') return false
throw error
})
assert.equal(exists, false)
}
🤖 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 `@src/main/__tests__/integration-event-bridge.test.ts` around lines 154 - 162,
The helper waitForPathMissing currently swallows all errors from stat (catch(()
=> null)), which treats permission/IO errors as “missing”; change the catch to
only treat ENOENT as missing and rethrow any other errors: in waitForPathMissing
wrap the stat call with a catch that checks err.code === 'ENOENT' then returns
null, otherwise rethrow the error so tests fail on non-ENOENT issues; reference
the waitForPathMissing function and the stat call to locate the change.

Comment on lines +2117 to +2177
test('confirmed Slack writeback success removes the local draft command file', async () => {
const workspaceRoot = await mkdtemp(join(tmpdir(), 'pear-writeback-cleanup-'))
const localRoot = join(workspaceRoot, 'workspace-id', 'slack', 'channels', 'C123ABC', 'messages')
const localDraftPath = join(localRoot, 'reply-confirmed.json')
const remoteDraftPath = '/slack/channels/C123ABC/messages/reply-confirmed.json'
await mkdir(localRoot, { recursive: true })
await writeFile(localDraftPath, JSON.stringify({ text: 'confirmed send' }))

try {
const harness = makeHarness(['alice'])
await harness.bridge.reconcile('project-1', [
integration({
provider: 'slack',
integrationId: 'slack-1',
mountPaths: ['/slack/channels/C123ABC/messages'],
localMountPaths: [localRoot],
scope: { notifyAgents: ['alice'] }
})
])

await harness.emit({
...changeEvent(remoteDraftPath, 'slack'),
type: 'writeback.succeeded'
} as ChangeEvent)
await waitForPathMissing(localDraftPath)

assert.deepEqual(harness.sent, [])
} finally {
await rm(workspaceRoot, { recursive: true, force: true })
}
})

test('Slack writeback draft cleanup waits for confirmed dispatch', async () => {
const workspaceRoot = await mkdtemp(join(tmpdir(), 'pear-writeback-cleanup-'))
const localRoot = join(workspaceRoot, 'workspace-id', 'slack', 'channels', 'C123ABC', 'messages')
const localDraftPath = join(localRoot, 'reply-pending.json')
const remoteDraftPath = '/slack/channels/C123ABC/messages/reply-pending.json'
await mkdir(localRoot, { recursive: true })
await writeFile(localDraftPath, JSON.stringify({ text: 'pending send' }))

try {
const harness = makeHarness(['alice'])
await harness.bridge.reconcile('project-1', [
integration({
provider: 'slack',
integrationId: 'slack-1',
mountPaths: ['/slack/channels/C123ABC/messages'],
localMountPaths: [localRoot],
scope: { notifyAgents: ['alice'] }
})
])

await harness.emit(changeEvent(remoteDraftPath, 'slack'))
await waitForDispatcherTick()

assert.equal(await stat(localDraftPath).then(() => true).catch(() => false), true)
assert.deepEqual(harness.sent, [])
} finally {
await rm(workspaceRoot, { recursive: true, force: true })
}
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add duplicate/replay coverage for confirmed writeback cleanup idempotency.

These tests validate confirmed and pending flows, but they don’t assert behavior when writeback.succeeded is replayed for the same draft (common on event streams). Add a second identical confirmed event and verify no error/no side effects after the first deletion.

As per coding guidelines, **/*.test.{js,ts,tsx} changes touching event streaming or integration notifications should include duplicate/replay cases, not only happy-path flow.

🤖 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 `@src/main/__tests__/integration-event-bridge.test.ts` around lines 2117 -
2177, Add a duplicate/replay check to the "confirmed Slack writeback success
removes the local draft command file" test: after the first harness.emit of the
writeback.succeeded ChangeEvent (remoteDraftPath) and waitForPathMissing, emit
the identical writeback.succeeded event again via harness.emit (same ChangeEvent
payload) and assert the local draft still does not exist (waitForPathMissing or
stat check) and harness.sent remains [] to ensure idempotent cleanup; update
assertions around localDraftPath, remoteDraftPath, harness.emit and
waitForPathMissing to reflect the replayed event case.

Source: Coding guidelines

@khaliqgant khaliqgant merged commit d719ade into main Jun 8, 2026
4 of 5 checks passed
@khaliqgant khaliqgant deleted the fix/integration-event-local-preview-cleanup branch June 8, 2026 08:27
@agent-relay-code
Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer push failed (exit 128) — fixes were not applied to the PR. The notes below are advisory and were not pushed.

Implemented fixes for the validated Gemini findings.

Changed src/main/integration-event-bridge.ts so confirmed Slack writeback cleanup:

  • no longer skips broader historical Slack mount roots.
  • continues scanning roots after deleting one local draft, so duplicate local mount roots are also cleaned.

Added regression coverage in src/main/__tests__/integration-event-bridge.test.ts.

Validation run:

  • node --experimental-strip-types --no-warnings --test src/main/__tests__/integration-event-bridge.test.ts
  • npm test
  • npx vitest run src/main/broker.test.ts src/main/integrations.test.ts
  • npm run verify:mcp-resources-drift
  • npm run build

GitHub status check at the end still showed packaged-mcp-smoke failed on the current remote head and cubic · AI code reviewer in progress, so I am not marking this READY.

Addressed comments

  • codeant-ai[bot]: raised only a PR review quota/plan limit message; no code issue to fix.
  • coderabbitai[bot]: review/status comment only; no actionable code finding.
  • gemini-code-assist[bot]: historical Slack mounts skipped by isSlackWritebackCommandRoot(root.remoteRoot); fixed in src/main/integration-event-bridge.ts:1884, regression added in src/main/__tests__/integration-event-bridge.test.ts:2149.
  • gemini-code-assist[bot]: cleanup returned after the first deletion and missed duplicate local roots; fixed in src/main/integration-event-bridge.ts:1896, regression added in src/main/__tests__/integration-event-bridge.test.ts:2182.
  • gemini-code-assist[bot]: review summary duplicated the two inline cleanup findings; fixed at the same locations above.
  • devin-ai-integration[bot]: reported no issues found; no code change needed.

@agent-relay-code
Copy link
Copy Markdown
Contributor

pr-reviewer applied fixes — committed and pushed 060b3f1 to this PR. The notes below describe what changed.

Implemented and verified fixes for the PR review findings.

Changes made:

  • Tightened waitForPathMissing so only ENOENT is treated as missing; other stat errors now fail the test.
  • Added replay coverage for repeated writeback.succeeded events.
  • Fixed confirmed Slack writeback cleanup to handle broader historical Slack local roots.
  • Fixed cleanup to continue through duplicate local roots instead of returning after the first deletion.
  • Added regressions for broad-root and duplicate-root cleanup.

Validation run:

  • node --experimental-strip-types --no-warnings --test src/main/__tests__/integration-event-bridge.test.ts
  • npx vitest run src/main/integrations.test.ts
  • npm test
  • npm run verify:mcp-resources-drift
  • npm run build after npm ci

Addressed comments

  • codeant-ai[bot]: raised only a PR review quota/plan-limit message; no code issue to fix.
  • coderabbitai[bot]: walkthrough/pre-merge summary only; no actionable code finding.
  • coderabbitai[bot]: waitForPathMissing swallowed all stat errors; fixed in src/main/__tests__/integration-event-bridge.test.ts:154.
  • coderabbitai[bot]: requested duplicate/replay coverage for confirmed writeback cleanup; fixed in src/main/__tests__/integration-event-bridge.test.ts:2144.
  • coderabbitai[bot]: review summary duplicated the two inline findings; fixed at the same locations above.
  • agent-relay-code[bot]: reported prior push failure and advisory Gemini findings; validated and fixed broader-root cleanup in src/main/integration-event-bridge.ts:1884, regression in src/main/__tests__/integration-event-bridge.test.ts:2161.
  • agent-relay-code[bot]: relayed Gemini finding that cleanup returned after first deletion and missed duplicate local roots; fixed in src/main/integration-event-bridge.ts:1896, regression in src/main/__tests__/integration-event-bridge.test.ts:2194.
  • agent-relay-code[bot]: relayed devin-ai-integration[bot] “no issues found”; no code change needed.

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