From d7cdbfc89bbc15a2f53d794d648a6107e6dea820 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Wed, 21 Oct 2026 08:34:48 +0800 Subject: [PATCH] Make the connectors test cleanup stable (ENOTEMPTY flake) A test's temp home can still be written by a background save that outlives dispose(), so the raw rmSync cleanup sometimes raced and failed the whole run with ENOTEMPTY. The cleanup now settles and retries, and leaves the temp dir when it is still racing; a test pins that contract. Test-only. Recaptured from #533 on a fresh branch (#532); the owner's session had let that branch go stale against main. Fixes #532 --- core/test/connectors.test.js | 78 ++++++++++++------ patches/ds-bot/0001-core-fixes.patch | 113 +++++++++++++++++++++------ 2 files changed, 146 insertions(+), 45 deletions(-) diff --git a/core/test/connectors.test.js b/core/test/connectors.test.js index 82bd7ac1..cb2d22fb 100644 --- a/core/test/connectors.test.js +++ b/core/test/connectors.test.js @@ -8,6 +8,40 @@ 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, remove = rmSync) { + for (let attempt = 0; attempt < 5; attempt++) { + try { + remove(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)) + } + } +} + +// #532: the cleanup itself is what has to be stable, so pin its contract — a racing save is waited +// out, a directory that never settles is left behind instead of failing the run, and any other +// error still surfaces. +test('the cleanup waits out a racing save and leaves a directory that never settles', async () => { + const tries = [] + await cleanHome('ignored', () => { + tries.push(1) + if (tries.length < 3) throw Object.assign(new Error('directory not empty'), { code: 'ENOTEMPTY' }) + }) + assert.equal(tries.length, 3, 'it settles after the third try') + tries.length = 0 + await cleanHome('ignored', () => { + tries.push(1) + throw Object.assign(new Error('still racing'), { code: 'EBUSY' }) + }) + assert.equal(tries.length, 5, 'it gives up quietly after five tries') + await assert.rejects(cleanHome('ignored', () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }) }), /denied/) +}) + const fresh = () => `ghp_${randomBytes(16).toString('hex')}` const answer = (status, body) => ({ status, ok: status >= 200 && status < 300, body, json: async () => body }) @@ -82,7 +116,7 @@ test('connecting saves the token for all of the owner\'s Bots (#545) and mounts 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 +149,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 +162,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 +178,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 +191,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 +207,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 +290,7 @@ test('connecting Composio saves the key for all of the owner\'s Bots (#545), kee 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 +341,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 +370,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 +383,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 +398,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 +415,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 +464,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 +496,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 +514,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 +540,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 +568,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 +618,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 +647,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 +662,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 +743,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 b95c4751..4e66e705 100644 --- a/patches/ds-bot/0001-core-fixes.patch +++ b/patches/ds-bot/0001-core-fixes.patch @@ -4231,7 +4231,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,45 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'node:test' import { apply } from '../index.js' @@ -4239,6 +4239,40 @@ 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, remove = rmSync) { ++ for (let attempt = 0; attempt < 5; attempt++) { ++ try { ++ remove(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)) ++ } ++ } ++} ++ ++// #532: the cleanup itself is what has to be stable, so pin its contract — a racing save is waited ++// out, a directory that never settles is left behind instead of failing the run, and any other ++// error still surfaces. ++test('the cleanup waits out a racing save and leaves a directory that never settles', async () => { ++ const tries = [] ++ await cleanHome('ignored', () => { ++ tries.push(1) ++ if (tries.length < 3) throw Object.assign(new Error('directory not empty'), { code: 'ENOTEMPTY' }) ++ }) ++ assert.equal(tries.length, 3, 'it settles after the third try') ++ tries.length = 0 ++ await cleanHome('ignored', () => { ++ tries.push(1) ++ throw Object.assign(new Error('still racing'), { code: 'EBUSY' }) ++ }) ++ assert.equal(tries.length, 5, 'it gives up quietly after five tries') ++ await assert.rejects(cleanHome('ignored', () => { throw Object.assign(new Error('denied'), { code: 'EACCES' }) }), /denied/) ++}) ++ 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 }) @@ -4273,7 +4307,7 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js return { ctx, home, mounts } } -@@ -75,7 +75,7 @@ async function call(ctx, endpoint, payload) { +@@ -75,20 +75,20 @@ async function call(ctx, endpoint, payload) { return response.json() } @@ -4282,7 +4316,9 @@ diff --git a/test/connectors.test.js b/test/connectors.test.js const realFetch = globalThis.fetch t.after(() => { globalThis.fetch = realFetch }) globalThis.fetch = async () => answer(200, { login: 'octo' }) -@@ -84,11 +84,11 @@ test('connecting saves the token, kept out of every Bot shell, and mounts GitHub + 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', {}) @@ -4311,8 +4347,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 @@ -4320,6 +4369,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') }) @@ -4330,19 +4388,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) }) @@ -4422,7 +4489,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') @@ -4473,7 +4540,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/) @@ -4502,7 +4569,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/) @@ -4515,7 +4582,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') @@ -4530,7 +4597,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']) @@ -4547,7 +4614,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/) @@ -4596,7 +4663,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') @@ -4628,7 +4695,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' }) @@ -4646,7 +4713,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'] }) @@ -4672,7 +4739,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 @@ -4700,7 +4767,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'] }) @@ -4750,7 +4817,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() @@ -4779,7 +4846,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' }) @@ -4794,7 +4861,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') @@ -4875,7 +4942,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' })