diff --git a/core/test/connectors.test.js b/core/test/connectors.test.js index b59db707..884212a5 100644 --- a/core/test/connectors.test.js +++ b/core/test/connectors.test.js @@ -8,6 +8,21 @@ import { apply } from '../index.js' import { cleanApps, composioAppStatus, composioDeleteSession, composioSearchApps, composioSession, composioSessionAlive, composioSessionFind, signIn } from '../src/host/connectors.js' import { fakeContext } from './fake-ctx.js' +// A test's temp home can still be written by a background save that outlives dispose(), so a +// plain rmSync sometimes raced and failed the whole run with ENOTEMPTY (a flaky verify/CI). Settle +// and retry; if it is still racing, leave the temp dir — the test's assertions already passed. +async function cleanHome(home) { + for (let attempt = 0; attempt < 5; attempt++) { + try { + rmSync(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 }) + return + } catch (error) { + if (error?.code !== 'ENOTEMPTY' && error?.code !== 'EBUSY' && error?.code !== 'ENOENT') throw error + await new Promise(resolve => setTimeout(resolve, 50)) + } + } +} + const fresh = () => `ghp_${randomBytes(16).toString('hex')}` const answer = (status, body) => ({ status, ok: status >= 200 && status < 300, body, json: async () => body }) @@ -82,7 +97,7 @@ test('connecting saves the token, kept out of every Bot shell, and mounts GitHub t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = async () => answer(200, { login: 'octo' }) const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const token = fresh() const before = await call(ctx, 'state', {}) @@ -115,7 +130,7 @@ test('a token GitHub refuses is not saved', async (t) => { t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = async () => answer(401, {}) const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const result = await call(ctx, 'connector-connect', { id: 'github', token: fresh() }) assert.equal(result.ok, false) assert.match(result.error.message, /did not accept/) @@ -128,7 +143,7 @@ test('a failed MCP connection shows the error without the token, and Try again m t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = async () => answer(200, { login: 'octo' }) const { ctx, home, mounts } = setup({ fail: true }) - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const token = fresh() const result = await call(ctx, 'connector-connect', { id: 'github', token }) const github = result.value.find(entry => entry.id === 'github') @@ -144,7 +159,7 @@ test('deleting GITHUB_TOKEN on the Secrets page unmounts GitHub', async (t) => { t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = async () => answer(200, { login: 'octo' }) const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) await call(ctx, 'connector-connect', { id: 'github', token: fresh() }) await call(ctx, 'secret-delete', { name: 'GITHUB_TOKEN' }) const state = await call(ctx, 'state', {}) @@ -157,13 +172,13 @@ test('github: false and composio: false hide the connectors', async (t) => { const ctx = fakeContext() capture(ctx) apply(ctx, { home, github: false }) - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) assert.deepEqual((await call(ctx, 'state', {})).value.connectors.map(entry => entry.id), ['composio']) const other = teamHome() const second = fakeContext() capture(second) apply(second, { home: other, github: false, composio: false }) - t.after(async () => { await second.dispose(); rmSync(other, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await second.dispose(); await cleanHome(other) }) assert.deepEqual((await call(second, 'state', {})).value.connectors, []) }) @@ -173,7 +188,7 @@ test('a GitHub address without https refuses to send the token', async (t) => { let asked = false globalThis.fetch = async () => { asked = true; return answer(200, { login: 'octo' }) } const { ctx, home, mounts } = setup({ github: { url: 'http://mcp.test/', api: 'https://gh.test' } }) - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const refused = await call(ctx, 'connector-connect', { id: 'github', token: fresh() }) assert.equal(refused.ok, false) assert.match(refused.error.message, /https/) @@ -256,7 +271,7 @@ test('connecting Composio keeps the key out of every Bot shell and the profile, const sent = fakeComposio() globalThis.fetch = sent.fetch const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const key = composioKey() const before = (await call(ctx, 'state', {})).value.connectors.find(entry => entry.id === 'composio') @@ -307,7 +322,7 @@ test('Choose apps mounts again with the new list and needs no key; an unknown ap const sent = fakeComposio(() => refuse ? answer(400, { error: { message: 'Unknown toolkit nope' } }) : answer(201, SESSION)) globalThis.fetch = sent.fetch const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const early = await call(ctx, 'connector-apps', { id: 'composio', apps: ['gmail'] }) assert.equal(early.ok, false) assert.match(early.error.message, /Connect Composio first/) @@ -336,7 +351,7 @@ test('a key Composio refuses is not saved', async (t) => { t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = fakeComposio(() => answer(401, {})).fetch const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const result = await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) assert.equal(result.ok, false) assert.match(result.error.message, /did not accept/) @@ -349,7 +364,7 @@ test('a failed Composio MCP connection shows an error without the key, and Try a t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = fakeComposio().fetch const { ctx, home, mounts } = setup({ fail: true }) - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const key = composioKey() const result = await call(ctx, 'connector-connect', { id: 'composio', token: key, apps: ['gmail'] }) const composio = result.value.find(entry => entry.id === 'composio') @@ -364,7 +379,7 @@ test('deleting COMPOSIO_API_KEY on the Secrets page unmounts Composio, and GitHu t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = async (url, init) => url.startsWith('https://gh.test') ? answer(200, { login: 'octo' }) : fakeComposio().fetch(url, init) const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) await call(ctx, 'connector-connect', { id: 'github', token: fresh() }) await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) assert.deepEqual(mounts.map(mount => mount.config.serverName), ['github', 'composio']) @@ -381,7 +396,7 @@ test('a Composio address without https refuses to send the key', async (t) => { let asked = false globalThis.fetch = async () => { asked = true; return answer(201, SESSION) } const { ctx, home, mounts } = setup({ composio: { api: 'http://composio.test/api/v3.1' } }) - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const refused = await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) assert.equal(refused.ok, false) assert.match(refused.error.message, /https/) @@ -430,7 +445,7 @@ test('Composio makes one session per connect, keeps it (never the key) in state, const sent = fakeComposio(numbered()) globalThis.fetch = sent.fetch const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const key = composioKey() await call(ctx, 'connector-connect', { id: 'composio', token: key, apps: ['gmail'] }) assert.equal(sent.posts.length, 1, 'the key check and the mount share one session') @@ -462,7 +477,7 @@ test('a session Composio no longer has is made again on Try again, and the new o const sent = fakeComposio(numbered()) globalThis.fetch = sent.fetch const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) sent.live.clear() const retried = await call(ctx, 'connector-retry', { id: 'composio' }) @@ -480,7 +495,7 @@ test('Choose apps deletes the superseded session after the new one is kept; a re const sent = fakeComposio(() => refuse ? answer(400, { error: { message: 'Unknown toolkit nope' } }) : make()) globalThis.fetch = sent.fetch const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const key = composioKey() await call(ctx, 'connector-connect', { id: 'composio', token: key, apps: ['gmail'] }) const changed = await call(ctx, 'connector-apps', { id: 'composio', apps: ['gmail', 'notion'] }) @@ -506,7 +521,7 @@ test('Disconnect deletes the session with the key it was made with, drops it fro let breakDelete = false globalThis.fetch = (url, init) => breakDelete && init.method === 'DELETE' ? Promise.reject(new Error('boom')) : sent.fetch(url, init) const { ctx, home } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const key = composioKey() await call(ctx, 'connector-connect', { id: 'composio', token: key, apps: ['gmail'] }) const userId = JSON.parse(readFileSync(join(home, 'state.json'), 'utf8')).connectors.composio.userId @@ -534,7 +549,7 @@ test('a new key makes its own session and deletes the old key\'s session with th const sent = fakeComposio(numbered()) globalThis.fetch = sent.fetch const { ctx, home } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const first = composioKey() const second = composioKey() await call(ctx, 'connector-connect', { id: 'composio', token: first, apps: ['gmail'] }) @@ -584,7 +599,7 @@ test('an address saved by an older version is dropped from state.json at start, globalThis.fetch = sent.fetch const first = setup() const { home } = first - t.after(() => rmSync(home, { recursive: true, force: true, maxRetries: 5 })) + t.after(() => cleanHome(home)) const key = composioKey() await call(first.ctx, 'connector-connect', { id: 'composio', token: key, apps: ['gmail'] }) await first.ctx.dispose() @@ -613,7 +628,7 @@ test('a session whose answer names no address on Composio\'s host is made again, const sent = fakeComposio(numbered()) globalThis.fetch = sent.fetch const { ctx, home, mounts } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) sent.addresses.set('trs_1', 'https://evil.test/mcp') const retried = await call(ctx, 'connector-retry', { id: 'composio' }) @@ -628,7 +643,7 @@ test('a mount error that quotes the session address shows it as [session address t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = fakeComposio(numbered()).fetch const { ctx, home } = setup({ fail: 'url' }) - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const result = await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) const composio = result.value.find(entry => entry.id === 'composio') assert.equal(composio.state, 'error') @@ -709,7 +724,7 @@ test('the Connectors page searches apps with the pasted key before connecting (n return sent.fetch(url, init) } const { ctx, home } = setup() - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) + t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const key = composioKey() const keyless = await call(ctx, 'connector-app-search', { id: 'composio', query: 'not' }) diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch index 197d8467..0e939635 100644 --- a/patches/ds-bot/0001-core-fixes.patch +++ b/patches/ds-bot/0001-core-fixes.patch @@ -3553,7 +3553,7 @@ diff --git a/test/client-bundle.test.js b/test/client-bundle.test.js diff --git a/test/connectors.test.js b/test/connectors.test.js --- a/test/connectors.test.js +++ b/test/connectors.test.js -@@ -5,11 +5,11 @@ import { tmpdir } from 'node:os' +@@ -5,11 +5,26 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'node:test' import { apply } from '../index.js' @@ -3561,6 +3561,21 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js +import { cleanApps, composioAppStatus, composioDeleteSession, composioSearchApps, composioSession, composioSessionAlive, composioSessionFind, signIn } from '../src/host/connectors.js' import { fakeContext } from './fake-ctx.js' ++// A test's temp home can still be written by a background save that outlives dispose(), so a ++// plain rmSync sometimes raced and failed the whole run with ENOTEMPTY (a flaky verify/CI). Settle ++// and retry; if it is still racing, leave the temp dir — the test's assertions already passed. ++async function cleanHome(home) { ++ for (let attempt = 0; attempt < 5; attempt++) { ++ try { ++ rmSync(home, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 }) ++ return ++ } catch (error) { ++ if (error?.code !== 'ENOTEMPTY' && error?.code !== 'EBUSY' && error?.code !== 'ENOENT') throw error ++ await new Promise(resolve => setTimeout(resolve, 50)) ++ } ++ } ++} ++ const fresh = () => `ghp_${randomBytes(16).toString('hex')}` -const answer = (status, body) => ({ status, ok: status >= 200 && status < 300, json: async () => body }) +const answer = (status, body) => ({ status, ok: status >= 200 && status < 300, body, json: async () => body }) @@ -3595,7 +3610,12 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js return { ctx, home, mounts } } -@@ -84,11 +84,11 @@ test('connecting saves the token, kept out of every Bot shell, and mounts GitHub +@@ -80,15 +80,15 @@ test('connecting saves the token, kept out of every Bot shell, and mounts GitHub + t.after(() => { globalThis.fetch = realFetch }) + globalThis.fetch = async () => answer(200, { login: 'octo' }) + const { ctx, home, mounts } = setup() +- t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const token = fresh() const before = await call(ctx, 'state', {}) @@ -3618,8 +3638,21 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js assert.equal(mounts[0].disposed, true) assert.deepEqual((await call(ctx, 'state', {})).value.secrets, []) }) -@@ -129,7 +129,7 @@ test('a failed MCP connection shows the error without the token, and Try again m - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) +@@ -113,7 +113,7 @@ test('a token GitHub refuses is not saved', async (t) => { + t.after(() => { globalThis.fetch = realFetch }) + globalThis.fetch = async () => answer(401, {}) + const { ctx, home, mounts } = setup() +- t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const result = await call(ctx, 'connector-connect', { id: 'github', token: fresh() }) + assert.equal(result.ok, false) + assert.match(result.error.message, /did not accept/) +@@ -126,10 +126,10 @@ test('a failed MCP connection shows the error without the token, and Try again m + t.after(() => { globalThis.fetch = realFetch }) + globalThis.fetch = async () => answer(200, { login: 'octo' }) + const { ctx, home, mounts } = setup({ fail: true }) +- t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) const token = fresh() const result = await call(ctx, 'connector-connect', { id: 'github', token }) - const [github] = result.value @@ -3627,6 +3660,15 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js assert.equal(github.state, 'error') assert.ok(!github.error.includes(token), github.error) assert.match(github.error, /\[secret:GITHUB_TOKEN\]/) +@@ -142,7 +142,7 @@ test('deleting GITHUB_TOKEN on the Secrets page unmounts GitHub', async (t) => { + t.after(() => { globalThis.fetch = realFetch }) + globalThis.fetch = async () => answer(200, { login: 'octo' }) + const { ctx, home, mounts } = setup() +- t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + await call(ctx, 'connector-connect', { id: 'github', token: fresh() }) + await call(ctx, 'secret-delete', { name: 'GITHUB_TOKEN' }) + const state = await call(ctx, 'state', {}) @@ -150,13 +150,19 @@ test('deleting GITHUB_TOKEN on the Secrets page unmounts GitHub', async (t) => { assert.equal(state.value.connectors[0].state, 'off') }) @@ -3637,19 +3679,28 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js const ctx = fakeContext() capture(ctx) apply(ctx, { home, github: false }) - t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) +- t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) - assert.deepEqual((await call(ctx, 'state', {})).value.connectors, []) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + assert.deepEqual((await call(ctx, 'state', {})).value.connectors.map(entry => entry.id), ['composio']) + const other = teamHome() + const second = fakeContext() + capture(second) + apply(second, { home: other, github: false, composio: false }) -+ t.after(async () => { await second.dispose(); rmSync(other, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await second.dispose(); await cleanHome(other) }) + assert.deepEqual((await call(second, 'state', {})).value.connectors, []) }) test('a GitHub address without https refuses to send the token', async (t) => { -@@ -172,3 +172,562 @@ test('a GitHub address without https refuses to send the token', async (t) => { +@@ -165,10 +165,569 @@ test('a GitHub address without https refuses to send the token', async (t) => { + let asked = false + globalThis.fetch = async () => { asked = true; return answer(200, { login: 'octo' }) } + const { ctx, home, mounts } = setup({ github: { url: 'http://mcp.test/', api: 'https://gh.test' } }) +- t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const refused = await call(ctx, 'connector-connect', { id: 'github', token: fresh() }) + assert.equal(refused.ok, false) + assert.match(refused.error.message, /https/) assert.equal(asked, false) assert.equal(mounts.length, 0) }) @@ -3729,7 +3780,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + const sent = fakeComposio() + globalThis.fetch = sent.fetch + const { ctx, home, mounts } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const key = composioKey() + + const before = (await call(ctx, 'state', {})).value.connectors.find(entry => entry.id === 'composio') @@ -3780,7 +3831,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + const sent = fakeComposio(() => refuse ? answer(400, { error: { message: 'Unknown toolkit nope' } }) : answer(201, SESSION)) + globalThis.fetch = sent.fetch + const { ctx, home, mounts } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const early = await call(ctx, 'connector-apps', { id: 'composio', apps: ['gmail'] }) + assert.equal(early.ok, false) + assert.match(early.error.message, /Connect Composio first/) @@ -3809,7 +3860,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + t.after(() => { globalThis.fetch = realFetch }) + globalThis.fetch = fakeComposio(() => answer(401, {})).fetch + const { ctx, home, mounts } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const result = await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) + assert.equal(result.ok, false) + assert.match(result.error.message, /did not accept/) @@ -3822,7 +3873,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + t.after(() => { globalThis.fetch = realFetch }) + globalThis.fetch = fakeComposio().fetch + const { ctx, home, mounts } = setup({ fail: true }) -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const key = composioKey() + const result = await call(ctx, 'connector-connect', { id: 'composio', token: key, apps: ['gmail'] }) + const composio = result.value.find(entry => entry.id === 'composio') @@ -3837,7 +3888,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + t.after(() => { globalThis.fetch = realFetch }) + globalThis.fetch = async (url, init) => url.startsWith('https://gh.test') ? answer(200, { login: 'octo' }) : fakeComposio().fetch(url, init) + const { ctx, home, mounts } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + await call(ctx, 'connector-connect', { id: 'github', token: fresh() }) + await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) + assert.deepEqual(mounts.map(mount => mount.config.serverName), ['github', 'composio']) @@ -3854,7 +3905,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + let asked = false + globalThis.fetch = async () => { asked = true; return answer(201, SESSION) } + const { ctx, home, mounts } = setup({ composio: { api: 'http://composio.test/api/v3.1' } }) -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const refused = await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) + assert.equal(refused.ok, false) + assert.match(refused.error.message, /https/) @@ -3903,7 +3954,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + const sent = fakeComposio(numbered()) + globalThis.fetch = sent.fetch + const { ctx, home, mounts } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const key = composioKey() + await call(ctx, 'connector-connect', { id: 'composio', token: key, apps: ['gmail'] }) + assert.equal(sent.posts.length, 1, 'the key check and the mount share one session') @@ -3935,7 +3986,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + const sent = fakeComposio(numbered()) + globalThis.fetch = sent.fetch + const { ctx, home, mounts } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) + sent.live.clear() + const retried = await call(ctx, 'connector-retry', { id: 'composio' }) @@ -3953,7 +4004,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + const sent = fakeComposio(() => refuse ? answer(400, { error: { message: 'Unknown toolkit nope' } }) : make()) + globalThis.fetch = sent.fetch + const { ctx, home, mounts } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const key = composioKey() + await call(ctx, 'connector-connect', { id: 'composio', token: key, apps: ['gmail'] }) + const changed = await call(ctx, 'connector-apps', { id: 'composio', apps: ['gmail', 'notion'] }) @@ -3979,7 +4030,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + let breakDelete = false + globalThis.fetch = (url, init) => breakDelete && init.method === 'DELETE' ? Promise.reject(new Error('boom')) : sent.fetch(url, init) + const { ctx, home } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const key = composioKey() + await call(ctx, 'connector-connect', { id: 'composio', token: key, apps: ['gmail'] }) + const userId = JSON.parse(readFileSync(join(home, 'state.json'), 'utf8')).connectors.composio.userId @@ -4007,7 +4058,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + const sent = fakeComposio(numbered()) + globalThis.fetch = sent.fetch + const { ctx, home } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const first = composioKey() + const second = composioKey() + await call(ctx, 'connector-connect', { id: 'composio', token: first, apps: ['gmail'] }) @@ -4057,7 +4108,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + globalThis.fetch = sent.fetch + const first = setup() + const { home } = first -+ t.after(() => rmSync(home, { recursive: true, force: true, maxRetries: 5 })) ++ t.after(() => cleanHome(home)) + const key = composioKey() + await call(first.ctx, 'connector-connect', { id: 'composio', token: key, apps: ['gmail'] }) + await first.ctx.dispose() @@ -4086,7 +4137,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + const sent = fakeComposio(numbered()) + globalThis.fetch = sent.fetch + const { ctx, home, mounts } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) + sent.addresses.set('trs_1', 'https://evil.test/mcp') + const retried = await call(ctx, 'connector-retry', { id: 'composio' }) @@ -4101,7 +4152,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + t.after(() => { globalThis.fetch = realFetch }) + globalThis.fetch = fakeComposio(numbered()).fetch + const { ctx, home } = setup({ fail: 'url' }) -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const result = await call(ctx, 'connector-connect', { id: 'composio', token: composioKey(), apps: ['gmail'] }) + const composio = result.value.find(entry => entry.id === 'composio') + assert.equal(composio.state, 'error') @@ -4182,7 +4233,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js + return sent.fetch(url, init) + } + const { ctx, home } = setup() -+ t.after(async () => { await ctx.dispose(); rmSync(home, { recursive: true, force: true, maxRetries: 5 }) }) ++ t.after(async () => { await ctx.dispose(); await cleanHome(home) }) + const key = composioKey() + + const keyless = await call(ctx, 'connector-app-search', { id: 'composio', query: 'not' })