Files
rods/solver-api/tests/api.test.js
Conner Majic 725a72a773 Initial commit: establish deterministic rod-string solver stack.
Set up the C solver core, Node API orchestration, TS GUI workflow, and engineering documentation with cleaned repo hygiene for private Git hosting.

Made-with: Cursor
2026-04-16 21:59:42 -06:00

138 lines
5.8 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import request from "supertest";
import { describe, expect, it } from "vitest";
import { buildApp } from "../src/app.js";
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../");
const xml = fs.readFileSync(path.join(ROOT, "data/cases/base-case.xml"), "utf-8");
const defaultGoldenHash = fs
.readFileSync(path.join(ROOT, "data/golden/default.solve.sha256"), "utf-8")
.trim();
describe("solver-api", () => {
it("solves using default base case endpoint", async () => {
const app = buildApp();
const response = await request(app).get("/solve/default");
expect(response.status).toBe(200);
expect(response.body.runMetadata.source).toBe("base-case.xml");
expect(response.body.solver.pointCount).toBe(200);
});
it("returns fea prototype result and comparison payload", async () => {
const app = buildApp();
const response = await request(app).get("/solve/default?solverModel=fea");
expect(response.status).toBe(200);
expect(response.body.runMetadata.solverModel).toBe("fea");
expect(response.body.solver.pointCount).toBe(200);
expect(response.body.solvers.fdm.pointCount).toBe(200);
expect(response.body.solvers.fea.pointCount).toBe(200);
expect(response.body.comparison).toBeTruthy();
expect(response.body.comparison.schemaVersion).toBe(2);
expect(response.body.comparison.peakLoadDeltas).toBeTruthy();
expect(response.body.comparison.residualSummary.points).toBeGreaterThan(0);
expect(Array.isArray(response.body.comparison.pointwiseResiduals.series)).toBe(true);
expect(response.body.comparison.fourier).toBeNull();
});
it("returns both model outputs when requested", async () => {
const app = buildApp();
const response = await request(app).post("/solve").send({ xml, solverModel: "both" });
expect(response.status).toBe(200);
expect(response.body.runMetadata.solverModel).toBe("both");
expect(response.body.solvers.fdm.pointCount).toBe(200);
expect(response.body.solvers.fea.pointCount).toBe(200);
});
it("supports diagnostic workflow from measured polished-rod data", async () => {
const app = buildApp();
const predictive = await request(app).get("/solve/default?solverModel=fdm");
expect(predictive.status).toBe(200);
const card = predictive.body.solver.card.slice(0, 120);
const surfaceCard = {
position: card.map((p) => p.position),
load: card.map((p) => p.polishedLoad)
};
const response = await request(app)
.post("/solve")
.send({ xml, solverModel: "fdm", workflow: "diagnostic", surfaceCard });
expect(response.status).toBe(200);
expect(response.body.runMetadata.workflow).toBe("diagnostic");
expect(response.body.pumpMovement.stroke).toBeTypeOf("number");
expect(response.body.verbose.references.length).toBeGreaterThan(0);
expect(response.body.verbose.rodString.hasTaper).toBe(true);
expect(response.body.verbose.trajectoryCoupling.frictionMultiplier).toBeGreaterThan(1);
});
it("returns parsed case and solver output", async () => {
const app = buildApp();
const response = await request(app).post("/solve").send({ xml });
expect(response.status).toBe(200);
expect(response.body.solver.pointCount).toBe(200);
expect(response.body.parsed.model.wellName).toContain("191/01-27-007-09W2/00");
expect(Array.isArray(response.body.parsed.unsupportedFields)).toBe(true);
});
it("is deterministic for the same input", async () => {
const app = buildApp();
const a = await request(app).post("/solve").send({ xml });
const b = await request(app).post("/solve").send({ xml });
expect(a.status).toBe(200);
expect(b.status).toBe(200);
expect(a.body.solver.card).toEqual(b.body.solver.card);
expect(a.body.solver.maxPolishedLoad).toBe(b.body.solver.maxPolishedLoad);
});
it("parses an uploaded XML via POST /case/parse (no solve)", async () => {
const app = buildApp();
const defaultResp = await request(app).get("/case/default");
expect(defaultResp.status).toBe(200);
const response = await request(app).post("/case/parse").send({ xml });
expect(response.status).toBe(200);
expect(response.body.schemaVersion).toBe(2);
expect(response.body.model.wellName).toBe(defaultResp.body.model.wellName);
expect(response.body.model.pumpDepth).toBe(defaultResp.body.model.pumpDepth);
expect(response.body.rawFields.WellName).toBe(defaultResp.body.rawFields.WellName);
expect(response.body.unsupportedFields.sort()).toEqual(
defaultResp.body.unsupportedFields.sort()
);
});
it("rejects empty body on POST /case/parse", async () => {
const app = buildApp();
const response = await request(app).post("/case/parse").send({});
expect(response.status).toBe(400);
expect(response.body.error).toMatch(/xml/i);
expect(response.body.schemaVersion).toBe(2);
});
it("matches golden fingerprint for default solve", async () => {
const app = buildApp();
const response = await request(app).get("/solve/default?solverModel=fdm");
expect(response.status).toBe(200);
expect(response.body.fingerprint).toBe(defaultGoldenHash);
});
it("returns extended physics payload when options are enabled", async () => {
const app = buildApp();
const response = await request(app).post("/solve").send({
xml,
solverModel: "both",
options: {
enableProfiles: true,
enableDiagnosticsDetail: true,
enableFourierBaseline: true,
fourierHarmonics: 10
}
});
expect(response.status).toBe(200);
expect(response.body.solver.profiles.nodeCount).toBeGreaterThan(0);
expect(response.body.solver.diagnostics.valveStates.length).toBe(response.body.solver.pointCount);
expect(response.body.comparison.fourier).toBeTruthy();
expect(response.body.comparison.fourier.harmonics).toBe(10);
});
});