NEO-55: make inventory Bruno tests order-independent

Shared inventory-api-helper seeds stock and records baselines in
pre-request scripts; assertions use exact deltas instead of cumulative
at.least checks. Harden C# remove round-trip add assertion.
pull/90/head
VinPropane 2026-05-23 22:57:53 -04:00
parent 00ada50b95
commit 1cbe6302b5
7 changed files with 122 additions and 29 deletions

View File

@ -6,12 +6,11 @@ meta {
docs {
NEO-55: per-player inventory read model; join with GET /game/world/item-definitions for stackMax and slot kinds.
Does not assume a fresh server — dev-local-1 inventory persists across Bruno runs (in-memory/Postgres).
Restart server (or truncate DB in Postgres mode) to see an all-empty grid again.
Order-independent: validates fixed slot arrays and slot JSON shape only (not empty-grid assumptions).
}
get {
url: {{baseUrl}}/game/players/dev-local-1/inventory
url: {{baseUrl}}/game/players/{{playerId}}/inventory
body: none
auth: none
}
@ -22,7 +21,7 @@ tests {
expect(res.getHeader("content-type")).to.contain("application/json");
const body = res.getBody();
expect(body.schemaVersion).to.equal(1);
expect(body.playerId).to.equal("dev-local-1");
expect(body.playerId).to.equal(bru.getEnvVar("playerId") || bru.getVar("playerId"));
expect(body.bagSlots).to.be.an("array");
expect(body.bagSlots.length).to.equal(24);
expect(body.equipmentSlots).to.be.an("array");

View File

@ -5,11 +5,17 @@ meta {
}
docs {
NEO-55: unknown itemId denies with invalid_item and echoes unchanged inventory.
NEO-55: unknown itemId denies with invalid_item. Pre-request snapshots scrap_metal_bulk total to prove inventory unchanged on deny.
}
script:pre-request {
const { getInventory, sumItemQuantity } = require("./inventory-api-helper.js");
const inventory = await getInventory(bru);
bru.setVar("scrapBeforeDeny", sumItemQuantity(inventory, "scrap_metal_bulk"));
}
post {
url: {{baseUrl}}/game/players/dev-local-1/inventory
url: {{baseUrl}}/game/players/{{playerId}}/inventory
body: json
auth: none
}
@ -24,7 +30,7 @@ body:json {
}
tests {
test("denies with invalid_item and echoes inventory", function () {
test("denies with invalid_item and leaves scrap_metal_bulk unchanged", function () {
expect(res.getStatus()).to.equal(200);
const body = res.getBody();
expect(body.schemaVersion).to.equal(1);
@ -32,5 +38,10 @@ tests {
expect(body.reasonCode).to.equal("invalid_item");
expect(body.inventory).to.be.an("object");
expect(body.inventory.bagSlots.length).to.equal(24);
const before = bru.getVar("scrapBeforeDeny");
const after = [...body.inventory.bagSlots, ...body.inventory.equipmentSlots]
.filter((slot) => slot.itemId === "scrap_metal_bulk")
.reduce((total, slot) => total + slot.quantity, 0);
expect(after).to.equal(before);
});
}

View File

@ -5,11 +5,18 @@ meta {
}
docs {
NEO-55: add stack via mutationKind add; see server README inventory HTTP section.
NEO-55: add stack via mutationKind add. Pre-request records scrap_metal_bulk baseline so assertions are order-independent.
}
script:pre-request {
const { getInventory, sumItemQuantity } = require("./inventory-api-helper.js");
const inventory = await getInventory(bru);
bru.setVar("scrapBeforeAdd", sumItemQuantity(inventory, "scrap_metal_bulk"));
bru.setVar("addQuantity", 5);
}
post {
url: {{baseUrl}}/game/players/dev-local-1/inventory
url: {{baseUrl}}/game/players/{{playerId}}/inventory
body: json
auth: none
}
@ -24,18 +31,19 @@ body:json {
}
tests {
test("add returns 200 with applied true and updated inventory", function () {
test("add returns 200 with applied true and increases scrap by requested quantity", function () {
expect(res.getStatus()).to.equal(200);
const body = res.getBody();
expect(body.schemaVersion).to.equal(1);
expect(body.applied).to.equal(true);
expect(body.reasonCode).to.equal(null);
expect(body.inventory).to.be.an("object");
expect(body.inventory.playerId).to.equal("dev-local-1");
const occupied = body.inventory.bagSlots.filter((s) => s.quantity > 0);
expect(occupied.length).to.be.at.least(1);
const scrap = occupied.find((s) => s.itemId === "scrap_metal_bulk");
expect(scrap).to.be.an("object");
expect(scrap.quantity).to.be.at.least(5);
expect(body.inventory.playerId).to.equal(bru.getEnvVar("playerId") || bru.getVar("playerId"));
const before = bru.getVar("scrapBeforeAdd");
const addQuantity = bru.getVar("addQuantity");
const after = [...body.inventory.bagSlots, ...body.inventory.equipmentSlots]
.filter((slot) => slot.itemId === "scrap_metal_bulk")
.reduce((total, slot) => total + slot.quantity, 0);
expect(after).to.equal(before + addQuantity);
});
}

View File

@ -5,12 +5,19 @@ meta {
}
docs {
NEO-55: remove stack via mutationKind remove. Run Post inventory add first on a fresh server,
or expect applied true when dev-local-1 already holds scrap_metal_bulk.
NEO-55: remove stack via mutationKind remove. Pre-request seeds scrap_metal_bulk stock when needed so this request is order-independent.
}
script:pre-request {
const { ensureItemQuantity } = require("./inventory-api-helper.js");
const removeQuantity = 2;
const before = await ensureItemQuantity(bru, "scrap_metal_bulk", removeQuantity);
bru.setVar("scrapBeforeRemove", before);
bru.setVar("removeQuantity", removeQuantity);
}
post {
url: {{baseUrl}}/game/players/dev-local-1/inventory
url: {{baseUrl}}/game/players/{{playerId}}/inventory
body: json
auth: none
}
@ -25,17 +32,18 @@ body:json {
}
tests {
test("remove returns 200 with applied true and reduced quantity", function () {
test("remove returns 200 with applied true and decreases scrap by requested quantity", function () {
expect(res.getStatus()).to.equal(200);
const body = res.getBody();
expect(body.schemaVersion).to.equal(1);
expect(body.applied).to.equal(true);
expect(body.reasonCode).to.equal(null);
expect(body.inventory).to.be.an("object");
const scrap = body.inventory.bagSlots.find((s) => s.itemId === "scrap_metal_bulk");
if (scrap) {
expect(scrap.quantity).to.be.a("number");
expect(scrap.quantity).to.be.at.least(0);
}
const before = bru.getVar("scrapBeforeRemove");
const removeQuantity = bru.getVar("removeQuantity");
const after = [...body.inventory.bagSlots, ...body.inventory.equipmentSlots]
.filter((slot) => slot.itemId === "scrap_metal_bulk")
.reduce((total, slot) => total + slot.quantity, 0);
expect(after).to.equal(before - removeQuantity);
});
}

View File

@ -3,7 +3,5 @@ meta {
}
docs {
NEO-55 inventory HTTP. GET does not assume an empty grid — dev-local-1 inventory persists across runs.
POST add/remove mutate dev-local-1; cumulative stacks from prior runs make add assertions use at.least(5).
Restart server (or truncate DB in Postgres mode) for a clean inventory baseline.
NEO-55 inventory HTTP. Requests are order-independent: pre-request scripts record baselines or seed stock via inventory-api-helper.js.
}

View File

@ -0,0 +1,65 @@
const axios = require("axios");
function resolveConfig(bru) {
return {
baseUrl: bru.getEnvVar("baseUrl") || bru.getVar("baseUrl"),
playerId: bru.getEnvVar("playerId") || bru.getVar("playerId"),
jsonHeaders: { headers: { "Content-Type": "application/json" } },
};
}
function inventoryUrl(baseUrl, playerId) {
return `${baseUrl}/game/players/${playerId}/inventory`;
}
function sumItemQuantity(inventory, itemId) {
const slots = [...inventory.bagSlots, ...inventory.equipmentSlots];
return slots
.filter((slot) => slot.itemId === itemId)
.reduce((total, slot) => total + slot.quantity, 0);
}
async function getInventory(bru) {
const { baseUrl, playerId } = resolveConfig(bru);
const response = await axios.get(inventoryUrl(baseUrl, playerId));
if (response.status !== 200) {
throw new Error(`GET inventory failed: ${response.status}`);
}
return response.data;
}
async function postMutation(bru, body) {
const { baseUrl, playerId, jsonHeaders } = resolveConfig(bru);
return axios.post(inventoryUrl(baseUrl, playerId), body, jsonHeaders);
}
async function ensureItemQuantity(bru, itemId, minQuantity) {
const inventory = await getInventory(bru);
const current = sumItemQuantity(inventory, itemId);
if (current >= minQuantity) {
return current;
}
const response = await postMutation(bru, {
schemaVersion: 1,
mutationKind: "add",
itemId,
quantity: minQuantity - current,
});
if (response.status !== 200 || response.data?.applied !== true) {
throw new Error(
`seed add ${itemId} failed: ${response.status} ${JSON.stringify(response.data)}`,
);
}
const after = await getInventory(bru);
return sumItemQuantity(after, itemId);
}
module.exports = {
sumItemQuantity,
getInventory,
postMutation,
ensureItemQuantity,
};

View File

@ -147,7 +147,7 @@ public sealed class PlayerInventoryApiTests
var client = factory.CreateClient();
// Act
_ = await client.PostAsJsonAsync(
var addResponse = await client.PostAsJsonAsync(
"/game/players/dev-local-1/inventory",
Mutation("add", "scrap_metal_bulk", quantity: 10));
var response = await client.PostAsJsonAsync(
@ -155,6 +155,10 @@ public sealed class PlayerInventoryApiTests
Mutation("remove", "scrap_metal_bulk", quantity: 4));
// Assert
Assert.Equal(HttpStatusCode.OK, addResponse.StatusCode);
var addBody = await addResponse.Content.ReadFromJsonAsync<PlayerInventoryMutationResponse>();
Assert.NotNull(addBody);
Assert.True(addBody!.Applied);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<PlayerInventoryMutationResponse>();
Assert.NotNull(body);