Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 67 additions & 14 deletions src/main/__tests__/integration-event-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,22 +880,22 @@ test('local fallback watchers require historical download even for command roots
integration({
provider: 'slack',
integrationId: 'slack-1',
mountPaths: ['/slack/.outbox'],
mountPaths: ['/slack/channels/C123ABC/messages'],
downloadHistoricalData: false,
localMountPaths: [
join('/tmp', 'relayfile', 'workspaces', 'workspace-id', 'slack', '.outbox')
join('/tmp', 'relayfile', 'workspaces', 'workspace-id', 'slack', 'channels', 'C123ABC', 'messages')
]
})
],
[
'/slack/.outbox/**'
'/slack/channels/C123ABC/messages/**'
]
)

assert.deepEqual(roots, [])
})

test('local fallback watchers reject bare outbox command roots', () => {
test('local fallback watchers reject non-canonical command-looking roots', () => {
const roots = localWatchRootsFor(
'workspace-id',
[
Expand All @@ -917,6 +917,28 @@ test('local fallback watchers reject bare outbox command roots', () => {
assert.deepEqual(roots, [])
})

test('local fallback watchers reject command roots with traversal segments', () => {
const roots = localWatchRootsFor(
'workspace-id',
[
integration({
provider: 'slack',
integrationId: 'slack-1',
mountPaths: ['/slack/channels/C123ABC/../messages'],
downloadHistoricalData: true,
localMountPaths: [
join('/tmp', 'relayfile', 'workspaces', 'workspace-id', 'slack', 'channels', 'C123ABC', '..', 'messages')
]
})
],
[
'/slack/channels/C123ABC/../messages/**'
]
)

assert.deepEqual(roots, [])
})

test('local fallback watchers do not watch broad provider history paths', () => {
const roots = localWatchRootsFor(
'workspace-id',
Expand Down Expand Up @@ -947,26 +969,26 @@ test('local fallback watchers are limited to bounded command roots when historic
integration({
provider: 'slack',
integrationId: 'slack-1',
mountPaths: ['/slack/.outbox'],
mountPaths: ['/slack/channels/C123ABC/messages'],
downloadHistoricalData: true,
localMountPaths: [
join('/tmp', 'relayfile', 'workspaces', 'workspace-id', 'slack', '.outbox')
join('/tmp', 'relayfile', 'workspaces', 'workspace-id', 'slack', 'channels', 'C123ABC', 'messages')
]
})
],
[
'/slack/.outbox/**',
'/slack/channels/C123ABC/messages/**',
'/slack/channels/C123ABC/**'
]
)

const byRemoteRoot = new Map(roots.map((root) => [root.remoteRoot, root.localRoot]))

assert.equal(
byRemoteRoot.get('/slack/.outbox')?.endsWith(join('workspace-id', 'slack', '.outbox')),
byRemoteRoot.get('/slack/channels/C123ABC/messages')?.endsWith(join('workspace-id', 'slack', 'channels', 'C123ABC', 'messages')),
true
)
assert.deepEqual(Array.from(byRemoteRoot.keys()), ['/slack/.outbox'])
assert.deepEqual(Array.from(byRemoteRoot.keys()), ['/slack/channels/C123ABC/messages'])
})

test('local fallback watchers accept legacy integration command mount paths', () => {
Expand All @@ -976,19 +998,47 @@ test('local fallback watchers accept legacy integration command mount paths', ()
integration({
provider: 'slack',
integrationId: 'slack-1',
mountPaths: ['/integrations/slack/.outbox'],
mountPaths: ['/integrations/slack/channels/C123ABC/messages'],
downloadHistoricalData: true,
localMountPaths: [
join('/tmp', 'relayfile', 'workspaces', 'workspace-id', 'slack', 'channels', 'C123ABC', 'messages')
]
})
],
[
'/slack/channels/C123ABC/messages/**'
]
)

assert.equal(roots.some((root) => root.remoteRoot === '/slack/channels/C123ABC/messages'), true)
})

test('local fallback watchers use the shared Slack users command-root grammar', () => {
const roots = localWatchRootsFor(
'workspace-id',
[
integration({
provider: 'slack',
integrationId: 'slack-1',
mountPaths: ['/slack/users/U123/messages'],
downloadHistoricalData: true,
localMountPaths: [
join('/tmp', 'relayfile', 'workspaces', 'workspace-id', 'slack', '.outbox')
join('/tmp', 'relayfile', 'workspaces', 'workspace-id', 'slack', 'users', 'U123', 'messages')
]
})
],
[
'/slack/.outbox/**'
'/slack/users/U123/messages/**'
]
)

assert.equal(roots.some((root) => root.remoteRoot === '/slack/.outbox'), true)
assert.equal(
roots.some((root) =>
root.remoteRoot === '/slack/users/U123/messages' &&
root.localRoot === join('/tmp', 'relayfile', 'workspaces', 'workspace-id', 'slack', 'users', 'U123', 'messages')
),
true
)
})

test('local watcher path construction does not duplicate remote path segments', () => {
Expand Down Expand Up @@ -1352,7 +1402,10 @@ test('integration event delivery failures use aggregated warn cadence by default
warnCalls.map((call) => (call[1] as { suppressedSinceLastLog: number }).suppressedSinceLastLog),
[0, 24]
)
assert.deepEqual(getIntegrationEventTelemetrySnapshot().projects['project-1'], {
const telemetry = getIntegrationEventTelemetrySnapshot().projects['project-1']
assert.ok(telemetry)
assert.equal(telemetry.brokerSendsDeferred >= 0, true)
assert.deepEqual({ ...telemetry, brokerSendsDeferred: 0 }, {
eventsReceived: 26,
eventsInjected: 0,
eventsCoalesced: 0,
Expand Down
25 changes: 25 additions & 0 deletions src/main/cloud-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const mock = vi.hoisted(() => {
workspaceId: string
localDir: string
remotePath: string
localLayout?: string
syncMode?: string
scopes?: string[]
}

Expand Down Expand Up @@ -386,10 +388,33 @@ describe('CloudAgentManager', () => {
workspaceId: 'relay-workspace-id',
localDir: mock.project.rootPath,
remotePath: '/remote/project-1',
localLayout: 'exact',
syncMode: 'mirror',
scopes: ['relayfile:fs:read:/**', 'relayfile:fs:write:/**']
})
})

it('mounts non-root sandbox relayfile paths at the project root without local double-pathing', async () => {
mock.boxResponses.push({
sandboxId: 'sandbox-1',
execUrl: 'https://sandbox.example',
relayfileToken: 'relayfile-token',
relayfileMountPath: '/workspace/project-1',
status: 'ready'
})
const manager = new CloudAgentManager()

await manager.attach('project-1', 'cloud-agent-1')

expect(mock.mountInputs[0]).toMatchObject({
localDir: '/tmp/project-1',
remotePath: '/workspace/project-1',
localLayout: 'exact',
syncMode: 'mirror'
})
expect(mock.mountInputs[0]?.localDir).not.toContain('/workspace/project-1')
})

it('keeps git-overlay clone source for dirty ssh-remote projects', async () => {
mock.project.rootPath = await createCleanGitProject()
await execFileAsync('git', ['remote', 'set-url', 'origin', 'git@github.com:acme/fast-repo.git'], {
Expand Down
4 changes: 4 additions & 0 deletions src/main/cloud-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,8 @@ export class CloudAgentManager {
localDir: project.rootPath,
remotePath: sandbox.relayfileMountPath,
mode: 'poll' as const,
localLayout: 'exact' as const,
syncMode: 'mirror' as const,
background: true,
agentName: `pear-${project.id}`,
scopes: ['relayfile:fs:read:/**', 'relayfile:fs:write:/**'],
Expand All @@ -1234,6 +1236,8 @@ export class CloudAgentManager {
...input,
env: {
...input.env,
RELAYFILE_MOUNT_LOCAL_LAYOUT: 'exact',
RELAYFILE_MOUNT_SYNC_MODE: 'mirror',
RELAYFILE_CONFLICT_POLICY: policy
},
onEvent: (event) => {
Expand Down
6 changes: 3 additions & 3 deletions src/main/integration-event-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
type Subscription
} from '@relayfile/sdk'
import type { ConnectedIntegration } from './integrations'
// @ts-expect-error Node's strip-types test runner requires the explicit .ts extension.
import { isSlackWritebackCommandRoot } from './slack-writeback-command-roots.ts'
import type {
IntegrationEventTelemetryCounters,
IntegrationEventTelemetrySnapshot
Expand Down Expand Up @@ -744,9 +746,7 @@ function allowsLocalMountWatching(integration: ConnectedIntegration): boolean {
}

function isBoundedLocalCommandRoot(remoteRoot: string): boolean {
const segments = pathSegments(remoteRoot)
if (segments.length === 0) return false
return segments.some((segment) => segment === '.outbox')
return isSlackWritebackCommandRoot(remoteRoot)
}

function watchableLocalIntegrations(integrations: ConnectedIntegration[]): ConnectedIntegration[] {
Expand Down
67 changes: 66 additions & 1 deletion src/main/integration-mounts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,22 @@ const mock = vi.hoisted(() => {
localDir: string
remotePath: string
mode: string
localLayout?: string
syncMode?: string
background: boolean
agentName: string
scopes?: string[]
readyTimeoutMs: number
launcher?: {
start?: (input: { env: Record<string, string> }) => Promise<unknown>
__options?: {
onEvent?: (event: { type: string; text?: string }) => void
}
}
}

const mountInputs: MountInput[] = []
const startMount = vi.fn(async () => undefined)
let currentAuth: MockCloudAuth | null = null
let mountExpiresAt: string | null = null
let mountSuggestedRefreshAt: string | null = null
Expand Down Expand Up @@ -52,6 +56,7 @@ const mock = vi.hoisted(() => {

return {
mountInputs,
startMount,
mkdir: vi.fn(async () => undefined),
chmod: vi.fn(async () => undefined),
rm: vi.fn(async () => undefined),
Expand Down Expand Up @@ -107,7 +112,7 @@ vi.mock('./auth', () => ({
}))

vi.mock('./relayfile-mount-launcher', () => ({
createPearMountLauncher: vi.fn((options) => ({ start: vi.fn(), __options: options }))
createPearMountLauncher: vi.fn((options) => ({ start: mock.startMount, __options: options }))
}))

import { IntegrationMountManager } from './integration-mounts'
Expand All @@ -118,6 +123,7 @@ describe('IntegrationMountManager', () => {
mock.mkdir.mockClear()
mock.chmod.mockClear()
mock.rm.mockClear()
mock.startMount.mockClear()
mock.currentAuth = {
apiUrl: 'https://cloud.example',
accessToken: 'account-token'
Expand Down Expand Up @@ -154,6 +160,8 @@ describe('IntegrationMountManager', () => {
workspaceId: 'account-workspace-id',
localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/github/repos',
remotePath: '/github/repos',
localLayout: 'exact',
syncMode: 'mirror',
agentName: 'pear-integrations-github-repos',
scopes: ['relayfile:fs:read:/github/repos/**', 'relayfile:fs:write:/github/repos/**']
})
Expand Down Expand Up @@ -195,6 +203,8 @@ describe('IntegrationMountManager', () => {
expect(mock.mountInputs[0]).toMatchObject({
localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/discovery/slack',
remotePath: '/discovery/slack',
localLayout: 'exact',
syncMode: 'mirror',
agentName: 'pear-integrations-discovery-slack',
scopes: ['relayfile:fs:read:/discovery/slack/**', 'relayfile:fs:write:/discovery/slack/**']
})
Expand All @@ -206,6 +216,61 @@ describe('IntegrationMountManager', () => {
])
})

it('mounts canonical Slack command roots exactly once in write-only mode', async () => {
const manager = new IntegrationMountManager()

await manager.ensureMounted([
{
provider: 'slack',
mountPaths: ['/slack/channels/C123/messages']
}
])

expect(mock.mountInputs).toHaveLength(1)
expect(mock.mountInputs[0]).toMatchObject({
localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/slack/channels/C123/messages',
remotePath: '/slack/channels/C123/messages',
localLayout: 'exact',
syncMode: 'write-only',
scopes: ['relayfile:fs:read:/slack/channels/C123/messages/**', 'relayfile:fs:write:/slack/channels/C123/messages/**']
})
expect(mock.mountInputs[0]?.localDir).not.toContain('messages/slack/channels/C123/messages')

const launcher = mock.mountInputs[0]?.launcher
const started = await launcher?.start?.({
env: {
RELAYFILE_REMOTE_PATH: '/slack/channels/C123/messages',
RELAYFILE_LOCAL_DIR: '/tmp/root'
}
})

expect(started).toBeUndefined()
expect(mock.startMount).toHaveBeenCalledWith(expect.objectContaining({
env: expect.objectContaining({
RELAYFILE_MOUNT_LOCAL_LAYOUT: 'exact',
RELAYFILE_MOUNT_SYNC_MODE: 'write-only'
})
}))
})

it('uses the shared Slack command-root grammar for write-only mode', async () => {
const manager = new IntegrationMountManager()

await manager.ensureMounted([
{
provider: 'slack',
mountPaths: ['/slack/users/U123/messages']
}
])

expect(mock.mountInputs[0]).toMatchObject({
localDir: '/tmp/pear-home/.agentworkforce/pear/relayfile/workspaces/account-workspace-id/slack/users/U123/messages',
remotePath: '/slack/users/U123/messages',
localLayout: 'exact',
syncMode: 'write-only'
})
})

it('scopes bare discovery mounts to the integration provider', async () => {
const manager = new IntegrationMountManager()

Expand Down
Loading
Loading