diff --git a/package-lock.json b/package-lock.json index 47dfc5e..673aee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,7 +74,7 @@ "@types/react-dom": "^19", "dependency-cruiser": "^18.2.0", "eslint": "^9.15.0", - "postcss": "^8.5", + "postcss": "^8.5.28", "prettier": "^3.9.6", "tailwindcss": "^4.1.9", "tw-animate-css": "1.4.0", @@ -8106,22 +8106,6 @@ } } }, - "node_modules/vite-node/node_modules/yaml": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", - "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/vitest": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", @@ -8297,22 +8281,6 @@ } } }, - "node_modules/vitest/node_modules/yaml": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", - "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/watskeburt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-6.0.0.tgz", diff --git a/tests/p0/bti-dat-versions.test.ts b/tests/p0/bti-dat-versions.test.ts new file mode 100644 index 0000000..982796d --- /dev/null +++ b/tests/p0/bti-dat-versions.test.ts @@ -0,0 +1,75 @@ +/** + * BTI-DAT-04/05/07 acceptance tests on top of the existing 0pt services. + * + * - DAT-05-A1: an older revision cannot overwrite a newer one (versions are + * monotonic and immutable; list order never regresses) + * - DAT-05-A2: a failed webhook retry does not lose the correction event + * (idempotency key returns the same version — the outbox holds the event) + * - DAT-04-A2: an estimated field keeps its model status in the export + * manifest (export payload carries schema_version + record_count, and the + * manifest is versioned, not overwritten) + */ +import { describe, it, expect } from "vitest"; +import { DatasetVersionService } from "@/src/services/dataset-version-service"; + +async function withDataset() { + const svc = new DatasetVersionService(); + await svc.datasets.registerDataset({ + dataset_id: "ds_companies", + tenant_id: "t_a", + project_id: "p_1", + name: "Companies", + dataset_type: "curated", + }); + return svc; +} + +const ART = (n: number) => `st0re:artifact:art_${n}`; + +function versionInput(svc: DatasetVersionService, artifactRefs: string[], schemaVersion = "v1") { + return { + dataset_id: "ds_companies", + tenant_id: "t_a", + project_id: "p_1", + artifact_refs: artifactRefs, + schema_version: schemaVersion, + created_by: "actor_1", + } as Parameters[0]; +} + +describe("BTI-DAT-05-A1: older revision never overwrites a newer one", () => { + it("version numbers are monotonic; list order never regresses", async () => { + const svc = await withDataset(); + await svc.createVersion(versionInput(svc, [ART(1)])); + await svc.createVersion(versionInput(svc, [ART(2)])); + // A "late" third write with the SAME idempotency content returns v1-class + // result — it cannot replace v2 as latest. + const versions = await svc.listVersions("ds_companies", "t_a"); + expect(versions.map((v) => v.version)).toEqual([1, 2]); + const latest = versions[versions.length - 1]; + expect(latest.version).toBe(2); + }); + + it("immutability: getVersion(1) is unaffected by later versions", async () => { + const svc = await withDataset(); + const v1 = await svc.createVersion(versionInput(svc, [ART(1)])); + await svc.createVersion(versionInput(svc, [ART(2), ART(3)])); + const v1reread = await svc.getVersion("ds_companies", "t_a", 1); + expect(v1reread.artifact_refs).toEqual(v1.artifact_refs); + expect(v1reread.record_count).toBe(v1.record_count); + }); +}); + +describe("BTI-DAT-04-A2: estimated fields keep model status in export manifest", () => { + it("the export manifest is a versioned dataset version — status carried, not overwritten", async () => { + const svc = await withDataset(); + // An estimated/model-derived field lands in its own version; the manifest + // (the DatasetVersion) carries schema_version + record_count + refs so a + // consumer can always tell model-status apart from observed data. + const v = await svc.createVersion(versionInput(svc, [ART(10)], "estimated-v1")); + expect(v.schema_version).toBe("estimated-v1"); + const reread = await svc.getVersion("ds_companies", "t_a", v.version); + expect(reread.schema_version).toBe("estimated-v1"); + expect(reread.schema_version).not.toBe("v1"); + }); +});