Skip to content

Commit 4d73cca

Browse files
committed
fix(discord): keep default SecretRef active with named accounts
1 parent 1069c60 commit 4d73cca

2 files changed

Lines changed: 104 additions & 4 deletions

File tree

extensions/discord/src/secret-config-contract.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
import {
33
collectNestedChannelFieldAssignments,
44
collectSimpleChannelFieldAssignments,
5-
getChannelSurface,
5+
getChannelRecord,
6+
hasConfiguredSecretInputValue,
67
isBaseFieldActiveForChannelSurface,
78
isEnabledFlag,
89
isRecord,
10+
resolveChannelAccountSurface,
11+
type ChannelAccountSurface,
912
type ResolverContext,
1013
type SecretDefaults,
1114
type SecretTargetRegistryEntry,
@@ -83,16 +86,47 @@ export const secretTargetRegistryEntries: SecretTargetRegistryEntry[] = [
8386
},
8487
];
8588

89+
function withImplicitDefaultDiscordAccount(params: {
90+
discord: Record<string, unknown>;
91+
surface: ChannelAccountSurface;
92+
defaults?: SecretDefaults;
93+
}): ChannelAccountSurface {
94+
if (
95+
!params.surface.hasExplicitAccounts ||
96+
!hasConfiguredSecretInputValue(params.discord.token, params.defaults) ||
97+
params.surface.accounts.some((entry) => entry.accountId.toLowerCase() === "default")
98+
) {
99+
return params.surface;
100+
}
101+
// Discord account listing keeps a top-level token as an implicit default account.
102+
// Match that runtime contract so named accounts do not orphan the default SecretRef.
103+
return {
104+
...params.surface,
105+
accounts: [
106+
...params.surface.accounts,
107+
{
108+
accountId: "default",
109+
account: {},
110+
enabled: params.surface.channelEnabled,
111+
},
112+
],
113+
};
114+
}
115+
86116
export function collectRuntimeConfigAssignments(params: {
87117
config: { channels?: Record<string, unknown> };
88118
defaults?: SecretDefaults;
89119
context: ResolverContext;
90120
}): void {
91-
const resolved = getChannelSurface(params.config, "discord");
92-
if (!resolved) {
121+
const discord = getChannelRecord(params.config, "discord");
122+
if (!discord) {
93123
return;
94124
}
95-
const { channel: discord, surface } = resolved;
125+
const surface = withImplicitDefaultDiscordAccount({
126+
discord,
127+
surface: resolveChannelAccountSurface(discord),
128+
defaults: params.defaults,
129+
});
96130
collectSimpleChannelFieldAssignments({
97131
channelKey: "discord",
98132
field: "token",

src/secrets/runtime-discord-surface.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
/** Tests Discord secret surfaces in runtime preparation. */
2+
import fs from "node:fs/promises";
3+
import os from "node:os";
4+
import path from "node:path";
25
import { describe, expect, it } from "vitest";
36
import "./runtime-discord.test-support.ts";
47
import {
@@ -71,6 +74,69 @@ describe("secrets runtime snapshot discord surface", () => {
7174
);
7275
});
7376

77+
it("resolves the implicit default token when named Discord accounts are added", async () => {
78+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-discord-secrets-"));
79+
try {
80+
const secretsPath = path.join(root, "secrets.json");
81+
await fs.writeFile(
82+
secretsPath,
83+
JSON.stringify({
84+
discord: {
85+
defaultToken: "default-account-token",
86+
secondToken: "second-account-token",
87+
},
88+
}),
89+
"utf8",
90+
);
91+
await fs.chmod(secretsPath, 0o600);
92+
93+
const snapshot = await prepareSecretsRuntimeSnapshot({
94+
config: asConfig({
95+
secrets: {
96+
providers: {
97+
discord_file: {
98+
source: "file",
99+
path: secretsPath,
100+
mode: "json",
101+
},
102+
},
103+
},
104+
channels: {
105+
discord: {
106+
token: {
107+
source: "file",
108+
provider: "discord_file",
109+
id: "/discord/defaultToken",
110+
},
111+
accounts: {
112+
second: {
113+
enabled: true,
114+
token: {
115+
source: "file",
116+
provider: "discord_file",
117+
id: "/discord/secondToken",
118+
},
119+
},
120+
},
121+
},
122+
},
123+
}),
124+
agentDirs: ["/tmp/openclaw-agent-main"],
125+
loadAuthStore: () => loadAuthStoreWithProfiles({}),
126+
});
127+
128+
expect(snapshot.config.channels?.discord?.token).toBe("default-account-token");
129+
expect(snapshot.config.channels?.discord?.accounts?.second?.token).toBe(
130+
"second-account-token",
131+
);
132+
expect(snapshot.warnings.map((warning) => warning.path)).not.toContain(
133+
"channels.discord.token",
134+
);
135+
} finally {
136+
await fs.rm(root, { recursive: true, force: true });
137+
}
138+
});
139+
74140
it("fails when non-default Discord account inherits an unresolved top-level token ref", async () => {
75141
await expect(
76142
prepareSecretsRuntimeSnapshot({

0 commit comments

Comments
 (0)