mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
test(qqbot): fix macOS-only outbound media test failures
The scoped outbound media tests added in #92872 assert raw mkdtemp paths, but prod resolvers canonicalize roots, so on macOS the /var -> /private/var tmpdir symlink made three tests fail (green on Linux CI). Realpath the tmp roots like sibling tests, and export the real UploadDailyLimitExceededError from the outbound-dispatch sender.js mock so prod instanceof checks in error paths hit real assertions instead of vitest's missing-export guard. Also add AGENTS.md test guidance covering both patterns.
This commit is contained in:
parent
6da5db21a6
commit
7a92c473c1
3 changed files with 40 additions and 21 deletions
|
|
@ -219,6 +219,8 @@ Skills own workflows; root owns hard policy and routing.
|
|||
- Prefer behavior tests over workflow/docs string greps. Put operator policy reminders in AGENTS/docs.
|
||||
- QA scenario sources are YAML only: `qa/scenarios/index.yaml` and `qa/scenarios/<theme>/*.yaml`. Do not add fenced `qa-scenario`/`qa-flow` Markdown files under `qa/scenarios/`.
|
||||
- Clean timers/env/globals/mocks/sockets/temp dirs/module state; `--isolate=false` safe.
|
||||
- Tests asserting resolver/root-containment paths: `fs.realpath` mkdtemp/tmp roots first. macOS `os.tmpdir()` is a `/var` -> `/private/var` symlink; prod resolvers return canonical paths, so raw mkdtemp assertions pass on Linux CI but fail on Mac.
|
||||
- Explicit `vi.mock` factories must export every binding prod touches, including error classes used in `instanceof` checks; `vi.importActual` the defining module for those instead of stub classes.
|
||||
- Prefer injection and narrow `*.runtime.ts` mocks over broad barrels or `openclaw/plugin-sdk/*`.
|
||||
- Do not edit baseline/inventory/ignore/snapshot/expected-failure files to silence checks without explicit approval.
|
||||
- Do not run independent `pnpm test`/Vitest commands concurrently in one worktree; Vitest cache races with `ENOTEMPTY`. Group one command or use distinct `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH`.
|
||||
|
|
|
|||
|
|
@ -34,24 +34,31 @@ const sendTextMock = vi.hoisted(() =>
|
|||
);
|
||||
const audioFileToSilkBase64Mock = vi.hoisted(() => vi.fn(async () => "silk-base64"));
|
||||
|
||||
vi.mock("../messaging/sender.js", () => ({
|
||||
accountToCreds: (account: GatewayAccount) => ({
|
||||
appId: account.appId,
|
||||
clientSecret: account.clientSecret,
|
||||
}),
|
||||
buildDeliveryTarget: (target: { type: string; senderId: string; groupOpenid?: string }) => ({
|
||||
type: target.type === "group" ? "group" : target.type === "c2c" ? "c2c" : target.type,
|
||||
id: target.type === "group" ? target.groupOpenid : target.senderId,
|
||||
}),
|
||||
initApiConfig: vi.fn(),
|
||||
sendFileMessage: vi.fn(),
|
||||
sendImage: vi.fn(),
|
||||
sendText: sendTextMock,
|
||||
sendVideoMessage: vi.fn(),
|
||||
sendVoiceMessage: sendVoiceMessageMock,
|
||||
sendMedia: sendMediaMock,
|
||||
withTokenRetry: async (_creds: unknown, fn: () => Promise<unknown>) => await fn(),
|
||||
}));
|
||||
vi.mock("../messaging/sender.js", async () => {
|
||||
// Real error class so prod `instanceof UploadDailyLimitExceededError` checks
|
||||
// in error paths don't trip vitest's missing-export guard on this mock.
|
||||
const { UploadDailyLimitExceededError } =
|
||||
await vi.importActual<typeof import("../api/media-chunked.js")>("../api/media-chunked.js");
|
||||
return {
|
||||
accountToCreds: (account: GatewayAccount) => ({
|
||||
appId: account.appId,
|
||||
clientSecret: account.clientSecret,
|
||||
}),
|
||||
buildDeliveryTarget: (target: { type: string; senderId: string; groupOpenid?: string }) => ({
|
||||
type: target.type === "group" ? "group" : target.type === "c2c" ? "c2c" : target.type,
|
||||
id: target.type === "group" ? target.groupOpenid : target.senderId,
|
||||
}),
|
||||
initApiConfig: vi.fn(),
|
||||
sendFileMessage: vi.fn(),
|
||||
sendImage: vi.fn(),
|
||||
sendText: sendTextMock,
|
||||
sendVideoMessage: vi.fn(),
|
||||
sendVoiceMessage: sendVoiceMessageMock,
|
||||
sendMedia: sendMediaMock,
|
||||
UploadDailyLimitExceededError,
|
||||
withTokenRetry: async (_creds: unknown, fn: () => Promise<unknown>) => await fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../utils/image-size.js", async () => {
|
||||
const actual =
|
||||
|
|
@ -309,7 +316,9 @@ describe("dispatchOutbound", () => {
|
|||
});
|
||||
|
||||
it("loads scoped media through host read callbacks", async () => {
|
||||
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qqbot-host-read-"));
|
||||
// realpath: macOS tmpdir is a /var -> /private/var symlink and root
|
||||
// containment checks compare against canonicalized roots.
|
||||
const tmpRoot = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "qqbot-host-read-")));
|
||||
try {
|
||||
const mediaPath = path.join(tmpRoot, "host-report.txt");
|
||||
const mediaReadFile = vi.fn(async () => Buffer.from("host report"));
|
||||
|
|
@ -370,7 +379,11 @@ describe("dispatchOutbound", () => {
|
|||
});
|
||||
|
||||
it("lets missing voice files inside scoped outbound roots reach the voice wait path", async () => {
|
||||
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "qqbot-scoped-voice-"));
|
||||
// realpath: missing-path resolution returns canonicalized-root joins, so a
|
||||
// symlinked macOS tmpdir root would change the asserted voice path.
|
||||
const tmpRoot = await fs.realpath(
|
||||
await fs.mkdtemp(path.join(os.tmpdir(), "qqbot-scoped-voice-")),
|
||||
);
|
||||
try {
|
||||
const missingVoicePath = path.join(tmpRoot, "pending.wav");
|
||||
const runtime = makeRuntime({
|
||||
|
|
|
|||
|
|
@ -101,7 +101,11 @@ function makeCtx() {
|
|||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
originalOpenClawHome = process.env.OPENCLAW_HOME;
|
||||
openclawHome = await fs.mkdtemp(path.join(os.tmpdir(), "qqbot-host-read-voice-"));
|
||||
// realpath: macOS tmpdir is a /var -> /private/var symlink and trusted-root
|
||||
// resolution returns canonicalized paths that assertions compare against.
|
||||
openclawHome = await fs.realpath(
|
||||
await fs.mkdtemp(path.join(os.tmpdir(), "qqbot-host-read-voice-")),
|
||||
);
|
||||
process.env.OPENCLAW_HOME = openclawHome;
|
||||
audioPortMock.audioFileToSilkBase64.mockResolvedValue(undefined);
|
||||
audioPortMock.isAudioFile.mockReturnValue(true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue