mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:30:44 +00:00
fix(release): stabilize beta validation
This commit is contained in:
@@ -333,6 +333,9 @@ jobs:
|
||||
cache: pnpm
|
||||
cache-dependency-path: ${{ inputs.candidate_artifact_name == '' && 'source/pnpm-lock.yaml' || 'workflow/pnpm-lock.yaml' }}
|
||||
|
||||
- name: Ensure pnpm cache path exists
|
||||
run: mkdir -p "$(pnpm store path --silent)"
|
||||
|
||||
- name: Build candidate artifact once
|
||||
if: inputs.candidate_artifact_name == ''
|
||||
env:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { createStartAccountContext } from "openclaw/plugin-sdk/channel-test-helpers";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
@@ -56,6 +57,30 @@ function createMockQaRuntime(params?: {
|
||||
sessionUpdatedAt.set(sessionKey, Date.now());
|
||||
},
|
||||
},
|
||||
text: {
|
||||
hasControlCommand() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
mentions: {
|
||||
buildMentionRegexes() {
|
||||
return [/\b@?openclaw\b/i];
|
||||
},
|
||||
matchesMentionPatterns(text: string, regexes: RegExp[]) {
|
||||
return regexes.some((regex) => regex.test(text));
|
||||
},
|
||||
resolveInboundMentionDecision,
|
||||
},
|
||||
groups: {
|
||||
resolveRequireMention() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
commands: {
|
||||
shouldHandleTextCommands() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
reply: {
|
||||
resolveEnvelopeFormatOptions() {
|
||||
return {};
|
||||
@@ -197,6 +222,78 @@ describe("qa-channel plugin", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("marks mentioned threaded group traffic before dispatch", { timeout: 20_000 }, async () => {
|
||||
let dispatchedCtx: Record<string, unknown> | null = null;
|
||||
const harness = await startQaChannelTestHarness({
|
||||
allowFrom: ["*"],
|
||||
runtime: createMockQaRuntime({
|
||||
onDispatch: (ctx) => {
|
||||
dispatchedCtx = ctx;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
harness.state.addInboundMessage({
|
||||
conversation: { id: "qa-room", kind: "channel", title: "QA Room" },
|
||||
senderId: "alice",
|
||||
senderName: "Alice",
|
||||
text: "@openclaw thread memory check",
|
||||
threadId: "thread-1",
|
||||
threadTitle: "Thread 1",
|
||||
});
|
||||
|
||||
const outbound = await harness.state.waitFor({
|
||||
kind: "message-text",
|
||||
textIncludes: "qa-echo: @openclaw thread memory check",
|
||||
direction: "outbound",
|
||||
timeoutMs: 15_000,
|
||||
});
|
||||
expect("threadId" in outbound && outbound.threadId).toBe("thread-1");
|
||||
expect(dispatchedCtx).toMatchObject({
|
||||
ChatType: "group",
|
||||
WasMentioned: true,
|
||||
MessageThreadId: "thread-1",
|
||||
});
|
||||
} finally {
|
||||
await harness.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("drops unmentioned group traffic when mention is required", { timeout: 20_000 }, async () => {
|
||||
let didDispatch = false;
|
||||
const harness = await startQaChannelTestHarness({
|
||||
allowFrom: ["*"],
|
||||
runtime: createMockQaRuntime({
|
||||
onDispatch: () => {
|
||||
didDispatch = true;
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
harness.state.addInboundMessage({
|
||||
conversation: { id: "qa-room", kind: "channel", title: "QA Room" },
|
||||
senderId: "alice",
|
||||
senderName: "Alice",
|
||||
text: "thread memory check",
|
||||
threadId: "thread-1",
|
||||
});
|
||||
|
||||
await expect(
|
||||
harness.state.waitFor({
|
||||
kind: "message-text",
|
||||
textIncludes: "qa-echo:",
|
||||
direction: "outbound",
|
||||
timeoutMs: 750,
|
||||
}),
|
||||
).rejects.toThrow(/wait timeout/i);
|
||||
expect(didDispatch).toBe(false);
|
||||
} finally {
|
||||
await harness.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("stages inbound image attachments into agent media payload", { timeout: 20_000 }, async () => {
|
||||
let dispatchedCtx: Record<string, unknown> | null = null;
|
||||
const harness = await startQaChannelTestHarness({
|
||||
|
||||
@@ -81,6 +81,49 @@ export async function handleQaInbound(params: {
|
||||
id: target,
|
||||
},
|
||||
});
|
||||
const isGroup = inbound.conversation.kind !== "direct";
|
||||
const mentionRegexes = isGroup
|
||||
? runtime.channel.mentions.buildMentionRegexes(params.config as OpenClawConfig, route.agentId)
|
||||
: [];
|
||||
const wasMentioned =
|
||||
isGroup && mentionRegexes.length > 0
|
||||
? runtime.channel.mentions.matchesMentionPatterns(inbound.text, mentionRegexes)
|
||||
: false;
|
||||
const allowTextCommands = runtime.channel.commands.shouldHandleTextCommands({
|
||||
cfg: params.config as OpenClawConfig,
|
||||
surface: params.channelId,
|
||||
});
|
||||
const hasControlCommand = runtime.channel.text.hasControlCommand(
|
||||
inbound.text,
|
||||
params.config as OpenClawConfig,
|
||||
);
|
||||
const commandAuthorized = true;
|
||||
const requireMention = isGroup
|
||||
? runtime.channel.groups.resolveRequireMention({
|
||||
cfg: params.config as OpenClawConfig,
|
||||
channel: params.channelId,
|
||||
groupId: inbound.conversation.id,
|
||||
groupChannel: inbound.conversation.id,
|
||||
accountId: params.account.accountId,
|
||||
})
|
||||
: false;
|
||||
const mentionDecision = runtime.channel.mentions.resolveInboundMentionDecision({
|
||||
facts: {
|
||||
canDetectMention: mentionRegexes.length > 0,
|
||||
wasMentioned,
|
||||
hasAnyMention: wasMentioned,
|
||||
},
|
||||
policy: {
|
||||
isGroup,
|
||||
requireMention,
|
||||
allowTextCommands,
|
||||
hasControlCommand,
|
||||
commandAuthorized,
|
||||
},
|
||||
});
|
||||
if (isGroup && mentionDecision.shouldSkip) {
|
||||
return;
|
||||
}
|
||||
const storePath = runtime.channel.session.resolveStorePath(params.config.session?.store, {
|
||||
agentId: route.agentId,
|
||||
});
|
||||
@@ -110,7 +153,7 @@ export async function handleQaInbound(params: {
|
||||
To: target,
|
||||
SessionKey: route.sessionKey,
|
||||
AccountId: route.accountId ?? params.account.accountId,
|
||||
ChatType: inbound.conversation.kind === "direct" ? "direct" : "group",
|
||||
ChatType: isGroup ? "group" : "direct",
|
||||
ConversationLabel:
|
||||
inbound.threadTitle ||
|
||||
inbound.conversation.title ||
|
||||
@@ -135,7 +178,8 @@ export async function handleQaInbound(params: {
|
||||
Timestamp: inbound.timestamp,
|
||||
OriginatingChannel: params.channelId,
|
||||
OriginatingTo: target,
|
||||
CommandAuthorized: true,
|
||||
WasMentioned: isGroup ? mentionDecision.effectiveWasMentioned : undefined,
|
||||
CommandAuthorized: commandAuthorized,
|
||||
...mediaPayload,
|
||||
});
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ describe("qa channel transport", () => {
|
||||
messages: {
|
||||
groupChat: {
|
||||
mentionPatterns: ["\\b@?openclaw\\b"],
|
||||
visibleReplies: "automatic",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -90,6 +90,7 @@ export function createQaChannelGatewayConfig(params: {
|
||||
messages: {
|
||||
groupChat: {
|
||||
mentionPatterns: ["\\b@?openclaw\\b"],
|
||||
visibleReplies: "automatic",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ function createQaChannelTransportParams(baseUrl = "http://127.0.0.1:43124") {
|
||||
messages: {
|
||||
groupChat: {
|
||||
mentionPatterns: ["\\b@?openclaw\\b"],
|
||||
visibleReplies: "automatic",
|
||||
},
|
||||
},
|
||||
} satisfies QaTransportGatewayConfig,
|
||||
@@ -78,6 +79,7 @@ describe("buildQaGatewayConfig", () => {
|
||||
pollTimeoutMs: 250,
|
||||
});
|
||||
expect(cfg.messages?.groupChat?.mentionPatterns).toEqual(["\\b@?openclaw\\b"]);
|
||||
expect(cfg.messages?.groupChat?.visibleReplies).toBe("automatic");
|
||||
});
|
||||
|
||||
it("maps provider-qualified openai and anthropic refs through the mock provider lane", () => {
|
||||
|
||||
@@ -595,12 +595,16 @@ export async function noteStateIntegrity(
|
||||
cfg: OpenClawConfig,
|
||||
prompter: DoctorPrompterLike,
|
||||
configPath?: string,
|
||||
params: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
homedir?: () => string;
|
||||
} = {},
|
||||
) {
|
||||
const warnings: string[] = [];
|
||||
const changes: string[] = [];
|
||||
const noteFn = prompter.note ?? note;
|
||||
const env = process.env;
|
||||
const homedir = () => resolveRequiredHomeDir(env, os.homedir);
|
||||
const env = params.env ?? process.env;
|
||||
const homedir = () => resolveRequiredHomeDir(env, params.homedir ?? os.homedir);
|
||||
const stateDir = resolveStateDir(env, homedir);
|
||||
const defaultStateDir = path.join(homedir(), ".openclaw");
|
||||
const oauthDir = resolveOAuthDir(env, stateDir);
|
||||
|
||||
@@ -24,6 +24,7 @@ export type DoctorHealthFlowContext = {
|
||||
cfgForPersistence: OpenClawConfig;
|
||||
sourceConfigValid: boolean;
|
||||
configPath: string;
|
||||
env: NodeJS.ProcessEnv;
|
||||
gatewayDetails?: ReturnType<typeof buildGatewayConnectionDetails>;
|
||||
healthOk?: boolean;
|
||||
gatewayMemoryProbe?: Awaited<ReturnType<typeof probeGatewayMemoryStatus>>;
|
||||
@@ -251,7 +252,7 @@ async function runBundledPluginRuntimeDepsHealth(ctx: DoctorHealthFlowContext):
|
||||
|
||||
async function runStateIntegrityHealth(ctx: DoctorHealthFlowContext): Promise<void> {
|
||||
const { noteStateIntegrity } = await import("../commands/doctor-state-integrity.js");
|
||||
await noteStateIntegrity(ctx.cfg, ctx.prompter, ctx.configPath);
|
||||
await noteStateIntegrity(ctx.cfg, ctx.prompter, ctx.configPath, { env: ctx.env });
|
||||
}
|
||||
|
||||
async function runSessionLocksHealth(ctx: DoctorHealthFlowContext): Promise<void> {
|
||||
|
||||
@@ -8,6 +8,7 @@ const outro = (message: string) => clackOutro(stylePromptTitle(message) ?? messa
|
||||
|
||||
export async function doctorCommand(runtime?: RuntimeEnv, options: DoctorOptions = {}) {
|
||||
const effectiveRuntime = runtime ?? (await import("../runtime.js")).defaultRuntime;
|
||||
const envSnapshot = { ...process.env };
|
||||
const { createDoctorPrompter } = await import("../commands/doctor-prompter.js");
|
||||
const { printWizardHeader } = await import("../commands/onboard-helpers.js");
|
||||
const prompter = createDoctorPrompter({ runtime: effectiveRuntime, options });
|
||||
@@ -57,6 +58,7 @@ export async function doctorCommand(runtime?: RuntimeEnv, options: DoctorOptions
|
||||
cfgForPersistence: structuredClone(configResult.cfg),
|
||||
sourceConfigValid: configResult.sourceConfigValid ?? true,
|
||||
configPath: configResult.path ?? CONFIG_PATH,
|
||||
env: envSnapshot,
|
||||
};
|
||||
const { runDoctorHealthContributions } = await import("./doctor-health-contributions.js");
|
||||
await runDoctorHealthContributions(ctx);
|
||||
|
||||
@@ -136,7 +136,7 @@ async function runNodeModule(
|
||||
const timeout = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
reject(new Error(`child process timed out\nstdout:\n${stdout}\nstderr:\n${stderr}`));
|
||||
}, 10_000);
|
||||
}, 45_000);
|
||||
|
||||
child.on("error", (err) => {
|
||||
clearTimeout(timeout);
|
||||
|
||||
Reference in New Issue
Block a user