076e96f3de
This PR fixes the issue where console monitoring was empty when a specific shard was targeted on the official Screeps server. ### Changes: - **Unified Subscription**: Changed WebSocket subscription path from `shard/console` (invalid) to the global `console` channel. - **Shard Filtering**: Implemented client-side filtering in `handleConsoleEvent` to only display logs matching the targeted shard. - **Precise Timing**: Retained the shard-specific `api.time()` call for accurate tick duration tracking. - **Tests**: Added and updated 48 unit tests verifying the fix and shard filtering logic. - **Distribution**: Rebuilt `dist/index.js` and bumped version to `v0.2.1`. Reviewed-on: #87
634 lines
21 KiB
JavaScript
634 lines
21 KiB
JavaScript
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
|
// ── mock @actions/core so tests never touch real CI outputs ─────────────────
|
|
vi.mock("@actions/core", () => ({
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warning: vi.fn(),
|
|
setFailed: vi.fn(),
|
|
setOutput: vi.fn(),
|
|
startGroup: vi.fn(),
|
|
endGroup: vi.fn(),
|
|
}));
|
|
|
|
import * as artifact from "@actions/artifact";
|
|
vi.mock("@actions/artifact", () => {
|
|
const mockClient = {
|
|
uploadArtifact: vi.fn().mockResolvedValue({}),
|
|
};
|
|
return {
|
|
create: vi.fn(() => mockClient),
|
|
};
|
|
});
|
|
|
|
import * as core from "@actions/core";
|
|
import fs from "fs";
|
|
import os from "os";
|
|
import path from "path";
|
|
|
|
import {
|
|
isOfficialServer,
|
|
buildSubscribePath,
|
|
detectTraceback,
|
|
detectWarning,
|
|
outputMultiline,
|
|
buildProgressMessage,
|
|
writeLogFile,
|
|
uploadLogArtifacts,
|
|
handleConsoleEvent,
|
|
monitorConsole,
|
|
} from "../monitor.js";
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// Pure helpers
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe("isOfficialServer", () => {
|
|
it("returns true for screeps.com", () => {
|
|
expect(isOfficialServer("screeps.com")).toBe(true);
|
|
});
|
|
|
|
it("returns false for a private hostname", () => {
|
|
expect(isOfficialServer("builder64")).toBe(false);
|
|
});
|
|
|
|
it("returns false for an IP address", () => {
|
|
expect(isOfficialServer("192.168.1.10")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("buildSubscribePath", () => {
|
|
it("returns console for official server (no shard provided)", () => {
|
|
expect(buildSubscribePath("screeps.com")).toBe("console");
|
|
});
|
|
|
|
it("returns console for private server (no shard provided)", () => {
|
|
expect(buildSubscribePath("builder64")).toBe("console");
|
|
});
|
|
|
|
it("returns console when shard is provided (official)", () => {
|
|
expect(buildSubscribePath("screeps.com", "shard3")).toBe("console");
|
|
});
|
|
|
|
it("returns console when shard is provided (private)", () => {
|
|
expect(buildSubscribePath("builder64", "myshard")).toBe("console");
|
|
});
|
|
});
|
|
|
|
describe("detectTraceback", () => {
|
|
it("returns true when error contains a stack frame line", () => {
|
|
const error =
|
|
"TypeError: Cannot read properties of undefined\n at Object.<anonymous> (main:1:42)";
|
|
expect(detectTraceback(error)).toBe(true);
|
|
});
|
|
|
|
it("returns false for a plain error message without stack frames", () => {
|
|
expect(detectTraceback("Something went wrong")).toBe(false);
|
|
});
|
|
|
|
it("returns false for null", () => {
|
|
expect(detectTraceback(null)).toBe(false);
|
|
});
|
|
|
|
it("returns false for undefined", () => {
|
|
expect(detectTraceback(undefined)).toBe(false);
|
|
});
|
|
|
|
it("returns false for an empty string", () => {
|
|
expect(detectTraceback("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("detectWarning", () => {
|
|
it("returns true for a line with orange font tag", () => {
|
|
const lines = ["<font color='orange'>WARN: low energy</font>"];
|
|
expect(detectWarning(lines)).toBe(true);
|
|
});
|
|
|
|
it("returns true for a line with yellow font tag", () => {
|
|
const lines = ['<font color="yellow">WARN: something</font>'];
|
|
expect(detectWarning(lines)).toBe(true);
|
|
});
|
|
|
|
it("returns false for a plain log line", () => {
|
|
expect(detectWarning(["Tick 123: harvesting"])).toBe(false);
|
|
});
|
|
|
|
it("returns false for an empty array", () => {
|
|
expect(detectWarning([])).toBe(false);
|
|
});
|
|
|
|
it("returns false for null", () => {
|
|
expect(detectWarning(null)).toBe(false);
|
|
});
|
|
|
|
it("returns true when only one line in a mixed array is a warning", () => {
|
|
const lines = ["normal line", "<font color='orange'>warn</font>"];
|
|
expect(detectWarning(lines)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("outputMultiline", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("prefixes lines with shard when provided", () => {
|
|
outputMultiline("line1\nline2", "info", "shard0");
|
|
expect(core.info).toHaveBeenCalledWith("[shard0] line1");
|
|
expect(core.info).toHaveBeenCalledWith("[shard0] line2");
|
|
});
|
|
|
|
it("does not prefix when shard is missing", () => {
|
|
outputMultiline("line1", "info");
|
|
expect(core.info).toHaveBeenCalledWith("line1");
|
|
});
|
|
|
|
it("uses core.warning for level=warning", () => {
|
|
outputMultiline("warn", "warning", "s0");
|
|
expect(core.warning).toHaveBeenCalledWith("[s0] warn");
|
|
});
|
|
|
|
it("uses core.error for level=error", () => {
|
|
outputMultiline("err", "error", "s0");
|
|
expect(core.error).toHaveBeenCalledWith("[s0] err");
|
|
});
|
|
});
|
|
|
|
describe("buildProgressMessage", () => {
|
|
it("formats correctly at 0 elapsed", () => {
|
|
expect(buildProgressMessage(0, 50)).toBe("[Monitor] 0/50 ticks elapsed...");
|
|
});
|
|
|
|
it("formats correctly midway", () => {
|
|
expect(buildProgressMessage(25, 50)).toBe(
|
|
"[Monitor] 25/50 ticks elapsed...",
|
|
);
|
|
});
|
|
|
|
it("formats correctly at 100%", () => {
|
|
expect(buildProgressMessage(50, 50)).toBe(
|
|
"[Monitor] 50/50 ticks elapsed...",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("uploadLogArtifacts", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("instantiates client and calls uploadArtifact", async () => {
|
|
await uploadLogArtifacts(
|
|
["/path/to/shard0_console_log.txt"],
|
|
"custom-name",
|
|
);
|
|
expect(artifact.create().uploadArtifact).toHaveBeenCalledWith(
|
|
"custom-name",
|
|
["/path/to/shard0_console_log.txt"],
|
|
"/path/to",
|
|
{ continueOnError: true },
|
|
);
|
|
});
|
|
|
|
it("does nothing if filePaths is empty", async () => {
|
|
await uploadLogArtifacts([]);
|
|
expect(artifact.create().uploadArtifact).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// handleConsoleEvent
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe("handleConsoleEvent", () => {
|
|
let state;
|
|
let shardBuffers;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
state = { sawTraceback: false, sawErrorLog: false, sawWarningLog: false };
|
|
shardBuffers = {};
|
|
});
|
|
|
|
const makeEvent = (log = [], results = [], error = null) => ({
|
|
data: { messages: { log, results }, error },
|
|
});
|
|
|
|
it("calls core.info for each stdout line with shard prefix when logToFile=false", () => {
|
|
const event = {
|
|
data: {
|
|
shard: "shard0",
|
|
messages: { log: ["line1"], results: ["line2"] },
|
|
},
|
|
};
|
|
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
|
|
expect(core.info).toHaveBeenCalledWith("[shard0] line1");
|
|
expect(core.info).toHaveBeenCalledWith("[shard0] line2");
|
|
expect(Object.keys(shardBuffers)).toHaveLength(0);
|
|
});
|
|
|
|
it("buffers stdout when logToFile=true; does not call core.info", () => {
|
|
const event = {
|
|
data: { shard: "shard0", messages: { log: ["line1", "line2"] } },
|
|
};
|
|
handleConsoleEvent(event, { logToFile: true }, shardBuffers, state);
|
|
expect(core.info).not.toHaveBeenCalled();
|
|
expect(shardBuffers["shard0"]).toEqual(["line1", "line2"]);
|
|
});
|
|
|
|
it("includes results lines in output", () => {
|
|
const event = makeEvent([], ["result1"]);
|
|
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
|
|
expect(core.info).toHaveBeenCalledWith("result1");
|
|
});
|
|
|
|
it("calls core.error when error field is non-empty", () => {
|
|
const event = makeEvent([], [], "Script crashed");
|
|
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
|
|
expect(core.error).toHaveBeenCalledWith("Script crashed");
|
|
expect(state.sawErrorLog).toBe(true);
|
|
});
|
|
|
|
it("sets state.sawTraceback when error contains stack frames", () => {
|
|
const error = "TypeError: boom\n at Object.<anonymous> (main:1:1)";
|
|
const event = makeEvent([], [], error);
|
|
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
|
|
expect(state.sawTraceback).toBe(true);
|
|
expect(state.sawErrorLog).toBe(true);
|
|
});
|
|
|
|
it("does not set sawTraceback for plain error without stack frames", () => {
|
|
const event = makeEvent([], [], "Script error: low CPU");
|
|
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
|
|
expect(state.sawTraceback).toBe(false);
|
|
expect(state.sawErrorLog).toBe(true);
|
|
});
|
|
|
|
it("sets state.sawWarningLog and calls core.warning for warn lines", () => {
|
|
const event = makeEvent(["<font color='orange'>low energy</font>"]);
|
|
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state);
|
|
expect(state.sawWarningLog).toBe(true);
|
|
expect(core.warning).toHaveBeenCalled();
|
|
});
|
|
|
|
it("calls core.warning regardless of logToFile setting", () => {
|
|
const event = makeEvent(["<font color='orange'>warn line</font>"]);
|
|
handleConsoleEvent(event, { logToFile: true }, shardBuffers, state);
|
|
expect(core.warning).toHaveBeenCalled();
|
|
});
|
|
|
|
it("handles missing messages gracefully (no crash on empty event)", () => {
|
|
const event = { data: {} };
|
|
expect(() =>
|
|
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("handles completely empty event gracefully", () => {
|
|
const event = {};
|
|
expect(() =>
|
|
handleConsoleEvent(event, { logToFile: false }, shardBuffers, state),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("filters messages by shard when targetShard is provided", () => {
|
|
const event = { data: { shard: "shard0", messages: { log: ["msg0"] } } };
|
|
handleConsoleEvent(event, { shard: "shard1" }, shardBuffers, state);
|
|
expect(core.info).not.toHaveBeenCalled();
|
|
|
|
handleConsoleEvent(event, { shard: "shard0" }, shardBuffers, state);
|
|
expect(core.info).toHaveBeenCalledWith("[shard0] msg0");
|
|
});
|
|
|
|
it("does not filter when targetShard is undefined", () => {
|
|
const event = { data: { shard: "shard0", messages: { log: ["msg0"] } } };
|
|
handleConsoleEvent(event, {}, shardBuffers, state);
|
|
expect(core.info).toHaveBeenCalledWith("[shard0] msg0");
|
|
});
|
|
|
|
it("buffers messages separately for different shards", () => {
|
|
const event0 = { data: { shard: "shard0", messages: { log: ["msg0"] } } };
|
|
const event1 = { data: { shard: "shard1", messages: { log: ["msg1"] } } };
|
|
handleConsoleEvent(event0, { logToFile: true }, shardBuffers, state);
|
|
handleConsoleEvent(event1, { logToFile: true }, shardBuffers, state);
|
|
expect(shardBuffers["shard0"]).toEqual(["msg0"]);
|
|
expect(shardBuffers["shard1"]).toEqual(["msg1"]);
|
|
});
|
|
|
|
it("uses 'default' key when shard is missing in event", () => {
|
|
const event = { data: { messages: { log: ["msg"] } } };
|
|
handleConsoleEvent(event, { logToFile: true }, shardBuffers, state);
|
|
expect(shardBuffers["default"]).toEqual(["msg"]);
|
|
});
|
|
});
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// writeLogFile
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
describe("writeLogFile", () => {
|
|
let tempDir;
|
|
let tempFile;
|
|
|
|
beforeEach(async () => {
|
|
tempDir = await fs.promises.mkdtemp(
|
|
path.join(os.tmpdir(), "monitor-test-"),
|
|
);
|
|
tempFile = path.join(tempDir, "log.txt");
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("writes all lines to file joined by newlines", async () => {
|
|
await writeLogFile(["line1", "line2", "line3"], tempFile);
|
|
const content = await fs.promises.readFile(tempFile, "utf8");
|
|
expect(content).toBe("line1\nline2\nline3");
|
|
});
|
|
|
|
it("writes an empty file when given an empty array", async () => {
|
|
await writeLogFile([], tempFile);
|
|
const content = await fs.promises.readFile(tempFile, "utf8");
|
|
expect(content).toBe("");
|
|
});
|
|
});
|
|
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
// monitorConsole — integration with mocked API
|
|
// ────────────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Builds a mock ScreepsAPI that:
|
|
* - api.time(shard) returns ticks from a list on each call
|
|
* - api.socket.connect() → resolves immediately
|
|
* - api.socket.subscribe() → resolves immediately (stores the callback)
|
|
* - api.socket.disconnect()→ no-op
|
|
*/
|
|
function buildMockApi({
|
|
ticks = [100, 101, 102, 103, 104, 105],
|
|
hostname = "builder64",
|
|
} = {}) {
|
|
let tickIndex = 0;
|
|
let consoleCallback = null;
|
|
|
|
const socket = {
|
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
subscribe: vi.fn().mockImplementation((_path, cb) => {
|
|
consoleCallback = cb;
|
|
return Promise.resolve();
|
|
}),
|
|
disconnect: vi.fn(),
|
|
};
|
|
|
|
const api = {
|
|
opts: { hostname },
|
|
time: vi.fn().mockImplementation(() => {
|
|
const t = ticks[Math.min(tickIndex, ticks.length - 1)];
|
|
tickIndex++;
|
|
return Promise.resolve({ time: t });
|
|
}),
|
|
socket,
|
|
// Expose so tests can fire console events
|
|
_fireConsole: (eventData) => {
|
|
if (consoleCallback) {
|
|
consoleCallback({ data: eventData });
|
|
}
|
|
},
|
|
};
|
|
|
|
return api;
|
|
}
|
|
|
|
const BASE_OPTS = {
|
|
monitor: 3,
|
|
logToFile: false,
|
|
onTraceback: "fail",
|
|
onErrorLog: "warn",
|
|
onWarningLog: "ignore",
|
|
monitorInterval: 2,
|
|
hostname: "builder64",
|
|
};
|
|
|
|
describe("monitorConsole", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it("returns all three flags false on a clean run", async () => {
|
|
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
|
|
const result = await monitorConsole(api, BASE_OPTS);
|
|
expect(result).toEqual({
|
|
sawTraceback: false,
|
|
sawErrorLog: false,
|
|
sawWarningLog: false,
|
|
});
|
|
});
|
|
|
|
it("calls api.socket.connect() exactly once", async () => {
|
|
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
|
|
await monitorConsole(api, BASE_OPTS);
|
|
expect(api.socket.connect).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("calls api.socket.disconnect() after completion", async () => {
|
|
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
|
|
await monitorConsole(api, BASE_OPTS);
|
|
expect(api.socket.disconnect).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("subscribes to 'console' for a private server", async () => {
|
|
const api = buildMockApi({
|
|
hostname: "builder64",
|
|
ticks: [100, 101, 102, 103],
|
|
});
|
|
await monitorConsole(api, { ...BASE_OPTS, hostname: "builder64" });
|
|
expect(api.socket.subscribe).toHaveBeenCalledWith(
|
|
"console",
|
|
expect.any(Function),
|
|
);
|
|
});
|
|
|
|
it("subscribes to 'console' for the official server (default)", async () => {
|
|
const api = buildMockApi({
|
|
hostname: "screeps.com",
|
|
ticks: [100, 101, 102, 103],
|
|
});
|
|
await monitorConsole(api, { ...BASE_OPTS, hostname: "screeps.com" });
|
|
expect(api.socket.subscribe).toHaveBeenCalledWith(
|
|
"console",
|
|
expect.any(Function),
|
|
);
|
|
});
|
|
|
|
it("subscribes to 'console' even when custom shard is provided", async () => {
|
|
const api = buildMockApi({
|
|
hostname: "screeps.com",
|
|
ticks: [100, 101, 102, 103],
|
|
});
|
|
await monitorConsole(api, {
|
|
...BASE_OPTS,
|
|
hostname: "screeps.com",
|
|
shard: "shard3",
|
|
});
|
|
expect(api.socket.subscribe).toHaveBeenCalledWith(
|
|
"console",
|
|
expect.any(Function),
|
|
);
|
|
// Verify polling still uses shard3 for timing
|
|
expect(api.time).toHaveBeenCalledWith("shard3");
|
|
});
|
|
|
|
it("returns sawTraceback=true when a traceback event arrives", async () => {
|
|
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
|
|
// Schedule console event before poll advances
|
|
setTimeout(() => {
|
|
api._fireConsole({
|
|
messages: { log: [], results: [] },
|
|
error: "TypeError: boom\n at Object.<anonymous> (main:1:1)",
|
|
});
|
|
}, 0);
|
|
const result = await monitorConsole(api, BASE_OPTS);
|
|
expect(result.sawTraceback).toBe(true);
|
|
});
|
|
|
|
it("returns sawErrorLog=true when an error (no traceback) event arrives", async () => {
|
|
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
|
|
setTimeout(() => {
|
|
api._fireConsole({
|
|
messages: { log: [], results: [] },
|
|
error: "Script error: CPU limit exceeded",
|
|
});
|
|
}, 0);
|
|
const result = await monitorConsole(api, BASE_OPTS);
|
|
expect(result.sawErrorLog).toBe(true);
|
|
expect(result.sawTraceback).toBe(false);
|
|
});
|
|
|
|
it("returns sawWarningLog=true when a warn log line arrives", async () => {
|
|
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
|
|
setTimeout(() => {
|
|
api._fireConsole({
|
|
messages: {
|
|
log: ["<font color='orange'>low energy</font>"],
|
|
results: [],
|
|
},
|
|
error: null,
|
|
});
|
|
}, 0);
|
|
const result = await monitorConsole(api, BASE_OPTS);
|
|
expect(result.sawWarningLog).toBe(true);
|
|
});
|
|
|
|
it("logs progress via core.info at monitorInterval boundaries when logToFile=true", async () => {
|
|
// ticks: start=100, then 101,102(interval),103(done at delta=3)
|
|
const api = buildMockApi({ ticks: [100, 100, 101, 102, 103] });
|
|
await monitorConsole(api, {
|
|
...BASE_OPTS,
|
|
logToFile: true,
|
|
monitorInterval: 2,
|
|
});
|
|
// Progress should be logged when elapsed reaches 2
|
|
const infoCalls = core.info.mock.calls.map((c) => c[0]);
|
|
expect(infoCalls.some((m) => m.includes("2/3"))).toBe(true);
|
|
});
|
|
|
|
it("calls api.socket.disconnect() even if an error occurs during polling", async () => {
|
|
const api = buildMockApi({ ticks: [100] });
|
|
// Make time() reject after first call
|
|
let calls = 0;
|
|
api.time = vi.fn().mockImplementation(() => {
|
|
calls++;
|
|
if (calls > 1) return Promise.reject(new Error("network error"));
|
|
return Promise.resolve({ time: 100 });
|
|
});
|
|
await expect(monitorConsole(api, BASE_OPTS)).rejects.toThrow(
|
|
"network error",
|
|
);
|
|
expect(api.socket.disconnect).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("exits early if a traceback occurs and onTraceback='fail'", async () => {
|
|
// startTick=100, monitor=10 ticks.
|
|
// If it didn't exit early, it would call api.time() many times.
|
|
const api = buildMockApi({
|
|
ticks: [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110],
|
|
});
|
|
|
|
// Fire a traceback event after the first poll
|
|
setTimeout(() => {
|
|
api._fireConsole({
|
|
messages: { log: [], results: [] },
|
|
error:
|
|
"TypeError: fail-fast test\n at Object.<anonymous> (main:1:1)",
|
|
});
|
|
}, 100);
|
|
|
|
const result = await monitorConsole(api, {
|
|
...BASE_OPTS,
|
|
monitor: 10,
|
|
onTraceback: "fail",
|
|
});
|
|
|
|
expect(result.sawTraceback).toBe(true);
|
|
// We expect it to have called api.time fewer than 10 times (excluding the startTick call)
|
|
expect(api.time.mock.calls.length).toBeLessThan(10);
|
|
});
|
|
|
|
it("detects encoded tracebacks in log lines", async () => {
|
|
const api = buildMockApi({ ticks: [100, 101, 102] });
|
|
setTimeout(() => {
|
|
api._fireConsole({
|
|
messages: {
|
|
log: [
|
|
"Error: ReferenceError: a is not defined%0A at eval (eval at <anonymous> (_console1778948572008_0:1:46), <anonymous>:1:1)%0A at _console1778948572008_0:1:46",
|
|
],
|
|
results: [],
|
|
},
|
|
error: null,
|
|
});
|
|
}, 50);
|
|
|
|
const result = await monitorConsole(api, {
|
|
...BASE_OPTS,
|
|
onTraceback: "fail",
|
|
});
|
|
expect(result.sawTraceback).toBe(true);
|
|
expect(result.sawErrorLog).toBe(true);
|
|
});
|
|
|
|
it("creates separate log files for different shards when logToFile=true", async () => {
|
|
const api = buildMockApi({ ticks: [100, 101, 102, 103] });
|
|
|
|
setTimeout(() => {
|
|
api._fireConsole({
|
|
shard: "shard0",
|
|
messages: { log: ["msg0"], results: [] },
|
|
error: null,
|
|
});
|
|
api._fireConsole({
|
|
shard: "shard1",
|
|
messages: { log: ["msg1"], results: [] },
|
|
error: null,
|
|
});
|
|
}, 50);
|
|
|
|
// Verify uploadArtifact was called with two files
|
|
await monitorConsole(api, {
|
|
...BASE_OPTS,
|
|
logToFile: true,
|
|
});
|
|
|
|
expect(artifact.create().uploadArtifact).toHaveBeenCalledWith(
|
|
"screeps-console-log",
|
|
expect.arrayContaining([
|
|
expect.stringContaining("shard0_console_log.txt"),
|
|
expect.stringContaining("shard1_console_log.txt"),
|
|
]),
|
|
expect.any(String),
|
|
{ continueOnError: true },
|
|
);
|
|
});
|
|
});
|