Files
0pt/tests/p0/dataset-postgres.realinfra.test.ts
admin 3edc56dcdb
Security Scan (SIAX Cloud) / security (push) Successful in 16s
CI (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / integration (push) Successful in 29s
CI (SIAX Cloud) / quality (push) Successful in 48s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Successful in 28s
CI (SIAX Cloud) / quality (pull_request) Successful in 48s
CI (SIAX Cloud) / integration (pull_request) Successful in 55s
Python Engine CI (SIAX Cloud) / python-engine (push) Failing after 4m52s
Python Engine CI (SIAX Cloud) / python-engine (pull_request) Failing after 4m48s
fix: slutgranskning av dataset/search/ER-ytorna + STATUS-sanning
Slutgate inför merge (per fortsättningsplan):

Kontrakt: CreateDatasetVersionInput.artifact_refs → readonly string[]
(anropare får skicka 'as const'; servicen muterar aldrig); postgres.js
tx.array() får muterbar kopia vid INSERT.

Testkorrigeringar (stale mot starkare implementation — aldrig svagare):
- entity-resolution-http: request(body, undefined) triggade default-param
  och skickade GILTIGT token → 200. Tomt token = 'utan token' → 401 fail-closed.
- entity-resolution: /two records/ → /between 2 and 5000/ (bounds-aware validering)
- search-service: omformat purpose-filter-test + NY negativ kontroll —
  rebuild avvisar dokument utan purpose_tags (fail closed, generation discarded)
- dataset-postgres.realinfra: sätter DATABASE_URL i setup så den durabla
  AUD0-outboxen aktiveras (testet ljög annars om durabilitet) — 5/5 mot live Postgres

STATUS.md: AUD0-klient + lineage-rader (outbox nu real-infra testad),
gap-listan (persistence/routes/outbox stängda, exakt vad som återstår),
P065 STÄNGD med closure-record (thin adapter, v3-semantik, hard-negative
transitivitet, fail-closed 401 — 4/4 planpunkter verifierade).

Verifierat: typecheck EXIT 0 · 945/945 unit · 5/5 real-Postgres · next build EXIT 0
2026-09-07 19:31:22 +02:00

223 lines
8.3 KiB
TypeScript

/**
* Real-Postgres proof for the dataset registry + immutable version metadata.
*
* Set DATASET_TEST_DATABASE_URL to a disposable Postgres 16 instance. When it
* is unset the suite skips honestly; Gitea CI should provide it for the
* blocking real-infrastructure job before this capability is promoted.
*/
import fs from "node:fs";
import path from "node:path";
import postgres from "postgres";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { closeRepositorySqlForTests } from "@/src/repositories/db";
import { DatasetService } from "@/src/services/dataset-service";
import { DatasetVersionService } from "@/src/services/dataset-version-service";
const TEST_DATABASE_URL = process.env.DATASET_TEST_DATABASE_URL;
const run = TEST_DATABASE_URL ? describe : describe.skip;
let admin: ReturnType<typeof postgres> | undefined;
let reachable = false;
let unreachableReason = "";
beforeAll(async () => {
if (!TEST_DATABASE_URL) {
console.warn("[realinfra] DATASET_TEST_DATABASE_URL unset — skipping honestly.");
return;
}
try {
admin = postgres(TEST_DATABASE_URL, { prepare: false, ssl: false, max: 3 });
await Promise.race([
admin`SELECT 1`,
new Promise((_, reject) => setTimeout(() => reject(new Error("connect timeout")), 5_000)),
]);
for (const migration of ["003_datasets.sql", "011_aud0_evidence_outbox.sql", "012_dataset_versions.sql"]) {
await admin.unsafe(fs.readFileSync(path.join(process.cwd(), "sql", migration), "utf8"));
}
// Aktivera den durabla AUD0-outboxen: createAud0OutboxRepositoryFromEnv()
// läser DATABASE_URL (se src/repositories/db.ts) — utan den blir klienten
// Noop och inga producer-rader skrivs (testet skulle ljugit om durabilitet).
process.env.DATABASE_URL = TEST_DATABASE_URL;
reachable = true;
} catch (error) {
unreachableReason = error instanceof Error ? error.message : String(error);
}
}, 30_000);
afterAll(async () => {
delete process.env.DATABASE_URL;
await admin?.end({ timeout: 5 });
await closeRepositorySqlForTests();
});
function datasetService(): DatasetService {
return new DatasetService({ databaseUrl: TEST_DATABASE_URL });
}
function versionService(): DatasetVersionService {
return new DatasetVersionService({ databaseUrl: TEST_DATABASE_URL });
}
run("Dataset registry + versions against LIVE Postgres", () => {
it("persists a dataset across independent service instances", async (ctx) => {
if (!reachable) {
console.warn(`[realinfra] Postgres unavailable (${unreachableReason}) — skipping honestly.`);
ctx.skip();
return;
}
const stamp = `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const datasetId = `ds_realpg_${stamp}`;
const tenantId = `t_realpg_${stamp}`;
await datasetService().registerDataset({
dataset_id: datasetId,
tenant_id: tenantId,
project_id: "p_main",
name: "Real Postgres dataset",
dataset_type: "curated",
created_by: "realinfra-test",
});
const reloaded = await datasetService().getDataset(datasetId);
expect(reloaded.dataset_id).toBe(datasetId);
expect(reloaded.tenant_id).toBe(tenantId);
const rows = await admin!<{ tenant_id: string; name: string }[]>`
SELECT tenant_id, name FROM opt_datasets WHERE dataset_id = ${datasetId}`;
expect(rows).toEqual([{ tenant_id: tenantId, name: "Real Postgres dataset" }]);
});
it("persists monotonic immutable versions across fresh service instances", async (ctx) => {
if (!reachable) return ctx.skip();
const stamp = `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const datasetId = `ds_versions_${stamp}`;
const tenantId = `t_versions_${stamp}`;
await datasetService().registerDataset({
dataset_id: datasetId,
tenant_id: tenantId,
project_id: "p_main",
name: "Versioned dataset",
dataset_type: "curated",
created_by: "realinfra-test",
});
const v1 = await versionService().createVersion({
dataset_id: datasetId,
tenant_id: tenantId,
project_id: "p_main",
artifact_refs: ["st0re:artifact:art_realpg_1"],
schema_version: "1",
record_count: 10,
created_by: "realinfra-test",
idempotency_key: `idem_${stamp}_1`,
});
const v2 = await versionService().createVersion({
dataset_id: datasetId,
tenant_id: tenantId,
project_id: "p_main",
artifact_refs: ["st0re:artifact:art_realpg_2"],
schema_version: "1",
record_count: 11,
created_by: "realinfra-test",
idempotency_key: `idem_${stamp}_2`,
});
expect([v1.version, v2.version]).toEqual([1, 2]);
const fresh = versionService();
expect((await fresh.listVersions(datasetId, tenantId)).map((item) => item.version)).toEqual([1, 2]);
expect((await fresh.getVersion(datasetId, tenantId, 2)).artifact_refs).toEqual(["st0re:artifact:art_realpg_2"]);
});
it("replays the same idempotency key without minting another version and rejects payload drift", async (ctx) => {
if (!reachable) return ctx.skip();
const stamp = `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const datasetId = `ds_idem_${stamp}`;
const tenantId = `t_idem_${stamp}`;
await datasetService().registerDataset({
dataset_id: datasetId,
tenant_id: tenantId,
project_id: "p_main",
name: "Idempotent dataset",
dataset_type: "derived",
created_by: "realinfra-test",
});
const input = {
dataset_id: datasetId,
tenant_id: tenantId,
project_id: "p_main",
artifact_refs: ["st0re:artifact:art_idem_1"],
schema_version: "1",
created_by: "realinfra-test",
idempotency_key: `idem_${stamp}`,
} as const;
const first = await versionService().createVersion(input);
const replay = await versionService().createVersion(input);
expect(replay.version).toBe(first.version);
expect(await versionService().listVersions(datasetId, tenantId)).toHaveLength(1);
await expect(versionService().createVersion({
...input,
artifact_refs: ["st0re:artifact:art_DIFFERENT"],
})).rejects.toMatchObject({ code: "CONFLICT" });
});
it("foreign tenant cannot read or version another tenant's dataset", async (ctx) => {
if (!reachable) return ctx.skip();
const stamp = `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const datasetId = `ds_tenant_${stamp}`;
const owner = `t_owner_${stamp}`;
const foreign = `t_foreign_${stamp}`;
await datasetService().registerDataset({
dataset_id: datasetId,
tenant_id: owner,
project_id: "p_main",
name: "Tenant isolated",
dataset_type: "raw",
created_by: "realinfra-test",
});
await expect(versionService().listVersions(datasetId, foreign)).rejects.toMatchObject({ code: "NOT_FOUND" });
await expect(versionService().createVersion({
dataset_id: datasetId,
tenant_id: foreign,
project_id: "p_main",
artifact_refs: ["st0re:artifact:art_foreign"],
schema_version: "1",
created_by: "intruder",
})).rejects.toMatchObject({ code: "NOT_FOUND" });
});
it("durable AUD0 producer rows are written alongside dataset operations", async (ctx) => {
if (!reachable) return ctx.skip();
const stamp = `${Date.now()}_${Math.random().toString(16).slice(2)}`;
const datasetId = `ds_evidence_${stamp}`;
const tenantId = `t_evidence_${stamp}`;
await datasetService().registerDataset({
dataset_id: datasetId,
tenant_id: tenantId,
project_id: "p_main",
name: "Evidence dataset",
dataset_type: "curated",
created_by: "realinfra-test",
});
await versionService().createVersion({
dataset_id: datasetId,
tenant_id: tenantId,
project_id: "p_main",
artifact_refs: ["st0re:artifact:art_evidence"],
schema_version: "1",
created_by: "realinfra-test",
});
const evidence = await admin!<{ evidence_kind: string; status: string }[]>`
SELECT evidence_kind, status
FROM opt_aud0_evidence_outbox
WHERE tenant_id = ${tenantId}
ORDER BY created_at`;
expect(evidence.length).toBeGreaterThanOrEqual(3); // register + version event + lineage
expect(evidence.every((row) => row.status === "pending")).toBe(true);
expect(new Set(evidence.map((row) => row.evidence_kind))).toEqual(new Set(["event", "openlineage"]));
});
});