mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
chore(lint): enable stricter oxlint rules
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// Secret scanning alert handler for OpenClaw maintainers.
|
||||
// Usage: node secret-scanning.mjs <command> [options]
|
||||
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
@@ -39,7 +39,9 @@ function gh(args, { json = true, allowFailure = false } = {}) {
|
||||
stderr: proc.stderr,
|
||||
};
|
||||
}
|
||||
if (!json) return proc.stdout;
|
||||
if (!json) {
|
||||
return proc.stdout;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(proc.stdout);
|
||||
} catch {
|
||||
@@ -70,7 +72,9 @@ export function loadBodyRedactionResult(locationType, resultFile) {
|
||||
if (!resultFile) {
|
||||
fail("Body notifications require a redaction result file from redact-body-if-needed");
|
||||
}
|
||||
if (!fs.existsSync(resultFile)) fail(`File not found: ${resultFile}`);
|
||||
if (!fs.existsSync(resultFile)) {
|
||||
fail(`File not found: ${resultFile}`);
|
||||
}
|
||||
|
||||
const result = JSON.parse(fs.readFileSync(resultFile, "utf8"));
|
||||
if (typeof result.notify_required !== "boolean") {
|
||||
@@ -182,10 +186,11 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
|
||||
failOnGraphQLFailure(gql, `Failed to fetch discussion #${discussionNumber}`);
|
||||
|
||||
const discussion = gql?.data?.repository?.discussion;
|
||||
if (!discussion)
|
||||
if (!discussion) {
|
||||
fail(
|
||||
`Discussion #${discussionNumber} not found — it may have been deleted. The alert cannot be processed via this skill.`,
|
||||
);
|
||||
}
|
||||
|
||||
discussionId = discussion.id;
|
||||
|
||||
@@ -205,15 +210,18 @@ function fetchDiscussionComment(discussionNumber, discussionCommentDbId) {
|
||||
`Failed to fetch replies for discussion comment ${topLevelComment.id}`,
|
||||
);
|
||||
const replies = replyPage?.data?.node?.replies;
|
||||
if (!replies)
|
||||
if (!replies) {
|
||||
fail(`Failed to paginate replies for discussion comment ${topLevelComment.id}`);
|
||||
}
|
||||
|
||||
reply = findDiscussionCommentNode(replies.nodes, discussionCommentDbId);
|
||||
hasMoreReplies = replies.pageInfo.hasNextPage;
|
||||
replyCursor = replies.pageInfo.endCursor;
|
||||
}
|
||||
|
||||
if (reply) return { discussionId, comment: reply };
|
||||
if (reply) {
|
||||
return { discussionId, comment: reply };
|
||||
}
|
||||
}
|
||||
|
||||
hasNextPage = discussion.comments.pageInfo.hasNextPage;
|
||||
@@ -241,7 +249,9 @@ function createDiscussionComment(discussionNodeId, body, replyToNodeId) {
|
||||
* Fetch alert metadata + locations. Never exposes .secret.
|
||||
*/
|
||||
function cmdFetchAlert(alertNumber) {
|
||||
if (!alertNumber) fail("Usage: fetch-alert <number>");
|
||||
if (!alertNumber) {
|
||||
fail("Usage: fetch-alert <number>");
|
||||
}
|
||||
|
||||
const alert = gh(["api", `repos/${REPO}/secret-scanning/alerts/${alertNumber}?hide_secret=true`]);
|
||||
|
||||
@@ -280,17 +290,23 @@ function cmdFetchAlert(alertNumber) {
|
||||
* Saves full body to a temp file. Prints metadata + file path to stdout.
|
||||
*/
|
||||
function cmdFetchContent(locationJson) {
|
||||
if (!locationJson) fail("Usage: fetch-content '<location-json>'");
|
||||
if (!locationJson) {
|
||||
fail("Usage: fetch-content '<location-json>'");
|
||||
}
|
||||
const location = JSON.parse(locationJson);
|
||||
const type = location.type;
|
||||
const details = location.details;
|
||||
|
||||
if (type === "discussion_comment") {
|
||||
const commentUrl = details.discussion_comment_url;
|
||||
if (!commentUrl) fail("No discussion_comment_url in location details");
|
||||
if (!commentUrl) {
|
||||
fail("No discussion_comment_url in location details");
|
||||
}
|
||||
|
||||
const urlMatch = commentUrl.match(/discussions\/(\d+)#discussioncomment-(\d+)/);
|
||||
if (!urlMatch) fail(`Cannot parse discussion comment URL: ${commentUrl}`);
|
||||
if (!urlMatch) {
|
||||
fail(`Cannot parse discussion comment URL: ${commentUrl}`);
|
||||
}
|
||||
const discussionNumber = urlMatch[1];
|
||||
const discussionCommentDbId = urlMatch[2];
|
||||
|
||||
@@ -298,10 +314,11 @@ function cmdFetchContent(locationJson) {
|
||||
discussionNumber,
|
||||
discussionCommentDbId,
|
||||
);
|
||||
if (!comment)
|
||||
if (!comment) {
|
||||
fail(
|
||||
`Discussion comment #${discussionCommentDbId} not found in discussion #${discussionNumber}`,
|
||||
);
|
||||
}
|
||||
|
||||
const bodyFile = tmpFile("body.md");
|
||||
fs.writeFileSync(bodyFile, comment.body || "");
|
||||
@@ -334,7 +351,9 @@ function cmdFetchContent(locationJson) {
|
||||
details.issue_comment_url ||
|
||||
details.pull_request_comment_url ||
|
||||
details.pull_request_review_comment_url;
|
||||
if (!commentUrl) fail(`No comment URL in location details`);
|
||||
if (!commentUrl) {
|
||||
fail(`No comment URL in location details`);
|
||||
}
|
||||
|
||||
const comment = gh(["api", commentUrl]);
|
||||
const bodyFile = tmpFile("body.md");
|
||||
@@ -378,7 +397,9 @@ function cmdFetchContent(locationJson) {
|
||||
);
|
||||
} else if (type === "issue_body") {
|
||||
const issueUrl = details.issue_body_url || details.issue_url;
|
||||
if (!issueUrl) fail("No issue URL in location details");
|
||||
if (!issueUrl) {
|
||||
fail("No issue URL in location details");
|
||||
}
|
||||
|
||||
const issue = gh(["api", issueUrl]);
|
||||
const bodyFile = tmpFile("body.md");
|
||||
@@ -414,7 +435,9 @@ function cmdFetchContent(locationJson) {
|
||||
);
|
||||
} else if (type === "pull_request_body") {
|
||||
const prUrl = details.pull_request_body_url || details.pull_request_url;
|
||||
if (!prUrl) fail("No PR URL in location details");
|
||||
if (!prUrl) {
|
||||
fail("No PR URL in location details");
|
||||
}
|
||||
|
||||
const pr = gh(["api", prUrl]);
|
||||
const bodyFile = tmpFile("body.md");
|
||||
@@ -490,7 +513,9 @@ function cmdRedactBody(kind, number, bodyFile) {
|
||||
if (!kind || !number || !bodyFile) {
|
||||
fail("Usage: redact-body <issue|pr> <number> <redacted-body-file>");
|
||||
}
|
||||
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
|
||||
if (!fs.existsSync(bodyFile)) {
|
||||
fail(`File not found: ${bodyFile}`);
|
||||
}
|
||||
|
||||
const endpoint =
|
||||
kind === "pr" ? `repos/${REPO}/pulls/${number}` : `repos/${REPO}/issues/${number}`;
|
||||
@@ -509,8 +534,12 @@ function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile,
|
||||
"Usage: redact-body-if-needed <issue|pr> <number> <current-body-file> <redacted-body-file> <result-file>",
|
||||
);
|
||||
}
|
||||
if (!fs.existsSync(currentBodyFile)) fail(`File not found: ${currentBodyFile}`);
|
||||
if (!fs.existsSync(redactedBodyFile)) fail(`File not found: ${redactedBodyFile}`);
|
||||
if (!fs.existsSync(currentBodyFile)) {
|
||||
fail(`File not found: ${currentBodyFile}`);
|
||||
}
|
||||
if (!fs.existsSync(redactedBodyFile)) {
|
||||
fail(`File not found: ${redactedBodyFile}`);
|
||||
}
|
||||
|
||||
const currentBody = fs.readFileSync(currentBodyFile, "utf8");
|
||||
const redactedBody = fs.readFileSync(redactedBodyFile, "utf8");
|
||||
@@ -541,7 +570,9 @@ function cmdRedactBodyIfNeeded(kind, number, currentBodyFile, redactedBodyFile,
|
||||
* Delete a comment (and all its edit history).
|
||||
*/
|
||||
function cmdDeleteComment(commentId) {
|
||||
if (!commentId) fail("Usage: delete-comment <comment-id>");
|
||||
if (!commentId) {
|
||||
fail("Usage: delete-comment <comment-id>");
|
||||
}
|
||||
gh(["api", `repos/${REPO}/issues/comments/${commentId}`, "-X", "DELETE"], { json: false });
|
||||
console.log(JSON.stringify({ ok: true, deleted_comment_id: Number(commentId) }));
|
||||
}
|
||||
@@ -551,7 +582,9 @@ function cmdDeleteComment(commentId) {
|
||||
* Delete a discussion comment via GraphQL (and all its edit history).
|
||||
*/
|
||||
function cmdDeleteDiscussionComment(nodeId) {
|
||||
if (!nodeId) fail("Usage: delete-discussion-comment <node-id>");
|
||||
if (!nodeId) {
|
||||
fail("Usage: delete-discussion-comment <node-id>");
|
||||
}
|
||||
const result = ghGraphQL(
|
||||
`mutation { deleteDiscussionComment(input: { id: "${nodeId}" }) { comment { id } } }`,
|
||||
);
|
||||
@@ -566,9 +599,12 @@ function cmdDeleteDiscussionComment(nodeId) {
|
||||
* Create a new discussion comment via GraphQL.
|
||||
*/
|
||||
function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId) {
|
||||
if (!discussionNodeId || !bodyFile)
|
||||
if (!discussionNodeId || !bodyFile) {
|
||||
fail("Usage: recreate-discussion-comment <discussion-node-id> <body-file> [reply-to-node-id]");
|
||||
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
|
||||
}
|
||||
if (!fs.existsSync(bodyFile)) {
|
||||
fail(`File not found: ${bodyFile}`);
|
||||
}
|
||||
|
||||
const body = fs.readFileSync(bodyFile, "utf8");
|
||||
const newComment = createDiscussionComment(discussionNodeId, body, replyToNodeId);
|
||||
@@ -586,8 +622,12 @@ function cmdRecreateDiscussionComment(discussionNodeId, bodyFile, replyToNodeId)
|
||||
* Create a new comment from a file.
|
||||
*/
|
||||
function cmdRecreateComment(issueNumber, bodyFile) {
|
||||
if (!issueNumber || !bodyFile) fail("Usage: recreate-comment <issue-number> <body-file>");
|
||||
if (!fs.existsSync(bodyFile)) fail(`File not found: ${bodyFile}`);
|
||||
if (!issueNumber || !bodyFile) {
|
||||
fail("Usage: recreate-comment <issue-number> <body-file>");
|
||||
}
|
||||
if (!fs.existsSync(bodyFile)) {
|
||||
fail(`File not found: ${bodyFile}`);
|
||||
}
|
||||
|
||||
const result = gh([
|
||||
"api",
|
||||
@@ -715,7 +755,9 @@ function cmdNotify(target, author, locationType, secretTypes, replyToNodeId) {
|
||||
* Close a secret scanning alert.
|
||||
*/
|
||||
function cmdResolve(alertNumber, resolution, comment) {
|
||||
if (!alertNumber) fail("Usage: resolve <alert-number> [resolution] [comment]");
|
||||
if (!alertNumber) {
|
||||
fail("Usage: resolve <alert-number> [resolution] [comment]");
|
||||
}
|
||||
|
||||
const res = resolution || "revoked";
|
||||
const resComment = comment || "Content redacted and author notified to rotate credentials.";
|
||||
@@ -773,8 +815,12 @@ function cmdListOpen() {
|
||||
* Print a formatted summary table from a JSON results file.
|
||||
*/
|
||||
function cmdSummary(jsonFile) {
|
||||
if (!jsonFile) fail("Usage: summary <json-file>");
|
||||
if (!fs.existsSync(jsonFile)) fail(`File not found: ${jsonFile}`);
|
||||
if (!jsonFile) {
|
||||
fail("Usage: summary <json-file>");
|
||||
}
|
||||
if (!fs.existsSync(jsonFile)) {
|
||||
fail(`File not found: ${jsonFile}`);
|
||||
}
|
||||
|
||||
const results = JSON.parse(fs.readFileSync(jsonFile, "utf8"));
|
||||
const lines = [];
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"eslint/no-return-assign": "error",
|
||||
"eslint/no-sequences": "error",
|
||||
"eslint/no-self-compare": "error",
|
||||
"eslint/no-shadow": "off",
|
||||
"eslint/no-shadow": "error",
|
||||
"eslint/no-implicit-coercion": "error",
|
||||
"eslint/no-var": "error",
|
||||
"eslint/no-useless-call": "error",
|
||||
@@ -35,7 +35,7 @@
|
||||
"eslint/no-useless-constructor": "error",
|
||||
"eslint/no-useless-rename": "error",
|
||||
"eslint/no-useless-return": "error",
|
||||
"eslint/no-unused-vars": "off",
|
||||
"eslint/no-unused-vars": "error",
|
||||
"eslint/no-warning-comments": "error",
|
||||
"eslint/no-unmodified-loop-condition": "error",
|
||||
"eslint/no-new-wrappers": "error",
|
||||
@@ -229,15 +229,7 @@
|
||||
"**/*test-support.ts"
|
||||
],
|
||||
"rules": {
|
||||
"typescript/no-explicit-any": "off",
|
||||
"typescript/unbound-method": "off",
|
||||
"eslint/no-unsafe-optional-chaining": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/agents/embedded-agent-runner/run/attempt.ts"],
|
||||
"rules": {
|
||||
"eslint/no-shadow": "error"
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -37,8 +37,8 @@ async function startRealService(state: DeferredServiceState): Promise<AcpRuntime
|
||||
throw new Error("ACPX runtime service is not started");
|
||||
}
|
||||
state.startPromise ??= (async () => {
|
||||
const { createAcpxRuntimeService } = await loadServiceModule();
|
||||
const service = createAcpxRuntimeService(state.params);
|
||||
const { createAcpxRuntimeService: createAcpxRuntimeServiceLocal } = await loadServiceModule();
|
||||
const service = createAcpxRuntimeServiceLocal(state.params);
|
||||
state.realService = service;
|
||||
await service.start(state.ctx as OpenClawPluginServiceContext);
|
||||
const backend = getAcpRuntimeBackend(ACPX_BACKEND_ID);
|
||||
|
||||
@@ -335,7 +335,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
});
|
||||
|
||||
await expect(async () => {
|
||||
for await (const eventValue of runtime.runTurn({
|
||||
for await (const ignoredEventValue of runtime.runTurn({
|
||||
handle: {
|
||||
sessionKey: "agent:codex:acp:test",
|
||||
backend: "acpx",
|
||||
@@ -346,6 +346,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
mode: "prompt",
|
||||
requestId: "turn-1",
|
||||
})) {
|
||||
void ignoredEventValue;
|
||||
// no-op
|
||||
}
|
||||
}).rejects.toMatchObject({
|
||||
@@ -568,7 +569,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
for await (const eventValue of runtime.runTurn({
|
||||
for await (const ignoredEventValue of runtime.runTurn({
|
||||
handle: {
|
||||
sessionKey: "agent:codex:acp:test",
|
||||
backend: "acpx",
|
||||
@@ -579,6 +580,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
mode: "prompt",
|
||||
requestId: "turn-1",
|
||||
})) {
|
||||
void ignoredEventValue;
|
||||
// no-op
|
||||
}
|
||||
|
||||
@@ -599,7 +601,8 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
mode: "prompt",
|
||||
requestId: "turn-2",
|
||||
});
|
||||
for await (const eventValue of turn.events) {
|
||||
for await (const ignoredEventValue of turn.events) {
|
||||
void ignoredEventValue;
|
||||
// no-op
|
||||
}
|
||||
await turn.result;
|
||||
@@ -947,16 +950,16 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toEqual({
|
||||
acpxRecordId: "stale",
|
||||
});
|
||||
expect(baseStore.load).toHaveBeenCalledTimes(1);
|
||||
expect(baseStore["load"]).toHaveBeenCalledTimes(1);
|
||||
|
||||
await runtime.prepareFreshSession({
|
||||
sessionKey: "agent:codex:acp:binding:test",
|
||||
});
|
||||
|
||||
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toBeUndefined();
|
||||
expect(baseStore.load).toHaveBeenCalledTimes(1);
|
||||
expect(baseStore["load"]).toHaveBeenCalledTimes(1);
|
||||
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toBeUndefined();
|
||||
expect(baseStore.load).toHaveBeenCalledTimes(1);
|
||||
expect(baseStore["load"]).toHaveBeenCalledTimes(1);
|
||||
|
||||
await wrappedStore.save({
|
||||
acpxRecordId: "fresh-record",
|
||||
@@ -966,7 +969,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toEqual({
|
||||
acpxRecordId: "stale",
|
||||
});
|
||||
expect(baseStore.load).toHaveBeenCalledTimes(2);
|
||||
expect(baseStore["load"]).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("marks the session fresh after discardPersistentState close", async () => {
|
||||
@@ -998,7 +1001,7 @@ describe("AcpxRuntime fresh reset wrapper", () => {
|
||||
discardPersistentState: true,
|
||||
});
|
||||
expect(await wrappedStore.load("agent:codex:acp:binding:test")).toBeUndefined();
|
||||
expect(baseStore.load).toHaveBeenCalledOnce();
|
||||
expect(baseStore["load"]).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("cleans up OpenClaw-owned ACPX process trees after close", async () => {
|
||||
|
||||
@@ -605,25 +605,25 @@ function resolveRecallRunChannelContext(params: {
|
||||
(!explicitProvider || explicitProvider === "webchat")
|
||||
? runnableExplicitChannel
|
||||
: undefined;
|
||||
const resolveReturnValue = (params: {
|
||||
const resolveReturnValue = (paramsLocal: {
|
||||
resolvedChannel?: string;
|
||||
resolvedChannelStrength?: "strong" | "weak";
|
||||
}) => {
|
||||
const trustedResolvedChannel =
|
||||
params.resolvedChannelStrength === "strong" ? params.resolvedChannel : undefined;
|
||||
paramsLocal.resolvedChannelStrength === "strong" ? paramsLocal.resolvedChannel : undefined;
|
||||
return {
|
||||
messageChannel:
|
||||
trustedExplicitChannel ??
|
||||
trustedResolvedChannel ??
|
||||
explicitProvider ??
|
||||
runnableExplicitChannel ??
|
||||
params.resolvedChannel,
|
||||
paramsLocal.resolvedChannel,
|
||||
messageProvider:
|
||||
trustedExplicitChannel ??
|
||||
trustedResolvedChannel ??
|
||||
explicitProvider ??
|
||||
runnableExplicitChannel ??
|
||||
params.resolvedChannel,
|
||||
paramsLocal.resolvedChannel,
|
||||
};
|
||||
};
|
||||
const resolvedSessionKey =
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Api, Model } from "openclaw/plugin-sdk/llm";
|
||||
import type { Model } from "openclaw/plugin-sdk/llm";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createMantleAnthropicStreamFn,
|
||||
|
||||
@@ -365,29 +365,29 @@ export async function createBedrockEmbeddingProvider(
|
||||
|
||||
const embedQuery = async (
|
||||
text: string,
|
||||
options?: { signal?: AbortSignal },
|
||||
optionsValue?: { signal?: AbortSignal },
|
||||
): Promise<number[]> => {
|
||||
if (!text.trim()) {
|
||||
return [];
|
||||
}
|
||||
if (isCohere) {
|
||||
return (await embedCohere([text], "search_query", options?.signal))[0] ?? [];
|
||||
return (await embedCohere([text], "search_query", optionsValue?.signal))[0] ?? [];
|
||||
}
|
||||
return embedSingle(text, options?.signal);
|
||||
return embedSingle(text, optionsValue?.signal);
|
||||
};
|
||||
|
||||
const embedBatch = async (
|
||||
texts: string[],
|
||||
options?: { signal?: AbortSignal },
|
||||
optionsLocal?: { signal?: AbortSignal },
|
||||
): Promise<number[][]> => {
|
||||
if (texts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
if (isCohere) {
|
||||
return embedCohere(texts, "search_document", options?.signal);
|
||||
return embedCohere(texts, "search_document", optionsLocal?.signal);
|
||||
}
|
||||
return Promise.all(
|
||||
texts.map((t) => (t.trim() ? embedSingle(t, options?.signal) : Promise.resolve([]))),
|
||||
texts.map((t) => (t.trim() ? embedSingle(t, optionsLocal?.signal) : Promise.resolve([]))),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -272,7 +272,7 @@ describe("amazon-bedrock provider plugin", () => {
|
||||
setBedrockAppProfileControlPlaneForTest((region) => ({
|
||||
async getInferenceProfile(input) {
|
||||
class GetInferenceProfileCommand {
|
||||
constructor(readonly input: Record<string, unknown> = {}) {}
|
||||
constructor(readonly inputLocal: Record<string, unknown> = {}) {}
|
||||
}
|
||||
bedrockClientConfigs.push(region ? { region } : {});
|
||||
return await sendBedrockCommand(new GetInferenceProfileCommand(input));
|
||||
|
||||
@@ -127,11 +127,11 @@ export default definePluginEntry({
|
||||
: undefined;
|
||||
},
|
||||
normalizeResolvedModel: ({ model }) => normalizeArceeResolvedModel(model),
|
||||
normalizeTransport: ({ api, baseUrl }) => {
|
||||
normalizeTransport: ({ api: apiLocal, baseUrl }) => {
|
||||
const normalizedBaseUrl = normalizeArceeOpenRouterBaseUrl(baseUrl);
|
||||
return normalizedBaseUrl && normalizedBaseUrl !== baseUrl
|
||||
? {
|
||||
api,
|
||||
api: apiLocal,
|
||||
baseUrl: normalizedBaseUrl,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -495,7 +495,10 @@ export async function startGatewayBonjourAdvertiser(
|
||||
return { responder, services };
|
||||
}
|
||||
|
||||
async function stopCycle(cycle: BonjourCycle | null, opts?: { shutdownResponder?: boolean }) {
|
||||
async function stopCycle(
|
||||
cycle: BonjourCycle | null,
|
||||
optsValue?: { shutdownResponder?: boolean },
|
||||
) {
|
||||
if (!cycle) {
|
||||
return;
|
||||
}
|
||||
@@ -507,7 +510,7 @@ export async function startGatewayBonjourAdvertiser(
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (opts?.shutdownResponder) {
|
||||
if (optsValue?.shutdownResponder) {
|
||||
await cycle.responder.shutdown();
|
||||
}
|
||||
} catch {
|
||||
@@ -615,7 +618,7 @@ export async function startGatewayBonjourAdvertiser(
|
||||
}
|
||||
};
|
||||
|
||||
const recreateAdvertiser = async (reason: string, opts?: { stuckState?: boolean }) => {
|
||||
const recreateAdvertiser = async (reason: string, optsLocal?: { stuckState?: boolean }) => {
|
||||
if (stopped || disabled) {
|
||||
return;
|
||||
}
|
||||
@@ -624,7 +627,9 @@ export async function startGatewayBonjourAdvertiser(
|
||||
}
|
||||
recreatePromise = (async () => {
|
||||
consecutiveRestarts += 1;
|
||||
consecutiveStuckStateRestarts = opts?.stuckState ? consecutiveStuckStateRestarts + 1 : 0;
|
||||
consecutiveStuckStateRestarts = optsLocal?.stuckState
|
||||
? consecutiveStuckStateRestarts + 1
|
||||
: 0;
|
||||
const now = Date.now();
|
||||
while (
|
||||
restartTimestamps.length > 0 &&
|
||||
|
||||
@@ -530,7 +530,7 @@ export function createBrowserTool(opts?: {
|
||||
});
|
||||
|
||||
const proxyRequest = nodeTarget
|
||||
? async (opts: {
|
||||
? async (optsLocal: {
|
||||
method: string;
|
||||
path: string;
|
||||
query?: Record<string, string | number | boolean | undefined>;
|
||||
@@ -540,12 +540,12 @@ export function createBrowserTool(opts?: {
|
||||
}) => {
|
||||
const proxy = await callBrowserProxy({
|
||||
nodeId: nodeTarget.nodeId,
|
||||
method: opts.method,
|
||||
path: opts.path,
|
||||
query: opts.query,
|
||||
body: opts.body,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
profile: opts.profile,
|
||||
method: optsLocal.method,
|
||||
path: optsLocal.path,
|
||||
query: optsLocal.query,
|
||||
body: optsLocal.body,
|
||||
timeoutMs: optsLocal.timeoutMs,
|
||||
profile: optsLocal.profile,
|
||||
});
|
||||
const mapping = await persistProxyFiles(proxy.files);
|
||||
applyProxyPaths(proxy.result, mapping);
|
||||
|
||||
@@ -232,12 +232,13 @@ describe("cdp-proxy-bypass", () => {
|
||||
describe("withNoProxyForLocalhost concurrency", () => {
|
||||
it("does not leak NO_PROXY when called concurrently", async () => {
|
||||
await withIsolatedNoProxyEnv(async () => {
|
||||
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
|
||||
const { withNoProxyForLocalhost: withNoProxyForLocalhostScoped } =
|
||||
await import("./cdp-proxy-bypass.js");
|
||||
|
||||
const releaseA = createDeferred();
|
||||
const enteredA = createDeferred();
|
||||
|
||||
const callA = withNoProxyForLocalhost(async () => {
|
||||
const callA = withNoProxyForLocalhostScoped(async () => {
|
||||
expect(process.env.NO_PROXY).toContain("localhost");
|
||||
expect(process.env.NO_PROXY).toContain("[::1]");
|
||||
enteredA.resolve();
|
||||
@@ -247,7 +248,7 @@ describe("withNoProxyForLocalhost concurrency", () => {
|
||||
|
||||
await enteredA.promise;
|
||||
|
||||
const callB = withNoProxyForLocalhost(async () => {
|
||||
const callB = withNoProxyForLocalhostScoped(async () => {
|
||||
return "b";
|
||||
});
|
||||
|
||||
@@ -264,21 +265,22 @@ describe("withNoProxyForLocalhost concurrency", () => {
|
||||
describe("withNoProxyForLocalhost reverse exit order", () => {
|
||||
it("restores NO_PROXY when first caller exits before second", async () => {
|
||||
await withIsolatedNoProxyEnv(async () => {
|
||||
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
|
||||
const { withNoProxyForLocalhost: withNoProxyForLocalhostItem } =
|
||||
await import("./cdp-proxy-bypass.js");
|
||||
|
||||
const enteredA = createDeferred();
|
||||
const enteredB = createDeferred();
|
||||
const releaseA = createDeferred();
|
||||
const releaseB = createDeferred();
|
||||
|
||||
const callA = withNoProxyForLocalhost(async () => {
|
||||
const callA = withNoProxyForLocalhostItem(async () => {
|
||||
enteredA.resolve();
|
||||
await releaseA.promise;
|
||||
return "a";
|
||||
});
|
||||
await enteredA.promise;
|
||||
|
||||
const callB = withNoProxyForLocalhost(async () => {
|
||||
const callB = withNoProxyForLocalhostItem(async () => {
|
||||
enteredB.resolve();
|
||||
await releaseB.promise;
|
||||
return "b";
|
||||
@@ -306,9 +308,10 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
|
||||
process.env.HTTP_PROXY = "http://proxy:8080";
|
||||
|
||||
try {
|
||||
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
|
||||
const { withNoProxyForLocalhost: withNoProxyForLocalhostCandidate } =
|
||||
await import("./cdp-proxy-bypass.js");
|
||||
|
||||
await withNoProxyForLocalhost(async () => {
|
||||
await withNoProxyForLocalhostCandidate(async () => {
|
||||
// Should not modify since loopback is already covered
|
||||
expect(process.env.NO_PROXY).toBe(userNoProxy);
|
||||
return "ok";
|
||||
@@ -332,9 +335,10 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
|
||||
process.env.HTTP_PROXY = "http://proxy:8080";
|
||||
|
||||
try {
|
||||
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
|
||||
const { withNoProxyForLocalhost: withNoProxyForLocalhostEntry } =
|
||||
await import("./cdp-proxy-bypass.js");
|
||||
|
||||
await withNoProxyForLocalhost(async () => {
|
||||
await withNoProxyForLocalhostEntry(async () => {
|
||||
expect(process.env.NO_PROXY).toBe(`${coveredNoProxy},localhost,127.0.0.1,[::1]`);
|
||||
expect(process.env.no_proxy).toBe(`${staleLowerNoProxy},localhost,127.0.0.1,[::1]`);
|
||||
});
|
||||
@@ -355,9 +359,10 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
|
||||
process.env.HTTP_PROXY = "http://proxy:8080";
|
||||
|
||||
try {
|
||||
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
|
||||
const { withNoProxyForLocalhost: withNoProxyForLocalhostResult } =
|
||||
await import("./cdp-proxy-bypass.js");
|
||||
|
||||
await withNoProxyForLocalhost(async () => {
|
||||
await withNoProxyForLocalhostResult(async () => {
|
||||
expect(process.env.NO_PROXY).toBe(`${lowerNoProxy},localhost,127.0.0.1,[::1]`);
|
||||
expect(process.env.no_proxy).toBe(`${lowerNoProxy},localhost,127.0.0.1,[::1]`);
|
||||
});
|
||||
@@ -378,9 +383,10 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
|
||||
process.env.HTTP_PROXY = "http://proxy:8080";
|
||||
|
||||
try {
|
||||
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
|
||||
const { withNoProxyForLocalhost: withNoProxyForLocalhostValue } =
|
||||
await import("./cdp-proxy-bypass.js");
|
||||
|
||||
await withNoProxyForLocalhost(async () => {
|
||||
await withNoProxyForLocalhostValue(async () => {
|
||||
expect(process.env.NO_PROXY).toBe(`${userNoProxy},localhost,127.0.0.1,[::1]`);
|
||||
expect(process.env.no_proxy).toBe(`${userNoProxy},localhost,127.0.0.1,[::1]`);
|
||||
delete process.env.no_proxy;
|
||||
@@ -402,9 +408,10 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
|
||||
process.env.HTTP_PROXY = "http://proxy:8080";
|
||||
|
||||
try {
|
||||
const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js");
|
||||
const { withNoProxyForLocalhost: withNoProxyForLocalhostLocal } =
|
||||
await import("./cdp-proxy-bypass.js");
|
||||
|
||||
await withNoProxyForLocalhost(async () => {
|
||||
await withNoProxyForLocalhostLocal(async () => {
|
||||
expect(process.env.NO_PROXY).toBe(`${userNoProxy},localhost,127.0.0.1,[::1]`);
|
||||
expect(process.env.no_proxy).toBe(`${userNoProxy},localhost,127.0.0.1,[::1]`);
|
||||
});
|
||||
|
||||
@@ -490,11 +490,11 @@ describe("chrome MCP page parsing", () => {
|
||||
url: "about:blank",
|
||||
type: "page",
|
||||
});
|
||||
expect(session.client.callTool).toHaveBeenCalledWith({
|
||||
expect(session.client["callTool"]).toHaveBeenCalledWith({
|
||||
name: "new_page",
|
||||
arguments: { url: "about:blank", timeout: 5000 },
|
||||
});
|
||||
const callToolMock = session.client.callTool as unknown as ToolCallMock;
|
||||
const callToolMock = session.client["callTool"] as unknown as ToolCallMock;
|
||||
const callNames = callToolMock.mock.calls.map(([call]) => call.name);
|
||||
expect(callNames).not.toContain("navigate_page");
|
||||
});
|
||||
@@ -917,7 +917,7 @@ describe("chrome MCP page parsing", () => {
|
||||
// intentionally no timeoutMs
|
||||
});
|
||||
|
||||
const callToolMock = session.client.callTool as unknown as ToolCallMock;
|
||||
const callToolMock = session.client["callTool"] as unknown as ToolCallMock;
|
||||
const navigateCall = callToolMock.mock.calls.find(
|
||||
([call]) => call.name === "navigate_page",
|
||||
)?.[0];
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("pw-tools-core", () => {
|
||||
await fs.writeFile(uploadPath, "fixture", "utf8");
|
||||
const canonicalUploadPath = await fs.realpath(uploadPath);
|
||||
const fileChooser = { setFiles: vi.fn(async () => {}) };
|
||||
const waitForEvent = vi.fn(async (eventValue: string, _opts: unknown) => fileChooser);
|
||||
const waitForEvent = vi.fn(async (_eventValue: string, _opts: unknown) => fileChooser);
|
||||
setPwToolsCoreCurrentPage({
|
||||
waitForEvent,
|
||||
keyboard: { press: vi.fn(async () => {}) },
|
||||
|
||||
@@ -86,10 +86,10 @@ export function registerBrowserAgentDebugRoutes(
|
||||
ctx,
|
||||
targetId,
|
||||
feature: "page errors",
|
||||
collect: async ({ cdpUrl, targetId, pw }) =>
|
||||
collect: async ({ cdpUrl, targetId: targetIdValue, pw }) =>
|
||||
await pw.getPageErrorsViaPlaywright({
|
||||
cdpUrl,
|
||||
targetId,
|
||||
targetId: targetIdValue,
|
||||
clear,
|
||||
}),
|
||||
});
|
||||
@@ -109,10 +109,10 @@ export function registerBrowserAgentDebugRoutes(
|
||||
ctx,
|
||||
targetId,
|
||||
feature: "network requests",
|
||||
collect: async ({ cdpUrl, targetId, pw }) =>
|
||||
collect: async ({ cdpUrl, targetId: targetIdLocal, pw }) =>
|
||||
await pw.getNetworkRequestsViaPlaywright({
|
||||
cdpUrl,
|
||||
targetId,
|
||||
targetId: targetIdLocal,
|
||||
filter: normalizeOptionalString(filter),
|
||||
clear,
|
||||
}),
|
||||
|
||||
@@ -25,14 +25,14 @@ const {
|
||||
stopKnownBrowserProfilesMock,
|
||||
trackedTabCleanupMock,
|
||||
} = vi.hoisted(() => {
|
||||
const trackedTabCleanupMock = vi.fn();
|
||||
const trackedTabCleanupMockLocal = vi.fn();
|
||||
return {
|
||||
ensureExtensionRelayForProfilesMock: vi.fn(async () => {}),
|
||||
getPwAiModuleMock: vi.fn(),
|
||||
isPwAiLoadedMock: vi.fn(() => false),
|
||||
startTrackedBrowserTabCleanupTimerMock: vi.fn(() => trackedTabCleanupMock),
|
||||
startTrackedBrowserTabCleanupTimerMock: vi.fn(() => trackedTabCleanupMockLocal),
|
||||
stopKnownBrowserProfilesMock: vi.fn(async () => {}),
|
||||
trackedTabCleanupMock,
|
||||
trackedTabCleanupMock: trackedTabCleanupMockLocal,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ export function createBrowserRouteContext(opts: ContextOptions): BrowserRouteCon
|
||||
isTransportAvailable: (timeoutMs) => getDefaultContext().isTransportAvailable(timeoutMs),
|
||||
isReachable: (timeoutMs, options) => getDefaultContext().isReachable(timeoutMs, options),
|
||||
listTabs: () => getDefaultContext().listTabs(),
|
||||
openTab: (url, opts) => getDefaultContext().openTab(url, opts),
|
||||
openTab: (url, optsLocal) => getDefaultContext().openTab(url, optsLocal),
|
||||
labelTab: (targetId, label) => getDefaultContext().labelTab(targetId, label),
|
||||
focusTab: (targetId) => getDefaultContext().focusTab(targetId),
|
||||
closeTab: (targetId) => getDefaultContext().closeTab(targetId),
|
||||
|
||||
@@ -11,14 +11,14 @@ import {
|
||||
type DescribeFn = ReturnType<typeof vi.fn>;
|
||||
|
||||
function makeDeps(
|
||||
describe: DescribeFn,
|
||||
describeCandidate: DescribeFn,
|
||||
overrides?: {
|
||||
normalizeBrowserScreenshot?: ReturnType<typeof vi.fn>;
|
||||
saveMediaBuffer?: ReturnType<typeof vi.fn>;
|
||||
},
|
||||
) {
|
||||
return {
|
||||
describeImageFile: describe as never,
|
||||
describeImageFile: describeCandidate as never,
|
||||
normalizeBrowserScreenshot:
|
||||
(overrides?.normalizeBrowserScreenshot as never) ??
|
||||
(vi.fn(async (buffer: Buffer) => ({ buffer })) as never),
|
||||
@@ -41,7 +41,7 @@ async function withTempImage<T>(fn: (filePath: string) => Promise<T>): Promise<T
|
||||
|
||||
describe("describeBrowserScreenshot", () => {
|
||||
it("uses existing image understanding config with a browser screenshot prompt", async () => {
|
||||
const describe = vi.fn().mockResolvedValue({
|
||||
const describeEntry = vi.fn().mockResolvedValue({
|
||||
text: "A login screen.",
|
||||
provider: "openai",
|
||||
model: "gpt-vision",
|
||||
@@ -62,7 +62,7 @@ describe("describeBrowserScreenshot", () => {
|
||||
activeModel: { provider: "anthropic", model: "claude-sonnet-4.6" },
|
||||
mediaScope: { sessionKey: "agent:main:telegram:dm:123", channel: "telegram" },
|
||||
},
|
||||
makeDeps(describe),
|
||||
makeDeps(describeEntry),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -71,7 +71,7 @@ describe("describeBrowserScreenshot", () => {
|
||||
model: "gpt-vision",
|
||||
decision: { outcome: "success" },
|
||||
});
|
||||
expect(describe).toHaveBeenCalledWith({
|
||||
expect(describeEntry).toHaveBeenCalledWith({
|
||||
filePath,
|
||||
cfg: {
|
||||
tools: {
|
||||
@@ -92,7 +92,7 @@ describe("describeBrowserScreenshot", () => {
|
||||
});
|
||||
|
||||
it("resizes screenshots before image understanding when image sanitization is configured", async () => {
|
||||
const describe = vi.fn().mockResolvedValue({ text: "Small screenshot." });
|
||||
const describeResult = vi.fn().mockResolvedValue({ text: "Small screenshot." });
|
||||
const normalizeBrowserScreenshot = vi.fn(async () => ({
|
||||
buffer: Buffer.from("small"),
|
||||
contentType: "image/jpeg" as const,
|
||||
@@ -106,7 +106,7 @@ describe("describeBrowserScreenshot", () => {
|
||||
filePath,
|
||||
imageSanitization: { maxDimensionPx: 800 },
|
||||
},
|
||||
makeDeps(describe, { normalizeBrowserScreenshot, saveMediaBuffer }),
|
||||
makeDeps(describeResult, { normalizeBrowserScreenshot, saveMediaBuffer }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -114,11 +114,11 @@ describe("describeBrowserScreenshot", () => {
|
||||
maxSide: 800,
|
||||
});
|
||||
expect(saveMediaBuffer).toHaveBeenCalledWith(Buffer.from("small"), "image/jpeg", "browser");
|
||||
expect(describe.mock.calls[0][0].filePath).toBe("/tmp/resized.jpg");
|
||||
expect(describeResult.mock.calls[0][0].filePath).toBe("/tmp/resized.jpg");
|
||||
});
|
||||
|
||||
it("returns null when image understanding is skipped or not configured", async () => {
|
||||
const describe = vi.fn().mockResolvedValue({
|
||||
const describeValue = vi.fn().mockResolvedValue({
|
||||
text: undefined,
|
||||
decision: { outcome: "skipped" },
|
||||
});
|
||||
@@ -126,13 +126,13 @@ describe("describeBrowserScreenshot", () => {
|
||||
await expect(
|
||||
describeBrowserScreenshot(
|
||||
{ cfg: { browser: {} }, filePath: "/tmp/screenshot.png" },
|
||||
makeDeps(describe),
|
||||
makeDeps(describeValue),
|
||||
),
|
||||
).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it("does not pass an incomplete active model to media understanding", async () => {
|
||||
const describe = vi.fn().mockResolvedValue({ text: "ok" });
|
||||
const describeLocal = vi.fn().mockResolvedValue({ text: "ok" });
|
||||
|
||||
await describeBrowserScreenshot(
|
||||
{
|
||||
@@ -144,10 +144,10 @@ describe("describeBrowserScreenshot", () => {
|
||||
filePath: "/tmp/screenshot.png",
|
||||
activeModel: { model: "missing-provider" },
|
||||
},
|
||||
makeDeps(describe),
|
||||
makeDeps(describeLocal),
|
||||
);
|
||||
|
||||
expect(describe.mock.calls[0][0].activeModel).toBeUndefined();
|
||||
expect(describeLocal.mock.calls[0][0].activeModel).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -495,8 +495,10 @@ export function registerBrowserManageCommands(
|
||||
if (printJsonResult(parent, result)) {
|
||||
return;
|
||||
}
|
||||
const tab = (result as { tab?: BrowserTab }).tab;
|
||||
defaultRuntime.log(`labeled tab ${tab?.tabId ?? targetId} as ${tab?.label ?? label}`);
|
||||
const tabValue = (result as { tab?: BrowserTab }).tab;
|
||||
defaultRuntime.log(
|
||||
`labeled tab ${tabValue?.tabId ?? targetId} as ${tabValue?.label ?? label}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -555,7 +557,7 @@ export function registerBrowserManageCommands(
|
||||
const parent = parentOpts(cmd);
|
||||
const profile = parent?.browserProfile;
|
||||
await runBrowserCommand(async () => {
|
||||
const tab = await callBrowserRequest<BrowserTab>(
|
||||
const tabLocal = await callBrowserRequest<BrowserTab>(
|
||||
parent,
|
||||
{
|
||||
method: "POST",
|
||||
@@ -565,11 +567,11 @@ export function registerBrowserManageCommands(
|
||||
},
|
||||
{ timeoutMs: BROWSER_MANAGE_REQUEST_TIMEOUT_MS },
|
||||
);
|
||||
if (printJsonResult(parent, tab)) {
|
||||
if (printJsonResult(parent, tabLocal)) {
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(
|
||||
`opened: ${tab.url}\n${tab.tabId ? `tab: ${tab.tabId}\n` : ""}${tab.label ? `label: ${tab.label}\n` : ""}id: ${tab.targetId}`,
|
||||
`opened: ${tabLocal.url}\n${tabLocal.tabId ? `tab: ${tabLocal.tabId}\n` : ""}${tabLocal.label ? `label: ${tabLocal.label}\n` : ""}id: ${tabLocal.targetId}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -216,7 +216,7 @@ describe("codex plugin", () => {
|
||||
|
||||
it("enables the native hook relay for public Codex side questions", async () => {
|
||||
const harness = createCodexAppServerAgentHarness({ pluginConfig: { appServer: {} } });
|
||||
const runSideQuestion = harness.runSideQuestion;
|
||||
const runSideQuestion = harness["runSideQuestion"];
|
||||
const result = { text: "ok" };
|
||||
runCodexAppServerSideQuestionMock.mockResolvedValueOnce(result);
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ describe("codex provider", () => {
|
||||
listModels: listTestCodexAppServerModels,
|
||||
});
|
||||
|
||||
expect(client.close).toHaveBeenCalledTimes(1);
|
||||
expect(client["close"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not close an active shared app-server client during live discovery", async () => {
|
||||
@@ -293,8 +293,8 @@ describe("codex provider", () => {
|
||||
listModels: listTestCodexAppServerModels,
|
||||
});
|
||||
|
||||
expect(activeClient.close).not.toHaveBeenCalled();
|
||||
expect(discoveryClient.close).toHaveBeenCalledTimes(1);
|
||||
expect(activeClient["close"]).not.toHaveBeenCalled();
|
||||
expect(discoveryClient["close"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("resolves arbitrary Codex app-server model ids as text-only until discovered", () => {
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("maybeCompactCodexAppServerSession", () => {
|
||||
);
|
||||
|
||||
expect(fake.request).toHaveBeenCalledWith("thread/compact/start", { threadId: "thread-1" });
|
||||
expect(fake.client.addNotificationHandler).not.toHaveBeenCalled();
|
||||
expect(fake.client["addNotificationHandler"]).not.toHaveBeenCalled();
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.compacted).toBe(false);
|
||||
expect(result.result?.tokensBefore).toBe(123);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { AgentToolResult } from "openclaw/plugin-sdk/agent-core";
|
||||
import {
|
||||
onInternalDiagnosticEvent,
|
||||
waitForDiagnosticEventsDrained,
|
||||
type DiagnosticEventPayload,
|
||||
} from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import type { AnyAgentTool } from "openclaw/plugin-sdk/agent-harness";
|
||||
import {
|
||||
HEARTBEAT_RESPONSE_TOOL_NAME,
|
||||
embeddedAgentLog,
|
||||
wrapToolWithBeforeToolCallHook,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
onInternalDiagnosticEvent,
|
||||
waitForDiagnosticEventsDrained,
|
||||
type DiagnosticEventPayload,
|
||||
} from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import {
|
||||
initializeGlobalHookRunner,
|
||||
resetGlobalHookRunner,
|
||||
@@ -85,12 +85,6 @@ function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function requireArray(value: unknown, label: string): Array<unknown> {
|
||||
expect(Array.isArray(value), label).toBe(true);
|
||||
return value as Array<unknown>;
|
||||
}
|
||||
|
||||
function callArg(
|
||||
mock: { mock: { calls: Array<Array<unknown>> } },
|
||||
callIndex: number,
|
||||
@@ -346,9 +340,7 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
}),
|
||||
);
|
||||
const blockedEvents = diagnosticEvents.filter(
|
||||
(
|
||||
event,
|
||||
): event is Extract<DiagnosticEventPayload, { type: "tool.execution.blocked" }> =>
|
||||
(event): event is Extract<DiagnosticEventPayload, { type: "tool.execution.blocked" }> =>
|
||||
event.type === "tool.execution.blocked",
|
||||
);
|
||||
expect(blockedEvents).toContainEqual(
|
||||
@@ -889,7 +881,7 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
|
||||
it("passes raw tool failure state into agent tool result middleware", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const handler = vi.fn(async (eventValue: { isError?: boolean }) => undefined);
|
||||
const handler = vi.fn(async (_eventValue: { isError?: boolean }) => undefined);
|
||||
registry.agentToolResultMiddlewares.push({
|
||||
pluginId: "tokenjuice",
|
||||
pluginName: "Tokenjuice",
|
||||
|
||||
@@ -312,7 +312,7 @@ export function createAppServerHarness(
|
||||
return {
|
||||
request,
|
||||
requests,
|
||||
async waitForMethod(method: string, timeoutMs: number = appServerHarnessWait.timeout) {
|
||||
waitForMethod: async (method: string, timeoutMs: number = appServerHarnessWait.timeout) => {
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
if (!requests.some((entry) => entry.method === method)) {
|
||||
@@ -330,15 +330,15 @@ export function createAppServerHarness(
|
||||
{ interval: 1, timeout: timeoutMs },
|
||||
);
|
||||
},
|
||||
async notify(notification: CodexServerNotification) {
|
||||
notify: async (notification: CodexServerNotification) => {
|
||||
await sendNotification(notification);
|
||||
},
|
||||
waitForServerRequestHandler,
|
||||
async handleServerRequest(request: Parameters<AppServerRequestHandler>[0]) {
|
||||
handleServerRequest: async (requestLocal: Parameters<AppServerRequestHandler>[0]) => {
|
||||
const handler = await waitForServerRequestHandler();
|
||||
return handler(request);
|
||||
return handler(requestLocal);
|
||||
},
|
||||
async completeTurn(params: { threadId: string; turnId: string }) {
|
||||
completeTurn: async (params: { threadId: string; turnId: string }) => {
|
||||
await sendNotification({
|
||||
method: "turn/completed",
|
||||
params: {
|
||||
@@ -348,7 +348,7 @@ export function createAppServerHarness(
|
||||
},
|
||||
});
|
||||
},
|
||||
close() {
|
||||
close: () => {
|
||||
for (const handler of closeHandlers) {
|
||||
handler();
|
||||
}
|
||||
|
||||
@@ -334,16 +334,17 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
|
||||
if (!contextEngine.bootstrap) {
|
||||
throw new Error("expected bootstrap hook");
|
||||
}
|
||||
expect(contextEngine.bootstrap).toHaveBeenCalledTimes(1);
|
||||
const bootstrapParams = requireFirstCallArg(contextEngine.bootstrap, "bootstrap") as Parameters<
|
||||
NonNullable<ContextEngine["bootstrap"]>
|
||||
>[0];
|
||||
expect(contextEngine["bootstrap"]).toHaveBeenCalledTimes(1);
|
||||
const bootstrapParams = requireFirstCallArg(
|
||||
contextEngine["bootstrap"],
|
||||
"bootstrap",
|
||||
) as Parameters<NonNullable<ContextEngine["bootstrap"]>>[0];
|
||||
expect(bootstrapParams.sessionId).toBe("session-1");
|
||||
expect(bootstrapParams.sessionKey).toBe("agent:main:session-1");
|
||||
expect(bootstrapParams.sessionFile).toBe(sessionFile);
|
||||
|
||||
expect(contextEngine.assemble).toHaveBeenCalledTimes(1);
|
||||
const assembleParams = requireFirstCallArg(contextEngine.assemble, "assemble") as Parameters<
|
||||
expect(contextEngine["assemble"]).toHaveBeenCalledTimes(1);
|
||||
const assembleParams = requireFirstCallArg(contextEngine["assemble"], "assemble") as Parameters<
|
||||
ContextEngine["assemble"]
|
||||
>[0];
|
||||
expect(assembleParams.sessionId).toBe("session-1");
|
||||
@@ -385,12 +386,13 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
|
||||
if (!contextEngine.bootstrap) {
|
||||
throw new Error("expected bootstrap hook");
|
||||
}
|
||||
const bootstrapParams = requireFirstCallArg(contextEngine.bootstrap, "bootstrap") as Parameters<
|
||||
NonNullable<ContextEngine["bootstrap"]>
|
||||
>[0];
|
||||
const bootstrapParams = requireFirstCallArg(
|
||||
contextEngine["bootstrap"],
|
||||
"bootstrap",
|
||||
) as Parameters<NonNullable<ContextEngine["bootstrap"]>>[0];
|
||||
expect(bootstrapParams.sessionKey).toBe("agent:main:main");
|
||||
|
||||
const assembleParams = requireFirstCallArg(contextEngine.assemble, "assemble") as Parameters<
|
||||
const assembleParams = requireFirstCallArg(contextEngine["assemble"], "assemble") as Parameters<
|
||||
ContextEngine["assemble"]
|
||||
>[0];
|
||||
expect(assembleParams.sessionKey).toBe("agent:main:main");
|
||||
@@ -1640,7 +1642,7 @@ describe("runCodexAppServerAttempt context-engine lifecycle", () => {
|
||||
await harness.completeTurn();
|
||||
await run;
|
||||
|
||||
const assembleParams = requireFirstCallArg(contextEngine.assemble, "assemble") as Parameters<
|
||||
const assembleParams = requireFirstCallArg(contextEngine["assemble"], "assemble") as Parameters<
|
||||
ContextEngine["assemble"]
|
||||
>[0];
|
||||
expect(assembleParams.messages.map((message) => message.role)).toEqual([
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
abortAgentHarnessRun,
|
||||
embeddedAgentLog,
|
||||
onAgentEvent,
|
||||
type AgentEventPayload,
|
||||
type EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { SessionManager } from "openclaw/plugin-sdk/agent-sessions";
|
||||
@@ -12,15 +9,10 @@ import {
|
||||
onInternalDiagnosticEvent,
|
||||
waitForDiagnosticEventsDrained,
|
||||
type DiagnosticEventPayload,
|
||||
type DiagnosticEventPrivateData,
|
||||
} from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import { initializeGlobalHookRunner, registerInternalHook } from "openclaw/plugin-sdk/hook-runtime";
|
||||
import { registerPluginCommand } from "openclaw/plugin-sdk/plugin-runtime";
|
||||
import {
|
||||
createMockPluginRegistry,
|
||||
onTrustedInternalDiagnosticEvent,
|
||||
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { registerSandboxBackend } from "openclaw/plugin-sdk/sandbox";
|
||||
import { createMockPluginRegistry } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import WebSocket from "ws";
|
||||
import { CODEX_GPT5_BEHAVIOR_CONTRACT } from "../../prompt-overlay.js";
|
||||
@@ -32,7 +24,6 @@ import {
|
||||
getCodexWorkspaceMemoryToolNames,
|
||||
prependCodexOpenClawPromptContext,
|
||||
} from "./attempt-context.js";
|
||||
import * as authBridge from "./auth-bridge.js";
|
||||
import { resolveCodexAppServerEnvApiKeyCacheKey } from "./auth-bridge.js";
|
||||
import { CodexAppServerRpcError } from "./client.js";
|
||||
import { readCodexPluginConfig, resolveCodexAppServerRuntimeOptions } from "./config.js";
|
||||
@@ -49,10 +40,10 @@ import { buildCodexPluginAppCacheKey } from "./plugin-app-cache-key.js";
|
||||
import { buildCodexPluginThreadConfig } from "./plugin-thread-config.js";
|
||||
import type { CodexServerNotification } from "./protocol.js";
|
||||
import {
|
||||
createAppServerHarness,
|
||||
assistantMessage,
|
||||
createParams,
|
||||
createAppServerHarness,
|
||||
createCodexRuntimePlanFixture,
|
||||
createParams,
|
||||
createResumeHarness,
|
||||
createStartedThreadHarness,
|
||||
fastWait,
|
||||
@@ -1638,11 +1629,11 @@ describe("runCodexAppServerAttempt", () => {
|
||||
"assistant",
|
||||
"toolResult",
|
||||
]);
|
||||
const assistantMessage = result.messagesSnapshot[1];
|
||||
if (assistantMessage?.role !== "assistant") {
|
||||
const assistantMessageLocal = result.messagesSnapshot[1];
|
||||
if (assistantMessageLocal?.role !== "assistant") {
|
||||
throw new Error("expected mirrored assistant tool-call message");
|
||||
}
|
||||
expect(assistantMessage.content).toStrictEqual([
|
||||
expect(assistantMessageLocal.content).toStrictEqual([
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call-wiki-status-1",
|
||||
|
||||
@@ -1217,7 +1217,7 @@ export async function runCodexAppServerAttempt(
|
||||
},
|
||||
});
|
||||
|
||||
const releaseTurnAfterTerminalDynamicTool = (params: {
|
||||
const releaseTurnAfterTerminalDynamicTool = (paramsValue: {
|
||||
call: CodexDynamicToolCallParams;
|
||||
response: CodexDynamicToolCallResponse;
|
||||
durationMs: number;
|
||||
@@ -1226,7 +1226,7 @@ export async function runCodexAppServerAttempt(
|
||||
!shouldReleaseTurnAfterTerminalDynamicTool({
|
||||
completed,
|
||||
aborted: runAbortController.signal.aborted,
|
||||
responseSuccess: params.response.success,
|
||||
responseSuccess: paramsValue.response.success,
|
||||
currentTurnHadNonTerminalDynamicToolResult,
|
||||
activeAppServerTurnRequests,
|
||||
activeTurnItemIdsCount: activeTurnItemIds.size,
|
||||
@@ -1237,22 +1237,22 @@ export async function runCodexAppServerAttempt(
|
||||
}
|
||||
pendingTerminalDynamicToolRelease = undefined;
|
||||
trajectoryRecorder?.recordEvent("turn.dynamic_tool_terminal_release", {
|
||||
threadId: params.call.threadId,
|
||||
turnId: params.call.turnId,
|
||||
toolCallId: params.call.callId,
|
||||
name: params.call.tool,
|
||||
durationMs: params.durationMs,
|
||||
threadId: paramsValue.call.threadId,
|
||||
turnId: paramsValue.call.turnId,
|
||||
toolCallId: paramsValue.call.callId,
|
||||
name: paramsValue.call.tool,
|
||||
durationMs: paramsValue.durationMs,
|
||||
});
|
||||
embeddedAgentLog.info("codex app-server turn released after terminal dynamic tool result", {
|
||||
threadId: params.call.threadId,
|
||||
turnId: params.call.turnId,
|
||||
toolCallId: params.call.callId,
|
||||
tool: params.call.tool,
|
||||
durationMs: params.durationMs,
|
||||
threadId: paramsValue.call.threadId,
|
||||
turnId: paramsValue.call.turnId,
|
||||
toolCallId: paramsValue.call.callId,
|
||||
tool: paramsValue.call.tool,
|
||||
durationMs: paramsValue.durationMs,
|
||||
});
|
||||
interruptCodexTurnBestEffort(client, {
|
||||
threadId: params.call.threadId,
|
||||
turnId: params.call.turnId,
|
||||
threadId: paramsValue.call.threadId,
|
||||
turnId: paramsValue.call.turnId,
|
||||
timeoutMs: CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS,
|
||||
});
|
||||
completed = true;
|
||||
@@ -1290,12 +1290,12 @@ export async function runCodexAppServerAttempt(
|
||||
immediate.unref?.();
|
||||
};
|
||||
|
||||
const scheduleTurnReleaseAfterTerminalDynamicTool = (params: {
|
||||
const scheduleTurnReleaseAfterTerminalDynamicTool = (paramsLocal: {
|
||||
call: CodexDynamicToolCallParams;
|
||||
response: CodexDynamicToolCallResponse;
|
||||
durationMs: number;
|
||||
}) => {
|
||||
pendingTerminalDynamicToolRelease = params;
|
||||
pendingTerminalDynamicToolRelease = paramsLocal;
|
||||
scheduleTerminalDynamicToolReleaseCheck();
|
||||
};
|
||||
|
||||
@@ -2090,8 +2090,8 @@ export async function runCodexAppServerAttempt(
|
||||
steeringQueueRef.current = activeSteeringQueue;
|
||||
const handle = {
|
||||
kind: "embedded" as const,
|
||||
queueMessage: async (text: string, options?: CodexSteeringQueueOptions) =>
|
||||
activeSteeringQueue.queue(text, options),
|
||||
queueMessage: async (text: string, optionsLocal?: CodexSteeringQueueOptions) =>
|
||||
activeSteeringQueue.queue(text, optionsLocal),
|
||||
isStreaming: () => !completed,
|
||||
isCompacting: () => projectorRef.current?.isCompacting() ?? false,
|
||||
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
||||
@@ -2280,7 +2280,8 @@ export async function runCodexAppServerAttempt(
|
||||
});
|
||||
}
|
||||
if (activeContextEngine) {
|
||||
const activeContextEnginePluginId = resolveContextEngineOwnerPluginId(activeContextEngine);
|
||||
const activeContextEnginePluginIdLocal =
|
||||
resolveContextEngineOwnerPluginId(activeContextEngine);
|
||||
const finalMessages =
|
||||
(await readMirroredSessionHistoryMessages(activeSessionFile)) ??
|
||||
historyMessages.concat(result.messagesSnapshot);
|
||||
@@ -2301,7 +2302,7 @@ export async function runCodexAppServerAttempt(
|
||||
cwd: effectiveCwd,
|
||||
agentDir,
|
||||
activeAgentId: sessionAgentId,
|
||||
contextEnginePluginId: activeContextEnginePluginId,
|
||||
contextEnginePluginId: activeContextEnginePluginIdLocal,
|
||||
tokenBudget: params.contextTokenBudget,
|
||||
lastCallUsage: result.attemptUsage,
|
||||
promptCache: result.promptCache,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventEmitter } from "node:events";
|
||||
import { PassThrough, Writable } from "node:stream";
|
||||
import type { Api, Model } from "openclaw/plugin-sdk/llm";
|
||||
import type { Model } from "openclaw/plugin-sdk/llm";
|
||||
import { vi } from "vitest";
|
||||
import { CodexAppServerClient } from "./client.js";
|
||||
|
||||
|
||||
@@ -335,8 +335,8 @@ function sanitizeValue(value: unknown, depth = 0, key = ""): unknown {
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
const next: Record<string, unknown> = {};
|
||||
for (const [key, child] of Object.entries(value).slice(0, 100)) {
|
||||
next[key] = sanitizeValue(child, depth + 1, key);
|
||||
for (const [keyLocal, child] of Object.entries(value).slice(0, 100)) {
|
||||
next[keyLocal] = sanitizeValue(child, depth + 1, keyLocal);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -218,11 +218,6 @@ function mockCall(mockFn: ReturnType<typeof vi.fn>, callIndex = 0): ReadonlyArra
|
||||
function mockArg(mockFn: ReturnType<typeof vi.fn>, callIndex: number, argIndex: number) {
|
||||
return mockCall(mockFn, callIndex)[argIndex];
|
||||
}
|
||||
|
||||
function requireRequestParams(call: unknown[] | undefined): Record<string, unknown> {
|
||||
return requireRecord(call?.[2], "expected request params object");
|
||||
}
|
||||
|
||||
function requestParams(mockFn: ReturnType<typeof vi.fn>, callIndex = 0): Record<string, unknown> {
|
||||
return requireRecord(mockArg(mockFn, callIndex, 2), "expected request params object");
|
||||
}
|
||||
@@ -347,8 +342,8 @@ describe("codex command", () => {
|
||||
const requests: Array<{ method: string; params: unknown }> = [];
|
||||
const deps = createDeps({
|
||||
codexControlRequest: vi.fn(
|
||||
async (_pluginConfig: unknown, method: string, requestParams: unknown) => {
|
||||
requests.push({ method, params: requestParams });
|
||||
async (_pluginConfig: unknown, method: string, requestParamsValue: unknown) => {
|
||||
requests.push({ method, params: requestParamsValue });
|
||||
return {
|
||||
thread: { id: "thread-123", cwd: "/repo" },
|
||||
model: "gpt-5.4",
|
||||
@@ -1757,7 +1752,8 @@ describe("codex command", () => {
|
||||
|
||||
it("respects openai-alias explicit order over stale lastGood for API key profiles", async () => {
|
||||
const config = {};
|
||||
const now = Date.now();
|
||||
const ignoredNow = Date.now();
|
||||
void ignoredNow;
|
||||
installAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
@@ -2384,12 +2380,14 @@ describe("codex command", () => {
|
||||
`${secondSessionFile}.codex-app-server.json`,
|
||||
JSON.stringify({ schemaVersion: 1, threadId: "thread-222", cwd: "/repo" }),
|
||||
);
|
||||
const safeCodexControlRequest = vi.fn(async (configForTest, _method, requestParams) => ({
|
||||
const safeCodexControlRequest = vi.fn(async (configForTest, _method, requestParamsLocal) => ({
|
||||
ok: true as const,
|
||||
value: {
|
||||
threadId:
|
||||
requestParams && typeof requestParams === "object" && "threadId" in requestParams
|
||||
? requestParams.threadId
|
||||
requestParamsLocal &&
|
||||
typeof requestParamsLocal === "object" &&
|
||||
"threadId" in requestParamsLocal
|
||||
? requestParamsLocal.threadId
|
||||
: undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -280,7 +280,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
|
||||
await expect(firstDispose).resolves.toBeUndefined();
|
||||
await expect(secondDispose).resolves.toBeUndefined();
|
||||
expect(pool.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(pool["dispose"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("dispose waits for in-flight runAttempt before disposing", async () => {
|
||||
@@ -301,14 +301,14 @@ describe("createCopilotAgentHarness", () => {
|
||||
|
||||
await flushAsyncWork();
|
||||
|
||||
expect(pool.dispose).not.toHaveBeenCalled();
|
||||
expect(pool["dispose"]).not.toHaveBeenCalled();
|
||||
expect(disposeSettled).toBe(false);
|
||||
|
||||
deferred.resolve(ATTEMPT_RESULT);
|
||||
|
||||
await expect(attemptPromise).resolves.toBe(ATTEMPT_RESULT);
|
||||
await expect(disposePromise).resolves.toBeUndefined();
|
||||
expect(pool.dispose).toHaveBeenCalledTimes(1);
|
||||
expect(pool["dispose"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("runAttempt after dispose rejects without creating a new pool", async () => {
|
||||
@@ -348,7 +348,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
await harness.runAttempt(ATTEMPT_PARAMS);
|
||||
await expect(harness.dispose?.()).resolves.toBeUndefined();
|
||||
|
||||
expect(pool.dispose).not.toHaveBeenCalled();
|
||||
expect(pool["dispose"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses options.pool when supplied", async () => {
|
||||
@@ -1170,7 +1170,7 @@ describe("createCopilotAgentHarness", () => {
|
||||
describe("runSideQuestion", () => {
|
||||
it("is not implemented; /btw falls through to the in-tree PI fallback path", () => {
|
||||
const harness = createCopilotAgentHarness({ pool: makePoolMock() });
|
||||
expect(harness.runSideQuestion).toBeUndefined();
|
||||
expect(harness["runSideQuestion"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -560,8 +560,7 @@ describe("runCopilotAttempt", () => {
|
||||
expect(sdk.resumeSession).toHaveBeenCalledTimes(1);
|
||||
expect(sdk.resumeSession.mock.calls[0]?.[0]).toBe("resume-1");
|
||||
expect(
|
||||
(sdk.resumeSession.mock.calls[0]?.[1] as { continuePendingWork?: boolean })
|
||||
.continuePendingWork,
|
||||
(sdk.resumeSession.mock.calls[0][1] as { continuePendingWork?: boolean }).continuePendingWork,
|
||||
).toBe(false);
|
||||
expect(sdk.createSession).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@@ -754,7 +753,7 @@ describe("runCopilotAttempt", () => {
|
||||
expect(result.aborted).toBe(true);
|
||||
expect(result.externalAbort).toBe(true);
|
||||
expect(sdk.createSession).toHaveBeenCalledTimes(0);
|
||||
expect(pool.acquire).toHaveBeenCalledTimes(0);
|
||||
expect(pool["acquire"]).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("abort path (signal fires after settled)", async () => {
|
||||
@@ -810,7 +809,7 @@ describe("runCopilotAttempt", () => {
|
||||
expect(bridgeCall.attemptParams).toBeDefined();
|
||||
expect(bridgeCall.sessionRef).toBeDefined();
|
||||
expect(
|
||||
((sdk.createSession.mock.calls[0] as unknown[] | undefined)?.[0] as { tools?: unknown[] })
|
||||
((sdk.createSession.mock.calls[0] as unknown[] | undefined)![0] as { tools?: unknown[] })
|
||||
.tools,
|
||||
).toBe(sdkTools);
|
||||
});
|
||||
@@ -930,8 +929,8 @@ describe("runCopilotAttempt", () => {
|
||||
"[copilot-attempt] tool-bridge construction failed: bridge failed",
|
||||
);
|
||||
expect(sdk.createSession).toHaveBeenCalledTimes(0);
|
||||
expect(pool.acquire).toHaveBeenCalledTimes(0);
|
||||
expect(pool.release).toHaveBeenCalledTimes(0);
|
||||
expect(pool["acquire"]).toHaveBeenCalledTimes(0);
|
||||
expect(pool["release"]).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("unsupported providers skip injected tool bridge wiring", async () => {
|
||||
@@ -958,7 +957,7 @@ describe("runCopilotAttempt", () => {
|
||||
await runCopilotAttempt(makeParams(), { pool });
|
||||
|
||||
const handler = (
|
||||
(sdk.createSession.mock.calls[0] as unknown[] | undefined)?.[0] as {
|
||||
(sdk.createSession.mock.calls[0] as unknown[] | undefined)![0] as {
|
||||
onPermissionRequest: (
|
||||
request: { kind: string },
|
||||
invocation: { sessionId: string },
|
||||
@@ -1371,8 +1370,8 @@ describe("runCopilotAttempt", () => {
|
||||
|
||||
expect(getPromptErrorCode(result)).toBe("model_not_supported");
|
||||
expect(sdk.createSession).toHaveBeenCalledTimes(0);
|
||||
expect(pool.acquire).toHaveBeenCalledTimes(0);
|
||||
expect(pool.release).toHaveBeenCalledTimes(0);
|
||||
expect(pool["acquire"]).toHaveBeenCalledTimes(0);
|
||||
expect(pool["release"]).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("acquire failure", async () => {
|
||||
@@ -1387,7 +1386,7 @@ describe("runCopilotAttempt", () => {
|
||||
|
||||
expect(result.promptError).toBe(error);
|
||||
expect(sdk.createSession).toHaveBeenCalledTimes(0);
|
||||
expect(pool.release).toHaveBeenCalledTimes(0);
|
||||
expect(pool["release"]).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("release failure after a successful send rejects the attempt", async () => {
|
||||
@@ -1449,7 +1448,7 @@ describe("runCopilotAttempt", () => {
|
||||
const session = sdk.sessions[0];
|
||||
expect(session.off).toHaveBeenCalledTimes(session.on.mock.calls.length);
|
||||
expect(session.disconnect).toHaveBeenCalledTimes(1);
|
||||
expect(pool.release).toHaveBeenCalledTimes(1);
|
||||
expect(pool["release"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("cleanup on send error", async () => {
|
||||
@@ -1467,7 +1466,7 @@ describe("runCopilotAttempt", () => {
|
||||
expect(result.promptError).toBe(error);
|
||||
expect(session.off).toHaveBeenCalledTimes(session.on.mock.calls.length);
|
||||
expect(session.disconnect).toHaveBeenCalledTimes(1);
|
||||
expect(pool.release).toHaveBeenCalledTimes(1);
|
||||
expect(pool["release"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("cleanup on disconnect throw", async () => {
|
||||
@@ -1504,10 +1503,10 @@ describe("runCopilotAttempt", () => {
|
||||
{ pool },
|
||||
);
|
||||
|
||||
const key = (vi.mocked(pool.acquire).mock.calls[0] as unknown[] | undefined)?.[0] as {
|
||||
const key = (vi.mocked(pool["acquire"]).mock.calls[0] as unknown[] | undefined)?.[0] as {
|
||||
authMode: string;
|
||||
};
|
||||
const options = (vi.mocked(pool.acquire).mock.calls[0] as unknown[] | undefined)?.[1] as {
|
||||
const options = (vi.mocked(pool["acquire"]).mock.calls[0] as unknown[] | undefined)?.[1] as {
|
||||
gitHubToken?: string;
|
||||
useLoggedInUser?: boolean;
|
||||
};
|
||||
@@ -1525,7 +1524,7 @@ describe("runCopilotAttempt", () => {
|
||||
).rejects.toThrow(
|
||||
"[copilot-attempt] gitHubToken auth requires profileId+profileVersion (pool keying safety; per Q5/Q1 decisions)",
|
||||
);
|
||||
expect(pool.acquire).toHaveBeenCalledTimes(0);
|
||||
expect(pool["acquire"]).toHaveBeenCalledTimes(0);
|
||||
expect(sdk.createSession).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@@ -1540,12 +1539,12 @@ describe("runCopilotAttempt", () => {
|
||||
{ pool },
|
||||
);
|
||||
|
||||
const key = (vi.mocked(pool.acquire).mock.calls[0] as unknown[] | undefined)?.[0] as {
|
||||
const key = (vi.mocked(pool["acquire"]).mock.calls[0] as unknown[] | undefined)?.[0] as {
|
||||
authMode: string;
|
||||
authProfileId?: string;
|
||||
authProfileVersion?: string;
|
||||
};
|
||||
const options = (vi.mocked(pool.acquire).mock.calls[0] as unknown[] | undefined)?.[1] as {
|
||||
const options = (vi.mocked(pool["acquire"]).mock.calls[0] as unknown[] | undefined)?.[1] as {
|
||||
gitHubToken?: string;
|
||||
useLoggedInUser?: boolean;
|
||||
};
|
||||
@@ -1926,12 +1925,12 @@ describe("runCopilotAttempt", () => {
|
||||
|
||||
const calls = dualWriteMock.dualWriteCopilotTranscriptBestEffort.mock.calls;
|
||||
const id1 = (
|
||||
calls[0]?.[0] as {
|
||||
calls[0][0] as {
|
||||
messages: Array<{ role: string; __openclaw?: { mirrorIdentity?: string } }>;
|
||||
}
|
||||
).messages.find((m) => m.role === "user")?.["__openclaw"]?.mirrorIdentity;
|
||||
const id2 = (
|
||||
calls[1]?.[0] as {
|
||||
calls[1][0] as {
|
||||
messages: Array<{ role: string; __openclaw?: { mirrorIdentity?: string } }>;
|
||||
}
|
||||
).messages.find((m) => m.role === "user")?.["__openclaw"]?.mirrorIdentity;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { resolve, join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { join, resolve } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
COPILOT_DEFAULT_AGENT_ID,
|
||||
COPILOT_TOKEN_PROFILE_ERROR,
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("createHooksBridge", () => {
|
||||
hookName: "onPostToolUse",
|
||||
error: expect.any(Error),
|
||||
});
|
||||
expect((onHookError.mock.calls[0]?.[0]?.error as Error).message).toBe("post boom");
|
||||
expect((onHookError.mock.calls[0][0]!.error as Error).message).toBe("post boom");
|
||||
});
|
||||
|
||||
it("isolates async rejections: returns undefined and notifies onHookError", async () => {
|
||||
|
||||
@@ -164,7 +164,7 @@ describe("createTraceContextProvider", () => {
|
||||
});
|
||||
await expect(provider()).resolves.toEqual({});
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect((onError.mock.calls[0]?.[0] as CopilotTraceContextErrorInfo).part).toBe("traceparent");
|
||||
expect((onError.mock.calls[0][0] as CopilotTraceContextErrorInfo).part).toBe("traceparent");
|
||||
});
|
||||
|
||||
it("getTracestate failure → partial success (traceparent kept) + notifier called", async () => {
|
||||
@@ -180,7 +180,7 @@ describe("createTraceContextProvider", () => {
|
||||
traceparent: "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01",
|
||||
});
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect((onError.mock.calls[0]?.[0] as CopilotTraceContextErrorInfo).part).toBe("tracestate");
|
||||
expect((onError.mock.calls[0][0] as CopilotTraceContextErrorInfo).part).toBe("tracestate");
|
||||
});
|
||||
|
||||
it("default notifier uses console.warn", async () => {
|
||||
|
||||
@@ -484,7 +484,7 @@ describe("deepseek provider plugin", () => {
|
||||
|
||||
expect(readThinking(capture.payload)?.type).toBe("disabled");
|
||||
expect(capture.payload).not.toHaveProperty("reasoning_effort");
|
||||
expect((capture.payload?.messages as Array<Record<string, unknown>>)[1]).not.toHaveProperty(
|
||||
expect((capture.payload!.messages as Array<Record<string, unknown>>)[1]).not.toHaveProperty(
|
||||
"reasoning_content",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -176,15 +176,15 @@ export class PlaywrightDiffScreenshotter implements DiffScreenshotter {
|
||||
await page.evaluate(() => {
|
||||
const html = document.documentElement;
|
||||
const body = document.body;
|
||||
const frame = document.querySelector(".oc-frame");
|
||||
const frameLocal = document.querySelector(".oc-frame");
|
||||
|
||||
html.style.background = "transparent";
|
||||
body.style.margin = "0";
|
||||
body.style.padding = "0";
|
||||
body.style.background = "transparent";
|
||||
body.style.setProperty("-webkit-print-color-adjust", "exact");
|
||||
if (frame instanceof HTMLElement) {
|
||||
frame.style.margin = "0";
|
||||
if (frameLocal instanceof HTMLElement) {
|
||||
frameLocal.style.margin = "0";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -67,8 +67,8 @@ describe("diffs tool rendered output guards", () => {
|
||||
mode: "file",
|
||||
});
|
||||
|
||||
expect(screenshotter.screenshotHtml).toHaveBeenCalledTimes(1);
|
||||
expect((result?.details as Record<string, unknown>).filePath).toMatch(/preview\.png$/);
|
||||
expect(screenshotter["screenshotHtml"]).toHaveBeenCalledTimes(1);
|
||||
expect((result.details as Record<string, unknown>).filePath).toMatch(/preview\.png$/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ describe("diffs tool", () => {
|
||||
expect(readTextContent(result, 0)).toContain(
|
||||
"https://example.com/openclaw/plugins/diffs/view/",
|
||||
);
|
||||
expect(String((result?.details as Record<string, unknown>).viewerUrl)).toContain(
|
||||
expect(String((result.details as Record<string, unknown>).viewerUrl)).toContain(
|
||||
"https://example.com/openclaw/plugins/diffs/view/",
|
||||
);
|
||||
});
|
||||
@@ -89,7 +89,7 @@ describe("diffs tool", () => {
|
||||
expect(readTextContent(result, 0)).toContain(
|
||||
"https://preview.example.com/review/plugins/diffs/view/",
|
||||
);
|
||||
expect(String((result?.details as Record<string, unknown>).viewerUrl)).toContain(
|
||||
expect(String((result.details as Record<string, unknown>).viewerUrl)).toContain(
|
||||
"https://preview.example.com/review/plugins/diffs/view/",
|
||||
);
|
||||
});
|
||||
@@ -127,7 +127,7 @@ describe("diffs tool", () => {
|
||||
mode: "image",
|
||||
});
|
||||
|
||||
expect(screenshotter.screenshotHtml).toHaveBeenCalledTimes(1);
|
||||
expect(screenshotter["screenshotHtml"]).toHaveBeenCalledTimes(1);
|
||||
expect(readTextContent(result, 0)).toContain("Diff PNG generated at:");
|
||||
expect(readTextContent(result, 0)).toContain("Use the `message` tool");
|
||||
expect(result?.content).toHaveLength(1);
|
||||
@@ -166,10 +166,10 @@ describe("diffs tool", () => {
|
||||
fileFormat: "pdf",
|
||||
});
|
||||
|
||||
expect(screenshotter.screenshotHtml).toHaveBeenCalledTimes(1);
|
||||
expect(screenshotter["screenshotHtml"]).toHaveBeenCalledTimes(1);
|
||||
expect(readTextContent(result, 0)).toContain("Diff PDF generated at:");
|
||||
expect((result?.details as Record<string, unknown>).format).toBe("pdf");
|
||||
expect((result?.details as Record<string, unknown>).filePath).toMatch(/preview\.pdf$/);
|
||||
expect((result.details as Record<string, unknown>).format).toBe("pdf");
|
||||
expect((result.details as Record<string, unknown>).filePath).toMatch(/preview\.pdf$/);
|
||||
});
|
||||
|
||||
it("accepts mode=file as an alias for file artifact rendering", async () => {
|
||||
@@ -258,7 +258,7 @@ describe("diffs tool", () => {
|
||||
after: "two\n",
|
||||
mode: "file",
|
||||
});
|
||||
const filePath = (result?.details as Record<string, unknown>).filePath as string;
|
||||
const filePath = (result.details as Record<string, unknown>).filePath as string;
|
||||
const stat = await fs.stat(filePath);
|
||||
expect(stat.isFile()).toBe(true);
|
||||
|
||||
@@ -290,9 +290,9 @@ describe("diffs tool", () => {
|
||||
imageMaxWidth: "1100",
|
||||
});
|
||||
|
||||
expect((result?.details as Record<string, unknown>).fileQuality).toBe("hq");
|
||||
expect((result?.details as Record<string, unknown>).fileScale).toBe(2.4);
|
||||
expect((result?.details as Record<string, unknown>).fileMaxWidth).toBe(1100);
|
||||
expect((result.details as Record<string, unknown>).fileQuality).toBe("hq");
|
||||
expect((result.details as Record<string, unknown>).fileScale).toBe(2.4);
|
||||
expect((result.details as Record<string, unknown>).fileMaxWidth).toBe(1100);
|
||||
});
|
||||
|
||||
it("accepts deprecated format alias for fileFormat", async () => {
|
||||
@@ -312,8 +312,8 @@ describe("diffs tool", () => {
|
||||
format: "pdf",
|
||||
});
|
||||
|
||||
expect((result?.details as Record<string, unknown>).fileFormat).toBe("pdf");
|
||||
expect((result?.details as Record<string, unknown>).filePath).toMatch(/preview\.pdf$/);
|
||||
expect((result.details as Record<string, unknown>).fileFormat).toBe("pdf");
|
||||
expect((result.details as Record<string, unknown>).filePath).toMatch(/preview\.pdf$/);
|
||||
});
|
||||
|
||||
it("honors defaults.mode=file when mode is omitted", async () => {
|
||||
@@ -351,8 +351,8 @@ describe("diffs tool", () => {
|
||||
|
||||
expect(result?.content).toHaveLength(1);
|
||||
expect(readTextContent(result, 0)).toContain("File rendering failed");
|
||||
expect((result?.details as Record<string, unknown>).fileError).toBe("browser missing");
|
||||
expect((result?.details as Record<string, unknown>).imageError).toBe("browser missing");
|
||||
expect((result.details as Record<string, unknown>).fileError).toBe("browser missing");
|
||||
expect((result.details as Record<string, unknown>).imageError).toBe("browser missing");
|
||||
});
|
||||
|
||||
it("rejects invalid base URLs as tool input errors", async () => {
|
||||
@@ -433,15 +433,15 @@ describe("diffs tool", () => {
|
||||
});
|
||||
|
||||
expect(readTextContent(result, 0)).toContain("Diff viewer ready.");
|
||||
expect((result?.details as Record<string, unknown>).mode).toBe("view");
|
||||
expect((result?.details as Record<string, unknown>).context).toEqual({
|
||||
expect((result.details as Record<string, unknown>).mode).toBe("view");
|
||||
expect((result.details as Record<string, unknown>).context).toEqual({
|
||||
agentId: "main",
|
||||
sessionId: "session-123",
|
||||
messageChannel: "discord",
|
||||
agentAccountId: "default",
|
||||
});
|
||||
|
||||
const viewerPath = String((result?.details as Record<string, unknown>).viewerPath);
|
||||
const viewerPath = String((result.details as Record<string, unknown>).viewerPath);
|
||||
const id = extractViewerArtifactId(viewerPath);
|
||||
const html = await store.readHtml(id);
|
||||
expect(html).toContain('body data-theme="light"');
|
||||
@@ -482,13 +482,13 @@ describe("diffs tool", () => {
|
||||
fileMaxWidth: 1320,
|
||||
});
|
||||
|
||||
expect((result?.details as Record<string, unknown>).mode).toBe("both");
|
||||
expect(screenshotter.screenshotHtml).toHaveBeenCalledTimes(1);
|
||||
expect((result?.details as Record<string, unknown>).format).toBe("png");
|
||||
expect((result?.details as Record<string, unknown>).fileQuality).toBe("print");
|
||||
expect((result?.details as Record<string, unknown>).fileScale).toBe(2.75);
|
||||
expect((result?.details as Record<string, unknown>).fileMaxWidth).toBe(1320);
|
||||
const viewerPath = String((result?.details as Record<string, unknown>).viewerPath);
|
||||
expect((result.details as Record<string, unknown>).mode).toBe("both");
|
||||
expect(screenshotter["screenshotHtml"]).toHaveBeenCalledTimes(1);
|
||||
expect((result.details as Record<string, unknown>).format).toBe("png");
|
||||
expect((result.details as Record<string, unknown>).fileQuality).toBe("print");
|
||||
expect((result.details as Record<string, unknown>).fileScale).toBe(2.75);
|
||||
expect((result.details as Record<string, unknown>).fileMaxWidth).toBe(1320);
|
||||
const viewerPath = String((result.details as Record<string, unknown>).viewerPath);
|
||||
const id = extractViewerArtifactId(viewerPath);
|
||||
const html = await store.readHtml(id);
|
||||
expect(html).toContain('body data-theme="dark"');
|
||||
@@ -509,7 +509,7 @@ describe("diffs tool", () => {
|
||||
mode: "file",
|
||||
});
|
||||
|
||||
expect((result?.details as Record<string, unknown>).context).toEqual({
|
||||
expect((result.details as Record<string, unknown>).context).toEqual({
|
||||
agentId: "reviewer",
|
||||
sessionId: "session-456",
|
||||
messageChannel: "telegram",
|
||||
@@ -559,9 +559,9 @@ function expectArtifactOnlyFileResult(
|
||||
screenshotter: DiffScreenshotter,
|
||||
result: { details?: unknown } | null | undefined,
|
||||
) {
|
||||
expect(screenshotter.screenshotHtml).toHaveBeenCalledTimes(1);
|
||||
expect((result?.details as Record<string, unknown>).mode).toBe("file");
|
||||
expect((result?.details as Record<string, unknown>).viewerUrl).toBeUndefined();
|
||||
expect(screenshotter["screenshotHtml"]).toHaveBeenCalledTimes(1);
|
||||
expect((result!.details as Record<string, unknown>).mode).toBe("file");
|
||||
expect((result!.details as Record<string, unknown>).viewerUrl).toBeUndefined();
|
||||
}
|
||||
|
||||
function createPngScreenshotter(
|
||||
|
||||
@@ -355,7 +355,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
|
||||
message: "deleteDays must be an integer from 0 to 7",
|
||||
}),
|
||||
});
|
||||
const senderUserId = normalizeOptionalString(ctx.requesterSenderId);
|
||||
const senderUserIdLocal = normalizeOptionalString(ctx.requesterSenderId);
|
||||
return await handleDiscordAction(
|
||||
{
|
||||
action: moderation.action,
|
||||
@@ -366,7 +366,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
|
||||
until: moderation.until,
|
||||
reason: moderation.reason,
|
||||
deleteMessageDays: moderation.deleteMessageDays,
|
||||
senderUserId,
|
||||
senderUserId: senderUserIdLocal,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
|
||||
@@ -548,8 +548,8 @@ describe("discordPlugin outbound", () => {
|
||||
}) => void)
|
||||
| undefined;
|
||||
probeDiscordMock.mockReturnValue(
|
||||
new Promise((resolve) => {
|
||||
resolveProbe = resolve;
|
||||
new Promise((resolveLocal) => {
|
||||
resolveProbe = resolveLocal;
|
||||
}),
|
||||
);
|
||||
monitorDiscordProviderMock.mockResolvedValue(undefined);
|
||||
|
||||
@@ -394,10 +394,10 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
||||
token: account.token,
|
||||
inputs,
|
||||
missingTokenNote: "missing Discord token",
|
||||
resolveWithToken: async ({ token, inputs }) =>
|
||||
resolveWithToken: async ({ token, inputs: inputsValue }) =>
|
||||
(await loadDiscordResolveChannelsModule()).resolveDiscordChannelAllowlist({
|
||||
token,
|
||||
entries: inputs,
|
||||
entries: inputsValue,
|
||||
}),
|
||||
mapResolved: (entry) => ({
|
||||
input: entry.input,
|
||||
@@ -415,10 +415,10 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount, DiscordProbe>
|
||||
token: account.token,
|
||||
inputs,
|
||||
missingTokenNote: "missing Discord token",
|
||||
resolveWithToken: async ({ token, inputs }) =>
|
||||
resolveWithToken: async ({ token, inputs: inputsLocal }) =>
|
||||
(await loadDiscordResolveUsersModule()).resolveDiscordUserAllowlist({
|
||||
token,
|
||||
entries: inputs,
|
||||
entries: inputsLocal,
|
||||
}),
|
||||
mapResolved: (entry) => ({
|
||||
input: entry.input,
|
||||
|
||||
@@ -390,7 +390,7 @@ export function buildDiscordComponentMessage(params: {
|
||||
const lastChild = containerChildren.at(-1);
|
||||
if (lastChild instanceof Row) {
|
||||
const row = lastChild;
|
||||
const hasSelect = row.components.some((entry) => isSelectComponent(entry));
|
||||
const hasSelect = row.components.some((entryLocal) => isSelectComponent(entryLocal));
|
||||
if (row.components.length < 5 && !hasSelect) {
|
||||
row.addComponent(component as Button);
|
||||
} else {
|
||||
|
||||
@@ -238,10 +238,10 @@ const optionComparisonOmittedFields = new Set([
|
||||
]);
|
||||
const nullableLocalizationFields = new Set(["description_localizations", "name_localizations"]);
|
||||
|
||||
function stableComparableObject(value: unknown, path: string[] = []): unknown {
|
||||
function stableComparableObject(value: unknown, pathValue: string[] = []): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
const normalized = value.map((entry) => stableComparableObject(entry, path));
|
||||
const key = path.at(-1);
|
||||
const normalized = value.map((entry) => stableComparableObject(entry, pathValue));
|
||||
const key = pathValue.at(-1);
|
||||
if (
|
||||
key &&
|
||||
unorderedCommandArrayFields.has(key) &&
|
||||
@@ -266,7 +266,7 @@ function stableComparableObject(value: unknown, path: string[] = []): unknown {
|
||||
if (entry === null && nullableLocalizationFields.has(key)) {
|
||||
return false;
|
||||
}
|
||||
if (path.includes("options") && optionComparisonOmittedFields.has(key)) {
|
||||
if (pathValue.includes("options") && optionComparisonOmittedFields.has(key)) {
|
||||
return false;
|
||||
}
|
||||
if ((key === "required" || key === "autocomplete") && entry === false) {
|
||||
@@ -277,21 +277,21 @@ function stableComparableObject(value: unknown, path: string[] = []): unknown {
|
||||
.toSorted(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, entry]) => [
|
||||
key,
|
||||
shouldNormalizeDescriptionValue(path, key, entry)
|
||||
shouldNormalizeDescriptionValue(pathValue, key, entry)
|
||||
? normalizeDescriptionForComparison(entry)
|
||||
: stableComparableObject(entry, [...path, key]),
|
||||
: stableComparableObject(entry, [...pathValue, key]),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function shouldNormalizeDescriptionValue(
|
||||
path: string[],
|
||||
pathLocal: string[],
|
||||
key: string,
|
||||
entry: unknown,
|
||||
): entry is string {
|
||||
return (
|
||||
typeof entry === "string" &&
|
||||
(key === "description" || path.at(-1) === "description_localizations")
|
||||
(key === "description" || pathLocal.at(-1) === "description_localizations")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -303,7 +303,7 @@ describe("GatewayPlugin", () => {
|
||||
|
||||
it("preserves MESSAGE_CREATE author payloads for inbound dispatch", async () => {
|
||||
const gateway = new GatewayPlugin({ autoInteractions: false });
|
||||
const dispatchGatewayEvent = vi.fn(async (eventValue: string, dataValue: unknown) => {});
|
||||
const dispatchGatewayEvent = vi.fn(async (_eventValue: string, _dataValue: unknown) => {});
|
||||
(gateway as unknown as { client: unknown }).client = {
|
||||
dispatchGatewayEvent,
|
||||
};
|
||||
|
||||
@@ -717,7 +717,7 @@ describe("RequestClient", () => {
|
||||
it("dispatches multipart uploads with a multipart/form-data content type", async () => {
|
||||
const fetchSpy = vi.fn(async (_input: string | URL | Request, init?: RequestInit) => {
|
||||
expect(init?.headers).toBeInstanceOf(Headers);
|
||||
expect((init?.headers as Headers).get("Content-Type")).toMatch(
|
||||
expect((init!.headers as Headers).get("Content-Type")).toMatch(
|
||||
/^multipart\/form-data; boundary=/,
|
||||
);
|
||||
expect(init?.body).not.toBeInstanceOf(FormData);
|
||||
|
||||
@@ -45,11 +45,11 @@ async function ensureDmComponentAuthorized(params: {
|
||||
allowNameMatching: isDangerousNameMatchingEnabled(ctx.discordConfig),
|
||||
cfg: ctx.cfg,
|
||||
token: ctx.token,
|
||||
readStoreAllowFrom: async ({ accountId, dmPolicy }) =>
|
||||
readStoreAllowFrom: async ({ accountId, dmPolicy: dmPolicyLocal }) =>
|
||||
await readChannelIngressStoreAllowFromForDmPolicy({
|
||||
provider: "discord",
|
||||
accountId,
|
||||
dmPolicy,
|
||||
dmPolicy: dmPolicyLocal,
|
||||
}),
|
||||
eventKind: "button",
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ export function normalizeDiscordAllowList(raw: string[] | undefined, prefixes: s
|
||||
ids.add(maybeId);
|
||||
continue;
|
||||
}
|
||||
const prefix = prefixes.find((entry) => text.startsWith(entry));
|
||||
const prefix = prefixes.find((entryLocal) => text.startsWith(entryLocal));
|
||||
if (prefix) {
|
||||
const candidate = text.slice(prefix.length);
|
||||
if (candidate) {
|
||||
|
||||
@@ -92,7 +92,7 @@ describe("discord exec approval monitor helpers", () => {
|
||||
|
||||
await button.run(interaction, { id: "", action: "" });
|
||||
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
expect(interaction["reply"]).toHaveBeenCalledWith({
|
||||
content: "This approval is no longer valid.",
|
||||
ephemeral: true,
|
||||
});
|
||||
@@ -107,7 +107,7 @@ describe("discord exec approval monitor helpers", () => {
|
||||
|
||||
await button.run(interaction, { id: "abc", action: "allow-once" });
|
||||
|
||||
expect(interaction.reply).toHaveBeenCalledWith({
|
||||
expect(interaction["reply"]).toHaveBeenCalledWith({
|
||||
content: "⛔ You are not authorized to approve exec requests.",
|
||||
ephemeral: true,
|
||||
});
|
||||
@@ -123,9 +123,9 @@ describe("discord exec approval monitor helpers", () => {
|
||||
|
||||
await button.run(interaction, { id: "abc", action: "allow-once" });
|
||||
|
||||
expect(interaction.acknowledge).toHaveBeenCalled();
|
||||
expect(interaction["acknowledge"]).toHaveBeenCalled();
|
||||
expect(resolveApproval).toHaveBeenCalledWith("abc", "allow-once");
|
||||
expect(interaction.followUp).not.toHaveBeenCalled();
|
||||
expect(interaction["followUp"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows a follow-up when gateway resolution fails", async () => {
|
||||
@@ -137,7 +137,7 @@ describe("discord exec approval monitor helpers", () => {
|
||||
|
||||
await button.run(interaction, { id: "abc", action: "deny" });
|
||||
|
||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||
expect(interaction["followUp"]).toHaveBeenCalledWith({
|
||||
content:
|
||||
"Failed to submit approval decision for **Denied**. The request may have expired or already been resolved.",
|
||||
ephemeral: true,
|
||||
@@ -153,8 +153,8 @@ describe("discord exec approval monitor helpers", () => {
|
||||
|
||||
await button.run(interaction, { id: "abc", action: "allow-once" });
|
||||
|
||||
expect(interaction.acknowledge).toHaveBeenCalled();
|
||||
expect(interaction.followUp).toHaveBeenCalledWith({
|
||||
expect(interaction["acknowledge"]).toHaveBeenCalled();
|
||||
expect(interaction["followUp"]).toHaveBeenCalledWith({
|
||||
content:
|
||||
"That approval request is no longer pending. It may have expired or already been resolved.",
|
||||
ephemeral: true,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT } from "./gateway-handle.js";
|
||||
|
||||
const { GatewayIntents, GatewayPlugin } = vi.hoisted(() => {
|
||||
const GatewayIntents = {
|
||||
const GatewayIntentsLocal = {
|
||||
Guilds: 1 << 0,
|
||||
GuildMessages: 1 << 1,
|
||||
MessageContent: 1 << 2,
|
||||
@@ -31,7 +31,7 @@ const { GatewayIntents, GatewayPlugin } = vi.hoisted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
class GatewayPlugin {
|
||||
class GatewayPluginLocal {
|
||||
options: unknown;
|
||||
gatewayInfo: unknown;
|
||||
emitter = new TestEmitter();
|
||||
@@ -44,12 +44,12 @@ const { GatewayIntents, GatewayPlugin } = vi.hoisted(() => {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
async registerClient(clientForTest: unknown): Promise<void> {}
|
||||
async registerClient(_clientForTest: unknown): Promise<void> {}
|
||||
|
||||
connect(_resume = false): void {}
|
||||
}
|
||||
|
||||
return { GatewayIntents, GatewayPlugin };
|
||||
return { GatewayIntents: GatewayIntentsLocal, GatewayPlugin: GatewayPluginLocal };
|
||||
});
|
||||
|
||||
vi.mock("../internal/gateway.js", () => ({
|
||||
|
||||
@@ -92,7 +92,7 @@ export async function deliverDiscordInteractionReply(params: {
|
||||
components?: TopLevelComponents[],
|
||||
) => {
|
||||
const contentPayload = content ? { content } : {};
|
||||
const payload =
|
||||
const payloadLocal =
|
||||
files && files.length > 0
|
||||
? {
|
||||
...contentPayload,
|
||||
@@ -117,12 +117,12 @@ export async function deliverDiscordInteractionReply(params: {
|
||||
};
|
||||
await safeDiscordInteractionCall("interaction send", async () => {
|
||||
if (!preferFollowUp && !hasReplied) {
|
||||
await interaction.reply(payload);
|
||||
await interaction.reply(payloadLocal);
|
||||
hasReplied = true;
|
||||
firstMessageComponents = undefined;
|
||||
return;
|
||||
}
|
||||
await interaction.followUp(payload);
|
||||
await interaction.followUp(payloadLocal);
|
||||
hasReplied = true;
|
||||
firstMessageComponents = undefined;
|
||||
});
|
||||
|
||||
@@ -29,17 +29,17 @@ const {
|
||||
unregisterGatewayMock,
|
||||
waitForDiscordGatewayStopMock,
|
||||
} = vi.hoisted(() => {
|
||||
const stopGatewayLoggingMock = vi.fn();
|
||||
const getDiscordGatewayEmitterMock = vi.fn<() => EventEmitter | undefined>(() => undefined);
|
||||
const stopGatewayLoggingMockLocal = vi.fn();
|
||||
const getDiscordGatewayEmitterMockLocal = vi.fn<() => EventEmitter | undefined>(() => undefined);
|
||||
return {
|
||||
attachDiscordGatewayLoggingMock: vi.fn(() => stopGatewayLoggingMock),
|
||||
getDiscordGatewayEmitterMock,
|
||||
attachDiscordGatewayLoggingMock: vi.fn(() => stopGatewayLoggingMockLocal),
|
||||
getDiscordGatewayEmitterMock: getDiscordGatewayEmitterMockLocal,
|
||||
waitForDiscordGatewayStopMock: vi.fn((_params: WaitForDiscordGatewayStopParams) =>
|
||||
Promise.resolve(),
|
||||
),
|
||||
registerGatewayMock: vi.fn(),
|
||||
unregisterGatewayMock: vi.fn(),
|
||||
stopGatewayLoggingMock,
|
||||
stopGatewayLoggingMock: stopGatewayLoggingMockLocal,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -46,16 +46,16 @@ const {
|
||||
httpsAgentSpy,
|
||||
wsProxyAgentSpy,
|
||||
} = vi.hoisted(() => {
|
||||
const wsProxyAgentSpy = vi.fn();
|
||||
const httpsAgentSpy = vi.fn();
|
||||
const globalFetchMock = vi.fn();
|
||||
const baseRegisterClientSpy = vi.fn();
|
||||
const webSocketSpy = vi.fn();
|
||||
const captureHttpExchangeSpy = vi.fn();
|
||||
const captureWsEventSpy = vi.fn();
|
||||
const resolveDebugProxySettingsMock = vi.fn(() => ({ enabled: false }));
|
||||
const fetchWithSsrFGuardMock = vi.fn(async (params: { url: string; init?: RequestInit }) => {
|
||||
const source = (await globalFetchMock(params.url, params.init)) as Response;
|
||||
const wsProxyAgentSpyLocal = vi.fn();
|
||||
const httpsAgentSpyLocal = vi.fn();
|
||||
const globalFetchMockLocal = vi.fn();
|
||||
const baseRegisterClientSpyLocal = vi.fn();
|
||||
const webSocketSpyLocal = vi.fn();
|
||||
const captureHttpExchangeSpyLocal = vi.fn();
|
||||
const captureWsEventSpyLocal = vi.fn();
|
||||
const resolveDebugProxySettingsMockLocal = vi.fn(() => ({ enabled: false }));
|
||||
const fetchWithSsrFGuardMockLocal = vi.fn(async (params: { url: string; init?: RequestInit }) => {
|
||||
const source = (await globalFetchMockLocal(params.url, params.init)) as Response;
|
||||
const body = await source.text();
|
||||
return {
|
||||
response: new Response(body, {
|
||||
@@ -67,7 +67,7 @@ const {
|
||||
};
|
||||
});
|
||||
|
||||
const GatewayIntents = {
|
||||
const GatewayIntentsLocal = {
|
||||
Guilds: 1 << 0,
|
||||
GuildMessages: 1 << 1,
|
||||
MessageContent: 1 << 2,
|
||||
@@ -79,7 +79,7 @@ const {
|
||||
GuildVoiceStates: 1 << 8,
|
||||
} as const;
|
||||
|
||||
class GatewayPlugin {
|
||||
class GatewayPluginLocal {
|
||||
options: unknown;
|
||||
gatewayInfo: unknown;
|
||||
client: unknown;
|
||||
@@ -93,53 +93,53 @@ const {
|
||||
this.isConnecting = false;
|
||||
}
|
||||
async registerClient(client: unknown) {
|
||||
baseRegisterClientSpy(client);
|
||||
baseRegisterClientSpyLocal(client);
|
||||
}
|
||||
}
|
||||
|
||||
class HttpsAgent {
|
||||
static lastCreated: HttpsAgent | undefined;
|
||||
class HttpsAgentLocal {
|
||||
static lastCreated: HttpsAgentLocal | undefined;
|
||||
options: unknown;
|
||||
constructor(options?: unknown) {
|
||||
this.options = options;
|
||||
HttpsAgent.lastCreated = this;
|
||||
httpsAgentSpy(options);
|
||||
HttpsAgentLocal.lastCreated = this;
|
||||
httpsAgentSpyLocal(options);
|
||||
}
|
||||
}
|
||||
|
||||
class HttpsProxyAgent {
|
||||
static lastCreated: HttpsProxyAgent | undefined;
|
||||
class HttpsProxyAgentLocal {
|
||||
static lastCreated: HttpsProxyAgentLocal | undefined;
|
||||
proxyUrl: string;
|
||||
constructor(proxyUrl: string) {
|
||||
if (proxyUrl === "bad-proxy") {
|
||||
throw new Error("bad proxy");
|
||||
}
|
||||
this.proxyUrl = proxyUrl;
|
||||
HttpsProxyAgent.lastCreated = this;
|
||||
wsProxyAgentSpy(proxyUrl);
|
||||
HttpsProxyAgentLocal.lastCreated = this;
|
||||
wsProxyAgentSpyLocal(proxyUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
baseRegisterClientSpy,
|
||||
GatewayIntents,
|
||||
GatewayPlugin,
|
||||
globalFetchMock,
|
||||
HttpsAgent,
|
||||
HttpsProxyAgent,
|
||||
fetchWithSsrFGuardMock,
|
||||
getLastAgent: () => HttpsAgent.lastCreated,
|
||||
getLastProxyAgent: () => HttpsProxyAgent.lastCreated,
|
||||
captureHttpExchangeSpy,
|
||||
captureWsEventSpy,
|
||||
httpsAgentSpy,
|
||||
resolveDebugProxySettingsMock,
|
||||
baseRegisterClientSpy: baseRegisterClientSpyLocal,
|
||||
GatewayIntents: GatewayIntentsLocal,
|
||||
GatewayPlugin: GatewayPluginLocal,
|
||||
globalFetchMock: globalFetchMockLocal,
|
||||
HttpsAgent: HttpsAgentLocal,
|
||||
HttpsProxyAgent: HttpsProxyAgentLocal,
|
||||
fetchWithSsrFGuardMock: fetchWithSsrFGuardMockLocal,
|
||||
getLastAgent: () => HttpsAgentLocal.lastCreated,
|
||||
getLastProxyAgent: () => HttpsProxyAgentLocal.lastCreated,
|
||||
captureHttpExchangeSpy: captureHttpExchangeSpyLocal,
|
||||
captureWsEventSpy: captureWsEventSpyLocal,
|
||||
httpsAgentSpy: httpsAgentSpyLocal,
|
||||
resolveDebugProxySettingsMock: resolveDebugProxySettingsMockLocal,
|
||||
resetLastAgent: () => {
|
||||
HttpsAgent.lastCreated = undefined;
|
||||
HttpsProxyAgent.lastCreated = undefined;
|
||||
HttpsAgentLocal.lastCreated = undefined;
|
||||
HttpsProxyAgentLocal.lastCreated = undefined;
|
||||
},
|
||||
webSocketSpy,
|
||||
wsProxyAgentSpy,
|
||||
webSocketSpy: webSocketSpyLocal,
|
||||
wsProxyAgentSpy: wsProxyAgentSpyLocal,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
|
||||
|
||||
const { undiciFetchMock, agentSpy, envHttpProxyAgentSpy, proxyAgentSpy, createMockUndiciRuntime } =
|
||||
vi.hoisted(() => {
|
||||
const undiciFetchMock = vi.fn();
|
||||
const agentSpy = vi.fn();
|
||||
const envHttpProxyAgentSpy = vi.fn();
|
||||
const proxyAgentSpy = vi.fn();
|
||||
const createMockUndiciRuntime = () => {
|
||||
const undiciFetchMockLocal = vi.fn();
|
||||
const agentSpyLocal = vi.fn();
|
||||
const envHttpProxyAgentSpyLocal = vi.fn();
|
||||
const proxyAgentSpyLocal = vi.fn();
|
||||
const createMockUndiciRuntimeLocal = () => {
|
||||
class Agent {
|
||||
options: unknown;
|
||||
constructor(options?: unknown) {
|
||||
this.options = options;
|
||||
agentSpy(options);
|
||||
agentSpyLocal(options);
|
||||
}
|
||||
}
|
||||
class EnvHttpProxyAgent {
|
||||
@@ -31,7 +31,7 @@ const { undiciFetchMock, agentSpy, envHttpProxyAgentSpy, proxyAgentSpy, createMo
|
||||
}
|
||||
}
|
||||
this.options = options;
|
||||
envHttpProxyAgentSpy(options);
|
||||
envHttpProxyAgentSpyLocal(options);
|
||||
}
|
||||
}
|
||||
class ProxyAgent {
|
||||
@@ -44,22 +44,22 @@ const { undiciFetchMock, agentSpy, envHttpProxyAgentSpy, proxyAgentSpy, createMo
|
||||
}
|
||||
this.options = resolved;
|
||||
this.uri = resolved.uri;
|
||||
proxyAgentSpy(resolved);
|
||||
proxyAgentSpyLocal(resolved);
|
||||
}
|
||||
}
|
||||
return {
|
||||
Agent,
|
||||
EnvHttpProxyAgent,
|
||||
ProxyAgent,
|
||||
fetch: undiciFetchMock,
|
||||
fetch: undiciFetchMockLocal,
|
||||
};
|
||||
};
|
||||
return {
|
||||
undiciFetchMock,
|
||||
agentSpy,
|
||||
envHttpProxyAgentSpy,
|
||||
proxyAgentSpy,
|
||||
createMockUndiciRuntime,
|
||||
undiciFetchMock: undiciFetchMockLocal,
|
||||
agentSpy: agentSpyLocal,
|
||||
envHttpProxyAgentSpy: envHttpProxyAgentSpyLocal,
|
||||
proxyAgentSpy: proxyAgentSpyLocal,
|
||||
createMockUndiciRuntime: createMockUndiciRuntimeLocal,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -274,8 +274,8 @@ export function createThreadBindingManager(params: {
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
const existing = BINDINGS_BY_THREAD_ID.get(key);
|
||||
if (!existing || existing.accountId !== accountId) {
|
||||
const existingResult = BINDINGS_BY_THREAD_ID.get(key);
|
||||
if (!existingResult || existingResult.accountId !== accountId) {
|
||||
return null;
|
||||
}
|
||||
const now = Date.now();
|
||||
@@ -284,8 +284,8 @@ export function createThreadBindingManager(params: {
|
||||
? Math.max(0, Math.floor(touchParams.at))
|
||||
: now;
|
||||
const nextRecord: ThreadBindingRecord = {
|
||||
...existing,
|
||||
lastActivityAt: Math.max(existing.lastActivityAt || 0, at),
|
||||
...existingResult,
|
||||
lastActivityAt: Math.max(existingResult.lastActivityAt || 0, at),
|
||||
};
|
||||
setBindingRecord(nextRecord);
|
||||
if (touchParams.persist ?? persist) {
|
||||
@@ -342,7 +342,7 @@ export function createThreadBindingManager(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const existing = manager.getByThreadId(threadId);
|
||||
const existingValue = manager.getByThreadId(threadId);
|
||||
const targetSessionKey = normalizeOptionalString(bindParams.targetSessionKey) ?? "";
|
||||
if (!targetSessionKey) {
|
||||
return null;
|
||||
@@ -351,11 +351,11 @@ export function createThreadBindingManager(params: {
|
||||
const targetKind = normalizeTargetKind(bindParams.targetKind, targetSessionKey);
|
||||
let webhookId =
|
||||
normalizeOptionalString(bindParams.webhookId) ??
|
||||
normalizeOptionalString(existing?.webhookId) ??
|
||||
normalizeOptionalString(existingValue?.webhookId) ??
|
||||
"";
|
||||
let webhookToken =
|
||||
normalizeOptionalString(bindParams.webhookToken) ??
|
||||
normalizeOptionalString(existing?.webhookToken) ??
|
||||
normalizeOptionalString(existingValue?.webhookToken) ??
|
||||
"";
|
||||
if (!directConversationBinding && (!webhookId || !webhookToken)) {
|
||||
const cachedWebhook = findReusableWebhook({ accountId, channelId });
|
||||
@@ -382,26 +382,29 @@ export function createThreadBindingManager(params: {
|
||||
targetSessionKey,
|
||||
agentId:
|
||||
normalizeOptionalString(bindParams.agentId) ??
|
||||
normalizeOptionalString(existing?.agentId) ??
|
||||
normalizeOptionalString(existingValue?.agentId) ??
|
||||
resolveAgentIdFromSessionKey(targetSessionKey),
|
||||
label:
|
||||
normalizeOptionalString(bindParams.label) ?? normalizeOptionalString(existing?.label),
|
||||
normalizeOptionalString(bindParams.label) ??
|
||||
normalizeOptionalString(existingValue?.label),
|
||||
webhookId: webhookId || undefined,
|
||||
webhookToken: webhookToken || undefined,
|
||||
boundBy:
|
||||
normalizeOptionalString(bindParams.boundBy) ??
|
||||
normalizeOptionalString(existing?.boundBy) ??
|
||||
normalizeOptionalString(existingValue?.boundBy) ??
|
||||
"system",
|
||||
boundAt: now,
|
||||
lastActivityAt: now,
|
||||
idleTimeoutMs:
|
||||
typeof existing?.idleTimeoutMs === "number" ? existing.idleTimeoutMs : idleTimeoutMs,
|
||||
maxAgeMs: typeof existing?.maxAgeMs === "number" ? existing.maxAgeMs : maxAgeMs,
|
||||
typeof existingValue?.idleTimeoutMs === "number"
|
||||
? existingValue.idleTimeoutMs
|
||||
: idleTimeoutMs,
|
||||
maxAgeMs: typeof existingValue?.maxAgeMs === "number" ? existingValue.maxAgeMs : maxAgeMs,
|
||||
metadata:
|
||||
bindParams.metadata && typeof bindParams.metadata === "object"
|
||||
? { ...existing?.metadata, ...bindParams.metadata }
|
||||
: existing?.metadata
|
||||
? { ...existing.metadata }
|
||||
? { ...existingValue?.metadata, ...bindParams.metadata }
|
||||
: existingValue?.metadata
|
||||
? { ...existingValue.metadata }
|
||||
: undefined,
|
||||
};
|
||||
|
||||
@@ -424,8 +427,8 @@ export function createThreadBindingManager(params: {
|
||||
if (!bindingKey) {
|
||||
return null;
|
||||
}
|
||||
const existing = BINDINGS_BY_THREAD_ID.get(bindingKey);
|
||||
if (!existing || existing.accountId !== accountId) {
|
||||
const existingLocal = BINDINGS_BY_THREAD_ID.get(bindingKey);
|
||||
if (!existingLocal || existingLocal.accountId !== accountId) {
|
||||
return null;
|
||||
}
|
||||
const removed = removeBindingRecord(bindingKey);
|
||||
|
||||
@@ -64,16 +64,16 @@ function resolveMemberGuildPermissionBits(params: {
|
||||
guild: Pick<APIGuild, "id" | "roles">;
|
||||
member: Pick<APIGuildMember, "roles">;
|
||||
}) {
|
||||
const rolesById = new Map<string, APIRole>(
|
||||
const rolesByIdLocal = new Map<string, APIRole>(
|
||||
(params.guild.roles ?? []).map((role) => [role.id, role]),
|
||||
);
|
||||
const everyoneRole = rolesById.get(params.guild.id);
|
||||
const everyoneRole = rolesByIdLocal.get(params.guild.id);
|
||||
let permissions = 0n;
|
||||
if (everyoneRole?.permissions) {
|
||||
permissions = addPermissionBits(permissions, everyoneRole.permissions);
|
||||
}
|
||||
for (const roleId of params.member.roles ?? []) {
|
||||
const role = rolesById.get(roleId);
|
||||
const role = rolesByIdLocal.get(roleId);
|
||||
if (role?.permissions) {
|
||||
permissions = addPermissionBits(permissions, role.permissions);
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ describe("resolveDiscordTarget", () => {
|
||||
|
||||
it("treats bare numeric ids in allowFrom as users even when channels are the default", async () => {
|
||||
const listPeers = vi.spyOn(directoryLive, "listDiscordDirectoryPeersLive");
|
||||
const cfg = {
|
||||
const cfgCandidate = {
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
@@ -150,14 +150,18 @@ describe("resolveDiscordTarget", () => {
|
||||
} as OpenClawConfig;
|
||||
|
||||
expectTargetFields(
|
||||
await resolveDiscordTarget("123", { cfg, accountId: "default" }, { defaultKind: "channel" }),
|
||||
await resolveDiscordTarget(
|
||||
"123",
|
||||
{ cfg: cfgCandidate, accountId: "default" },
|
||||
{ defaultKind: "channel" },
|
||||
),
|
||||
{ kind: "user", id: "123", normalized: "user:123" },
|
||||
);
|
||||
expect(listPeers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses legacy dm.allowFrom when disambiguating bare numeric ids", async () => {
|
||||
const cfg = {
|
||||
const cfgEntry = {
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
@@ -170,13 +174,17 @@ describe("resolveDiscordTarget", () => {
|
||||
} as OpenClawConfig;
|
||||
|
||||
expectTargetFields(
|
||||
await resolveDiscordTarget("456", { cfg, accountId: "default" }, { defaultKind: "channel" }),
|
||||
await resolveDiscordTarget(
|
||||
"456",
|
||||
{ cfg: cfgEntry, accountId: "default" },
|
||||
{ defaultKind: "channel" },
|
||||
),
|
||||
{ kind: "user", id: "456", normalized: "user:456" },
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers top-level allowFrom over legacy dm.allowFrom for bare numeric ids", async () => {
|
||||
const cfg = {
|
||||
const cfgResult = {
|
||||
channels: {
|
||||
discord: {
|
||||
accounts: {
|
||||
@@ -190,13 +198,17 @@ describe("resolveDiscordTarget", () => {
|
||||
} as OpenClawConfig;
|
||||
|
||||
expectTargetFields(
|
||||
await resolveDiscordTarget("456", { cfg, accountId: "default" }, { defaultKind: "channel" }),
|
||||
await resolveDiscordTarget(
|
||||
"456",
|
||||
{ cfg: cfgResult, accountId: "default" },
|
||||
{ defaultKind: "channel" },
|
||||
),
|
||||
{ kind: "channel", id: "456", normalized: "channel:456" },
|
||||
);
|
||||
});
|
||||
|
||||
it("uses account legacy dm.allowFrom before inherited root allowFrom for bare numeric ids", async () => {
|
||||
const cfg = {
|
||||
const cfgValue = {
|
||||
channels: {
|
||||
discord: {
|
||||
allowFrom: ["123"],
|
||||
@@ -210,17 +222,25 @@ describe("resolveDiscordTarget", () => {
|
||||
} as OpenClawConfig;
|
||||
|
||||
expectTargetFields(
|
||||
await resolveDiscordTarget("456", { cfg, accountId: "work" }, { defaultKind: "channel" }),
|
||||
await resolveDiscordTarget(
|
||||
"456",
|
||||
{ cfg: cfgValue, accountId: "work" },
|
||||
{ defaultKind: "channel" },
|
||||
),
|
||||
{ kind: "user", id: "456", normalized: "user:456" },
|
||||
);
|
||||
expectTargetFields(
|
||||
await resolveDiscordTarget("123", { cfg, accountId: "work" }, { defaultKind: "channel" }),
|
||||
await resolveDiscordTarget(
|
||||
"123",
|
||||
{ cfg: cfgValue, accountId: "work" },
|
||||
{ defaultKind: "channel" },
|
||||
),
|
||||
{ kind: "channel", id: "123", normalized: "channel:123" },
|
||||
);
|
||||
});
|
||||
|
||||
it("caches username lookups under the configured default account when accountId is omitted", async () => {
|
||||
const cfg = {
|
||||
const cfgLocal = {
|
||||
channels: {
|
||||
discord: {
|
||||
defaultAccount: "work",
|
||||
@@ -237,7 +257,7 @@ describe("resolveDiscordTarget", () => {
|
||||
{ kind: "user", id: "user:999", name: "Jane" } as const,
|
||||
]);
|
||||
|
||||
expectTargetFields(await resolveDiscordTarget("jane", { cfg }), {
|
||||
expectTargetFields(await resolveDiscordTarget("jane", { cfg: cfgLocal }), {
|
||||
kind: "user",
|
||||
id: "999",
|
||||
normalized: "user:999",
|
||||
|
||||
@@ -57,7 +57,7 @@ const {
|
||||
handlers: Map<string, EventHandler>;
|
||||
};
|
||||
|
||||
const createConnectionMock = (): MockConnection => {
|
||||
const createConnectionMockLocal = (): MockConnection => {
|
||||
const handlers = new Map<string, EventHandler>();
|
||||
const daveSetPassthroughMode = vi.fn();
|
||||
const connection: MockConnection = {
|
||||
@@ -98,9 +98,9 @@ const {
|
||||
return connection;
|
||||
};
|
||||
|
||||
const getVoiceConnectionMock = vi.fn((): MockConnection | undefined => undefined);
|
||||
const getVoiceConnectionMockLocal = vi.fn((): MockConnection | undefined => undefined);
|
||||
|
||||
const realtimeSessionMock = {
|
||||
const realtimeSessionMockLocal = {
|
||||
bridge: { supportsToolResultContinuation: true },
|
||||
acknowledgeMark: vi.fn(),
|
||||
close: vi.fn(),
|
||||
@@ -114,9 +114,9 @@ const {
|
||||
};
|
||||
|
||||
return {
|
||||
createConnectionMock,
|
||||
getVoiceConnectionMock,
|
||||
joinVoiceChannelMock: vi.fn(() => createConnectionMock()),
|
||||
createConnectionMock: createConnectionMockLocal,
|
||||
getVoiceConnectionMock: getVoiceConnectionMockLocal,
|
||||
joinVoiceChannelMock: vi.fn(() => createConnectionMockLocal()),
|
||||
entersStateMock: vi.fn(async (_target?: unknown, _state?: string, _timeoutMs?: number) => {
|
||||
return undefined;
|
||||
}),
|
||||
@@ -148,7 +148,7 @@ const {
|
||||
provider: { id: "openai" },
|
||||
providerConfig: { model: "gpt-realtime-2", voice: "cedar" },
|
||||
})),
|
||||
createRealtimeVoiceBridgeSessionMock: vi.fn((_params?: unknown) => realtimeSessionMock),
|
||||
createRealtimeVoiceBridgeSessionMock: vi.fn((_params?: unknown) => realtimeSessionMockLocal),
|
||||
controlRealtimeVoiceAgentRunMock: vi.fn<() => Promise<RealtimeVoiceAgentControlResult>>(
|
||||
async () => ({
|
||||
ok: false,
|
||||
@@ -163,7 +163,7 @@ const {
|
||||
suppress: false,
|
||||
}),
|
||||
),
|
||||
realtimeSessionMock,
|
||||
realtimeSessionMock: realtimeSessionMockLocal,
|
||||
decodeOpusStreamMock: vi.fn(),
|
||||
decodeOpusStreamChunksMock: vi.fn(),
|
||||
updateVoiceStateMock: vi.fn(),
|
||||
|
||||
@@ -683,7 +683,7 @@ export class DiscordVoiceManager {
|
||||
};
|
||||
const stopEntry = (
|
||||
entry: VoiceSessionEntry,
|
||||
options: { destroyConnection: boolean; reason: string },
|
||||
optionsLocal: { destroyConnection: boolean; reason: string },
|
||||
) => {
|
||||
if (stopped) {
|
||||
return;
|
||||
@@ -710,11 +710,11 @@ export class DiscordVoiceManager {
|
||||
entry.realtime?.close();
|
||||
entry.realtime = undefined;
|
||||
player.stop();
|
||||
if (options.destroyConnection) {
|
||||
if (optionsLocal.destroyConnection) {
|
||||
destroyVoiceConnectionSafely({
|
||||
connection,
|
||||
voiceSdk,
|
||||
reason: options.reason,
|
||||
reason: optionsLocal.reason,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -512,8 +512,8 @@ async function fetchImageBuffer(
|
||||
const mimeType = response.headers.get("content-type")?.trim() || "image/png";
|
||||
return {
|
||||
buffer: await readResponseWithLimit(response, maxBytes, {
|
||||
onOverflow: ({ maxBytes }) =>
|
||||
new Error(`fal generated image download exceeds ${maxBytes} bytes`),
|
||||
onOverflow: ({ maxBytes: maxBytesLocal }) =>
|
||||
new Error(`fal generated image download exceeds ${maxBytesLocal} bytes`),
|
||||
}),
|
||||
mimeType,
|
||||
};
|
||||
|
||||
@@ -207,9 +207,9 @@ async function downloadFalVideo(
|
||||
let buffer: Buffer;
|
||||
try {
|
||||
buffer = await readResponseWithLimit(response, maxBytes, {
|
||||
onOverflow: ({ maxBytes }) => {
|
||||
onOverflow: ({ maxBytes: maxBytesLocal }) => {
|
||||
exceededMaxBytes = true;
|
||||
return new Error(`fal generated video download exceeds ${maxBytes} bytes`);
|
||||
return new Error(`fal generated video download exceeds ${maxBytesLocal} bytes`);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -168,14 +168,14 @@ describe("parseFeishuMessageEvent – mentionedBot", () => {
|
||||
});
|
||||
|
||||
it("returns mentionedBot=true for post message with at (no top-level mentions)", () => {
|
||||
const BOT_OPEN_ID = "ou_bot_123";
|
||||
const BOT_OPEN_IDLocal = "ou_bot_123";
|
||||
const event = makePostEvent({
|
||||
content: [
|
||||
[{ tag: "at", user_id: BOT_OPEN_ID, user_name: "claw" }],
|
||||
[{ tag: "at", user_id: BOT_OPEN_IDLocal, user_name: "claw" }],
|
||||
[{ tag: "text", text: "What does this document say" }],
|
||||
],
|
||||
});
|
||||
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_ID);
|
||||
const ctx = parseFeishuMessageEvent(event, BOT_OPEN_IDLocal);
|
||||
expect(ctx.mentionedBot).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -162,13 +162,6 @@ function buildDefaultResolveRoute(): ResolvedAgentRoute {
|
||||
matchedBy: "default",
|
||||
};
|
||||
}
|
||||
|
||||
function createUnboundConfiguredRoute(
|
||||
route: NonNullable<ConfiguredBindingRoute>["route"],
|
||||
): ConfiguredBindingRoute {
|
||||
return { bindingResolution: null, route };
|
||||
}
|
||||
|
||||
function createFeishuBotRuntime(overrides: DeepPartial<PluginRuntime> = {}): PluginRuntime {
|
||||
return {
|
||||
channel: {
|
||||
|
||||
@@ -808,10 +808,10 @@ export async function handleFeishuMessage(params: {
|
||||
if (!isGroup && route.matchedBy === "default") {
|
||||
const dynamicCfg = feishuCfg?.dynamicAgentCreation as DynamicAgentCreationConfig | undefined;
|
||||
if (dynamicCfg?.enabled) {
|
||||
const runtime = getFeishuRuntime();
|
||||
const runtimeLocal = getFeishuRuntime();
|
||||
const result = await maybeCreateDynamicAgent({
|
||||
cfg,
|
||||
runtime,
|
||||
runtime: runtimeLocal,
|
||||
senderOpenId: ctx.senderOpenId,
|
||||
dynamicCfg,
|
||||
configWritesAllowed: resolveChannelConfigWrites({
|
||||
@@ -1387,22 +1387,22 @@ export async function handleFeishuMessage(params: {
|
||||
.map((id) => (id ? normalizeFeishuAllowEntry(id) : ""))
|
||||
.find((recipient) => recipient === pinnedMainDmOwner)
|
||||
: undefined;
|
||||
const buildFeishuInboundLastRouteUpdate = (params: {
|
||||
const buildFeishuInboundLastRouteUpdate = (paramsLocal: {
|
||||
accountId: string;
|
||||
sessionKey: string;
|
||||
}) => {
|
||||
const inboundLastRouteSessionKey =
|
||||
params.sessionKey === route.sessionKey
|
||||
paramsLocal.sessionKey === route.sessionKey
|
||||
? resolveInboundLastRouteSessionKey({
|
||||
route,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionKey: paramsLocal.sessionKey,
|
||||
})
|
||||
: params.sessionKey;
|
||||
: paramsLocal.sessionKey;
|
||||
return {
|
||||
sessionKey: inboundLastRouteSessionKey,
|
||||
channel: "feishu" as const,
|
||||
to: feishuTo,
|
||||
accountId: params.accountId,
|
||||
accountId: paramsLocal.accountId,
|
||||
...(lastRouteThreadId ? { threadId: lastRouteThreadId } : {}),
|
||||
mainDmOwnerPin:
|
||||
!isGroup && inboundLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner
|
||||
|
||||
@@ -279,7 +279,7 @@ describe("feishuPlugin actions", () => {
|
||||
});
|
||||
|
||||
it("honors the selected Feishu account during discovery", () => {
|
||||
const cfg = {
|
||||
const cfgLocal = {
|
||||
channels: {
|
||||
feishu: {
|
||||
enabled: true,
|
||||
@@ -302,7 +302,7 @@ describe("feishuPlugin actions", () => {
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(getDescribedActions(cfg, "default")).toEqual([
|
||||
expect(getDescribedActions(cfgLocal, "default")).toEqual([
|
||||
"send",
|
||||
"read",
|
||||
"edit",
|
||||
@@ -314,7 +314,7 @@ describe("feishuPlugin actions", () => {
|
||||
"channel-info",
|
||||
"channel-list",
|
||||
]);
|
||||
expect(getDescribedActions(cfg, "work")).toEqual([
|
||||
expect(getDescribedActions(cfgLocal, "work")).toEqual([
|
||||
"send",
|
||||
"read",
|
||||
"edit",
|
||||
|
||||
@@ -276,7 +276,7 @@ describe("feishu_doc image fetch hardening", () => {
|
||||
|
||||
const call = blockDescendantCreateMock.mock.calls[0]?.[0];
|
||||
expect(call?.data.children_id).toEqual(["h1", "p1", "h2", "list1"]);
|
||||
expect((call?.data.descendants as Array<{ block_id: string }>).map((b) => b.block_id)).toEqual([
|
||||
expect((call!.data.descendants as Array<{ block_id: string }>).map((b) => b.block_id)).toEqual([
|
||||
"h1",
|
||||
"p1",
|
||||
"h2",
|
||||
|
||||
@@ -28,8 +28,6 @@ const {
|
||||
sendMessageFeishuMock,
|
||||
withReplyDispatcherMock,
|
||||
} = getFeishuLifecycleTestMocks();
|
||||
|
||||
let handlers: Record<string, (data: unknown) => Promise<void>> = {};
|
||||
let lastRuntime = createRuntimeEnv();
|
||||
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
const { cfg: lifecycleConfig, account: lifecycleAccount } = createFeishuLifecycleFixture({
|
||||
@@ -62,9 +60,7 @@ async function setupLifecycleMonitor() {
|
||||
lastRuntime = createRuntimeEnv();
|
||||
return setupFeishuLifecycleHandler({
|
||||
createEventDispatcherMock,
|
||||
onRegister: (registered) => {
|
||||
handlers = registered;
|
||||
},
|
||||
onRegister: () => {},
|
||||
runtime: lastRuntime,
|
||||
cfg: lifecycleConfig,
|
||||
account: lifecycleAccount,
|
||||
@@ -77,7 +73,6 @@ describe("Feishu ACP-init failure lifecycle", () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
resetFeishuLifecycleTestMocks();
|
||||
handlers = {};
|
||||
lastRuntime = createRuntimeEnv();
|
||||
setFeishuLifecycleStateDir("openclaw-feishu-acp-failure");
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ const {
|
||||
touchBindingMock,
|
||||
withReplyDispatcherMock,
|
||||
} = getFeishuLifecycleTestMocks();
|
||||
|
||||
let handlers: Record<string, (data: unknown) => Promise<void>> = {};
|
||||
let lastRuntime = createRuntimeEnv();
|
||||
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
const lifecycleConfig = createFeishuLifecycleConfig({
|
||||
@@ -77,9 +75,7 @@ async function setupLifecycleMonitor() {
|
||||
lastRuntime = createRuntimeEnv();
|
||||
return setupFeishuLifecycleHandler({
|
||||
createEventDispatcherMock,
|
||||
onRegister: (registered) => {
|
||||
handlers = registered;
|
||||
},
|
||||
onRegister: () => {},
|
||||
runtime: lastRuntime,
|
||||
cfg: lifecycleConfig,
|
||||
account: lifecycleAccount,
|
||||
@@ -92,7 +88,6 @@ describe("Feishu bot-menu lifecycle", () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
resetFeishuLifecycleTestMocks();
|
||||
handlers = {};
|
||||
lastRuntime = createRuntimeEnv();
|
||||
setFeishuLifecycleStateDir("openclaw-feishu-bot-menu");
|
||||
|
||||
|
||||
@@ -33,8 +33,6 @@ const {
|
||||
touchBindingMock,
|
||||
withReplyDispatcherMock,
|
||||
} = getFeishuLifecycleTestMocks();
|
||||
|
||||
let handlers: Record<string, (data: unknown) => Promise<void>> = {};
|
||||
let lastRuntime = createRuntimeEnv();
|
||||
const originalStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
const lifecycleConfig = createFeishuLifecycleConfig({
|
||||
@@ -104,9 +102,7 @@ async function setupLifecycleMonitor() {
|
||||
lastRuntime = createRuntimeEnv();
|
||||
return setupFeishuLifecycleHandler({
|
||||
createEventDispatcherMock,
|
||||
onRegister: (registered) => {
|
||||
handlers = registered;
|
||||
},
|
||||
onRegister: () => {},
|
||||
runtime: lastRuntime,
|
||||
cfg: lifecycleConfig,
|
||||
account: lifecycleAccount,
|
||||
@@ -143,7 +139,6 @@ describe("Feishu card-action lifecycle", () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
resetFeishuLifecycleTestMocks();
|
||||
handlers = {};
|
||||
lastRuntime = createRuntimeEnv();
|
||||
resetProcessedFeishuCardActionTokensForTests();
|
||||
setFeishuLifecycleStateDir("openclaw-feishu-card-action");
|
||||
|
||||
@@ -166,8 +166,8 @@ export function createFeishuMessageReceiveHandler({
|
||||
recordProcessedMessage,
|
||||
getBotOpenId = () => undefined,
|
||||
getBotName = () => undefined,
|
||||
resolveSequentialKey = ({ accountId, event }) =>
|
||||
`feishu:${accountId}:${event.message.chat_id?.trim() || "unknown"}`,
|
||||
resolveSequentialKey = ({ accountId: accountIdLocal, event }) =>
|
||||
`feishu:${accountIdLocal}:${event.message.chat_id?.trim() || "unknown"}`,
|
||||
}: FeishuMessageReceiveHandlerContext): (data: unknown) => Promise<void> {
|
||||
const inboundDebounceMs = channelRuntime.debounce.resolveInboundDebounceMs({
|
||||
cfg,
|
||||
|
||||
@@ -440,27 +440,27 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
flushStreamingCardUpdate(buildCombinedStreamText(reasoningText, streamText));
|
||||
};
|
||||
|
||||
const sendChunkedTextReply = async (params: {
|
||||
const sendChunkedTextReply = async (paramsLocal: {
|
||||
text: string;
|
||||
useCard: boolean;
|
||||
infoKind?: string;
|
||||
sendChunk: (params: { chunk: string; isFirst: boolean }) => Promise<void>;
|
||||
}) => {
|
||||
const chunkSource = params.useCard
|
||||
? params.text
|
||||
: core.channel.text.convertMarkdownTables(params.text, tableMode);
|
||||
const chunkSource = paramsLocal.useCard
|
||||
? paramsLocal.text
|
||||
: core.channel.text.convertMarkdownTables(paramsLocal.text, tableMode);
|
||||
const chunks = resolveTextChunksWithFallback(
|
||||
chunkSource,
|
||||
core.channel.text.chunkTextWithMode(chunkSource, textChunkLimit, chunkMode),
|
||||
);
|
||||
for (const [index, chunk] of chunks.entries()) {
|
||||
await params.sendChunk({
|
||||
await paramsLocal.sendChunk({
|
||||
chunk,
|
||||
isFirst: index === 0,
|
||||
});
|
||||
}
|
||||
if (params.infoKind === "final") {
|
||||
deliveredFinalTexts.add(params.text);
|
||||
if (paramsLocal.infoKind === "final") {
|
||||
deliveredFinalTexts.add(paramsLocal.text);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -744,7 +744,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
if (!isChannelProgressDraftWorkToolName(payload.name)) {
|
||||
return;
|
||||
}
|
||||
const statusLine = formatChannelProgressDraftLineForEntry(
|
||||
const statusLineLocal = formatChannelProgressDraftLineForEntry(
|
||||
account.config,
|
||||
{
|
||||
event: "tool",
|
||||
@@ -756,8 +756,8 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
||||
detailMode: payload.detailMode,
|
||||
},
|
||||
);
|
||||
if (statusLine) {
|
||||
updateStreamingStatusLine(statusLine);
|
||||
if (statusLineLocal) {
|
||||
updateStreamingStatusLine(statusLineLocal);
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -163,7 +163,7 @@ export function createFeishuThreadBindingManager(params: {
|
||||
if (!normalizedConversationId || !normalizedTargetSessionKey) {
|
||||
return null;
|
||||
}
|
||||
const existing = getState().bindingsByAccountConversation.get(
|
||||
const existingLocal = getState().bindingsByAccountConversation.get(
|
||||
resolveBindingKey({ accountId, conversationId: normalizedConversationId }),
|
||||
);
|
||||
const now = Date.now();
|
||||
@@ -171,29 +171,29 @@ export function createFeishuThreadBindingManager(params: {
|
||||
accountId,
|
||||
conversationId: normalizedConversationId,
|
||||
parentConversationId:
|
||||
normalizeOptionalString(parentConversationId) ?? existing?.parentConversationId,
|
||||
normalizeOptionalString(parentConversationId) ?? existingLocal?.parentConversationId,
|
||||
deliveryTo:
|
||||
typeof metadata?.deliveryTo === "string" && metadata.deliveryTo.trim()
|
||||
? metadata.deliveryTo.trim()
|
||||
: existing?.deliveryTo,
|
||||
: existingLocal?.deliveryTo,
|
||||
deliveryThreadId:
|
||||
typeof metadata?.deliveryThreadId === "string" && metadata.deliveryThreadId.trim()
|
||||
? metadata.deliveryThreadId.trim()
|
||||
: existing?.deliveryThreadId,
|
||||
: existingLocal?.deliveryThreadId,
|
||||
targetKind: toFeishuTargetKind(targetKind),
|
||||
targetSessionKey: normalizedTargetSessionKey,
|
||||
agentId:
|
||||
typeof metadata?.agentId === "string" && metadata.agentId.trim()
|
||||
? metadata.agentId.trim()
|
||||
: (existing?.agentId ?? resolveAgentIdFromSessionKey(normalizedTargetSessionKey)),
|
||||
: (existingLocal?.agentId ?? resolveAgentIdFromSessionKey(normalizedTargetSessionKey)),
|
||||
label:
|
||||
typeof metadata?.label === "string" && metadata.label.trim()
|
||||
? metadata.label.trim()
|
||||
: existing?.label,
|
||||
: existingLocal?.label,
|
||||
boundBy:
|
||||
typeof metadata?.boundBy === "string" && metadata.boundBy.trim()
|
||||
? metadata.boundBy.trim()
|
||||
: existing?.boundBy,
|
||||
: existingLocal?.boundBy,
|
||||
boundAt: now,
|
||||
lastActivityAt: now,
|
||||
};
|
||||
|
||||
@@ -83,12 +83,14 @@ function lastClientAppId(): string | undefined {
|
||||
describe("feishu tool account routing", () => {
|
||||
beforeAll(async () => {
|
||||
({ registerFeishuBitableTools, registerFeishuDriveTools, registerFeishuPermTools } =
|
||||
await import("./bitable.js").then(async ({ registerFeishuBitableTools }) => ({
|
||||
registerFeishuBitableTools,
|
||||
...(await import("./drive.js")),
|
||||
...(await import("./perm.js")),
|
||||
...(await import("./wiki.js")),
|
||||
})));
|
||||
await import("./bitable.js").then(
|
||||
async ({ registerFeishuBitableTools: registerFeishuBitableToolsLocal }) => ({
|
||||
registerFeishuBitableTools: registerFeishuBitableToolsLocal,
|
||||
...(await import("./drive.js")),
|
||||
...(await import("./perm.js")),
|
||||
...(await import("./wiki.js")),
|
||||
}),
|
||||
));
|
||||
({ registerFeishuWikiTools } = await import("./wiki.js"));
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { gzipSync } from "node:zlib";
|
||||
import type { OpenClawPluginNodeInvokePolicyContext } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -417,17 +417,17 @@ export async function runFirecrawlSearch(
|
||||
errorLabel: "Firecrawl Search",
|
||||
},
|
||||
async (response) => {
|
||||
const payload = await readFirecrawlJsonResponse(response, "Firecrawl Search API error");
|
||||
if (payload.success === false) {
|
||||
const payloadValue = await readFirecrawlJsonResponse(response, "Firecrawl Search API error");
|
||||
if (payloadValue.success === false) {
|
||||
const error =
|
||||
typeof payload.error === "string"
|
||||
? payload.error
|
||||
: typeof payload.message === "string"
|
||||
? payload.message
|
||||
typeof payloadValue.error === "string"
|
||||
? payloadValue.error
|
||||
: typeof payloadValue.message === "string"
|
||||
? payloadValue.message
|
||||
: "unknown error";
|
||||
throw new Error(`Firecrawl Search API error: ${error}`);
|
||||
}
|
||||
return payload;
|
||||
return payloadValue;
|
||||
},
|
||||
);
|
||||
const result = buildSearchPayload({
|
||||
@@ -573,19 +573,19 @@ export async function runFirecrawlScrape(
|
||||
},
|
||||
},
|
||||
async (response) => {
|
||||
const payload = await readFirecrawlJsonResponse(response, "Firecrawl fetch failed");
|
||||
if (payload.success === false) {
|
||||
const payloadLocal = await readFirecrawlJsonResponse(response, "Firecrawl fetch failed");
|
||||
if (payloadLocal.success === false) {
|
||||
const detail =
|
||||
typeof payload.error === "string"
|
||||
? payload.error
|
||||
: typeof payload.message === "string"
|
||||
? payload.message
|
||||
typeof payloadLocal.error === "string"
|
||||
? payloadLocal.error
|
||||
: typeof payloadLocal.message === "string"
|
||||
? payloadLocal.message
|
||||
: response.statusText;
|
||||
throw new Error(
|
||||
`Firecrawl fetch failed (${response.status}): ${wrapWebContent(detail, "web_fetch")}`.trim(),
|
||||
);
|
||||
}
|
||||
return payload;
|
||||
return payloadLocal;
|
||||
},
|
||||
);
|
||||
const result = parseFirecrawlScrapePayload({
|
||||
|
||||
@@ -83,11 +83,6 @@ async function createAgentDir() {
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function registerProviderForTest() {
|
||||
return registerProviderWithPluginConfig({});
|
||||
}
|
||||
|
||||
function requireFirstMockArg<T>(
|
||||
mock: { mock: { calls: Array<[T, ...unknown[]]> } },
|
||||
label: string,
|
||||
|
||||
@@ -1997,7 +1997,9 @@ describe("google-meet plugin", () => {
|
||||
"Chrome observe-only mode does not require a realtime audio bridge",
|
||||
);
|
||||
expect(
|
||||
result.details.checks?.filter((check) => check.id === "chrome-local-audio-device"),
|
||||
result.details.checks?.filter(
|
||||
(checkLocal) => checkLocal.id === "chrome-local-audio-device",
|
||||
),
|
||||
).toStrictEqual([]);
|
||||
expect(runCommandWithTimeout).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("google-meet config compatibility", () => {
|
||||
]);
|
||||
expect(
|
||||
(
|
||||
migration?.config.plugins?.entries?.["google-meet"] as {
|
||||
migration!.config.plugins!.entries!["google-meet"] as {
|
||||
config?: { realtime?: Record<string, unknown> };
|
||||
}
|
||||
).config?.realtime,
|
||||
@@ -85,7 +85,7 @@ describe("google-meet config compatibility", () => {
|
||||
expect(migration.changes).toStrictEqual([]);
|
||||
expect(
|
||||
(
|
||||
migration.config.plugins?.entries?.["google-meet"] as {
|
||||
migration.config.plugins!.entries!["google-meet"] as {
|
||||
config?: { realtime?: Record<string, unknown> };
|
||||
}
|
||||
).config?.realtime,
|
||||
|
||||
@@ -521,8 +521,8 @@ export async function startCommandAgentAudioBridge(params: {
|
||||
{ onEvent: recordTalkObservabilityEvent },
|
||||
);
|
||||
const recentTalkEvents: TalkEvent[] = [];
|
||||
const emitTalkEvent = (input: TalkEventInput) =>
|
||||
pushGoogleMeetTalkEvent(recentTalkEvents, talk.emit(input));
|
||||
const emitTalkEvent = (inputResult: TalkEventInput) =>
|
||||
pushGoogleMeetTalkEvent(recentTalkEvents, talk.emit(inputResult));
|
||||
const ensureTalkTurn = () => {
|
||||
const turn = talk.ensureTurn({
|
||||
payload: { meetingSessionId: params.meetingSessionId },
|
||||
@@ -1081,8 +1081,8 @@ export async function startCommandRealtimeAudioBridge(params: {
|
||||
pushGoogleMeetTalkEvent(recentTalkEvents, event);
|
||||
}
|
||||
};
|
||||
const emitTalkEvent = (input: TalkEventInput): void => {
|
||||
rememberTalkEvent(talk.emit(input));
|
||||
const emitTalkEvent = (inputValue: TalkEventInput): void => {
|
||||
rememberTalkEvent(talk.emit(inputValue));
|
||||
};
|
||||
const ensureTalkTurn = (): string => {
|
||||
const turn = talk.ensureTurn({
|
||||
@@ -1270,7 +1270,8 @@ export async function startCommandRealtimeAudioBridge(params: {
|
||||
meetingSessionId: params.meetingSessionId,
|
||||
requesterSessionKey: params.requesterSessionKey,
|
||||
transcript,
|
||||
onTalkEvent: (input) => emitTalkEvent({ ...input, turnId: input.turnId ?? turnId }),
|
||||
onTalkEvent: (inputLocal) =>
|
||||
emitTalkEvent({ ...inputLocal, turnId: inputLocal.turnId ?? turnId }),
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
@@ -363,13 +363,13 @@ export async function createGeminiEmbeddingProvider(
|
||||
|
||||
const embedBatch = async (
|
||||
texts: string[],
|
||||
options?: { signal?: AbortSignal },
|
||||
optionsLocal?: { signal?: AbortSignal },
|
||||
): Promise<number[][]> => {
|
||||
return await embedBatchInputs(
|
||||
texts.map((text) => ({
|
||||
text,
|
||||
})),
|
||||
options,
|
||||
optionsLocal,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ describeLive("google plugin live", () => {
|
||||
|
||||
expect(result?.provider).toBe("gemini");
|
||||
expect(typeof result?.content).toBe("string");
|
||||
expect((result?.content as string).length).toBeGreaterThan(20);
|
||||
expect((result!.content as string).length).toBeGreaterThan(20);
|
||||
expect(Array.isArray(result?.citations)).toBe(true);
|
||||
}, 120_000);
|
||||
|
||||
@@ -171,9 +171,9 @@ describeLive("google plugin live", () => {
|
||||
expect(process.env.GOOGLE_API_KEY).toBeUndefined();
|
||||
expect(result?.provider).toBe("gemini");
|
||||
expect(typeof result?.content).toBe("string");
|
||||
expect((result?.content as string).length).toBeGreaterThan(20);
|
||||
expect((result!.content as string).length).toBeGreaterThan(20);
|
||||
expect(Array.isArray(result?.citations)).toBe(true);
|
||||
expect((result?.citations as unknown[]).length).toBeGreaterThan(0);
|
||||
expect((result!.citations as unknown[]).length).toBeGreaterThan(0);
|
||||
});
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { createGoogleGenAIMock, generateContentMock } = vi.hoisted(() => {
|
||||
const generateContentMock = vi.fn();
|
||||
const createGoogleGenAIMock = vi.fn(() => {
|
||||
const generateContentMockLocal = vi.fn();
|
||||
const createGoogleGenAIMockLocal = vi.fn(() => {
|
||||
return {
|
||||
models: {
|
||||
generateContent: generateContentMock,
|
||||
generateContent: generateContentMockLocal,
|
||||
},
|
||||
};
|
||||
});
|
||||
return { createGoogleGenAIMock, generateContentMock };
|
||||
return {
|
||||
createGoogleGenAIMock: createGoogleGenAIMockLocal,
|
||||
generateContentMock: generateContentMockLocal,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./google-genai-runtime.js", () => ({
|
||||
|
||||
@@ -21,17 +21,21 @@ type MockGoogleLiveConnectParams = {
|
||||
};
|
||||
|
||||
const { connectMock, createTokenMock, session } = vi.hoisted(() => {
|
||||
const session: MockGoogleLiveSession = {
|
||||
const sessionValue: MockGoogleLiveSession = {
|
||||
close: vi.fn(),
|
||||
sendClientContent: vi.fn(),
|
||||
sendRealtimeInput: vi.fn(),
|
||||
sendToolResponse: vi.fn(),
|
||||
};
|
||||
const connectMock = vi.fn(async (_params: MockGoogleLiveConnectParams) => session);
|
||||
const createTokenMock = vi.fn(async (_params: unknown) => ({
|
||||
const connectMockLocal = vi.fn(async (_params: MockGoogleLiveConnectParams) => sessionValue);
|
||||
const createTokenMockLocal = vi.fn(async (_params: unknown) => ({
|
||||
name: "auth_tokens/browser-session",
|
||||
}));
|
||||
return { connectMock, createTokenMock, session };
|
||||
return {
|
||||
connectMock: connectMockLocal,
|
||||
createTokenMock: createTokenMockLocal,
|
||||
session: sessionValue,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./google-genai-runtime.js", () => ({
|
||||
@@ -365,7 +369,7 @@ describe("buildGoogleRealtimeVoiceProvider", () => {
|
||||
it("creates constrained browser sessions for Google Live Talk", async () => {
|
||||
const provider = buildGoogleRealtimeVoiceProvider();
|
||||
|
||||
const session = await provider.createBrowserSession?.({
|
||||
const sessionLocal = await provider.createBrowserSession?.({
|
||||
providerConfig: {
|
||||
apiKey: "gemini-key",
|
||||
model: "gemini-live-2.5-flash-preview",
|
||||
@@ -420,9 +424,9 @@ describe("buildGoogleRealtimeVoiceProvider", () => {
|
||||
expect(liveConstraints?.config?.tools?.[0]?.functionDeclarations?.[0]?.behavior).toBe(
|
||||
"NON_BLOCKING",
|
||||
);
|
||||
expect(session?.provider).toBe("google");
|
||||
expect(session?.transport).toBe("provider-websocket");
|
||||
const websocketSession = session as {
|
||||
expect(sessionLocal?.provider).toBe("google");
|
||||
expect(sessionLocal?.transport).toBe("provider-websocket");
|
||||
const websocketSession = sessionLocal as {
|
||||
audio: {
|
||||
inputEncoding: string;
|
||||
inputSampleRateHz: number;
|
||||
|
||||
@@ -10,14 +10,14 @@ const {
|
||||
googleAuthGetAccessTokenMock,
|
||||
googleAuthMock,
|
||||
} = vi.hoisted(() => {
|
||||
const googleAuthGetAccessTokenMock = vi.fn();
|
||||
const googleAuthGetAccessTokenMockLocal = vi.fn();
|
||||
return {
|
||||
buildGuardedModelFetchMock: vi.fn(),
|
||||
guardedFetchMock: vi.fn(),
|
||||
googleAuthGetAccessTokenMock,
|
||||
googleAuthGetAccessTokenMock: googleAuthGetAccessTokenMockLocal,
|
||||
googleAuthMock: vi.fn(function GoogleAuthMock() {
|
||||
return {
|
||||
getAccessToken: googleAuthGetAccessTokenMock,
|
||||
getAccessToken: googleAuthGetAccessTokenMockLocal,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -3,23 +3,28 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vites
|
||||
|
||||
const { createGoogleGenAIMock, downloadMock, generateVideosMock, getVideosOperationMock } =
|
||||
vi.hoisted(() => {
|
||||
const generateVideosMock = vi.fn();
|
||||
const getVideosOperationMock = vi.fn();
|
||||
const downloadMock = vi.fn();
|
||||
const createGoogleGenAIMock = vi.fn(() => {
|
||||
const generateVideosMockLocal = vi.fn();
|
||||
const getVideosOperationMockLocal = vi.fn();
|
||||
const downloadMockLocal = vi.fn();
|
||||
const createGoogleGenAIMockLocal = vi.fn(() => {
|
||||
return {
|
||||
models: {
|
||||
generateVideos: generateVideosMock,
|
||||
generateVideos: generateVideosMockLocal,
|
||||
},
|
||||
operations: {
|
||||
getVideosOperation: getVideosOperationMock,
|
||||
getVideosOperation: getVideosOperationMockLocal,
|
||||
},
|
||||
files: {
|
||||
download: downloadMock,
|
||||
download: downloadMockLocal,
|
||||
},
|
||||
};
|
||||
});
|
||||
return { createGoogleGenAIMock, downloadMock, generateVideosMock, getVideosOperationMock };
|
||||
return {
|
||||
createGoogleGenAIMock: createGoogleGenAIMockLocal,
|
||||
downloadMock: downloadMockLocal,
|
||||
generateVideosMock: generateVideosMockLocal,
|
||||
getVideosOperationMock: getVideosOperationMockLocal,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./google-genai-runtime.js", () => ({
|
||||
|
||||
@@ -287,13 +287,13 @@ export const googlechatOutboundAdapter = {
|
||||
typeof threadId === "number" ? String(threadId) : (threadId ?? replyToId ?? undefined);
|
||||
const maxBytes = resolveChannelMediaMaxBytes({
|
||||
cfg,
|
||||
resolveChannelLimitMb: ({ cfg, accountId }) =>
|
||||
resolveChannelLimitMb: ({ cfg: cfgLocal, accountId: accountIdLocal }) =>
|
||||
(
|
||||
cfg.channels?.googlechat as
|
||||
cfgLocal.channels?.googlechat as
|
||||
| { accounts?: Record<string, { mediaMaxMb?: number }>; mediaMaxMb?: number }
|
||||
| undefined
|
||||
)?.accounts?.[accountId]?.mediaMaxMb ??
|
||||
(cfg.channels?.googlechat as { mediaMaxMb?: number } | undefined)?.mediaMaxMb,
|
||||
)?.accounts?.[accountIdLocal]?.mediaMaxMb ??
|
||||
(cfgLocal.channels?.googlechat as { mediaMaxMb?: number } | undefined)?.mediaMaxMb,
|
||||
accountId,
|
||||
});
|
||||
const effectiveMaxBytes = maxBytes ?? (account.config.mediaMaxMb ?? 20) * 1024 * 1024;
|
||||
|
||||
@@ -381,12 +381,12 @@ describe("googlechat google auth runtime", () => {
|
||||
try {
|
||||
const transport = await getGoogleAuthTransport();
|
||||
const transportDefaults = transport.defaults as { fetchImplementation?: unknown };
|
||||
const requestInterceptorAdd = transport.interceptors.request.add as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const responseInterceptorAdd = transport.interceptors.response.add as unknown as ReturnType<
|
||||
const requestInterceptorAdd = transport.interceptors.request["add"] as unknown as ReturnType<
|
||||
typeof vi.fn
|
||||
>;
|
||||
const responseInterceptorAdd = transport.interceptors.response[
|
||||
"add"
|
||||
] as unknown as ReturnType<typeof vi.fn>;
|
||||
const requestInterceptor = mockCallArg(requestInterceptorAdd) as
|
||||
| { resolved?: unknown }
|
||||
| undefined;
|
||||
@@ -414,10 +414,10 @@ describe("googlechat google auth runtime", () => {
|
||||
|
||||
expect(first).not.toBe(second);
|
||||
expect(mocks.gaxiosCtor).toHaveBeenCalledTimes(2);
|
||||
expect(first.interceptors.request.add).toHaveBeenCalledOnce();
|
||||
expect(first.interceptors.response.add).toHaveBeenCalledOnce();
|
||||
expect(second.interceptors.request.add).toHaveBeenCalledOnce();
|
||||
expect(second.interceptors.response.add).toHaveBeenCalledOnce();
|
||||
expect(first.interceptors.request["add"]).toHaveBeenCalledOnce();
|
||||
expect(first.interceptors.response["add"]).toHaveBeenCalledOnce();
|
||||
expect(second.interceptors.request["add"]).toHaveBeenCalledOnce();
|
||||
expect(second.interceptors.response["add"]).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("normalizes Google auth request headers before upstream interceptors run", () => {
|
||||
|
||||
@@ -267,9 +267,9 @@ describe("googlechat monitor webhook", () => {
|
||||
},
|
||||
});
|
||||
resolveWebhookTargetWithAuthOrReject.mockImplementation(async ({ isMatch, targets }) => {
|
||||
for (const target of targets) {
|
||||
if (await isMatch(target)) {
|
||||
return target;
|
||||
for (const targetLocal of targets) {
|
||||
if (await isMatch(targetLocal)) {
|
||||
return targetLocal;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -221,7 +221,7 @@ async function processMessageWithPipeline(params: {
|
||||
senderEmail,
|
||||
rawBody,
|
||||
statusSink,
|
||||
logVerbose: (message) => logVerbose(core, runtime, message),
|
||||
logVerbose: (messageLocal) => logVerbose(core, runtime, messageLocal),
|
||||
});
|
||||
if (!access.ok) {
|
||||
return;
|
||||
|
||||
@@ -52,8 +52,8 @@ export async function gradiumTTS(params: {
|
||||
await assertOkOrThrowProviderError(response, "Gradium API error");
|
||||
|
||||
return await readResponseWithLimit(response, maxBytes, {
|
||||
onOverflow: ({ maxBytes }) =>
|
||||
new Error(`Gradium TTS audio response exceeds ${maxBytes} bytes`),
|
||||
onOverflow: ({ maxBytes: maxBytesLocal }) =>
|
||||
new Error(`Gradium TTS audio response exceeds ${maxBytesLocal} bytes`),
|
||||
});
|
||||
} finally {
|
||||
await release();
|
||||
|
||||
@@ -189,20 +189,20 @@ async function runIMessageCliJson(
|
||||
let stderr = "";
|
||||
let killEscalation: ReturnType<typeof setTimeout> | null = null;
|
||||
let settled = false;
|
||||
const clearTimers = (options: { keepKillEscalation?: boolean } = {}): void => {
|
||||
const clearTimers = (optionsValue: { keepKillEscalation?: boolean } = {}): void => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
if (killEscalation && !options.keepKillEscalation) {
|
||||
if (killEscalation && !optionsValue.keepKillEscalation) {
|
||||
clearTimeout(killEscalation);
|
||||
}
|
||||
};
|
||||
const fail = (error: Error, options: { keepKillEscalation?: boolean } = {}): void => {
|
||||
const fail = (error: Error, optionsLocal: { keepKillEscalation?: boolean } = {}): void => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimers(options);
|
||||
clearTimers(optionsLocal);
|
||||
reject(error);
|
||||
};
|
||||
const succeed = (value: Record<string, unknown>): void => {
|
||||
|
||||
@@ -201,8 +201,10 @@ function readPersistedTarget(value: unknown): IMessageApprovalReactionTarget | n
|
||||
return null;
|
||||
}
|
||||
const allowedDecisions = target.allowedDecisions
|
||||
.map((value) => (typeof value === "string" ? normalizeApprovalDecision(value) : null))
|
||||
.filter((value): value is ExecApprovalReplyDecision => Boolean(value));
|
||||
.map((valueValue) =>
|
||||
typeof valueValue === "string" ? normalizeApprovalDecision(valueValue) : null,
|
||||
)
|
||||
.filter((valueLocal): valueLocal is ExecApprovalReplyDecision => Boolean(valueLocal));
|
||||
if (allowedDecisions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -419,9 +419,9 @@ describe("performIMessageCatchup", () => {
|
||||
// clamped to `earliestHeldFailureRow.rowid - 1` (== 9) so the next pass
|
||||
// refetches row 10.
|
||||
let dispatchCount = 0;
|
||||
const dispatch = vi.fn<CatchupDispatchFn>(async (row) => {
|
||||
const dispatch = vi.fn<CatchupDispatchFn>(async (rowLocal) => {
|
||||
dispatchCount += 1;
|
||||
if (row.guid === "A") {
|
||||
if (rowLocal.guid === "A") {
|
||||
return { ok: false };
|
||||
}
|
||||
return { ok: true };
|
||||
|
||||
@@ -96,7 +96,7 @@ describe("combineIMessagePayloads", () => {
|
||||
const payloads = Array.from({ length: 6 }, (_, i) =>
|
||||
makePayload({
|
||||
guid: `row-${i}`,
|
||||
attachments: Array.from({ length: 5 }, (_, j) => ({
|
||||
attachments: Array.from({ length: 5 }, (_Local, j) => ({
|
||||
original_path: `/tmp/${i}-${j}.jpg`,
|
||||
mime_type: "image/jpeg",
|
||||
})),
|
||||
|
||||
@@ -118,7 +118,7 @@ describe("sendMessageIMessage receipts", () => {
|
||||
expect(result.echoText).toBe("<media:image>");
|
||||
expect(result.receipt.primaryPlatformMessageId).toBe("p:0/media-guid");
|
||||
expect(result.receipt.platformMessageIds).toEqual(["p:0/media-guid"]);
|
||||
expect(client.request).not.toHaveBeenCalled();
|
||||
expect(client["request"]).not.toHaveBeenCalled();
|
||||
expect(runCliJson.mock.calls).toEqual([
|
||||
[["send-attachment", "--chat", "chat-1", "--file", "/tmp/image.png", "--transport", "auto"]],
|
||||
]);
|
||||
@@ -162,7 +162,7 @@ describe("sendMessageIMessage receipts", () => {
|
||||
});
|
||||
|
||||
expect(result.messageId).toBe("p:0/media-guid");
|
||||
expect(client.request).not.toHaveBeenCalled();
|
||||
expect(client["request"]).not.toHaveBeenCalled();
|
||||
expect(runCliJson.mock.calls).toEqual([
|
||||
[["group", "--chat-id", "42"]],
|
||||
[
|
||||
@@ -208,7 +208,7 @@ describe("sendMessageIMessage receipts", () => {
|
||||
expect(runCliJson.mock.calls).toEqual([
|
||||
[["send-attachment", "--chat", "chat-1", "--file", "/tmp/image.png", "--transport", "auto"]],
|
||||
]);
|
||||
expect(client.request).toHaveBeenCalledWith(
|
||||
expect(client["request"]).toHaveBeenCalledWith(
|
||||
"send",
|
||||
expect.objectContaining({
|
||||
chat_guid: "chat-1",
|
||||
@@ -233,7 +233,7 @@ describe("sendMessageIMessage receipts", () => {
|
||||
|
||||
expect(result.messageId).toBe("12345");
|
||||
expect(runCliJson.mock.calls).toEqual([[["group", "--chat-id", "42"]]]);
|
||||
expect(client.request).toHaveBeenCalledWith(
|
||||
expect(client["request"]).toHaveBeenCalledWith(
|
||||
"send",
|
||||
expect.objectContaining({
|
||||
chat_id: 42,
|
||||
@@ -259,7 +259,7 @@ describe("sendMessageIMessage receipts", () => {
|
||||
runCliJson,
|
||||
}),
|
||||
).rejects.toThrow("attachment delivery failed");
|
||||
expect(client.request).not.toHaveBeenCalled();
|
||||
expect(client["request"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes DM handle media-only sends through send-attachment", async () => {
|
||||
@@ -395,7 +395,7 @@ describe("sendMessageIMessage receipts", () => {
|
||||
});
|
||||
|
||||
expect(runCliJson).not.toHaveBeenCalled();
|
||||
expect(client.request).toHaveBeenCalledWith(
|
||||
expect(client["request"]).toHaveBeenCalledWith(
|
||||
"send",
|
||||
expect.objectContaining({
|
||||
chat_identifier: "team-thread",
|
||||
@@ -545,7 +545,7 @@ describe("sendMessageIMessage receipts", () => {
|
||||
|
||||
expect(result.sentText).toBe("literal <media:image> text");
|
||||
expect(result.echoText).toBe("literal <media:image> text");
|
||||
expect(client.request).toHaveBeenCalledWith(
|
||||
expect(client["request"]).toHaveBeenCalledWith(
|
||||
"send",
|
||||
expect.objectContaining({
|
||||
chat_id: 42,
|
||||
@@ -619,14 +619,14 @@ describe("sendMessageIMessage receipts", () => {
|
||||
|
||||
it("recovers approval prompt GUID without resending when rpc send times out", async () => {
|
||||
const client = createRejectingClient(new Error("imsg rpc timeout (send)"));
|
||||
const createClient = vi.fn(async () => client);
|
||||
const createClientLocal = vi.fn(async () => client);
|
||||
const runCliJson = vi.fn();
|
||||
const resolveSentMessageGuidImpl = vi.fn(async () => "p:0/fallback-guid");
|
||||
const approvalText = createApprovalText();
|
||||
|
||||
const result = await sendMessageIMessage("chat_id:42", approvalText, {
|
||||
config: IMESSAGE_TEST_CFG,
|
||||
createClient,
|
||||
createClient: createClientLocal,
|
||||
runCliJson,
|
||||
service: "sms",
|
||||
dbPath: "/Users/me/Library/Messages/chat.db",
|
||||
@@ -635,7 +635,7 @@ describe("sendMessageIMessage receipts", () => {
|
||||
|
||||
expect(result.messageId).toBe("p:0/fallback-guid");
|
||||
expect(result.guid).toBe("p:0/fallback-guid");
|
||||
expect(client.stop).toHaveBeenCalledOnce();
|
||||
expect(client["stop"]).toHaveBeenCalledOnce();
|
||||
expect(runCliJson).not.toHaveBeenCalled();
|
||||
expect(resolveSentMessageGuidImpl).toHaveBeenCalledWith({
|
||||
dbPath: "/Users/me/Library/Messages/chat.db",
|
||||
|
||||
@@ -106,10 +106,13 @@ export function createIMessagePluginBase(params: {
|
||||
return {
|
||||
...base,
|
||||
messaging: {
|
||||
resolveInboundAttachmentRoots: (params) =>
|
||||
resolveIMessageAttachmentRoots({ accountId: params.accountId, cfg: params.cfg }),
|
||||
resolveRemoteInboundAttachmentRoots: (params) =>
|
||||
resolveIMessageRemoteAttachmentRoots({ accountId: params.accountId, cfg: params.cfg }),
|
||||
resolveInboundAttachmentRoots: (paramsValue) =>
|
||||
resolveIMessageAttachmentRoots({ accountId: paramsValue.accountId, cfg: paramsValue.cfg }),
|
||||
resolveRemoteInboundAttachmentRoots: (paramsLocal) =>
|
||||
resolveIMessageRemoteAttachmentRoots({
|
||||
accountId: paramsLocal.accountId,
|
||||
cfg: paramsLocal.cfg,
|
||||
}),
|
||||
},
|
||||
} as Pick<
|
||||
ChannelPlugin<ResolvedIMessageAccount>,
|
||||
|
||||
@@ -207,7 +207,7 @@ export async function handleIrcInbound(params: {
|
||||
providerKey: "irc",
|
||||
accountId: account.accountId,
|
||||
blockedLabel: GROUP_POLICY_BLOCKED_LABEL.channel,
|
||||
log: (message) => runtime.log?.(message),
|
||||
log: (messageLocal) => runtime.log?.(messageLocal),
|
||||
});
|
||||
|
||||
const groupMatch = resolveIrcGroupMatch({
|
||||
|
||||
@@ -75,11 +75,11 @@ describe("downloadLineMedia", () => {
|
||||
saveMediaStreamMock.mockReset();
|
||||
saveMediaStreamMock.mockImplementation(
|
||||
async (stream: AsyncIterable<Buffer>, contentType?: string, subdir?: string) => {
|
||||
const chunks: Buffer[] = [];
|
||||
const chunksLocal: Buffer[] = [];
|
||||
for await (const chunk of stream) {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
chunksLocal.push(Buffer.from(chunk));
|
||||
}
|
||||
const buffer = Buffer.concat(chunks);
|
||||
const buffer = Buffer.concat(chunksLocal);
|
||||
return {
|
||||
path: `/home/user/.openclaw/media/${subdir ?? "unknown"}/saved-media`,
|
||||
contentType: detectMockContentType(buffer, contentType),
|
||||
|
||||
@@ -91,7 +91,7 @@ export function createInfoCard(title: string, body: string, footer?: string): Fl
|
||||
*/
|
||||
export function createListCard(title: string, items: ListItem[]): FlexBubble {
|
||||
const itemContents: FlexComponent[] = items.slice(0, 8).map((item, index) => {
|
||||
const itemContents: FlexComponent[] = [
|
||||
const itemContentsLocal: FlexComponent[] = [
|
||||
{
|
||||
type: "text",
|
||||
text: item.title,
|
||||
@@ -103,7 +103,7 @@ export function createListCard(title: string, items: ListItem[]): FlexBubble {
|
||||
];
|
||||
|
||||
if (item.subtitle) {
|
||||
itemContents.push({
|
||||
itemContentsLocal.push({
|
||||
type: "text",
|
||||
text: item.subtitle,
|
||||
size: "sm",
|
||||
@@ -140,7 +140,7 @@ export function createListCard(title: string, items: ListItem[]): FlexBubble {
|
||||
{
|
||||
type: "box",
|
||||
layout: "vertical",
|
||||
contents: itemContents,
|
||||
contents: itemContentsLocal,
|
||||
flex: 1,
|
||||
} as FlexBox,
|
||||
],
|
||||
|
||||
@@ -14,11 +14,14 @@ import {
|
||||
} from "./rich-menu.js";
|
||||
|
||||
const { setRichMenuImageMock, MessagingApiBlobClientMock } = vi.hoisted(() => {
|
||||
const setRichMenuImageMock = vi.fn();
|
||||
const MessagingApiBlobClientMock = vi.fn(function () {
|
||||
return { setRichMenuImage: setRichMenuImageMock };
|
||||
const setRichMenuImageMockLocal = vi.fn();
|
||||
const MessagingApiBlobClientMockLocal = vi.fn(function () {
|
||||
return { setRichMenuImage: setRichMenuImageMockLocal };
|
||||
});
|
||||
return { setRichMenuImageMock, MessagingApiBlobClientMock };
|
||||
return {
|
||||
setRichMenuImageMock: setRichMenuImageMockLocal,
|
||||
MessagingApiBlobClientMock: MessagingApiBlobClientMockLocal,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@line/bot-sdk", () => ({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user