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
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:
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user