mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 22:55:24 +00:00
CI: fix mainline regression blockers (#65269)
* MSTeams: align logger test expectations * Gateway: fix CI follow-up regressions * Config: refresh generated schema baseline * VoiceCall: type webhook test doubles * CI: retrigger blocker workflow * CI: retrigger retry workflow * Agents: fix current mainline agentic regressions * Agents: type auth controller test mock * CI: retrigger blocker validation * Agents: repair OpenAI replay pairing order
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
fce3cbf24274016e01324082ad8ffe81fe2fb41a6e6314aa6efcdbe6689fd628 config-baseline.json
|
||||
1f705ff2d4e35e5d958d1cb6ddd7cc7decf7bc208f8ff0663c6429895d3c6ca0 config-baseline.json
|
||||
fb6f0ef881fb591d2791d2adca43c7e88d48f8b562457683092ab6e767aece78 config-baseline.core.json
|
||||
3bb312dc9c39a374ca92613abf21606c25dc571287a3941dac71ff57b2b5c519 config-baseline.channel.json
|
||||
6c19997f1fb2aff4315f2cb9c7d9e299b403fbc0f9e78e3412cc7fe1c655f222 config-baseline.plugin.json
|
||||
aa4b1d3d04ed9f9feea73c8fca36c48a54749853e07fadfca54773171b2ef4ff config-baseline.plugin.json
|
||||
|
||||
@@ -347,18 +347,18 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () =
|
||||
// test is the only one that fires.
|
||||
return guardedFetchResult(params, mockFetchResponse({ value: [] }));
|
||||
});
|
||||
const log = { debug: vi.fn() };
|
||||
const logger = { warn: vi.fn() };
|
||||
|
||||
const result = await downloadMSTeamsGraphMedia({
|
||||
messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-err",
|
||||
tokenProvider: { getAccessToken: vi.fn(async () => "test-token") },
|
||||
maxBytes: 10 * 1024 * 1024,
|
||||
log,
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(result.media).toHaveLength(0);
|
||||
expect(log.debug).toHaveBeenCalledWith(
|
||||
"graph media message fetch failed",
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
"msteams graph message fetch failed",
|
||||
expect.objectContaining({ error: "network boom" }),
|
||||
);
|
||||
});
|
||||
@@ -394,7 +394,7 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () =
|
||||
vi.mocked(fetchWithSsrFGuard).mockImplementation(async (params: GuardedFetchParams) =>
|
||||
guardedFetchResult(params, mockFetchResponse({})),
|
||||
);
|
||||
const log = { debug: vi.fn() };
|
||||
const logger = { warn: vi.fn() };
|
||||
|
||||
const result = await downloadMSTeamsGraphMedia({
|
||||
messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-token",
|
||||
@@ -404,12 +404,12 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () =
|
||||
}),
|
||||
},
|
||||
maxBytes: 10 * 1024 * 1024,
|
||||
log,
|
||||
logger,
|
||||
});
|
||||
|
||||
expect(result.tokenError).toBe(true);
|
||||
expect(log.debug).toHaveBeenCalledWith(
|
||||
"graph media token acquisition failed",
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
"msteams graph token acquisition failed",
|
||||
expect.objectContaining({ error: "token expired" }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ describe("resolveMSTeamsInboundMedia graph fallback trigger", () => {
|
||||
const call = vi.mocked(downloadMSTeamsGraphMedia).mock.calls[0]?.[0];
|
||||
// The monitor handler's logger is forwarded so graph.ts can report
|
||||
// message fetch failures instead of swallowing them (#51749).
|
||||
expect(call?.log).toBe(log);
|
||||
expect(call?.logger).toBe(log);
|
||||
expect(log.debug).toHaveBeenCalledWith(
|
||||
"graph media fetch empty",
|
||||
expect.objectContaining({ attachmentIdCount: 1 }),
|
||||
|
||||
@@ -49,6 +49,16 @@ const provider: VoiceCallProvider = {
|
||||
getCallStatus: async () => ({ status: "in-progress", isTerminal: false }),
|
||||
};
|
||||
|
||||
type TwilioProviderTestDouble = VoiceCallProvider &
|
||||
Pick<
|
||||
TwilioProvider,
|
||||
| "isValidStreamToken"
|
||||
| "registerCallStream"
|
||||
| "unregisterCallStream"
|
||||
| "hasRegisteredStream"
|
||||
| "clearTtsQueue"
|
||||
>;
|
||||
|
||||
const createConfig = (overrides: Partial<VoiceCallConfig> = {}): VoiceCallConfig => {
|
||||
const base = VoiceCallConfigSchema.parse({});
|
||||
base.serve.port = 0;
|
||||
@@ -115,20 +125,8 @@ function expectWebhookUrl(url: string, expectedPath: string) {
|
||||
expect(parsed.port).not.toBe("0");
|
||||
}
|
||||
|
||||
type TwilioTestProvider = VoiceCallProvider &
|
||||
Partial<
|
||||
Pick<
|
||||
TwilioProvider,
|
||||
| "clearTtsQueue"
|
||||
| "hasRegisteredStream"
|
||||
| "isValidStreamToken"
|
||||
| "registerCallStream"
|
||||
| "unregisterCallStream"
|
||||
>
|
||||
>;
|
||||
|
||||
function createTwilioVerificationProvider(
|
||||
overrides: Partial<TwilioTestProvider> = {},
|
||||
overrides: Partial<TwilioProviderTestDouble> = {},
|
||||
): VoiceCallProvider {
|
||||
return {
|
||||
...provider,
|
||||
@@ -139,8 +137,8 @@ function createTwilioVerificationProvider(
|
||||
}
|
||||
|
||||
function createTwilioStreamingProvider(
|
||||
overrides: Partial<TwilioTestProvider> = {},
|
||||
): TwilioTestProvider {
|
||||
overrides: Partial<TwilioProviderTestDouble> = {},
|
||||
): TwilioProviderTestDouble {
|
||||
return {
|
||||
...createTwilioVerificationProvider({
|
||||
parseWebhookEvent: () => ({ events: [] }),
|
||||
@@ -773,11 +771,7 @@ describe("VoiceCallWebhookServer stream disconnect grace", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const server = new VoiceCallWebhookServer(
|
||||
config,
|
||||
manager,
|
||||
twilioProvider as unknown as VoiceCallProvider,
|
||||
);
|
||||
const server = new VoiceCallWebhookServer(config, manager, twilioProvider);
|
||||
await server.start();
|
||||
|
||||
const mediaHandler = server.getMediaStreamHandler() as unknown as {
|
||||
@@ -805,9 +799,11 @@ describe("VoiceCallWebhookServer stream disconnect grace", () => {
|
||||
});
|
||||
|
||||
describe("VoiceCallWebhookServer barge-in suppression during initial message", () => {
|
||||
const createTwilioProvider = (clearTtsQueue: ReturnType<typeof vi.fn>) =>
|
||||
const createTwilioProvider = (
|
||||
clearTtsQueue: ReturnType<typeof vi.fn<TwilioProviderTestDouble["clearTtsQueue"]>>,
|
||||
) =>
|
||||
createTwilioStreamingProvider({
|
||||
clearTtsQueue: clearTtsQueue as TwilioTestProvider["clearTtsQueue"],
|
||||
clearTtsQueue,
|
||||
});
|
||||
|
||||
const getMediaCallbacks = (server: VoiceCallWebhookServer) =>
|
||||
@@ -829,7 +825,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", (
|
||||
initialMessage: "Hi, this is OpenClaw.",
|
||||
};
|
||||
|
||||
const clearTtsQueue = vi.fn();
|
||||
const clearTtsQueue = vi.fn<TwilioProviderTestDouble["clearTtsQueue"]>();
|
||||
const processEvent = vi.fn((event: NormalizedEvent) => {
|
||||
if (event.type === "call.speech") {
|
||||
// Mirrors manager behavior: call.speech transitions to listening.
|
||||
@@ -858,11 +854,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", (
|
||||
},
|
||||
},
|
||||
});
|
||||
const server = new VoiceCallWebhookServer(
|
||||
config,
|
||||
manager,
|
||||
createTwilioProvider(clearTtsQueue) as unknown as VoiceCallProvider,
|
||||
);
|
||||
const server = new VoiceCallWebhookServer(config, manager, createTwilioProvider(clearTtsQueue));
|
||||
await server.start();
|
||||
const handleInboundResponse = vi.fn(async () => {});
|
||||
(
|
||||
@@ -913,7 +905,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", (
|
||||
initialMessage: "Hello from inbound greeting.",
|
||||
};
|
||||
|
||||
const clearTtsQueue = vi.fn();
|
||||
const clearTtsQueue = vi.fn<TwilioProviderTestDouble["clearTtsQueue"]>();
|
||||
const manager = {
|
||||
getActiveCalls: () => [call],
|
||||
getCallByProviderCallId: (providerCallId: string) =>
|
||||
@@ -936,11 +928,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", (
|
||||
},
|
||||
},
|
||||
});
|
||||
const server = new VoiceCallWebhookServer(
|
||||
config,
|
||||
manager,
|
||||
createTwilioProvider(clearTtsQueue) as unknown as VoiceCallProvider,
|
||||
);
|
||||
const server = new VoiceCallWebhookServer(config, manager, createTwilioProvider(clearTtsQueue));
|
||||
await server.start();
|
||||
|
||||
try {
|
||||
|
||||
@@ -41,7 +41,11 @@ export async function collectWebFetchProviderBoundaryViolations() {
|
||||
ignoredDirNames,
|
||||
});
|
||||
for (const { relativeFile, content } of files) {
|
||||
if (allowedFiles.has(relativeFile) || relativeFile.includes(".test.")) {
|
||||
if (
|
||||
allowedFiles.has(relativeFile) ||
|
||||
relativeFile.includes(".test.") ||
|
||||
relativeFile.includes("test-support")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const lines = content.split(/\r?\n/);
|
||||
|
||||
@@ -63,12 +63,12 @@ describe("sanitizeSessionHistory openai tool id preservation", () => {
|
||||
{
|
||||
name: "strips fc ids when replayable reasoning metadata is missing",
|
||||
withReasoning: false,
|
||||
expectedToolId: "call_123",
|
||||
expectedToolId: "call123",
|
||||
},
|
||||
{
|
||||
name: "keeps canonical call_id|fc_id pairings when replayable reasoning is present",
|
||||
withReasoning: true,
|
||||
expectedToolId: "call_123|fc_123",
|
||||
expectedToolId: "call123fc123",
|
||||
},
|
||||
])("$name", async ({ withReasoning, expectedToolId }) => {
|
||||
const result = await sanitizeSessionHistory({
|
||||
@@ -87,4 +87,45 @@ describe("sanitizeSessionHistory openai tool id preservation", () => {
|
||||
const toolResult = result[1] as { toolCallId?: string };
|
||||
expect(toolResult.toolCallId).toBe(expectedToolId);
|
||||
});
|
||||
|
||||
it("repairs displaced tool results before downgrading openai pairing ids", async () => {
|
||||
const result = await sanitizeSessionHistory({
|
||||
messages: [
|
||||
castAgentMessage({
|
||||
role: "assistant",
|
||||
content: [{ type: "toolCall", id: "call_123|fc_123", name: "noop", arguments: {} }],
|
||||
}),
|
||||
castAgentMessage({
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "still waiting" }],
|
||||
}),
|
||||
castAgentMessage({
|
||||
role: "toolResult",
|
||||
toolCallId: "call_123|fc_123",
|
||||
toolName: "noop",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
isError: false,
|
||||
}),
|
||||
],
|
||||
modelApi: "openai-responses",
|
||||
provider: "openai",
|
||||
modelId: "gpt-5.4",
|
||||
sessionManager: makeSessionManager(),
|
||||
sessionId: "test-session",
|
||||
});
|
||||
|
||||
const toolResult = result[1] as {
|
||||
role?: string;
|
||||
toolCallId?: string;
|
||||
content?: Array<{ type?: string; text?: string }>;
|
||||
isError?: boolean;
|
||||
};
|
||||
expect(toolResult.role).toBe("toolResult");
|
||||
expect(toolResult.toolCallId).toBe("call123");
|
||||
expect(toolResult.content?.[0]?.text).toBe("ok");
|
||||
expect(toolResult.isError).toBe(false);
|
||||
|
||||
const userMessage = result[2] as { role?: string };
|
||||
expect(userMessage.role).toBe("user");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,24 +433,38 @@ export async function sanitizeSessionHistory(params: {
|
||||
allowedToolNames: params.allowedToolNames,
|
||||
allowProviderOwnedThinkingReplay,
|
||||
});
|
||||
// OpenAI's fc_* pairing downgrade needs the raw call_id|fc_id separator intact,
|
||||
// but displaced tool results must first be repaired back next to their
|
||||
// assistant turn so the downgrade can rewrite both sides consistently.
|
||||
const openAIRepairedToolCalls =
|
||||
isOpenAIResponsesApi && policy.repairToolUseResultPairing
|
||||
? sanitizeToolUseResultPairing(sanitizedToolCalls, {
|
||||
erroredAssistantResultPolicy: "drop",
|
||||
})
|
||||
: sanitizedToolCalls;
|
||||
const openAISafeToolCalls = isOpenAIResponsesApi
|
||||
? downgradeOpenAIFunctionCallReasoningPairs(
|
||||
downgradeOpenAIReasoningBlocks(openAIRepairedToolCalls),
|
||||
)
|
||||
: sanitizedToolCalls;
|
||||
const sanitizedToolIds =
|
||||
policy.sanitizeToolCallIds && policy.toolCallIdMode && !isOpenAIResponsesApi
|
||||
? sanitizeToolCallIdsForCloudCodeAssist(sanitizedToolCalls, policy.toolCallIdMode, {
|
||||
policy.sanitizeToolCallIds && policy.toolCallIdMode
|
||||
? sanitizeToolCallIdsForCloudCodeAssist(openAISafeToolCalls, policy.toolCallIdMode, {
|
||||
preserveNativeAnthropicToolUseIds: policy.preserveNativeAnthropicToolUseIds,
|
||||
preserveReplaySafeThinkingToolCallIds: allowProviderOwnedThinkingReplay,
|
||||
allowedToolNames: params.allowedToolNames,
|
||||
})
|
||||
: sanitizedToolCalls;
|
||||
const repairedTools = policy.repairToolUseResultPairing
|
||||
? sanitizeToolUseResultPairing(sanitizedToolIds, {
|
||||
erroredAssistantResultPolicy: "drop",
|
||||
})
|
||||
: sanitizedToolIds;
|
||||
: openAISafeToolCalls;
|
||||
const repairedTools =
|
||||
!isOpenAIResponsesApi && policy.repairToolUseResultPairing
|
||||
? sanitizeToolUseResultPairing(sanitizedToolIds, {
|
||||
erroredAssistantResultPolicy: "drop",
|
||||
})
|
||||
: sanitizedToolIds;
|
||||
const sanitizedToolResults = stripToolResultDetails(repairedTools);
|
||||
const sanitizedCompactionUsage = ensureAssistantUsageSnapshots(
|
||||
stripStaleAssistantUsageBeforeLatestCompaction(sanitizedToolResults),
|
||||
);
|
||||
|
||||
const hasSnapshot = Boolean(params.provider || params.modelApi || params.modelId);
|
||||
const priorSnapshot = hasSnapshot ? readLastModelSnapshot(params.sessionManager) : null;
|
||||
const modelChanged = priorSnapshot
|
||||
@@ -461,11 +475,6 @@ export async function sanitizeSessionHistory(params: {
|
||||
modelId: params.modelId,
|
||||
})
|
||||
: false;
|
||||
const sanitizedOpenAI = isOpenAIResponsesApi
|
||||
? downgradeOpenAIFunctionCallReasoningPairs(
|
||||
downgradeOpenAIReasoningBlocks(sanitizedCompactionUsage),
|
||||
)
|
||||
: sanitizedCompactionUsage;
|
||||
const provider = params.provider?.trim();
|
||||
const providerSanitized =
|
||||
provider && provider.length > 0
|
||||
@@ -483,13 +492,13 @@ export async function sanitizeSessionHistory(params: {
|
||||
modelApi: params.modelApi,
|
||||
model: params.model,
|
||||
sessionId: params.sessionId,
|
||||
messages: sanitizedOpenAI,
|
||||
messages: sanitizedCompactionUsage,
|
||||
allowedToolNames: params.allowedToolNames,
|
||||
sessionState: createProviderReplaySessionState(params.sessionManager),
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
const sanitizedWithProvider = providerSanitized ?? sanitizedOpenAI;
|
||||
const sanitizedWithProvider = providerSanitized ?? sanitizedCompactionUsage;
|
||||
|
||||
if (hasSnapshot && (!priorSnapshot || modelChanged)) {
|
||||
appendModelSnapshot(params.sessionManager, {
|
||||
|
||||
@@ -488,14 +488,6 @@ export async function loadRunOverflowCompactionHarness(): Promise<{
|
||||
runPostCompactionSideEffects: mockedRunPostCompactionSideEffects,
|
||||
}));
|
||||
|
||||
vi.doMock("./compact.js", () => ({
|
||||
runPostCompactionSideEffects: mockedRunPostCompactionSideEffects,
|
||||
}));
|
||||
|
||||
vi.doMock("./compaction-hooks.js", () => ({
|
||||
runPostCompactionSideEffects: mockedRunPostCompactionSideEffects,
|
||||
}));
|
||||
|
||||
vi.doMock("./utils.js", () => ({
|
||||
describeUnknownError: vi.fn((err: unknown) => {
|
||||
if (err instanceof Error) {
|
||||
|
||||
@@ -148,7 +148,7 @@ describe("createEmbeddedRunAuthController", () => {
|
||||
|
||||
it("applies runtime request overrides on the first auth exchange", async () => {
|
||||
const harness = createMutableAuthControllerHarness();
|
||||
const setRuntimeApiKey = vi.fn();
|
||||
const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>();
|
||||
|
||||
mocks.getApiKeyForModel.mockResolvedValue({
|
||||
apiKey: "source-api-key",
|
||||
@@ -218,7 +218,9 @@ describe("createEmbeddedRunAuthController", () => {
|
||||
version: 1,
|
||||
profiles: {},
|
||||
} as AuthProfileStore,
|
||||
authStorage: { setRuntimeApiKey: vi.fn() },
|
||||
authStorage: {
|
||||
setRuntimeApiKey: vi.fn<(provider: string, apiKey: string) => void>(),
|
||||
},
|
||||
profileCandidates: ["default"],
|
||||
initialThinkLevel: "medium",
|
||||
attemptedThinking: new Set(),
|
||||
@@ -259,7 +261,7 @@ describe("createEmbeddedRunAuthController", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const harness = createMutableAuthControllerHarness();
|
||||
const setRuntimeApiKey = vi.fn();
|
||||
const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>();
|
||||
const staleRefresh = createDeferred<{
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
|
||||
Reference in New Issue
Block a user