From 1b25a32cb9f5f377a9d71dda381c0d8ac819e249 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 8 Sep 2026 22:39:45 -0700 Subject: [PATCH 1/3] fix: remaining strong box file --- lib/webdriveragent.ts | 20 ++--- test/unit/wda-cleanup.spec.ts | 137 ++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 test/unit/wda-cleanup.spec.ts diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index 736b3fdf8..af5636357 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -584,23 +584,18 @@ export class WebDriverAgent { const packageInfo = JSON.parse(await fs.readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8')); const box = strongbox(packageInfo.name); - let boxItem = box.getItem(RECENT_MODULE_VERSION_ITEM_NAME); - if (!boxItem) { + // Each Strongbox instance starts with an empty item map. Load the persisted value from disk. + const boxItem = await box.createItem(RECENT_MODULE_VERSION_ITEM_NAME); + let recentModuleVersion = boxItem.value; + if (recentModuleVersion === undefined) { const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH); if (await fs.exists(timestampPath)) { - // TODO: It is probably a bit ugly to hardcode the recent version string, - // TODO: hovewer it should do the job as a temporary transition trick - // TODO: to switch from a hardcoded file path to the strongbox usage. - try { - boxItem = await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, '5.0.0'); - } catch (e: any) { - this.log.warn(`The actual module version cannot be persisted: ${e.message}`); - return; - } + // Migrate the legacy marker only when no version has been persisted yet. + recentModuleVersion = '5.0.0'; } else { this.log.info('There is no need to perform the project cleanup. A fresh install has been detected'); try { - await box.createItemWithValue(RECENT_MODULE_VERSION_ITEM_NAME, packageInfo.version); + await boxItem.write(packageInfo.version); } catch (e: any) { this.log.warn(`The actual module version cannot be persisted: ${e.message}`); } @@ -608,7 +603,6 @@ export class WebDriverAgent { } } - let recentModuleVersion = await boxItem.read(); try { recentModuleVersion = util.coerceVersion(recentModuleVersion, true); } catch (e: any) { diff --git a/test/unit/wda-cleanup.spec.ts b/test/unit/wda-cleanup.spec.ts new file mode 100644 index 000000000..62a0c53da --- /dev/null +++ b/test/unit/wda-cleanup.spec.ts @@ -0,0 +1,137 @@ +import assert from 'node:assert/strict'; +import {mkdtemp, readFile, rm} from 'node:fs/promises'; +import {tmpdir} from 'node:os'; +import path from 'node:path'; +import {afterEach, beforeEach, describe, it, mock} from 'node:test'; + +import {strongbox} from '@appium/strongbox'; +import {fs} from '@appium/support'; +import sinon from 'sinon'; + +import {WDA_UPGRADE_TIMESTAMP_PATH} from '../../lib/constants.js'; +import {BOOTSTRAP_PATH} from '../../lib/utils/index.js'; + +let container: string; +function isolatedStrongbox(name: string) { + const box = strongbox(name); + // Preserve the temporary path verbatim instead of Strongbox's container slugification. + Object.defineProperty(box, 'container', {value: container}); + return box; +} +mock.module('@appium/strongbox', { + namedExports: { + strongbox: isolatedStrongbox, + }, +}); +const {WebDriverAgent} = await import('../../lib/webdriveragent.js'); +const packageInfo = JSON.parse(await readFile(path.join(BOOTSTRAP_PATH, 'package.json'), 'utf8')); +const itemName = 'recentWdaModuleVersion'; + +describe('WDA project cleanup persistence', function () { + let sandbox: sinon.SinonSandbox; + let legacyMarker: sinon.SinonStub; + + beforeEach(async function () { + container = await mkdtemp(path.join(tmpdir(), 'wda-cleanup-')); + sandbox = sinon.createSandbox(); + legacyMarker = sandbox + .stub(fs, 'exists') + .withArgs(path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH)) + .resolves(false); + }); + + afterEach(async function () { + sandbox.restore(); + await rm(container, {recursive: true, force: true}); + }); + + function newAgent() { + const agent = new WebDriverAgent({device: {udid: 'test-udid'}, platformVersion: '17.2'}); + const clean = sandbox.stub(agent.xcodebuild, 'cleanProject').resolves(); + return {clean, run: async () => await (agent as any)._cleanupProjectIfFresh()}; + } + + async function persist(version: string) { + await isolatedStrongbox(packageInfo.name).createItemWithValue(itemName, version); + } + + async function persistedVersion() { + return (await isolatedStrongbox(packageInfo.name).createItem(itemName)).value; + } + + it('reuses the persisted version despite a legacy marker across new agents', async function () { + legacyMarker.resolves(true); + await persist(packageInfo.version); + for (let i = 0; i < 2; i++) { + const agent = newAgent(); + await agent.run(); + sandbox.assert.notCalled(agent.clean); + } + sandbox.assert.notCalled(legacyMarker); + assert.equal(await persistedVersion(), packageInfo.version); + }); + + it('cleans an older persisted version once without a legacy marker', async function () { + await persist('5.0.0'); + const first = newAgent(); + await first.run(); + sandbox.assert.calledOnce(first.clean); + assert.equal(await persistedVersion(), packageInfo.version); + const second = newAgent(); + await second.run(); + sandbox.assert.notCalled(second.clean); + sandbox.assert.notCalled(legacyMarker); + }); + + it('migrates a legacy installation once even when its marker remains', async function () { + legacyMarker.resolves(true); + const first = newAgent(); + await first.run(); + sandbox.assert.calledOnce(first.clean); + assert.equal(await persistedVersion(), packageInfo.version); + const second = newAgent(); + await second.run(); + sandbox.assert.notCalled(second.clean); + sandbox.assert.calledOnce(legacyMarker); + }); + + it('initializes a fresh installation without cleaning', async function () { + for (let i = 0; i < 2; i++) { + const agent = newAgent(); + await agent.run(); + sandbox.assert.notCalled(agent.clean); + } + sandbox.assert.calledOnce(legacyMarker); + assert.equal(await persistedVersion(), packageInfo.version); + }); + + it('preserves a newer persisted version', async function () { + await persist('999.0.0'); + const agent = newAgent(); + await agent.run(); + sandbox.assert.notCalled(agent.clean); + assert.equal(await persistedVersion(), '999.0.0'); + }); + + it('repairs a damaged persisted version without treating it as legacy state', async function () { + legacyMarker.resolves(true); + await persist('not-a-version'); + const agent = newAgent(); + await agent.run(); + sandbox.assert.notCalled(agent.clean); + sandbox.assert.notCalled(legacyMarker); + assert.equal(await persistedVersion(), packageInfo.version); + }); + + it('retries cleanup after a failure without recording a successful upgrade', async function () { + await persist('5.0.0'); + const first = newAgent(); + first.clean.rejects(new Error('cleanup failed')); + await first.run(); + assert.equal(await persistedVersion(), '5.0.0'); + const second = newAgent(); + await second.run(); + sandbox.assert.calledOnce(second.clean); + assert.equal(await persistedVersion(), packageInfo.version); + }); +}); From 45721075956481ab616860afe7718ee1932acc53 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 8 Sep 2026 22:42:42 -0700 Subject: [PATCH 2/3] leave the todo --- lib/webdriveragent.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index af5636357..f5989d79b 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -591,6 +591,7 @@ export class WebDriverAgent { const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH); if (await fs.exists(timestampPath)) { // Migrate the legacy marker only when no version has been persisted yet. + // TODO: Replace the hardcoded version used for migration from the legacy timestamp file. recentModuleVersion = '5.0.0'; } else { this.log.info('There is no need to perform the project cleanup. A fresh install has been detected'); From 83b74b7a42d76db9ea923733838028e8cf608611 Mon Sep 17 00:00:00 2001 From: Kazuaki Matsuo Date: Tue, 8 Sep 2026 23:31:39 -0700 Subject: [PATCH 3/3] leave exported const as deprecated --- lib/constants.ts | 3 +++ lib/webdriveragent.ts | 26 +++++++------------------- test/unit/wda-cleanup.spec.ts | 11 +++++------ 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/lib/constants.ts b/lib/constants.ts index 44561c17a..98235a2d8 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -13,6 +13,9 @@ export const PLATFORM_NAME_IOS = 'iOS'; export const SDK_DEVICE = 'iphoneos'; +/** + * @deprecated The WDA upgrade timestamp path is no longer used. + */ export const WDA_UPGRADE_TIMESTAMP_PATH = path.join('.appium', 'webdriveragent', 'upgrade.time'); /** diff --git a/lib/webdriveragent.ts b/lib/webdriveragent.ts index f5989d79b..b55a48167 100644 --- a/lib/webdriveragent.ts +++ b/lib/webdriveragent.ts @@ -7,12 +7,7 @@ import type {AppiumLogger, StringRecord} from '@appium/types'; import AsyncLock from 'async-lock'; import {waitForCondition} from 'asyncbox'; -import { - WDA_RUNNER_BUNDLE_ID, - WDA_BASE_URL, - WDA_UPGRADE_TIMESTAMP_PATH, - DEFAULT_TEST_BUNDLE_SUFFIX, -} from './constants.js'; +import {WDA_RUNNER_BUNDLE_ID, WDA_BASE_URL, DEFAULT_TEST_BUNDLE_SUFFIX} from './constants.js'; import {log as defaultLogger} from './logger.js'; import {NoSessionProxy} from './no-session-proxy.js'; import type { @@ -588,20 +583,13 @@ export class WebDriverAgent { const boxItem = await box.createItem(RECENT_MODULE_VERSION_ITEM_NAME); let recentModuleVersion = boxItem.value; if (recentModuleVersion === undefined) { - const timestampPath = path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH); - if (await fs.exists(timestampPath)) { - // Migrate the legacy marker only when no version has been persisted yet. - // TODO: Replace the hardcoded version used for migration from the legacy timestamp file. - recentModuleVersion = '5.0.0'; - } else { - this.log.info('There is no need to perform the project cleanup. A fresh install has been detected'); - try { - await boxItem.write(packageInfo.version); - } catch (e: any) { - this.log.warn(`The actual module version cannot be persisted: ${e.message}`); - } - return; + this.log.info('There is no need to perform the project cleanup. A fresh install has been detected'); + try { + await boxItem.write(packageInfo.version); + } catch (e: any) { + this.log.warn(`The actual module version cannot be persisted: ${e.message}`); } + return; } try { diff --git a/test/unit/wda-cleanup.spec.ts b/test/unit/wda-cleanup.spec.ts index 62a0c53da..ed1489800 100644 --- a/test/unit/wda-cleanup.spec.ts +++ b/test/unit/wda-cleanup.spec.ts @@ -8,7 +8,6 @@ import {strongbox} from '@appium/strongbox'; import {fs} from '@appium/support'; import sinon from 'sinon'; -import {WDA_UPGRADE_TIMESTAMP_PATH} from '../../lib/constants.js'; import {BOOTSTRAP_PATH} from '../../lib/utils/index.js'; let container: string; @@ -36,7 +35,7 @@ describe('WDA project cleanup persistence', function () { sandbox = sinon.createSandbox(); legacyMarker = sandbox .stub(fs, 'exists') - .withArgs(path.resolve(process.env.HOME ?? '', WDA_UPGRADE_TIMESTAMP_PATH)) + .withArgs(path.resolve(process.env.HOME ?? '', '.appium', 'webdriveragent', 'upgrade.time')) .resolves(false); }); @@ -83,16 +82,16 @@ describe('WDA project cleanup persistence', function () { sandbox.assert.notCalled(legacyMarker); }); - it('migrates a legacy installation once even when its marker remains', async function () { + it('initializes missing version state without consulting a legacy marker', async function () { legacyMarker.resolves(true); const first = newAgent(); await first.run(); - sandbox.assert.calledOnce(first.clean); + sandbox.assert.notCalled(first.clean); assert.equal(await persistedVersion(), packageInfo.version); const second = newAgent(); await second.run(); sandbox.assert.notCalled(second.clean); - sandbox.assert.calledOnce(legacyMarker); + sandbox.assert.notCalled(legacyMarker); }); it('initializes a fresh installation without cleaning', async function () { @@ -101,7 +100,7 @@ describe('WDA project cleanup persistence', function () { await agent.run(); sandbox.assert.notCalled(agent.clean); } - sandbox.assert.calledOnce(legacyMarker); + sandbox.assert.notCalled(legacyMarker); assert.equal(await persistedVersion(), packageInfo.version); });