diff --git a/src/RedPill.tsx b/src/RedPill.tsx
index 2b566b4d9..217c0aebc 100644
--- a/src/RedPill.tsx
+++ b/src/RedPill.tsx
@@ -10,6 +10,7 @@ import { Page } from "./ui/Router";
import { prestigeSourceFile } from "./Prestige";
import { getDefaultBitNodeOptions, setBitNodeOptions } from "./BitNode/BitNodeUtils";
import { prestigeWorkerScripts } from "./NetscriptWorker";
+import { exceptionAlert } from "./utils/helpers/exceptionAlert";
function giveSourceFile(bitNodeNumber: number): void {
const sourceFileKey = "SourceFile" + bitNodeNumber.toString();
@@ -76,14 +77,7 @@ export function enterBitNode(
try {
setBitNodeOptions(bitNodeOptions);
} catch (error) {
- dialogBoxCreate(
- <>
- Invalid BitNode options. This is a bug. Please report it to developers.
-
-
- {error instanceof Error ? error.stack : String(error)}
- >,
- );
+ exceptionAlert(error);
// Use default options
setBitNodeOptions(getDefaultBitNodeOptions());
}
diff --git a/src/Server/AllServers.ts b/src/Server/AllServers.ts
index f2c5ba30b..4b4fbd110 100644
--- a/src/Server/AllServers.ts
+++ b/src/Server/AllServers.ts
@@ -44,6 +44,25 @@ export function GetServer(s: string): BaseServer | null {
return null;
}
+/**
+ * In our codebase, we usually have to call GetServer() like this:
+ * ```
+ * const server = GetServer(hostname);
+ * if (!server) {
+ * throw new Error("Error message");
+ * }
+ * // Use server
+ * ```
+ * With this utility function, we don't need to write boilerplate code.
+ */
+export function GetServerOrThrow(serverId: string): BaseServer {
+ const server = GetServer(serverId);
+ if (!server) {
+ throw new Error(`Server ${serverId} does not exist.`);
+ }
+ return server;
+}
+
//Get server by IP or hostname. Returns null if invalid or unreachable.
export function GetReachableServer(s: string): BaseServer | null {
const server = GetServer(s);
diff --git a/test/jest/Netscript/RunScript.test.ts b/test/jest/Netscript/RunScript.test.ts
index 7e520cf7f..c4353c86e 100644
--- a/test/jest/Netscript/RunScript.test.ts
+++ b/test/jest/Netscript/RunScript.test.ts
@@ -13,9 +13,9 @@ declare const importActual: (typeof EvaluatorConfig)["doImport"];
// Replace Blob/ObjectURL functions, because they don't work natively in Jest
global.Blob = class extends Blob {
code: string;
- constructor(blobParts?: BlobPart[], options?: BlobPropertyBag) {
+ constructor(blobParts?: BlobPart[], __options?: BlobPropertyBag) {
super();
- this.code = (blobParts ?? [])[0] + "";
+ this.code = String((blobParts ?? [])[0]);
}
};
global.URL.revokeObjectURL = function () {};
@@ -77,11 +77,11 @@ test.each([
},
])("Netscript execution: $name", async function ({ expected: expectedLog, scripts }) {
global.URL.createObjectURL = function (blob) {
- return "data:text/javascript," + encodeURIComponent(blob.code);
+ return "data:text/javascript," + encodeURIComponent((blob as unknown as { code: string }).code);
};
let server = {} as Server;
- let eventDelete = () => {};
+ const eventDelete = () => {};
let alertDelete = () => {};
try {
const alerted = new Promise((resolve) => {
@@ -98,7 +98,7 @@ test.each([
const ramUsage = script.getRamUsage(server.scripts);
if (!ramUsage) throw new Error(`ramUsage calculated to be ${ramUsage}`);
- const runningScript = new RunningScript(script, ramUsage as number);
+ const runningScript = new RunningScript(script, ramUsage);
const pid = startWorkerScript(runningScript, server);
expect(pid).toBeGreaterThan(0);
// Manually attach an atExit to the now-created WorkerScript, so we can
@@ -107,6 +107,7 @@ test.each([
expect(ws).toBeDefined();
const result = await Promise.race([
alerted,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- ws was asserted above
new Promise((resolve) => (ws!.atExit = new Map([["default", resolve]]))),
]);
// If an error alert was thrown, we catch it here.
diff --git a/test/jest/Netscript/Singularity.test.ts b/test/jest/Netscript/Singularity.test.ts
new file mode 100644
index 000000000..b92bb26ff
--- /dev/null
+++ b/test/jest/Netscript/Singularity.test.ts
@@ -0,0 +1,221 @@
+import { installAugmentations } from "../../../src/Augmentation/AugmentationHelpers";
+import { blackOpsArray } from "../../../src/Bladeburner/data/BlackOperations";
+import { AugmentationName } from "../../../src/Enums";
+import { WorkerScript } from "../../../src/Netscript/WorkerScript";
+import { NetscriptFunctions, type NSFull } from "../../../src/NetscriptFunctions";
+import type { ScriptFilePath } from "../../../src/Paths/ScriptFilePath";
+import { PlayerObject } from "../../../src/PersonObjects/Player/PlayerObject";
+import { Player, setPlayer } from "../../../src/Player";
+import { RunningScript } from "../../../src/Script/RunningScript";
+import { GetServerOrThrow, initForeignServers, prestigeAllServers } from "../../../src/Server/AllServers";
+import { SpecialServers } from "../../../src/Server/data/SpecialServers";
+import { initSourceFiles } from "../../../src/SourceFile/SourceFiles";
+import { FormatsNeedToChange } from "../../../src/ui/formatNumber";
+import { Router } from "../../../src/ui/GameRoot";
+
+function setupBasicTestingEnvironment(): void {
+ prestigeAllServers();
+ setPlayer(new PlayerObject());
+ Player.init();
+ Player.sourceFiles.set(4, 3);
+ initForeignServers(Player.getHomeComputer());
+}
+
+function setNumBlackOpsComplete(value: number): void {
+ if (!Player.bladeburner) {
+ throw new Error("Invalid Bladeburner data");
+ }
+ Player.bladeburner.numBlackOpsComplete = value;
+}
+
+function getNS(): NSFull {
+ const home = GetServerOrThrow(SpecialServers.Home);
+ home.maxRam = 1024;
+ const filePath = "test.js" as ScriptFilePath;
+ home.writeToScriptFile(filePath, "");
+ const script = home.scripts.get(filePath);
+ if (!script) {
+ throw new Error("Invalid script");
+ }
+ const runningScript = new RunningScript(script, 1024);
+ const workerScript = new WorkerScript(runningScript, 1, NetscriptFunctions);
+ const ns = workerScript.env.vars;
+ if (!ns) {
+ throw new Error("Invalid NS instance");
+ }
+ return ns;
+}
+
+// We need to patch this function. Some APIs call it, but it only works properly after the main UI is loaded.
+Router.toPage = () => {};
+
+/**
+ * In src\ui\formatNumber.ts, there are some variables that need to be initialized before other functions can be
+ * called. We have to call FormatsNeedToChange.emit() to initialize those variables.
+ */
+FormatsNeedToChange.emit();
+
+initSourceFiles();
+
+const nextBN = 3;
+
+describe("b1tflum3", () => {
+ beforeEach(() => {
+ setupBasicTestingEnvironment();
+ Player.queueAugmentation(AugmentationName.TheRedPill);
+ installAugmentations();
+ Player.gainHackingExp(1e100);
+ const wdServer = GetServerOrThrow(SpecialServers.WorldDaemon);
+ wdServer.hasAdminRights = true;
+ });
+ // Make sure that the player is in the next BN without SF rewards.
+ const expectSucceedInB1tflum3 = () => {
+ expect(Player.bitNodeN).toStrictEqual(nextBN);
+ expect(Player.augmentations.length).toStrictEqual(0);
+ expect(Player.sourceFileLvl(1)).toStrictEqual(0);
+ };
+
+ describe("Success", () => {
+ test("Without BN options", () => {
+ const ns = getNS();
+ ns.singularity.b1tflum3(nextBN);
+ expectSucceedInB1tflum3();
+ });
+ test("With BN options", () => {
+ const ns = getNS();
+ ns.singularity.b1tflum3(nextBN, undefined, {
+ ...ns.getResetInfo().bitNodeOptions,
+ sourceFileOverrides: new Map(),
+ intelligenceOverride: 1,
+ });
+ expectSucceedInB1tflum3();
+ });
+ });
+
+ describe("Failure", () => {
+ // Make sure that the player is still in the same BN without SF rewards.
+ const expectFailToB1tflum3 = () => {
+ expect(Player.bitNodeN).toStrictEqual(1);
+ expect(Player.augmentations.length).toStrictEqual(1);
+ expect(Player.sourceFileLvl(1)).toStrictEqual(0);
+ };
+ test("Invalid intelligenceOverride", () => {
+ const ns = getNS();
+ expect(() => {
+ ns.singularity.b1tflum3(nextBN, undefined, {
+ ...ns.getResetInfo().bitNodeOptions,
+ intelligenceOverride: -1,
+ });
+ }).toThrow();
+ expectFailToB1tflum3();
+ });
+ test("Invalid sourceFileOverrides", () => {
+ const ns = getNS();
+ expect(() => {
+ ns.singularity.b1tflum3(nextBN, undefined, {
+ ...ns.getResetInfo().bitNodeOptions,
+ sourceFileOverrides: [] as unknown as Map,
+ });
+ }).toThrow();
+ expectFailToB1tflum3();
+ });
+ });
+});
+
+describe("destroyW0r1dD43m0n", () => {
+ beforeEach(() => {
+ setupBasicTestingEnvironment();
+ Player.queueAugmentation(AugmentationName.TheRedPill);
+ installAugmentations();
+ Player.gainHackingExp(1e100);
+ const wdServer = GetServerOrThrow(SpecialServers.WorldDaemon);
+ wdServer.hasAdminRights = true;
+ Player.startBladeburner();
+ setNumBlackOpsComplete(blackOpsArray.length);
+ });
+
+ describe("Success", () => {
+ // Make sure that the player is in the next BN and received SF rewards.
+ const expectSucceedInDestroyingWD = () => {
+ expect(Player.bitNodeN).toStrictEqual(nextBN);
+ expect(Player.augmentations.length).toStrictEqual(0);
+ expect(Player.sourceFileLvl(1)).toStrictEqual(1);
+ };
+ test("Hacking route", () => {
+ setNumBlackOpsComplete(0);
+ const ns = getNS();
+ ns.singularity.destroyW0r1dD43m0n(nextBN);
+ expectSucceedInDestroyingWD();
+ });
+ test("Hacking route with BN options", () => {
+ setNumBlackOpsComplete(0);
+ const ns = getNS();
+ ns.singularity.destroyW0r1dD43m0n(nextBN, undefined, {
+ ...ns.getResetInfo().bitNodeOptions,
+ sourceFileOverrides: new Map(),
+ intelligenceOverride: 1,
+ });
+ expectSucceedInDestroyingWD();
+ });
+ test("Bladeburner route", () => {
+ Player.skills.hacking = 0;
+ const ns = getNS();
+ ns.singularity.destroyW0r1dD43m0n(nextBN);
+ expectSucceedInDestroyingWD();
+ });
+ test("Bladeburner route with BN options", () => {
+ Player.skills.hacking = 0;
+ const ns = getNS();
+ ns.singularity.destroyW0r1dD43m0n(nextBN, undefined, {
+ ...ns.getResetInfo().bitNodeOptions,
+ sourceFileOverrides: new Map(),
+ intelligenceOverride: 1,
+ });
+ expectSucceedInDestroyingWD();
+ });
+ });
+
+ describe("Failure", () => {
+ // Make sure that the player is still in the same BN without SF rewards.
+ const expectFailToDestroyWD = () => {
+ expect(Player.bitNodeN).toStrictEqual(1);
+ expect(Player.augmentations.length).toStrictEqual(1);
+ expect(Player.sourceFileLvl(1)).toStrictEqual(0);
+ };
+ test("Do not have enough hacking level and numBlackOpsComplete", () => {
+ Player.skills.hacking = 0;
+ setNumBlackOpsComplete(0);
+ const ns = getNS();
+ ns.singularity.destroyW0r1dD43m0n(nextBN);
+ expectFailToDestroyWD();
+ });
+ test("Do not have admin rights on WD and do not have enough numBlackOpsComplete", () => {
+ const wdServer = GetServerOrThrow(SpecialServers.WorldDaemon);
+ wdServer.hasAdminRights = false;
+ setNumBlackOpsComplete(0);
+ const ns = getNS();
+ ns.singularity.destroyW0r1dD43m0n(nextBN);
+ expectFailToDestroyWD();
+ });
+ test("Invalid intelligenceOverride", () => {
+ const ns = getNS();
+ expect(() => {
+ ns.singularity.destroyW0r1dD43m0n(nextBN, undefined, {
+ ...ns.getResetInfo().bitNodeOptions,
+ intelligenceOverride: -1,
+ });
+ }).toThrow();
+ expectFailToDestroyWD();
+ });
+ test("Invalid sourceFileOverrides", () => {
+ const ns = getNS();
+ expect(() => {
+ ns.singularity.destroyW0r1dD43m0n(nextBN, undefined, {
+ ...ns.getResetInfo().bitNodeOptions,
+ sourceFileOverrides: [] as unknown as Map,
+ });
+ }).toThrow();
+ expectFailToDestroyWD();
+ });
+ });
+});