API: Standardize "nextCompletion" promise in tasks (#2687)

This commit is contained in:
catloversg
2026-04-28 13:49:53 +07:00
committed by GitHub
parent ee3014b029
commit 36e1adf2d2
72 changed files with 1750 additions and 364 deletions
+319 -1
View File
@@ -1,5 +1,21 @@
import { installAugmentations } from "../../../src/Augmentation/AugmentationHelpers";
import { AugmentationName, CompanyName, CompletedProgramName, FactionName, JobField, JobName } from "@enums";
import {
AugmentationName,
BladeburnerContractName,
BladeburnerGeneralActionName,
CityName,
CompanyName,
CompletedProgramName,
CrimeType,
FactionName,
FactionWorkType,
GymType,
JobField,
JobName,
LocationName,
SpecialBladeburnerActionTypeForSleeve,
UniversityClassType,
} from "@enums";
import { Player } from "@player";
import { prestigeSourceFile } from "../../../src/Prestige";
import { disconnectServers, GetServerOrThrow } from "../../../src/Server/AllServers";
@@ -14,6 +30,7 @@ import { CompanyPositions } from "../../../src/Company/CompanyPositions";
import { getTorRouter } from "../../../src/Server/ServerHelpers";
import * as exceptionAlertModule from "../../../src/utils/helpers/exceptionAlert";
import { numberOfBlackOperations } from "../../../src/Bladeburner/data/BlackOperations";
import type { SleeveTask, Task } from "@nsdefs";
const nextBN = 4;
@@ -844,3 +861,304 @@ describe("Intelligence", () => {
expectIntelligenceExp(50);
});
});
const nextCompletionTestCases = [
{
action: () =>
expect(
getNS().singularity.universityCourse(LocationName.Sector12RothmanUniversity, UniversityClassType.algorithms),
).toStrictEqual(true),
taskType: "CLASS",
isPlayerTask: true,
},
{
action: () =>
expect(getNS().singularity.gymWorkout(LocationName.Sector12PowerhouseGym, GymType.strength)).toStrictEqual(true),
taskType: "CLASS",
isPlayerTask: true,
},
{
action: () => {
const ns = getNS();
ns.singularity.applyToCompany(LocationName.Sector12JoesGuns, JobField.employee);
expect(ns.singularity.workForCompany(LocationName.Sector12JoesGuns)).toStrictEqual(true);
},
taskType: "COMPANY",
isPlayerTask: true,
},
{
action: () => expect(getNS().singularity.createProgram(CompletedProgramName.bruteSsh)).toStrictEqual(true),
taskType: "CREATE_PROGRAM",
isPlayerTask: true,
},
{
action: () => {
const ns = getNS();
ns.singularity.commitCrime(CrimeType.mug);
expect(ns.singularity.getCurrentWork()?.type === "CRIME").toStrictEqual(true);
},
taskType: "CRIME",
isPlayerTask: true,
},
{
action: () =>
expect(getNS().singularity.workForFaction(FactionName.Sector12, FactionWorkType.hacking)).toStrictEqual(true),
taskType: "FACTION",
isPlayerTask: true,
},
{
action: () => {
const ns = getNS();
ns.singularity.travelToCity(CityName.NewTokyo);
expect(ns.grafting.graftAugmentation(AugmentationName.Targeting1)).toStrictEqual(true);
},
taskType: "GRAFTING",
isPlayerTask: true,
},
{
action: () =>
expect(
getNS().sleeve.setToUniversityCourse(0, LocationName.Sector12RothmanUniversity, UniversityClassType.algorithms),
).toStrictEqual(true),
taskType: "CLASS",
isPlayerTask: false,
},
{
action: () =>
expect(getNS().sleeve.setToGymWorkout(0, LocationName.Sector12PowerhouseGym, GymType.strength)).toStrictEqual(
true,
),
taskType: "CLASS",
isPlayerTask: false,
},
{
action: () => {
const ns = getNS();
ns.singularity.applyToCompany(LocationName.Sector12JoesGuns, JobField.employee);
expect(ns.sleeve.setToCompanyWork(0, LocationName.Sector12JoesGuns)).toStrictEqual(true);
},
taskType: "COMPANY",
isPlayerTask: false,
},
{
action: () => expect(getNS().sleeve.setToCommitCrime(0, CrimeType.mug)).toStrictEqual(true),
taskType: "CRIME",
isPlayerTask: false,
},
{
action: () =>
expect(getNS().sleeve.setToFactionWork(0, FactionName.Sector12, FactionWorkType.hacking)).toStrictEqual(true),
taskType: "FACTION",
isPlayerTask: false,
},
{
action: () => expect(getNS().sleeve.setToShockRecovery(0)).toStrictEqual(true),
taskType: "RECOVERY",
isPlayerTask: false,
},
{
action: () => expect(getNS().sleeve.setToSynchronize(0)).toStrictEqual(true),
taskType: "SYNCHRO",
isPlayerTask: false,
},
{
action: () =>
expect(getNS().sleeve.setToBladeburnerAction(0, BladeburnerGeneralActionName.Training)).toStrictEqual(true),
taskType: "BLADEBURNER",
isPlayerTask: false,
},
{
action: () =>
expect(
getNS().sleeve.setToBladeburnerAction(0, SpecialBladeburnerActionTypeForSleeve.InfiltrateSynthoids),
).toStrictEqual(true),
taskType: "INFILTRATE",
isPlayerTask: false,
},
{
action: () =>
expect(
getNS().sleeve.setToBladeburnerAction(0, SpecialBladeburnerActionTypeForSleeve.SupportMainSleeve),
).toStrictEqual(true),
taskType: "SUPPORT",
isPlayerTask: false,
},
{
action: () =>
expect(
getNS().sleeve.setToBladeburnerAction(
0,
SpecialBladeburnerActionTypeForSleeve.TakeOnContracts,
BladeburnerContractName.Tracking,
),
).toStrictEqual(true),
taskType: "BLADEBURNER",
isPlayerTask: false,
},
] as const;
function assertNoCurrentTask(): void {
expect(Player.currentWork).toBeNull();
expect(Player.sleeves[0].currentWork).toBeNull();
}
async function testNextCompletion(
action: () => void,
taskType: Task["type"] | SleeveTask["type"],
isPlayerTask: boolean,
): Promise<void> {
const ns = getNS();
let isCompletable;
let isRepeatable = false;
switch (taskType) {
case "CLASS":
isCompletable = false;
break;
case "COMPANY":
isCompletable = false;
break;
case "CREATE_PROGRAM":
isCompletable = true;
break;
case "CRIME":
isCompletable = true;
isRepeatable = true;
break;
case "FACTION":
isCompletable = false;
break;
case "GRAFTING":
isCompletable = true;
break;
case "BLADEBURNER":
isCompletable = true;
isRepeatable = true;
break;
case "INFILTRATE":
isCompletable = true;
isRepeatable = true;
break;
case "RECOVERY":
isCompletable = false;
break;
case "SUPPORT":
isCompletable = false;
break;
case "SYNCHRO":
isCompletable = false;
break;
default: {
// Verify type switch statement is exhaustive
const __a: never = taskType;
throw new Error(`Invalid taskType: ${taskType}`);
}
}
const processTask = async (cycles: number) => {
if (isPlayerTask) {
Player.processWork(cycles);
} else {
Player.sleeves[0].currentWork?.process(Player.sleeves[0], cycles);
}
// Yield to the microtask queue.
await Promise.resolve();
};
const cancelTask = async () => {
if (isPlayerTask) {
ns.singularity.stopAction();
} else {
ns.sleeve.setToIdle(0);
}
// Yield to the microtask queue.
await Promise.resolve();
};
const assertCurrentTask = () => {
if (isPlayerTask) {
expect(Player.currentWork).not.toBeNull();
} else {
expect(Player.sleeves[0].currentWork).not.toBeNull();
}
};
let isResolved = false;
const setUpNextCompletionPromise = () => {
if (isPlayerTask) {
void ns.singularity.getCurrentWork()?.nextCompletion.then(() => (isResolved = true));
} else {
void ns.sleeve.getTask(0)?.nextCompletion.then(() => (isResolved = true));
}
};
assertNoCurrentTask();
action();
setUpNextCompletionPromise();
expect(isResolved).toStrictEqual(false);
// The current task should remain incomplete after 1 cycle.
await processTask(1);
assertCurrentTask();
expect(isResolved).toStrictEqual(false);
// Run many cycles to ensure all completable tasks are completed.
await processTask(1e4);
if (isCompletable) {
// The nextCompletion promise should be resolved now.
expect(isResolved).toStrictEqual(true);
if (isRepeatable) {
assertCurrentTask();
// Create the promise again to verify cancellation.
isResolved = false;
setUpNextCompletionPromise();
} else {
assertNoCurrentTask();
// Run the action again. We will cancel it later to verify cancellation.
if (taskType !== "GRAFTING") {
// Delete the completed program before creating it again.
if (taskType === "CREATE_PROGRAM") {
ns.rm(CompletedProgramName.bruteSsh);
}
action();
} else {
// Graft a different augmentation.
ns.grafting.graftAugmentation(AugmentationName.BitWire);
}
// Create the promise again to verify cancellation.
isResolved = false;
setUpNextCompletionPromise();
}
}
expect(isResolved).toStrictEqual(false);
// Verify cancellation.
await cancelTask();
assertNoCurrentTask();
expect(isResolved).toStrictEqual(true);
}
describe("nextCompletion", () => {
beforeEach(() => {
setupBasicTestingEnvironment();
Player.sourceFiles.set(7, 3);
Player.sourceFiles.set(10, 3);
prestigeSourceFile(true);
Player.money = 1e15;
gainTonsOfExp();
const ns = getNS();
expect(ns.bladeburner.joinBladeburnerDivision()).toStrictEqual(true);
if (!Player.bladeburner) {
throw new Error("Bladeburner was not initialized");
}
Player.bladeburner.contracts[BladeburnerContractName.Tracking].count = 1e6;
ns.singularity.checkFactionInvitations();
expect(ns.singularity.joinFaction(FactionName.Sector12)).toStrictEqual(true);
ns.sleeve.setToIdle(0);
});
test.each(nextCompletionTestCases)(
"Task type: $taskType - Is player task: $isPlayerTask",
async ({ action, taskType, isPlayerTask }) => {
await testNextCompletion(action, taskType, isPlayerTask);
},
);
});