refactor: share webhook channel status helpers

This commit is contained in:
Peter Steinberger
2026-03-29 02:10:58 +01:00
parent 2afc655bd5
commit 148a65fe90
16 changed files with 610 additions and 418 deletions

View File

@@ -1,6 +1,7 @@
export {
createAccountListHelpers,
describeAccountSnapshot,
describeWebhookAccountSnapshot,
mergeAccountConfig,
resolveMergedAccountConfig,
} from "../channels/plugins/account-helpers.js";

View File

@@ -6,8 +6,10 @@ import {
buildComputedAccountStatusSnapshot,
buildRuntimeAccountStatusSnapshot,
createComputedAccountStatusAdapter,
buildWebhookChannelStatusSummary,
buildTokenChannelStatusSummary,
collectStatusIssuesFromLastError,
createDependentCredentialStatusIssueCollector,
createDefaultChannelRuntimeState,
} from "./status-helpers.js";
@@ -351,6 +353,62 @@ describe("buildTokenChannelStatusSummary", () => {
});
});
describe("buildWebhookChannelStatusSummary", () => {
it("defaults mode to webhook and keeps supplied extras", () => {
expect(
buildWebhookChannelStatusSummary(
{
configured: true,
running: true,
},
{
secretSource: "env",
},
),
).toEqual({
configured: true,
running: true,
lastStartAt: null,
lastStopAt: null,
lastError: null,
mode: "webhook",
secretSource: "env",
});
});
});
describe("createDependentCredentialStatusIssueCollector", () => {
it("uses source metadata from sanitized snapshots to pick the missing field", () => {
const collect = createDependentCredentialStatusIssueCollector({
channel: "line",
dependencySourceKey: "tokenSource",
missingPrimaryMessage: "LINE channel access token not configured",
missingDependentMessage: "LINE channel secret not configured",
});
expect(
collect([
{ accountId: "default", configured: false, tokenSource: "none" },
{ accountId: "work", configured: false, tokenSource: "env" },
{ accountId: "ok", configured: true, tokenSource: "env" },
]),
).toEqual([
{
channel: "line",
accountId: "default",
kind: "config",
message: "LINE channel access token not configured",
},
{
channel: "line",
accountId: "work",
kind: "config",
message: "LINE channel secret not configured",
},
]);
});
});
describe("collectStatusIssuesFromLastError", () => {
it("returns runtime issues only for non-empty string lastError values", () => {
expect(

View File

@@ -40,6 +40,11 @@ type ComputedAccountStatusAdapterParams<ResolvedAccount, Probe, Audit> = {
type ComputedAccountStatusSnapshot<TExtra extends StatusSnapshotExtra = StatusSnapshotExtra> =
ComputedAccountStatusBase & { extra?: TExtra };
type ConfigIssueAccount = {
accountId?: string | null;
configured?: boolean | null;
} & Record<string, unknown>;
/** Create the baseline runtime snapshot shape used by channel/account status stores. */
export function createDefaultChannelRuntimeState<T extends Record<string, unknown>>(
accountId: string,
@@ -102,6 +107,24 @@ export function buildProbeChannelStatusSummary<TExtra extends Record<string, unk
};
}
/** Build webhook channel summaries with a stable default mode. */
export function buildWebhookChannelStatusSummary<TExtra extends StatusSnapshotExtra>(
snapshot: {
configured?: boolean | null;
mode?: string | null;
running?: boolean | null;
lastStartAt?: number | null;
lastStopAt?: number | null;
lastError?: string | null;
},
extra?: TExtra,
) {
return buildBaseChannelStatusSummary(snapshot, {
mode: snapshot.mode ?? "webhook",
...(extra ?? ({} as TExtra)),
});
}
/** Build the standard per-account status payload from config metadata plus runtime state. */
export function buildBaseAccountStatusSnapshot<TExtra extends StatusSnapshotExtra>(
params: {
@@ -290,6 +313,36 @@ export function buildTokenChannelStatusSummary(
};
}
/** Build a config-issue collector from snapshot-safe source metadata only. */
export function createDependentCredentialStatusIssueCollector(options: {
channel: string;
dependencySourceKey: string;
missingPrimaryMessage: string;
missingDependentMessage: string;
isDependencyConfigured?: ((value: unknown) => boolean) | undefined;
}) {
const isDependencyConfigured =
options.isDependencyConfigured ??
((value: unknown) => typeof value === "string" && value.trim().length > 0 && value !== "none");
return (accounts: ConfigIssueAccount[]): ChannelStatusIssue[] =>
accounts.flatMap((account) => {
if (account.configured !== false) {
return [];
}
return [
{
channel: options.channel,
accountId: account.accountId ?? "",
kind: "config",
message: isDependencyConfigured(account[options.dependencySourceKey])
? options.missingDependentMessage
: options.missingPrimaryMessage,
},
];
});
}
/** Convert account runtime errors into the generic channel status issue format. */
export function collectStatusIssuesFromLastError(
channel: string,