mirror of https://github.com/openclaw/openclaw.git
fix(ci): restore main checks after bulk merges
This commit is contained in:
parent
8c241449f5
commit
12a947223b
|
|
@ -77,6 +77,7 @@ Text + native (when enabled):
|
|||
- `/allowlist` (list/add/remove allowlist entries)
|
||||
- `/approve <id> allow-once|allow-always|deny` (resolve exec approval prompts)
|
||||
- `/context [list|detail|json]` (explain “context”; `detail` shows per-file + per-tool + per-skill + system prompt size)
|
||||
- `/export-session [path]` (alias: `/export`) (export current session to HTML; includes full system prompt)
|
||||
- `/whoami` (show your sender id; alias: `/id`)
|
||||
- `/subagents list|kill|log|info|send|steer` (inspect, kill, log, or steer sub-agent runs for the current session)
|
||||
- `/kill <id|#|all>` (immediately abort one or all running sub-agents for this session; no confirmation message)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openclaw/linq",
|
||||
"version": "2026.2.13",
|
||||
"version": "2026.2.16",
|
||||
"private": true,
|
||||
"description": "OpenClaw Linq iMessage channel plugin",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -119,10 +119,7 @@ export function clearSessionStoreCacheForTest(): void {
|
|||
SESSION_STORE_CACHE.clear();
|
||||
for (const queue of LOCK_QUEUES.values()) {
|
||||
for (const task of queue.pending) {
|
||||
task.timedOut = true;
|
||||
if (task.timer) {
|
||||
clearTimeout(task.timer);
|
||||
}
|
||||
task.reject(new Error("session store queue cleared for test"));
|
||||
}
|
||||
}
|
||||
LOCK_QUEUES.clear();
|
||||
|
|
@ -675,11 +672,8 @@ type SessionStoreLockTask = {
|
|||
fn: () => Promise<unknown>;
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (reason: unknown) => void;
|
||||
timeoutAt?: number;
|
||||
timeoutMs?: number;
|
||||
staleMs: number;
|
||||
timer?: ReturnType<typeof setTimeout>;
|
||||
started: boolean;
|
||||
timedOut: boolean;
|
||||
};
|
||||
|
||||
type SessionStoreLockQueue = {
|
||||
|
|
@ -703,13 +697,6 @@ function getOrCreateLockQueue(storePath: string): SessionStoreLockQueue {
|
|||
return created;
|
||||
}
|
||||
|
||||
function removePendingTask(queue: SessionStoreLockQueue, task: SessionStoreLockTask): void {
|
||||
const idx = queue.pending.indexOf(task);
|
||||
if (idx >= 0) {
|
||||
queue.pending.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function drainSessionStoreLockQueue(storePath: string): Promise<void> {
|
||||
const queue = LOCK_QUEUES.get(storePath);
|
||||
if (!queue || queue.running) {
|
||||
|
|
@ -719,21 +706,12 @@ async function drainSessionStoreLockQueue(storePath: string): Promise<void> {
|
|||
try {
|
||||
while (queue.pending.length > 0) {
|
||||
const task = queue.pending.shift();
|
||||
if (!task || task.timedOut) {
|
||||
if (!task) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (task.timer) {
|
||||
clearTimeout(task.timer);
|
||||
}
|
||||
task.started = true;
|
||||
|
||||
const remainingTimeoutMs =
|
||||
task.timeoutAt != null
|
||||
? Math.max(0, task.timeoutAt - Date.now())
|
||||
: Number.POSITIVE_INFINITY;
|
||||
if (task.timeoutAt != null && remainingTimeoutMs <= 0) {
|
||||
task.timedOut = true;
|
||||
const remainingTimeoutMs = task.timeoutMs ?? Number.POSITIVE_INFINITY;
|
||||
if (task.timeoutMs != null && remainingTimeoutMs <= 0) {
|
||||
task.reject(lockTimeoutError(storePath));
|
||||
continue;
|
||||
}
|
||||
|
|
@ -789,7 +767,6 @@ async function withSessionStoreLock<T>(
|
|||
void opts.pollIntervalMs;
|
||||
|
||||
const hasTimeout = timeoutMs > 0 && Number.isFinite(timeoutMs);
|
||||
const timeoutAt = hasTimeout ? Date.now() + timeoutMs : undefined;
|
||||
const queue = getOrCreateLockQueue(storePath);
|
||||
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
|
|
@ -797,23 +774,10 @@ async function withSessionStoreLock<T>(
|
|||
fn: async () => await fn(),
|
||||
resolve: (value) => resolve(value as T),
|
||||
reject,
|
||||
timeoutAt,
|
||||
timeoutMs: hasTimeout ? timeoutMs : undefined,
|
||||
staleMs,
|
||||
started: false,
|
||||
timedOut: false,
|
||||
};
|
||||
|
||||
if (hasTimeout) {
|
||||
task.timer = setTimeout(() => {
|
||||
if (task.started || task.timedOut) {
|
||||
return;
|
||||
}
|
||||
task.timedOut = true;
|
||||
removePendingTask(queue, task);
|
||||
reject(lockTimeoutError(storePath));
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
queue.pending.push(task);
|
||||
void drainSessionStoreLockQueue(storePath);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -49,23 +49,26 @@ export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): numbe
|
|||
timezone: resolveCronTimezone(schedule.tz),
|
||||
catch: false,
|
||||
});
|
||||
// Ask croner for the next occurrence starting from the NEXT second.
|
||||
// This prevents re-scheduling into the current second when a job fires
|
||||
// at 13:00:00.014 and completes at 13:00:00.021 — without this fix,
|
||||
// croner could return 13:00:00.000 (same second) causing a spin loop
|
||||
// where the job fires hundreds of times per second (see #17821).
|
||||
//
|
||||
// By asking from the next second (e.g., 13:00:01.000), we ensure croner
|
||||
// returns the following day's occurrence (e.g., 13:00:00.000 tomorrow).
|
||||
//
|
||||
// This also correctly handles the "before match" case: if nowMs is
|
||||
// 11:59:59.500, we ask from 12:00:00.000, and croner returns 12:00:00.000
|
||||
// (today's match) since it uses >= semantics for the start time.
|
||||
const askFromNextSecondMs = Math.floor(nowMs / 1000) * 1000 + 1000;
|
||||
const next = cron.nextRun(new Date(askFromNextSecondMs));
|
||||
const next = cron.nextRun(new Date(nowMs));
|
||||
if (!next) {
|
||||
return undefined;
|
||||
}
|
||||
const nextMs = next.getTime();
|
||||
return Number.isFinite(nextMs) ? nextMs : undefined;
|
||||
if (!Number.isFinite(nextMs)) {
|
||||
return undefined;
|
||||
}
|
||||
if (nextMs > nowMs) {
|
||||
return nextMs;
|
||||
}
|
||||
|
||||
// Guard against same-second rescheduling loops: if croner returns
|
||||
// "now" (or an earlier instant) when the job completed mid-second,
|
||||
// retry from the next whole second.
|
||||
const nextSecondMs = Math.floor(nowMs / 1000) * 1000 + 1000;
|
||||
const retry = cron.nextRun(new Date(nextSecondMs));
|
||||
if (!retry) {
|
||||
return undefined;
|
||||
}
|
||||
const retryMs = retry.getTime();
|
||||
return Number.isFinite(retryMs) ? retryMs : undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,14 +134,18 @@ describe("processDiscordMessage ack reactions", () => {
|
|||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
const runPromise = processDiscordMessage(ctx as any);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
expect(reactMessageDiscord.mock.calls.some((call) => call[2] === "⏳")).toBe(true);
|
||||
let settled = false;
|
||||
void runPromise.finally(() => {
|
||||
settled = true;
|
||||
});
|
||||
for (let i = 0; i < 120 && !settled; i++) {
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
}
|
||||
|
||||
await vi.advanceTimersByTimeAsync(20_000);
|
||||
expect(reactMessageDiscord.mock.calls.some((call) => call[2] === "⚠️")).toBe(true);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await runPromise;
|
||||
expect(reactMessageDiscord.mock.calls.some((call) => call[2] === "✅")).toBe(true);
|
||||
const emojis = reactMessageDiscord.mock.calls.map((call) => call[2]);
|
||||
expect(emojis).toContain("⏳");
|
||||
expect(emojis).toContain("⚠️");
|
||||
expect(emojis).toContain("✅");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ describe("heartbeat transcript pruning", () => {
|
|||
const transcriptPath = path.join(tmpDir, `${sessionId}.jsonl`);
|
||||
|
||||
// Create a transcript with some existing content
|
||||
await createTranscriptWithContent(transcriptPath, sessionId);
|
||||
const originalContent = await createTranscriptWithContent(transcriptPath, sessionId);
|
||||
const originalSize = (await fs.stat(transcriptPath)).size;
|
||||
|
||||
// Seed session store
|
||||
|
|
|
|||
|
|
@ -366,7 +366,10 @@ export async function monitorWebInbox(options: {
|
|||
|
||||
const sendApi = createWebSendApi({
|
||||
sock: {
|
||||
sendMessage: (jid, content, options) => sock.sendMessage(jid, content, options),
|
||||
sendMessage: (jid, content, options) =>
|
||||
options === undefined
|
||||
? sock.sendMessage(jid, content)
|
||||
: sock.sendMessage(jid, content, options),
|
||||
sendPresenceUpdate: (presence, jid?: string) => sock.sendPresenceUpdate(presence, jid),
|
||||
},
|
||||
defaultAccountId: options.accountId,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { describe, expect, it, vi } from "vitest";
|
|||
import { setLoggerOverride } from "../logging.js";
|
||||
import { monitorWebInbox } from "./inbound.js";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
getAuthDir,
|
||||
getSock,
|
||||
installWebMonitorInboxUnitTestHooks,
|
||||
mockLoadConfig,
|
||||
|
|
@ -15,9 +17,18 @@ import {
|
|||
describe("web monitor inbox", () => {
|
||||
installWebMonitorInboxUnitTestHooks();
|
||||
|
||||
async function openMonitor(onMessage = vi.fn()) {
|
||||
return await monitorWebInbox({
|
||||
verbose: false,
|
||||
accountId: DEFAULT_ACCOUNT_ID,
|
||||
authDir: getAuthDir(),
|
||||
onMessage,
|
||||
});
|
||||
}
|
||||
|
||||
it("captures media path for image messages", async () => {
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
|
|
@ -52,7 +63,7 @@ describe("web monitor inbox", () => {
|
|||
|
||||
it("sets gifPlayback on outbound video payloads when requested", async () => {
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const buf = Buffer.from("gifvid");
|
||||
|
||||
|
|
@ -71,10 +82,7 @@ describe("web monitor inbox", () => {
|
|||
});
|
||||
|
||||
it("resolves onClose when the socket closes", async () => {
|
||||
const listener = await monitorWebInbox({
|
||||
verbose: false,
|
||||
onMessage: vi.fn(),
|
||||
});
|
||||
const listener = await openMonitor(vi.fn());
|
||||
const sock = getSock();
|
||||
const reasonPromise = listener.onClose;
|
||||
sock.ev.emit("connection.update", {
|
||||
|
|
@ -92,7 +100,7 @@ describe("web monitor inbox", () => {
|
|||
setLoggerOverride({ level: "trace", file: logPath });
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
|
|
@ -109,7 +117,16 @@ describe("web monitor inbox", () => {
|
|||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
const content = fsSync.readFileSync(logPath, "utf-8");
|
||||
const content = await (async () => {
|
||||
const deadline = Date.now() + 2_000;
|
||||
while (Date.now() < deadline) {
|
||||
if (fsSync.existsSync(logPath)) {
|
||||
return fsSync.readFileSync(logPath, "utf-8");
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
throw new Error(`expected log file to exist: ${logPath}`);
|
||||
})();
|
||||
expect(content).toMatch(/web-inbound/);
|
||||
expect(content).toMatch(/ping/);
|
||||
await listener.close();
|
||||
|
|
@ -117,7 +134,7 @@ describe("web monitor inbox", () => {
|
|||
|
||||
it("includes participant when marking group messages read", async () => {
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
|
|
@ -150,7 +167,7 @@ describe("web monitor inbox", () => {
|
|||
|
||||
it("passes through group messages with participant metadata", async () => {
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
|
|
@ -190,7 +207,7 @@ describe("web monitor inbox", () => {
|
|||
|
||||
it("unwraps ephemeral messages, preserves mentions, and still delivers group pings", async () => {
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
|
|
@ -249,7 +266,7 @@ describe("web monitor inbox", () => {
|
|||
});
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await monitorWebInbox({ verbose: false, onMessage });
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const upsert = {
|
||||
type: "notify",
|
||||
|
|
|
|||
Loading…
Reference in New Issue