Discord: thread bindings idle + max-age lifecycle (#27845) (thanks @osolmaz)

* refactor discord thread bindings to idle and max-age lifecycle

* fix: migrate legacy thread binding expiry and reduce hot-path disk writes

* refactor: remove remaining thread-binding ttl legacy paths

* fix: harden thread-binding lifecycle persistence

* Discord: fix thread binding types in message/reply paths

* Infra: handle win32 unknown inode in file identity checks

* Infra: relax win32 guarded-open identity checks

* Config: migrate threadBindings ttlHours to idleHours

* Revert "Infra: relax win32 guarded-open identity checks"

This reverts commit de94126771.

* Revert "Infra: handle win32 unknown inode in file identity checks"

This reverts commit 96fc5ddfb3.

* Discord: re-read live binding state before sweep unbind

* fix: add changelog note for thread binding lifecycle update (#27845) (thanks @osolmaz)

---------

Co-authored-by: Onur Solmaz <onur@textcortex.com>
This commit is contained in:
Onur Solmaz
2026-02-27 10:02:39 +01:00
committed by GitHub
parent 0fb7add7d6
commit a7929abad8
45 changed files with 1656 additions and 402 deletions

View File

@@ -0,0 +1,146 @@
import { describe, expect, it } from "vitest";
import { migrateLegacyConfig } from "./legacy-migrate.js";
import { validateConfigObjectRaw } from "./validation.js";
describe("thread binding config keys", () => {
it("rejects legacy session.threadBindings.ttlHours", () => {
const result = validateConfigObjectRaw({
session: {
threadBindings: {
ttlHours: 24,
},
},
});
expect(result.ok).toBe(false);
if (result.ok) {
return;
}
expect(result.issues).toContainEqual(
expect.objectContaining({
path: "session.threadBindings",
message: expect.stringContaining("ttlHours"),
}),
);
});
it("rejects legacy channels.discord.threadBindings.ttlHours", () => {
const result = validateConfigObjectRaw({
channels: {
discord: {
threadBindings: {
ttlHours: 24,
},
},
},
});
expect(result.ok).toBe(false);
if (result.ok) {
return;
}
expect(result.issues).toContainEqual(
expect.objectContaining({
path: "channels.discord.threadBindings",
message: expect.stringContaining("ttlHours"),
}),
);
});
it("rejects legacy channels.discord.accounts.<id>.threadBindings.ttlHours", () => {
const result = validateConfigObjectRaw({
channels: {
discord: {
accounts: {
alpha: {
threadBindings: {
ttlHours: 24,
},
},
},
},
},
});
expect(result.ok).toBe(false);
if (result.ok) {
return;
}
expect(result.issues).toContainEqual(
expect.objectContaining({
path: "channels.discord.accounts",
message: expect.stringContaining("ttlHours"),
}),
);
});
it("migrates session.threadBindings.ttlHours to idleHours", () => {
const result = migrateLegacyConfig({
session: {
threadBindings: {
ttlHours: 24,
},
},
});
expect(result.config?.session?.threadBindings?.idleHours).toBe(24);
const normalized = result.config?.session?.threadBindings as
| Record<string, unknown>
| undefined;
expect(normalized?.ttlHours).toBeUndefined();
expect(result.changes).toContain(
"Moved session.threadBindings.ttlHours → session.threadBindings.idleHours.",
);
});
it("migrates Discord threadBindings.ttlHours for root and account entries", () => {
const result = migrateLegacyConfig({
channels: {
discord: {
threadBindings: {
ttlHours: 12,
},
accounts: {
alpha: {
threadBindings: {
ttlHours: 6,
},
},
beta: {
threadBindings: {
idleHours: 4,
ttlHours: 9,
},
},
},
},
},
});
const discord = result.config?.channels?.discord;
expect(discord?.threadBindings?.idleHours).toBe(12);
expect(
(discord?.threadBindings as Record<string, unknown> | undefined)?.ttlHours,
).toBeUndefined();
expect(discord?.accounts?.alpha?.threadBindings?.idleHours).toBe(6);
expect(
(discord?.accounts?.alpha?.threadBindings as Record<string, unknown> | undefined)?.ttlHours,
).toBeUndefined();
expect(discord?.accounts?.beta?.threadBindings?.idleHours).toBe(4);
expect(
(discord?.accounts?.beta?.threadBindings as Record<string, unknown> | undefined)?.ttlHours,
).toBeUndefined();
expect(result.changes).toContain(
"Moved channels.discord.threadBindings.ttlHours → channels.discord.threadBindings.idleHours.",
);
expect(result.changes).toContain(
"Moved channels.discord.accounts.alpha.threadBindings.ttlHours → channels.discord.accounts.alpha.threadBindings.idleHours.",
);
expect(result.changes).toContain(
"Removed channels.discord.accounts.beta.threadBindings.ttlHours (channels.discord.accounts.beta.threadBindings.idleHours already set).",
);
});
});