From e2340c9a6bf7cd89ea6e14cce70d6a9228d7d117 Mon Sep 17 00:00:00 2001 From: Danyang Chen Date: Tue, 10 Nov 2026 16:37:22 +0800 Subject: [PATCH] test(home-lock): remove the home after the host disposes tempHome() registered its removal before start() registered dispose(), and node:test runs after-hooks in registration order, so every run of test/home-lock.test.js left two ds-bot-lock-* directories in TMPDIR: dispose() appends to /logs/diagnostics.jsonl and that append re-creates the folder. A host test now asks tempHome(t, { defer: true }) and start() registers dispose() first and the removal after it (#2730). Docs-not-needed: test-only after-hook ordering; no owned path or architecture change. --- core/test/home-lock.test.js | 28 +++++++++++------ patches/ds-bot/0001-core-fixes.patch | 47 +++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/core/test/home-lock.test.js b/core/test/home-lock.test.js index b59a845fe..ea24b6241 100644 --- a/core/test/home-lock.test.js +++ b/core/test/home-lock.test.js @@ -9,9 +9,16 @@ import { createRuntime, provideFlyBot } from '../index.js' import { homeLock } from '../src/host/home-lock.js' import { fakeContext } from './fake-ctx.js' -const tempHome = (t) => { +const removeHome = (home) => rmSync(home, { recursive: true, force: true, maxRetries: 5 }) + +// A host writes to /logs/diagnostics.jsonl while it disposes, and that write re-creates the +// folder it goes in (src/host/diagnostics.js). node:test runs after-hooks in the order they were +// registered, so a removal registered here runs before dispose() and leaves a fresh +// ds-bot-lock-* directory in TMPDIR on every run (#2730). A test that starts a host passes +// `defer: true`, and start() registers dispose() first and the removal after it. +const tempHome = (t, { defer = false } = {}) => { const home = mkdtempSync(join(tmpdir(), 'ds-bot-lock-')) - t.after(() => rmSync(home, { recursive: true, force: true, maxRetries: 5 })) + if (!defer) t.after(() => removeHome(home)) return home } @@ -123,6 +130,7 @@ function start(t, home) { const rt = createRuntime(ctx, { home }) provideFlyBot(ctx, rt.flybot) t.after(() => ctx.dispose()) + t.after(() => removeHome(home)) const call = async (endpoint, payload) => (await route.fetch(new Request('http://localhost/api/bot', { method: 'POST', body: JSON.stringify({ endpoint, payload }) }))).json() call.ctx = ctx call.rt = rt @@ -130,7 +138,7 @@ function start(t, home) { } test('while another dsh holds the team, the channel reads it, refuses changes, then takes over', async (t) => { - const home = tempHome(t) + const home = tempHome(t, { defer: true }) writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) // The test runner's parent process: alive, on this host, and not this process. writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) @@ -160,7 +168,7 @@ test('while another dsh holds the team, the channel reads it, refuses changes, t }) test('a reading host follows the other host\'s keys, never writes them, and keeps them after taking over', async (t) => { - const home = tempHome(t) + const home = tempHome(t, { defer: true }) const secretsPath = join(home, 'secrets.json') const key = (name, value) => ({ name, value, purpose: '', scope: 'all', requestedBy: null, createdAt: 1, updatedAt: 1 }) const writeKeys = (keys, revision) => { @@ -191,7 +199,7 @@ test('a reading host follows the other host\'s keys, never writes them, and keep }) test('#1027: a save in a stale read-only window takes the lock and writes', async (t) => { - const home = tempHome(t) + const home = tempHome(t, { defer: true }) writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) const call = start(t, home) @@ -209,7 +217,7 @@ test('#1027: a save in a stale read-only window takes the lock and writes', asyn }) test('#1032: a change made before save() lands; taking over does not replace it with what the other host left', async (t) => { - const home = tempHome(t) + const home = tempHome(t, { defer: true }) writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) const call = start(t, home) @@ -231,7 +239,7 @@ test('#1032: a change made before save() lands; taking over does not replace it }) test('#1032: mutate() adopts what the other host left before the change, so both survive', async (t) => { - const home = tempHome(t) + const home = tempHome(t, { defer: true }) writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) const call = start(t, home) @@ -250,7 +258,7 @@ test('#1032: mutate() adopts what the other host left before the change, so both }) test('#1041: a save() that takes over the lock says so once, naming the host it took it from', async (t) => { - const home = tempHome(t) + const home = tempHome(t, { defer: true }) writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) const call = start(t, home) @@ -277,7 +285,7 @@ test('#1041: a save() that takes over the lock says so once, naming the host it }) test('read-only host (#14): FlyBot queries answer, plugin reads answer, writes and polling stop', async (t) => { - const home = tempHome(t) + const home = tempHome(t, { defer: true }) writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) const call = start(t, home) @@ -298,7 +306,7 @@ test('read-only host (#14): FlyBot queries answer, plugin reads answer, writes a }) test('#1363: the delivery board endpoints are gone, and a reading host still refuses a write', async (t) => { - const home = tempHome(t) + const home = tempHome(t, { defer: true }) writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) const call = start(t, home) diff --git a/patches/ds-bot/0001-core-fixes.patch b/patches/ds-bot/0001-core-fixes.patch index a23d498a3..9f111d567 100644 --- a/patches/ds-bot/0001-core-fixes.patch +++ b/patches/ds-bot/0001-core-fixes.patch @@ -12362,7 +12362,7 @@ diff --git a/test/history.test.js b/test/history.test.js diff --git a/test/home-lock.test.js b/test/home-lock.test.js --- a/test/home-lock.test.js +++ b/test/home-lock.test.js -@@ -5,7 +5,7 @@ import { syncBuiltinESMExports } from 'node:module' +@@ -5,13 +5,20 @@ import { syncBuiltinESMExports } from 'node:module' import { hostname, tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'node:test' @@ -12371,7 +12371,22 @@ diff --git a/test/home-lock.test.js b/test/home-lock.test.js import { homeLock } from '../src/host/home-lock.js' import { fakeContext } from './fake-ctx.js' -@@ -118,10 +118,14 @@ function start(t, home) { +-const tempHome = (t) => { ++const removeHome = (home) => rmSync(home, { recursive: true, force: true, maxRetries: 5 }) ++ ++// A host writes to /logs/diagnostics.jsonl while it disposes, and that write re-creates the ++// folder it goes in (src/host/diagnostics.js). node:test runs after-hooks in the order they were ++// registered, so a removal registered here runs before dispose() and leaves a fresh ++// ds-bot-lock-* directory in TMPDIR on every run (#2730). A test that starts a host passes ++// `defer: true`, and start() registers dispose() first and the removal after it. ++const tempHome = (t, { defer = false } = {}) => { + const home = mkdtempSync(join(tmpdir(), 'ds-bot-lock-')) +- t.after(() => rmSync(home, { recursive: true, force: true, maxRetries: 5 })) ++ if (!defer) t.after(() => removeHome(home)) + return home + } + +@@ -118,15 +118,20 @@ function start(t, home) { route = options return register(options) } @@ -12381,19 +12396,35 @@ diff --git a/test/home-lock.test.js b/test/home-lock.test.js + const rt = createRuntime(ctx, { home }) + provideFlyBot(ctx, rt.flybot) t.after(() => ctx.dispose()) ++ t.after(() => removeHome(home)) const call = async (endpoint, payload) => (await route.fetch(new Request('http://localhost/api/bot', { method: 'POST', body: JSON.stringify({ endpoint, payload }) }))).json() call.ctx = ctx + call.rt = rt return call } + test('while another dsh holds the team, the channel reads it, refuses changes, then takes over', async (t) => { +- const home = tempHome(t) ++ const home = tempHome(t, { defer: true }) + writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) + // The test runner's parent process: alive, on this host, and not this process. + writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) +@@ -156,7 +156,7 @@ test('while another dsh holds the team, the channel reads it, refuses changes, t + }) + + test('a reading host follows the other host\'s keys, never writes them, and keeps them after taking over', async (t) => { +- const home = tempHome(t) ++ const home = tempHome(t, { defer: true }) + const secretsPath = join(home, 'secrets.json') + const key = (name, value) => ({ name, value, purpose: '', scope: 'all', requestedBy: null, createdAt: 1, updatedAt: 1 }) + const writeKeys = (keys, revision) => { @@ -185,3 +185,129 @@ test('a reading host follows the other host\'s keys, never writes them, and keep const saved = JSON.parse(readFileSync(secretsPath, 'utf8')).secrets.map(secret => secret.name).sort() assert.deepEqual(saved, ['FIRST_KEY', 'MINE', 'SECOND_KEY', 'THIRD_KEY']) }) + +test('#1027: a save in a stale read-only window takes the lock and writes', async (t) => { -+ const home = tempHome(t) ++ const home = tempHome(t, { defer: true }) + writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) + writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) + const call = start(t, home) @@ -12411,7 +12442,7 @@ diff --git a/test/home-lock.test.js b/test/home-lock.test.js +}) + +test('#1032: a change made before save() lands; taking over does not replace it with what the other host left', async (t) => { -+ const home = tempHome(t) ++ const home = tempHome(t, { defer: true }) + writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) + writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) + const call = start(t, home) @@ -12433,7 +12464,7 @@ diff --git a/test/home-lock.test.js b/test/home-lock.test.js +}) + +test('#1032: mutate() adopts what the other host left before the change, so both survive', async (t) => { -+ const home = tempHome(t) ++ const home = tempHome(t, { defer: true }) + writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) + writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) + const call = start(t, home) @@ -12452,7 +12483,7 @@ diff --git a/test/home-lock.test.js b/test/home-lock.test.js +}) + +test('#1041: a save() that takes over the lock says so once, naming the host it took it from', async (t) => { -+ const home = tempHome(t) ++ const home = tempHome(t, { defer: true }) + writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) + writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) + const call = start(t, home) @@ -12479,7 +12510,7 @@ diff --git a/test/home-lock.test.js b/test/home-lock.test.js +}) + +test('read-only host (#14): FlyBot queries answer, plugin reads answer, writes and polling stop', async (t) => { -+ const home = tempHome(t) ++ const home = tempHome(t, { defer: true }) + writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) + writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) + const call = start(t, home) @@ -12500,7 +12531,7 @@ diff --git a/test/home-lock.test.js b/test/home-lock.test.js +}) + +test('#1363: the delivery board endpoints are gone, and a reading host still refuses a write', async (t) => { -+ const home = tempHome(t) ++ const home = tempHome(t, { defer: true }) + writeFileSync(join(home, 'state.json'), JSON.stringify(TEAM)) + writeFileSync(join(home, 'state.lock'), JSON.stringify({ pid: process.ppid, host: hostname(), token: 'other' })) + const call = start(t, home)