From 1b5500e0185d322e6b40594531892304c2d39e53 Mon Sep 17 00:00:00 2001 From: omsd512-W Date: Fri, 14 Aug 2026 16:00:47 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E9=81=BF=E5=85=8D=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=92=9E=E9=94=81=E5=90=8E=E8=AF=AF=E6=8A=A5?= =?UTF-8?q?=E6=88=90=E5=8A=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/sync.js | 2 +- test/sync-background.test.js | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/commands/sync.js b/src/commands/sync.js index 11c0a6f9e..5db4fb3b2 100644 --- a/src/commands/sync.js +++ b/src/commands/sync.js @@ -363,7 +363,7 @@ async function acquireSyncLock( priorityPollMs = PRIORITY_LOCK_POLL_MS, } = {}, ) { - const waitsForPriority = Boolean(opts.drain || opts.publishAccount); + const waitsForPriority = Boolean(!opts.auto || opts.drain || opts.publishAccount); let lock = await openLock(lockPath, { quietIfLocked: opts.auto || waitsForPriority, }); diff --git a/test/sync-background.test.js b/test/sync-background.test.js index bfb2122f9..1cf714940 100644 --- a/test/sync-background.test.js +++ b/test/sync-background.test.js @@ -4,7 +4,7 @@ const os = require("node:os"); const path = require("node:path"); const { test } = require("node:test"); -const { cmdSync } = require("../src/commands/sync"); +const { acquireSyncLock, cmdSync } = require("../src/commands/sync"); const { openLock } = require("../src/lib/fs"); function tokenCountLine({ ts, totalTokens }) { @@ -265,6 +265,29 @@ test("native account publication waits for an overlapping sync instead of report }); }); +test("manual sync waits for an overlapping background sync instead of reporting success", async () => { + await withTempSyncEnv(async (home) => { + const trackerDir = path.join(home, ".tokentracker", "tracker"); + await fs.mkdir(trackerDir, { recursive: true }); + const lockPath = path.join(trackerDir, "sync.lock"); + const active = await openLock(lockPath, { + quietIfLocked: true, + }); + assert.ok(active); + + const waitingLock = acquireSyncLock( + lockPath, + { auto: false, drain: false, publishAccount: false }, + { priorityWaitMs: 500, priorityPollMs: 10 }, + ); + await new Promise((resolve) => setTimeout(resolve, 30)); + await active.release(); + const acquired = await waitingLock; + assert.ok(acquired); + await acquired.release(); + }); +}); + test("manual drain fails explicitly when the sync lock stays busy", async () => { await withTempSyncEnv(async (home) => { const trackerDir = path.join(home, ".tokentracker", "tracker"); From 8deea96320840c94b1ad02aa830c2db761477646 Mon Sep 17 00:00:00 2001 From: omsd512-W Date: Fri, 14 Aug 2026 16:49:33 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E6=94=B6=E7=AA=84=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E9=94=81=E7=AD=89=E5=BE=85=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/sync.js | 4 +++- src/lib/local-api.js | 3 +++ test/local-api-background.test.js | 13 +++++++++++-- test/local-api-security.test.js | 25 +++++++++++++++++++++---- test/sync-background.test.js | 25 +++++++++++++------------ 5 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/commands/sync.js b/src/commands/sync.js index 5db4fb3b2..8d067060d 100644 --- a/src/commands/sync.js +++ b/src/commands/sync.js @@ -363,7 +363,7 @@ async function acquireSyncLock( priorityPollMs = PRIORITY_LOCK_POLL_MS, } = {}, ) { - const waitsForPriority = Boolean(!opts.auto || opts.drain || opts.publishAccount); + const waitsForPriority = Boolean(opts.waitForLock || opts.drain || opts.publishAccount); let lock = await openLock(lockPath, { quietIfLocked: opts.auto || waitsForPriority, }); @@ -2835,6 +2835,7 @@ function parseArgs(argv) { fromOpenclaw: false, source: null, drain: false, + waitForLock: false, background: false, publishAccount: false, allLocalSources: false, @@ -2852,6 +2853,7 @@ function parseArgs(argv) { } else if (a.startsWith("--source=")) out.source = normalizeSyncSource(a.slice("--source=".length)); else if (a === "--drain") out.drain = true; + else if (a === "--wait-for-lock") out.waitForLock = true; else if (a === "--background" || a === "--lightweight") out.background = true; else if (a === "--publish-account") out.publishAccount = true; else if (a === "--all-local-sources") out.allLocalSources = true; diff --git a/src/lib/local-api.js b/src/lib/local-api.js index d1a9bc9f6..be01075b8 100644 --- a/src/lib/local-api.js +++ b/src/lib/local-api.js @@ -880,6 +880,7 @@ function runSyncCommand(extraEnv = {}, opts = {}) { if (opts.publishAccount === true) args.push("--publish-account"); if (opts.allLocalSources === true) args.push("--all-local-sources"); if (opts.drain === true) args.push("--drain"); + if (opts.waitForLock === true) args.push("--wait-for-lock"); const child = spawn(process.execPath, args, { env: { ...process.env, ...extraEnv }, stdio: ["ignore", "pipe", "pipe"], @@ -1971,6 +1972,8 @@ function createLocalApiHandler({ queuePath }) { background, publishAccount, allLocalSources, + waitForLock: + !drain && !background && Boolean(extraEnv.TOKENTRACKER_DEVICE_TOKEN), }); try { const { resetUsageLimitsCache } = require("./usage-limits"); diff --git a/test/local-api-background.test.js b/test/local-api-background.test.js index f7600b3ec..de2731067 100644 --- a/test/local-api-background.test.js +++ b/test/local-api-background.test.js @@ -210,7 +210,12 @@ test("local-api background and lightweight require boolean true", async () => { for (const body of cases) { const call = await runLocalSync({ auto: true, ...body }); const args = call.args; - assert.deepEqual(args.slice(-3), [path.join(process.cwd(), "bin/tracker.js"), "sync", "--auto"]); + assert.deepEqual(args.slice(-4), [ + path.join(process.cwd(), "bin/tracker.js"), + "sync", + "--auto", + "--wait-for-lock", + ]); } }); @@ -356,7 +361,11 @@ test("local-api manual and drain sync still issue relayed cloud device tokens", const savedBaseUrl = process.env.TOKENTRACKER_INSFORGE_BASE_URL; const savedFetch = global.fetch; const cases = [ - { prefix: "tokentracker-local-api-manual-cloud-", body: {}, expectedArgs: ["sync"] }, + { + prefix: "tokentracker-local-api-manual-cloud-", + body: {}, + expectedArgs: ["sync", "--wait-for-lock"], + }, { prefix: "tokentracker-local-api-drain-cloud-", body: { drain: true }, expectedArgs: ["sync", "--drain"] }, ]; diff --git a/test/local-api-security.test.js b/test/local-api-security.test.js index 738f36b4d..dfd7c5e04 100644 --- a/test/local-api-security.test.js +++ b/test/local-api-security.test.js @@ -492,7 +492,12 @@ test("local sync only treats boolean true as background or lightweight", async ( assert.equal(handled, true); assert.equal(res.statusCode, 200); assert.equal(calls.length, 1); - assert.deepEqual(calls[0].args.slice(-3), [path.join(process.cwd(), "bin/tracker.js"), "sync", "--auto"]); + assert.deepEqual(calls[0].args.slice(-4), [ + path.join(process.cwd(), "bin/tracker.js"), + "sync", + "--auto", + "--wait-for-lock", + ]); } finally { restore(); } @@ -533,7 +538,11 @@ test("local sync only treats boolean true as drain", async () => { assert.equal(handled, true); assert.equal(res.statusCode, 200); assert.equal(calls.length, 1); - assert.deepEqual(calls[0].args.slice(-2), [path.join(process.cwd(), "bin/tracker.js"), "sync"]); + assert.deepEqual(calls[0].args.slice(-3), [ + path.join(process.cwd(), "bin/tracker.js"), + "sync", + "--wait-for-lock", + ]); } finally { restore(); } @@ -587,7 +596,11 @@ test("local sync non-drain request mints a device token from relayed login when assert.equal(handled, true); assert.equal(res.statusCode, 200); assert.equal(calls.length, 1); - assert.deepEqual(calls[0].args.slice(-2), [path.join(process.cwd(), "bin/tracker.js"), "sync"]); + assert.deepEqual(calls[0].args.slice(-3), [ + path.join(process.cwd(), "bin/tracker.js"), + "sync", + "--wait-for-lock", + ]); assert.equal(calls[0].options.env.TOKENTRACKER_DEVICE_TOKEN, "issued-device-token"); assert.ok(fetchCalls.some((c) => c.url.endsWith("/api/auth/refresh?client_type=mobile"))); assert.ok(fetchCalls.some((c) => c.url.endsWith("/functions/tokentracker-device-token-issue"))); @@ -1022,7 +1035,11 @@ test("local sync non-drain request keeps explicit device token without relayed m assert.equal(handled, true); assert.equal(res.statusCode, 200); assert.equal(calls.length, 1); - assert.deepEqual(calls[0].args.slice(-2), [path.join(process.cwd(), "bin/tracker.js"), "sync"]); + assert.deepEqual(calls[0].args.slice(-3), [ + path.join(process.cwd(), "bin/tracker.js"), + "sync", + "--wait-for-lock", + ]); assert.equal(calls[0].options.env.TOKENTRACKER_DEVICE_TOKEN, "explicit-device-token"); } finally { restore(); diff --git a/test/sync-background.test.js b/test/sync-background.test.js index 1cf714940..bec7706ef 100644 --- a/test/sync-background.test.js +++ b/test/sync-background.test.js @@ -4,7 +4,7 @@ const os = require("node:os"); const path = require("node:path"); const { test } = require("node:test"); -const { acquireSyncLock, cmdSync } = require("../src/commands/sync"); +const { cmdSync } = require("../src/commands/sync"); const { openLock } = require("../src/lib/fs"); function tokenCountLine({ ts, totalTokens }) { @@ -265,7 +265,7 @@ test("native account publication waits for an overlapping sync instead of report }); }); -test("manual sync waits for an overlapping background sync instead of reporting success", async () => { +test("explicit wait-for-lock fails instead of reporting success when the lock stays busy", async () => { await withTempSyncEnv(async (home) => { const trackerDir = path.join(home, ".tokentracker", "tracker"); await fs.mkdir(trackerDir, { recursive: true }); @@ -275,16 +275,17 @@ test("manual sync waits for an overlapping background sync instead of reporting }); assert.ok(active); - const waitingLock = acquireSyncLock( - lockPath, - { auto: false, drain: false, publishAccount: false }, - { priorityWaitMs: 500, priorityPollMs: 10 }, - ); - await new Promise((resolve) => setTimeout(resolve, 30)); - await active.release(); - const acquired = await waitingLock; - assert.ok(acquired); - await acquired.release(); + try { + await assert.rejects( + cmdSync( + ["--wait-for-lock"], + { lockWaitOptions: { priorityWaitMs: 40, priorityPollMs: 5 } }, + ), + (error) => error.code === "SYNC_BUSY", + ); + } finally { + await active.release(); + } }); });