Merge pull request #150 from ZHANGTIANYAO1/fix/avatar-upload-before-connect

fix(avatar): 初始化阶段不再发起注定失败的头像上传
This commit is contained in:
TIANYAO ZHANG authored and GitHub committed 2026-08-14 01:22:58 +08:00
commit c7b577adba
3 files changed
+78 -1

No files matched your search

+7 -1
View File
@@ -295,7 +295,13 @@ export class BotInstance extends EventEmitter {
const relPath = this.database.getCustomAvatarPath(this.id); const relPath = this.database.getCustomAvatarPath(this.id);
if (relPath) { if (relPath) {
const buf = this.avatarStore.read(relPath); const buf = this.avatarStore.read(relPath);
if (buf) this.profileManager.setCustomAvatar(buf); // loadCustomAvatar, NOT setCustomAvatar (#148): we are still in the
// constructor, so tsClient has not connected. setCustomAvatar would
// start a file transfer right here and fail. profileManager.onConnect()
// uploads it for real once the handshake completes.
// `length > 0` because avatarStore.write is delete-then-write, so a
// crash mid-write leaves a 0-byte file that is truthy as a Buffer.
if (buf && buf.length > 0) this.profileManager.loadCustomAvatar(buf);
} }
} catch (err) { } catch (err) {
this.logger.warn({ err }, "Failed to load custom avatar — skipping"); this.logger.warn({ err }, "Failed to load custom avatar — skipping");
+57
View File
@@ -145,3 +145,60 @@ describe("BotProfileManager custom avatar precedence", () => {
expect(ts.clearCalls).toBe(0); expect(ts.clearCalls).toBe(0);
}); });
}); });
// #148: the persisted avatar is loaded in the BotInstance constructor, before
// tsClient.connect() has run. Loading it must not touch the wire at all.
describe("BotProfileManager loadCustomAvatar (pre-connect load, #148)", () => {
let ts: ReturnType<typeof makeMockTs>;
beforeEach(() => { ts = makeMockTs(); });
it("does not upload or clear anything when called before connect", async () => {
const pm = new BotProfileManager(ts as any, noopLogger, cfgOn, "Bot");
pm.loadCustomAvatar(Buffer.from([7, 7, 7]));
await flush();
expect(ts.uploadCalls.length).toBe(0);
expect(ts.clearCalls).toBe(0);
expect(ts.fileTransferInitUpload).not.toHaveBeenCalled();
});
it("the loaded avatar is uploaded once onConnect fires", async () => {
const pm = new BotProfileManager(ts as any, noopLogger, cfgOn, "Bot");
pm.loadCustomAvatar(Buffer.from([7, 7, 7]));
await flush();
pm.onConnect();
await flush();
expect(ts.uploadCalls.length).toBe(1);
expect(ts.uploadCalls[0].equals(Buffer.from([7, 7, 7]))).toBe(true);
});
it("survives a reconnect: onConnect re-applies the loaded avatar every time", async () => {
const pm = new BotProfileManager(ts as any, noopLogger, cfgOn, "Bot");
pm.loadCustomAvatar(Buffer.from([8]));
pm.onConnect();
await flush();
pm.onConnect();
await flush();
expect(ts.uploadCalls.length).toBe(2);
});
it("loading null leaves the wire untouched and onConnect stays quiet", async () => {
const pm = new BotProfileManager(ts as any, noopLogger, cfgOn, "Bot");
pm.loadCustomAvatar(null);
pm.onConnect();
await flush();
expect(ts.uploadCalls.length).toBe(0);
expect(ts.clearCalls).toBe(0);
});
it("setCustomAvatar still uploads immediately after connect (post-connect edit unchanged)", async () => {
const pm = new BotProfileManager(ts as any, noopLogger, cfgOn, "Bot");
pm.loadCustomAvatar(Buffer.from([1]));
pm.onConnect();
await flush();
ts.uploadCalls.length = 0;
pm.setCustomAvatar(Buffer.from([2, 2]));
await flush();
expect(ts.uploadCalls.length).toBe(1);
expect(ts.uploadCalls[0].equals(Buffer.from([2, 2]))).toBe(true);
});
});
+14
View File
@@ -65,6 +65,20 @@ export class BotProfileManager {
// --- Public API --- // --- Public API ---
/**
* Store a persisted custom avatar WITHOUT touching TeamSpeak (#148).
*
* Used during BotInstance construction, when the TS connection does not
* exist yet: setCustomAvatar would immediately fire the three-step file
* transfer (fileTransferInitUpload → uploadFileData → clientupdate) against
* a client that has not connected, so the upload always failed and the
* saved avatar never appeared. onConnect() re-applies this.customAvatar
* once the handshake completes, so loading it silently here loses nothing.
*/
loadCustomAvatar(buffer: Buffer | null): void {
this.customAvatar = buffer;
}
/** /**
* Set/clear the persistent idle avatar. Pass null to remove. * Set/clear the persistent idle avatar. Pass null to remove.
* *