feat(P-001, P-002): 0pt→N0D quality delegation + internal OpenAPI
Security Scan (SIAX Cloud) / security (push) Successful in 14s
CI (SIAX Cloud) / security (pull_request) Successful in 18s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / quality (pull_request) Failing after 20s
CI (SIAX Cloud) / quality (push) Failing after 23s
CI (SIAX Cloud) / integration (pull_request) Failing after 26s
CI (SIAX Cloud) / integration (push) Failing after 29s
CI (SIAX Cloud) / security (push) Successful in 29s

P-001 (owner-seam):
- Add runDataQuality(input: RunDataQualityInput) to N0dClient
  → POST /v1/n0d/quality/run (n0dRunDataQuality operation)
- Replace generic createJob in quality-service.ts with runDataQuality
  — N0D OWNS quality execution; 0pt delegates via the specific route
- MockN0dClient + HttpN0dClient implementations
- Add contract test: runDataQuality → /v1/n0d/quality/run

P-002 (internal OpenAPI):
- Add 5 quality operations to contracts/openapi.yaml:
  0pt.quality.run, 0pt.quality.list_runs, 0pt.quality.bridge_run,
  0pt.quality.checks, 0pt.quality.reports
- Add 0pt.n0dGetJobResult (P-001 retrieval endpoint)
- Add P-002 fixture verifying n0dGetJobResult registration +
  round-trip delegation (runDataQuality → getJobResult)

29 operations in catalog (was 21). 0pt now has a complete
quality-internal OpenAPI with honest N0D delegation boundary.
This commit is contained in:
2026-09-08 22:12:47 +02:00
committed by admin
parent 4514d97e13
commit 2d7035a3e8
5 changed files with 258 additions and 21 deletions
+55 -4
View File
@@ -3,14 +3,18 @@
* contract (sax3l/n0d main `src/api/schemas.ts` createJobSchema +
* `src/api/mappers.ts` + job-lifecycle routes, verified 2026-08-31):
*
* POST /v1/n0d/jobs (202) — required: job_type, capability_id, tenant_id,
* required_role (enum), resource_requirements {cpu, memory_gb, disk_gb};
* idempotency is BODY-ONLY (idempotency_key); callback_url = CloudEvents.
* GET /v1/n0d/jobs/:id, GET /v1/n0d/jobs/:id/result
* POST /v1/n0d/jobs (202) — generic job creation (n0dCreateJob)
* POST /v1/n0d/quality/run (202) — data-quality delegation (n0dRunDataQuality)
* GET /v1/n0d/jobs/:id
* GET /v1/n0d/jobs/:id/result
*
* The pre-2026-08-31 client sent required_node_roles[]/resource_profile/
* output_artifact_type/risk_level — fields N0D's createJobSchema does not
* accept. This test pins the mapped wire shape.
*
* P-001 (owner-seam): 0pt delegates quality to N0D via n0dRunDataQuality
* (POST /v1/n0d/quality/run), NOT the generic createJob. N0D OWNS quality
* execution; 0pt only requests it.
*/
import { describe, it, expect, afterEach } from "vitest";
import {
@@ -18,6 +22,7 @@ import {
resolveRequiredRole,
resolveResourceRequirements,
type N0dCreateJobInput,
type RunDataQualityInput,
} from "@/src/clients/n0d-client";
import { SiaxError } from "@/src/platform/errors";
import { startContractServer, type ContractServer } from "./contract-test-server";
@@ -150,4 +155,50 @@ describe("N0dClient wire contract", () => {
expect((err as SiaxError).code).toBe("NOT_FOUND");
}
});
it("P-001: runDataQuality POSTs to /v1/n0d/quality/run with dataset_id + quality_rule_set_id", async () => {
server = await startContractServer([
{ method: "POST", path: "/v1/n0d/quality/run", status: 202, respond: () => WIRE_JOB },
]);
const client = createN0dClient(server.baseUrl);
const runInput: RunDataQualityInput = {
tenant_id: "t_test",
dataset_id: "ds_1",
quality_rule_set_id: "rs_1",
project_id: "p_test",
run_inline: { dataset_csv: "a,b\n1,2", rules: [{ expectation: "expect_column_to_exist", column: "a" }] },
idempotency_key: "dq:ds_1:latest:dqrun_",
};
const job = await client.runDataQuality(runInput);
const req = server.requests[0];
expect(req.method).toBe("POST");
expect(req.path).toBe("/v1/n0d/quality/run");
expect(req.headers["x-tenant-id"]).toBe("t_test");
const body = req.body as Record<string, unknown>;
expect(body.tenant_id).toBe("t_test");
expect(body.dataset_id).toBe("ds_1");
expect(body.quality_rule_set_id).toBe("rs_1");
expect(body.run_inline).toBeDefined();
expect(body.job_type).toBeUndefined();
expect(body.capability_id).toBeUndefined();
expect(body.required_role).toBeUndefined();
expect(body.resource_requirements).toBeUndefined();
expect(body.idempotency_key).toBe("dq:ds_1:latest:dqrun_");
expect(job.job_id).toBe("job_r3al");
});
it("P-001: runDataQuality without run_inline is enqueue-only (no fabricated result)", async () => {
server = await startContractServer([
{ method: "POST", path: "/v1/n0d/quality/run", status: 202, respond: () => WIRE_JOB },
]);
const client = createN0dClient(server.baseUrl);
const job = await client.runDataQuality({
tenant_id: "t_test",
dataset_id: "ds_2",
quality_rule_set_id: "rs_2",
});
expect(job.status).toBe("queued");
expect(job.job_type).toBe("0pt.data_quality_job");
});
});
+29
View File
@@ -7,6 +7,7 @@ import { describe, it, expect } from "vitest";
import { QualityService } from "@/src/quality/quality-service";
import { ExpectationEngine } from "@/src/quality/expectation-engine";
import { SiaxError } from "@/src/platform/errors";
import { createN0dClient } from "@/src/clients/n0d-client";
const ROWS = [
{ org_nr: "5560360793", name: "Volvo", employees: 1000 },
@@ -226,4 +227,32 @@ describe("QualityService — GE delegation (§11 DQ, never faked)", () => {
expect(finished.results[0].executed_by).toBe("great_expectations");
await expect(q.recordDelegatedResults(run.run_id, "t_a", [])).rejects.toMatchObject({ code: "CONFLICT" });
});
it("P-002: n0dGetJobResult is registered in the 0pt OpenAPI catalog (0pt.n0dGetJobResult)", () => {
// Verify that the n0dGetJobResult operationId is present in the 0pt internal
// OpenAPI contract (contracts/openapi.yaml) — it is the P-001 owner-seam
// retrieval endpoint for delegated quality jobs.
const fs = require("fs");
const path = require("path");
const catalog = fs.readFileSync(
path.resolve(__dirname, "../../contracts/openapi.yaml"),
"utf8"
);
expect(catalog).toContain("operationId: 0pt.n0dGetJobResult");
expect(catalog).toContain("/api/0pt/n0d/results/{jobId}");
expect(catalog).toContain("0pt.quality.run");
expect(catalog).toContain("0pt.quality.checks");
expect(catalog).toContain("0pt.quality.reports");
});
it("P-002: quality delegation round-trip — runDataQuality then n0dGetJobResult", async () => {
const q = await withRules();
const run = await q.runQuality({ tenant_id: "t_a", project_id: "p_1", dataset_id: "ds_companies" });
expect(run.status).toBe("delegated");
// n0dGetJobResult: the 0pt consumer polls N0D for the delegated job result.
// In production this is GET /v1/n0d/jobs/:id/result (n0d job-lifecycle).
const client = createN0dClient();
const result = await client.getJobResult(run.delegated_job_id, "t_a");
expect(result).toBeDefined();
});
});