Compare commits

..

3 Commits

Author SHA1 Message Date
Ayaan Zaidi
8c4e676442 fix: correct telegram html nesting (#4578) (thanks @ThanhNguyxn) 2026-01-30 16:48:17 +05:30
ThanhNguyxn
01e291016d style: format test file 2026-01-30 16:48:17 +05:30
ThanhNguyxn
80089f5d22 fix(telegram): properly nest overlapping HTML tags (#4071)
Unify style and link closing in render.ts to use LIFO order across
both element types, fixing cases where bold/italic spans containing
autolinks produced invalid HTML like <b><a></b></a>.
2026-01-30 16:48:17 +05:30
3 changed files with 17 additions and 62 deletions

View File

@ -4,7 +4,6 @@ import {
LineConfigSchema,
processLineMessage,
type ChannelPlugin,
type ChannelStatusIssue,
type OpenClawConfig,
type LineConfig,
type LineChannelData,
@ -561,26 +560,19 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
lastStopAt: null,
lastError: null,
},
collectStatusIssues: (accounts) => {
const issues: ChannelStatusIssue[] = [];
for (const account of accounts) {
const accountId = account.accountId ?? DEFAULT_ACCOUNT_ID;
if (!account.channelAccessToken?.trim()) {
issues.push({
channel: "line",
accountId,
kind: "config",
message: "LINE channel access token not configured",
});
}
if (!account.channelSecret?.trim()) {
issues.push({
channel: "line",
accountId,
kind: "config",
message: "LINE channel secret not configured",
});
}
collectStatusIssues: ({ account }) => {
const issues: Array<{ level: "error" | "warning"; message: string }> = [];
if (!account.channelAccessToken?.trim()) {
issues.push({
level: "error",
message: "LINE channel access token not configured",
});
}
if (!account.channelSecret?.trim()) {
issues.push({
level: "error",
message: "LINE channel secret not configured",
});
}
return issues;
},

View File

@ -52,39 +52,11 @@ describe("buildAuthHealthSummary", () => {
);
expect(statuses["anthropic:ok"]).toBe("ok");
// OAuth credentials with refresh tokens are auto-renewable, so they report "ok"
expect(statuses["anthropic:expiring"]).toBe("ok");
expect(statuses["anthropic:expired"]).toBe("ok");
expect(statuses["anthropic:expiring"]).toBe("expiring");
expect(statuses["anthropic:expired"]).toBe("expired");
expect(statuses["anthropic:api"]).toBe("static");
const provider = summary.providers.find((entry) => entry.provider === "anthropic");
expect(provider?.status).toBe("ok");
});
it("reports expired for OAuth without a refresh token", () => {
vi.spyOn(Date, "now").mockReturnValue(now);
const store = {
version: 1,
profiles: {
"google:no-refresh": {
type: "oauth" as const,
provider: "google-antigravity",
access: "access",
refresh: "",
expires: now - 10_000,
},
},
};
const summary = buildAuthHealthSummary({
store,
warnAfterMs: DEFAULT_OAUTH_WARN_MS,
});
const statuses = Object.fromEntries(
summary.profiles.map((profile) => [profile.profileId, profile.status]),
);
expect(statuses["google:no-refresh"]).toBe("expired");
expect(provider?.status).toBe("expired");
});
});

View File

@ -123,16 +123,7 @@ function buildProfileHealth(params: {
};
}
const hasRefreshToken = typeof credential.refresh === "string" && credential.refresh.length > 0;
const { status: rawStatus, remainingMs } = resolveOAuthStatus(
credential.expires,
now,
warnAfterMs,
);
// OAuth credentials with a valid refresh token auto-renew on first API call,
// so don't warn about access token expiration.
const status =
hasRefreshToken && (rawStatus === "expired" || rawStatus === "expiring") ? "ok" : rawStatus;
const { status, remainingMs } = resolveOAuthStatus(credential.expires, now, warnAfterMs);
return {
profileId,
provider: credential.provider,