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
138 lines
5.8 KiB
JavaScript
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);
|
|
});
|
|
});
|