test: add mock tracking diagnostics for Forgejo CI debugging

DI fix passes locally in grunt-runner Docker but still fails on
actual Forgejo runner. Adding diagnostic tests to isolate whether
mock() tracking itself is broken, or something else in the runner
environment causes it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vlad MocanuandClaude Opus 4.6 committed 2026-02-26 10:45:09 +02:00
1 parent a6b3c94004
commit 7011b0f71a
2 files changed
+164 -9

No files matched your search

+10 -9
View File
@@ -31,17 +31,18 @@ Systematically exercise each untested scenario on the live Forgejo instance (git
- Ensure the workflow has the correct permissions for each trigger type
- **Validation**: Workflow YAML includes all required `on:` triggers
- [x] **M1.5: Fix image-downloader test failures blocking CI**
- [ ] **M1.5: Fix image-downloader test failures blocking CI**
- **Problem**: All 18 `downloadCommentImages` tests failed on Forgejo CI (Linux Bun 1.2.21) while passing locally on macOS. This blocked all PRs.
- **Root cause**: `mock.module("fs/promises")` on Linux Bun poisons the entire mock tracking system — not just fs mocks, but ALL mock/spy assertions in the file (octokit parameter mocks, `spyOn(global, "fetch")`, etc.) stopped recording calls. The only passing test used a **negative** assertion (`not.toHaveBeenCalledWith`) which trivially passes when tracking is broken.
- **History of failed attempts**:
1. Upstream uses `spyOn(fs, "mkdir")` — works on GitHub CI (Bun 1.2.12/ubuntu) but fails on Forgejo Linux (different `fs/promises` object references between test and source modules)
- **Problem**: All 18 `downloadCommentImages` tests failed on Forgejo CI (Linux Bun 1.2.21 in `harbor.wxs.ro/wxs/grunt-runner:v1.9.0`) while passing locally on macOS. This blocks all PRs.
- **What we know**: On this Linux Docker image, `mock()` and `spyOn()` call tracking from `bun:test` is broken for this file. Even with DI (no module-level mocking at all), mock functions report "not called" despite being invoked. The ONLY passing test uses a negative assertion. This is NOT a `mock.module` issue — it affects plain `mock()` functions passed as parameters too.
- **History of failed attempts** (5 so far):
1. Upstream uses `spyOn(fs, "mkdir")` — works on GitHub CI (Bun 1.2.12/ubuntu) but fails on Forgejo Linux
2. `96ec04e` tried `_fsForTest` mutable export — abandoned
3. `cff360d` tried dependency injection — replaced
4. `dab92ef` tried `mock.module("fs/promises")` — works on macOS, poisons mock tracking on Linux
- **Fix**: Dependency injection via optional `fsDeps` parameter on `downloadCommentImages()`. Tests pass mock `mkdir`/`writeFile` directly — no module-level mocking at all. Production callers omit the parameter and get real `fs`. Zero `mock.module` usage in the test file.
- **Validation**: 797/797 tests pass locally, typecheck clean. Awaiting CI confirmation.
3. `cff360d` tried dependency injection for fs mocks — still failed on CI
4. `dab92ef` tried `mock.module("fs/promises")` — works on macOS, fails on Linux
5. `c6ca8a7` removed `mock.module` entirely, used DI with `fsDeps` param — **still fails on CI** (mock tracking broken at a deeper level)
- **Next step**: Set up local Docker reproduction using `grunt-runner:v1.9.0` to debug interactively
- **Validation**: Pending — need local Docker repro first
- [ ] **M2: Test PR review comment trigger**
+154
View File
@@ -0,0 +1,154 @@
/**
* Diagnostic test file to debug mock tracking failures on Forgejo CI.
* Tests pass locally (macOS + Docker grunt-runner) but image-downloader
* tests fail on the actual Forgejo runner. This file isolates mock/spy
* tracking behavior to find the exact failure point.
*
* DELETE THIS FILE once the root cause is found and fixed.
*/
import {
describe,
test,
expect,
spyOn,
mock,
beforeEach,
afterEach,
} from "bun:test";
describe("DEBUG: mock tracking diagnostics", () => {
test("basic mock() tracks calls", () => {
const fn = mock(() => 42);
fn("hello");
console.log("[DIAG] mock.calls:", JSON.stringify(fn.mock.calls));
expect(fn).toHaveBeenCalledWith("hello");
expect(fn).toHaveBeenCalledTimes(1);
});
test("mock() tracks calls through object property", () => {
const obj = { method: mock(() => "ok") };
obj.method("arg1", "arg2");
console.log(
"[DIAG] obj.method.mock.calls:",
JSON.stringify(obj.method.mock.calls),
);
expect(obj.method).toHaveBeenCalledWith("arg1", "arg2");
});
test("mock() tracks calls after destructuring", () => {
const fn = mock(() => Promise.resolve("done"));
const obj = { fn };
const { fn: extracted } = obj;
extracted("test");
console.log(
"[DIAG] destructured mock.calls:",
JSON.stringify(fn.mock.calls),
);
expect(fn).toHaveBeenCalledWith("test");
});
test("mock() tracks calls when passed as parameter", async () => {
const mockFn = mock(() => Promise.resolve(undefined));
const deps = { action: mockFn as (...args: unknown[]) => Promise<void> };
async function doWork(d: {
action: (...args: unknown[]) => Promise<void>;
}) {
const { action } = d;
await action("path", { recursive: true });
}
await doWork(deps);
console.log(
"[DIAG] param-passed mock.calls:",
JSON.stringify(mockFn.mock.calls),
);
expect(mockFn).toHaveBeenCalledWith("path", { recursive: true });
});
test("spyOn(console) tracks calls", () => {
const spy = spyOn(console, "warn").mockImplementation(() => {});
console.warn("test warning");
// Check raw calls before expect
console.log(
"[DIAG] console.warn spy.mock.calls:",
JSON.stringify(spy.mock.calls),
);
expect(spy).toHaveBeenCalledWith("test warning");
spy.mockRestore();
});
test("spyOn(global, 'fetch') tracks calls", async () => {
const fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
arrayBuffer: async () => new ArrayBuffer(0),
} as Response);
await fetch("https://example.com");
console.log(
"[DIAG] fetch spy.mock.calls:",
JSON.stringify(fetchSpy.mock.calls),
);
expect(fetchSpy).toHaveBeenCalledWith("https://example.com");
fetchSpy.mockRestore();
});
test("mock() with mockClear preserves tracking", () => {
const fn = mock(() => "first");
fn("before-clear");
fn.mockClear();
fn("after-clear");
console.log(
"[DIAG] after mockClear, calls:",
JSON.stringify(fn.mock.calls),
);
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("after-clear");
});
describe("mock created at describe scope", () => {
const describeMock = mock(() => Promise.resolve(undefined as any));
beforeEach(() => {
describeMock.mockClear();
});
test("tracks calls from describe-scoped mock", async () => {
await describeMock("test-arg");
console.log(
"[DIAG] describe-scope mock.calls:",
JSON.stringify(describeMock.mock.calls),
);
expect(describeMock).toHaveBeenCalledWith("test-arg");
});
test("tracks calls via DI pattern (like image-downloader)", async () => {
const deps = {
mkdir: describeMock as any,
};
// Simulate the exact pattern from image-downloader.ts
const { mkdir } = deps;
await mkdir("/tmp/github-images", { recursive: true });
console.log(
"[DIAG] DI-pattern mock.calls:",
JSON.stringify(describeMock.mock.calls),
);
expect(describeMock).toHaveBeenCalledWith("/tmp/github-images", {
recursive: true,
});
});
});
describe("env var diagnostics", () => {
test("GITHUB_SERVER_URL value", () => {
console.log("[DIAG] GITHUB_SERVER_URL:", process.env.GITHUB_SERVER_URL);
console.log("[DIAG] FORGEJO:", process.env.FORGEJO);
console.log("[DIAG] RUNNER_OS:", process.env.RUNNER_OS);
console.log("[DIAG] Bun.version:", Bun.version);
console.log("[DIAG] process.arch:", process.arch);
console.log("[DIAG] process.platform:", process.platform);
expect(true).toBe(true);
});
});
});