feat(engine): register MCP profiles and provider transitions
This commit is contained in:
parent
951b884c4b
commit
e9e03143cd
|
|
@ -0,0 +1,276 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import {
|
||||||
|
copyFile,
|
||||||
|
lstat,
|
||||||
|
mkdir,
|
||||||
|
mkdtemp,
|
||||||
|
readFile,
|
||||||
|
rm,
|
||||||
|
writeFile,
|
||||||
|
} from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const workspaceRoot = resolve(scriptDir, "../../..");
|
||||||
|
const engineRoot = resolve(
|
||||||
|
process.env.NODEDC_ENGINE_SOURCE_ROOT
|
||||||
|
|| join(workspaceRoot, "NODEDC_ENGINE_INFRA"),
|
||||||
|
);
|
||||||
|
const artifactDir = resolve(
|
||||||
|
process.env.NODEDC_DEPLOY_ARTIFACT_DIR
|
||||||
|
|| resolve(scriptDir, "../deploy-artifacts"),
|
||||||
|
);
|
||||||
|
const [patchId = "engine-l1-agent-init-concurrency-20260724-042", ...extra] =
|
||||||
|
process.argv.slice(2);
|
||||||
|
|
||||||
|
if (
|
||||||
|
extra.length
|
||||||
|
|| !/^engine-l1-agent-init-concurrency-\d{8}-\d{3}$/.test(patchId)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"usage: build-engine-l1-agent-init-concurrency-artifact.mjs "
|
||||||
|
+ "[engine-l1-agent-init-concurrency-YYYYMMDD-NNN]",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceCommit = "c4077fe0d8d824fa58616eeb986dc78ca43070da";
|
||||||
|
const predecessorSha256 = Object.freeze({
|
||||||
|
"nodedc-source/src/App.tsx":
|
||||||
|
"b3e61a89330b774f309660208d05143d299ab582f18765c1c14e2122a62c4d5e",
|
||||||
|
"nodedc-source/src/driveinspector/nodes/N8n.definition.ts":
|
||||||
|
"a4890cca12b269643ec3255c2fc6b7e2be96aa042ffc6087772e63cbf8323c3a",
|
||||||
|
"nodedc-source/src/nodes/N8nNode.tsx":
|
||||||
|
"a125f7cde66085008dafe5a340a1143094c59769573815851ce6c8adfe4c0220",
|
||||||
|
"nodedc-source/src/store.ts":
|
||||||
|
"2128cd582a947582bb7736792c3a71551712262ef04882c189220ec79f16fb8e",
|
||||||
|
"nodedc-source/dist/index.html":
|
||||||
|
"90d72f8790dc8cf951066b1210447c84d640373117bae23f3a4cb3227fb96d6d",
|
||||||
|
"nodedc-source/dist/assets/index-CqvJfRRS.js":
|
||||||
|
"06e6c23b03ea2ba8a4890e1f1714fd08ebaf18d735834200af6ab430af360ca8",
|
||||||
|
});
|
||||||
|
const targetSha256 = Object.freeze({
|
||||||
|
"nodedc-source/src/App.tsx":
|
||||||
|
"fc580dc3a1a97393af81dd75c54ae8b37db1f6fda8364042198493493b15f2c2",
|
||||||
|
"nodedc-source/src/driveinspector/nodes/N8n.definition.ts":
|
||||||
|
"945839541547b2fd02b5424954e00483d6c76f82a333d6a7a652ccd1b74d04cf",
|
||||||
|
"nodedc-source/src/nodes/N8nNode.tsx":
|
||||||
|
"1130e355d86e06a18df775b2d3517bfdbf98fdc494ca707d23c78648fbfd3cb4",
|
||||||
|
"nodedc-source/src/store.ts":
|
||||||
|
"46884488254c16cd3af9b25556dd0db4f5a3a20047abdb496db831f204e40673",
|
||||||
|
"nodedc-source/src/utils/workflowWriteQueue.js":
|
||||||
|
"41f73ccb5e1dafbdae501b7c0b9fa5c47fdbd52564061b9c4ca6ca1c75ceccdf",
|
||||||
|
"nodedc-source/dist/index.html":
|
||||||
|
"1554e2418aead13b56328c5721dd677400d84ee087559a88fe60b295b7ff5412",
|
||||||
|
"nodedc-source/dist/assets/index-r1_2ECzJ.js":
|
||||||
|
"0d25dd843da7078b8dbad5cb4b78724bf1ca8aa067e9d0add1e09ce8f421b822",
|
||||||
|
});
|
||||||
|
const entries = Object.freeze(Object.keys(targetSha256));
|
||||||
|
const artifact = join(artifactDir, `nodedc-${patchId}.tgz`);
|
||||||
|
const checksum = `${artifact}.sha256`;
|
||||||
|
|
||||||
|
assertSourceCommit();
|
||||||
|
await assertPredecessorSources();
|
||||||
|
await assertExactTargets();
|
||||||
|
await assertRuntimeContract();
|
||||||
|
await assertFresh(artifact);
|
||||||
|
|
||||||
|
const stage = await mkdtemp(join(tmpdir(), "nodedc-engine-l1-agent-init-"));
|
||||||
|
try {
|
||||||
|
for (const relativePath of entries) {
|
||||||
|
const destination = join(stage, "payload", relativePath);
|
||||||
|
await mkdir(dirname(destination), { recursive: true });
|
||||||
|
await copyFile(join(engineRoot, relativePath), destination);
|
||||||
|
}
|
||||||
|
await writeFile(
|
||||||
|
join(stage, "manifest.env"),
|
||||||
|
`id=${patchId}\ncomponent=engine\ntype=app-overlay\n`,
|
||||||
|
{ encoding: "utf8", flag: "wx", mode: 0o644 },
|
||||||
|
);
|
||||||
|
await writeFile(join(stage, "files.txt"), `${entries.join("\n")}\n`, {
|
||||||
|
encoding: "utf8",
|
||||||
|
flag: "wx",
|
||||||
|
mode: 0o644,
|
||||||
|
});
|
||||||
|
await mkdir(artifactDir, { recursive: true });
|
||||||
|
run("python3", ["-c", canonicalTarScript(), artifact, stage]);
|
||||||
|
const sha256 = digest(await readFile(artifact));
|
||||||
|
await writeFile(checksum, `${sha256} ${artifact.split("/").at(-1)}\n`, {
|
||||||
|
encoding: "utf8",
|
||||||
|
flag: "wx",
|
||||||
|
mode: 0o644,
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
patchId,
|
||||||
|
artifact,
|
||||||
|
checksum,
|
||||||
|
sha256,
|
||||||
|
sourceCommit,
|
||||||
|
predecessorSha256,
|
||||||
|
targetSha256,
|
||||||
|
entries,
|
||||||
|
services: ["nodedc-backend", "app"],
|
||||||
|
healthchecks: [
|
||||||
|
"http://127.0.0.1:8080/",
|
||||||
|
"http://127.0.0.1:3001/health",
|
||||||
|
],
|
||||||
|
concurrencyContract: "per-workflow-fifo-latest-revision-v1",
|
||||||
|
n8nCoreChanged: false,
|
||||||
|
untouched: [
|
||||||
|
"n8n runtime",
|
||||||
|
"L1 workflow data",
|
||||||
|
"L2 workflow data",
|
||||||
|
"credentials",
|
||||||
|
"databases",
|
||||||
|
"MCP catalogs",
|
||||||
|
],
|
||||||
|
}, null, 2));
|
||||||
|
} finally {
|
||||||
|
await rm(stage, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertSourceCommit() {
|
||||||
|
const actual = run("git", ["-C", engineRoot, "rev-parse", "HEAD"]).stdout.trim();
|
||||||
|
if (actual !== sourceCommit) {
|
||||||
|
throw new Error(
|
||||||
|
`engine_l1_agent_init_source_commit_mismatch:`
|
||||||
|
+ `expected=${sourceCommit}:actual=${actual}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertPredecessorSources() {
|
||||||
|
for (const [relativePath, expected] of Object.entries(predecessorSha256)) {
|
||||||
|
const result = run("git", [
|
||||||
|
"-C",
|
||||||
|
engineRoot,
|
||||||
|
"show",
|
||||||
|
`${sourceCommit}:${relativePath}`,
|
||||||
|
]);
|
||||||
|
const actual = digest(result.stdoutBuffer);
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(
|
||||||
|
`engine_l1_agent_init_predecessor_mismatch:${relativePath}:`
|
||||||
|
+ `expected=${expected}:actual=${actual}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const newFile = "nodedc-source/src/utils/workflowWriteQueue.js";
|
||||||
|
const probe = spawnSync(
|
||||||
|
"git",
|
||||||
|
["-C", engineRoot, "cat-file", "-e", `${sourceCommit}:${newFile}`],
|
||||||
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
||||||
|
);
|
||||||
|
if (probe.status === 0) {
|
||||||
|
throw new Error("engine_l1_agent_init_queue_not_new");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertExactTargets() {
|
||||||
|
for (const [relativePath, expected] of Object.entries(targetSha256)) {
|
||||||
|
const source = join(engineRoot, relativePath);
|
||||||
|
const info = await lstat(source);
|
||||||
|
if (!info.isFile() || info.isSymbolicLink()) {
|
||||||
|
throw new Error(`engine_l1_agent_init_source_unsafe:${relativePath}`);
|
||||||
|
}
|
||||||
|
const actual = digest(await readFile(source));
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(
|
||||||
|
`engine_l1_agent_init_target_mismatch:${relativePath}:`
|
||||||
|
+ `expected=${expected}:actual=${actual}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertRuntimeContract() {
|
||||||
|
const app = await readFile(join(engineRoot, "nodedc-source/src/App.tsx"), "utf8");
|
||||||
|
const definition = await readFile(
|
||||||
|
join(engineRoot, "nodedc-source/src/driveinspector/nodes/N8n.definition.ts"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const store = await readFile(join(engineRoot, "nodedc-source/src/store.ts"), "utf8");
|
||||||
|
const node = await readFile(
|
||||||
|
join(engineRoot, "nodedc-source/src/nodes/N8nNode.tsx"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const queue = await readFile(
|
||||||
|
join(engineRoot, "nodedc-source/src/utils/workflowWriteQueue.js"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const index = await readFile(
|
||||||
|
join(engineRoot, "nodedc-source/dist/index.html"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!app.includes("createWorkflowWriteQueue")
|
||||||
|
|| !app.includes("putExistingWorkflow")
|
||||||
|
|| !app.includes("expectedRevision: currentWorkflowRevisionRef.current")
|
||||||
|
|| !queue.includes("previous.catch(() => undefined).then(() => operation())")
|
||||||
|
|| !definition.includes("nodeName: 'NDC'")
|
||||||
|
|| !definition.includes("default: 'NDC'")
|
||||||
|
|| !definition.includes("data?.title || data?.nodeName || 'NDC'")
|
||||||
|
|| !store.includes("nodeName: 'NDC'")
|
||||||
|
|| !node.includes("data?.nodeName ?? 'NDC'")
|
||||||
|
|| !index.includes("/assets/index-r1_2ECzJ.js")
|
||||||
|
|| /gelios|robot2b/i.test(queue)
|
||||||
|
) {
|
||||||
|
throw new Error("engine_l1_agent_init_runtime_contract_invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertFresh(path) {
|
||||||
|
try {
|
||||||
|
await lstat(path);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === "ENOENT") return;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error("artifact_already_exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalTarScript() {
|
||||||
|
return [
|
||||||
|
"import gzip,io,pathlib,sys,tarfile",
|
||||||
|
"root=pathlib.Path(sys.argv[2])",
|
||||||
|
"with open(sys.argv[1],'xb') as out:",
|
||||||
|
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
|
||||||
|
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
|
||||||
|
" for top in ('manifest.env','files.txt','payload'):",
|
||||||
|
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
|
||||||
|
" for x in paths:",
|
||||||
|
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())",
|
||||||
|
" info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; info.mode=0o755 if info.isdir() else 0o644",
|
||||||
|
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: tar.addfile(info,src if info.isfile() else None)",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function digest(value) {
|
||||||
|
return createHash("sha256").update(value).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command, args) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
encoding: null,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
maxBuffer: 128 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`${command}_failed:${Buffer.from(result.stderr || "").toString("utf8")}`
|
||||||
|
+ `${Buffer.from(result.stdout || "").toString("utf8")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stdoutBuffer: Buffer.from(result.stdout || ""),
|
||||||
|
stderrBuffer: Buffer.from(result.stderr || ""),
|
||||||
|
stdout: Buffer.from(result.stdout || "").toString("utf8"),
|
||||||
|
stderr: Buffer.from(result.stderr || "").toString("utf8"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import {
|
||||||
|
copyFile,
|
||||||
|
lstat,
|
||||||
|
mkdir,
|
||||||
|
mkdtemp,
|
||||||
|
readFile,
|
||||||
|
rm,
|
||||||
|
writeFile,
|
||||||
|
} from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const workspaceRoot = resolve(scriptDir, "../../..");
|
||||||
|
const engineRoot = resolve(
|
||||||
|
process.env.NODEDC_ENGINE_SOURCE_ROOT
|
||||||
|
|| join(workspaceRoot, "NODEDC_ENGINE_INFRA"),
|
||||||
|
);
|
||||||
|
const artifactDir = resolve(
|
||||||
|
process.env.NODEDC_DEPLOY_ARTIFACT_DIR
|
||||||
|
|| resolve(scriptDir, "../deploy-artifacts"),
|
||||||
|
);
|
||||||
|
const [
|
||||||
|
patchId = "engine-mcp-gelios-units-items-envelope-20260724-049",
|
||||||
|
...extra
|
||||||
|
] = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (
|
||||||
|
extra.length
|
||||||
|
|| !/^engine-mcp-gelios-units-items-envelope-\d{8}-\d{3}$/.test(patchId)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"usage: build-engine-mcp-gelios-units-items-envelope-artifact.mjs "
|
||||||
|
+ "[engine-mcp-gelios-units-items-envelope-YYYYMMDD-NNN]",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedSha256 = Object.freeze({
|
||||||
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
||||||
|
"d0cc95fa5e55bda315947fdb13bd8a0d8931b9484677e4c4cc0dcb0e9614e215",
|
||||||
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json":
|
||||||
|
"1f1b866b6f837bffc6d529dfffd8bfa9c87495bfcd9ae3515a79765143d94b1d",
|
||||||
|
"nodedc-source/server/deployTransitions/geliosUnitsItemsEnvelopeV12Patch1.json":
|
||||||
|
"7a1594573e98342d3a3edcc0dc4f663f06b30ca24cc20adb5f36d50591066d96",
|
||||||
|
});
|
||||||
|
const files = Object.freeze(Object.keys(expectedSha256));
|
||||||
|
const artifact = join(artifactDir, `nodedc-${patchId}.tgz`);
|
||||||
|
const checksum = `${artifact}.sha256`;
|
||||||
|
const stage = await mkdtemp(
|
||||||
|
join(tmpdir(), "nodedc-engine-gelios-units-items-envelope-"),
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertFresh(artifact);
|
||||||
|
await assertExactSources();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const relativePath of files) {
|
||||||
|
const destination = join(stage, "payload", relativePath);
|
||||||
|
await mkdir(dirname(destination), { recursive: true });
|
||||||
|
await copyFile(join(engineRoot, relativePath), destination);
|
||||||
|
}
|
||||||
|
await writeFile(
|
||||||
|
join(stage, "manifest.env"),
|
||||||
|
`id=${patchId}\ncomponent=engine\ntype=app-overlay\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
await writeFile(join(stage, "files.txt"), `${files.join("\n")}\n`, "utf8");
|
||||||
|
await mkdir(artifactDir, { recursive: true });
|
||||||
|
run("python3", ["-c", canonicalTarScript(), artifact, stage]);
|
||||||
|
const sha256 = digest(await readFile(artifact));
|
||||||
|
await writeFile(
|
||||||
|
checksum,
|
||||||
|
`${sha256} ${artifact.split("/").at(-1)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
patchId,
|
||||||
|
artifact,
|
||||||
|
checksum,
|
||||||
|
sha256,
|
||||||
|
services: ["nodedc-backend"],
|
||||||
|
transition:
|
||||||
|
"registered-profiles-v2-to-gelios-v12.0.1-units-items-envelope",
|
||||||
|
mcpVersion: "0.12.0",
|
||||||
|
providerPackage: {
|
||||||
|
id: "gelios.provider.v12",
|
||||||
|
predecessorVersion: "12.0.0",
|
||||||
|
targetVersion: "12.0.1",
|
||||||
|
},
|
||||||
|
registeredProfile: "gelios.units.profile.cold.v1",
|
||||||
|
responseCollectionPath: "items",
|
||||||
|
existingMaterializerReused: true,
|
||||||
|
compilerVersion: "1.4.0",
|
||||||
|
engineRuntimeCodeChanged: false,
|
||||||
|
n8nCoreChanged: false,
|
||||||
|
l1Changed: false,
|
||||||
|
l2GraphChanged: false,
|
||||||
|
engineUiChanged: false,
|
||||||
|
databasesChanged: false,
|
||||||
|
credentialsChanged: false,
|
||||||
|
foundryChanged: false,
|
||||||
|
ontologyChanged: false,
|
||||||
|
providerEndpointsChanged: false,
|
||||||
|
files,
|
||||||
|
}, null, 2));
|
||||||
|
} finally {
|
||||||
|
await rm(stage, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertExactSources() {
|
||||||
|
for (const [relativePath, expected] of Object.entries(expectedSha256)) {
|
||||||
|
const sourcePath = join(engineRoot, relativePath);
|
||||||
|
const info = await lstat(sourcePath);
|
||||||
|
if (!info.isFile() || info.isSymbolicLink()) {
|
||||||
|
throw new Error(
|
||||||
|
`engine_gelios_units_items_source_unsafe:${relativePath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const actual = digest(await readFile(sourcePath));
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(
|
||||||
|
`engine_gelios_units_items_target_mismatch:${relativePath}:`
|
||||||
|
+ `expected=${expected}:actual=${actual}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const execution = JSON.parse(await readFile(
|
||||||
|
join(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
));
|
||||||
|
const security = JSON.parse(await readFile(
|
||||||
|
join(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
));
|
||||||
|
const descriptor = JSON.parse(await readFile(
|
||||||
|
join(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/server/deployTransitions/"
|
||||||
|
+ "geliosUnitsItemsEnvelopeV12Patch1.json",
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
));
|
||||||
|
const providerPackage = execution.packages.find(
|
||||||
|
(item) => item.id === "gelios.provider.v12",
|
||||||
|
);
|
||||||
|
const profile = providerPackage?.profiles.find(
|
||||||
|
(item) => item.id === "gelios.units.profile.cold.v1",
|
||||||
|
);
|
||||||
|
const extract = profile?.executionPlanTemplate?.steps.find(
|
||||||
|
(step) => (
|
||||||
|
step.kind === "extract_items"
|
||||||
|
&& step.config?.capabilityId === "gelios.units.current.read"
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const securityPackage = security.packages.find(
|
||||||
|
(item) => item.id === "gelios.provider.v12",
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
providerPackage?.version !== "12.0.1"
|
||||||
|
|| securityPackage?.version !== "12.0.1"
|
||||||
|
|| profile?.dataProductId !== "fleet.units.profile.current.v1"
|
||||||
|
|| profile?.executionPlanTemplate?.compilerVersion !== "1.4.0"
|
||||||
|
|| extract?.config?.response?.collectionPaths?.[0] !== "items"
|
||||||
|
|| descriptor?.id
|
||||||
|
!== "engine-mcp-gelios-units-items-envelope-v12-patch1"
|
||||||
|
|| descriptor?.predecessor
|
||||||
|
!== "engine-mcp-registered-execution-profiles-v2"
|
||||||
|
|| descriptor?.mcpVersion !== "0.12.0"
|
||||||
|
|| descriptor?.registeredProfile?.materializer
|
||||||
|
!== "existing-immutable-two-phase"
|
||||||
|
|| Object.values(descriptor?.boundaries || {}).some(Boolean)
|
||||||
|
) {
|
||||||
|
throw new Error("engine_gelios_units_items_boundary_invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertFresh(path) {
|
||||||
|
try {
|
||||||
|
await lstat(path);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === "ENOENT") return;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error("artifact_already_exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalTarScript() {
|
||||||
|
return [
|
||||||
|
"import gzip,io,pathlib,sys,tarfile",
|
||||||
|
"root=pathlib.Path(sys.argv[2])",
|
||||||
|
"with open(sys.argv[1],'xb') as out:",
|
||||||
|
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
|
||||||
|
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
|
||||||
|
" for top in ('manifest.env','files.txt','payload'):",
|
||||||
|
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
|
||||||
|
" for x in paths:",
|
||||||
|
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())",
|
||||||
|
" info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; "
|
||||||
|
+ "info.mode=0o755 if info.isdir() else 0o644",
|
||||||
|
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: "
|
||||||
|
+ "tar.addfile(info,src if info.isfile() else None)",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function digest(value) {
|
||||||
|
return createHash("sha256").update(value).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command, args) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
encoding: "utf8",
|
||||||
|
maxBuffer: 128 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`${command}_failed:${result.stderr || result.stdout}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,279 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import {
|
||||||
|
copyFile,
|
||||||
|
lstat,
|
||||||
|
mkdir,
|
||||||
|
mkdtemp,
|
||||||
|
readFile,
|
||||||
|
rm,
|
||||||
|
writeFile,
|
||||||
|
} from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const workspaceRoot = resolve(scriptDir, "../../..");
|
||||||
|
const engineRoot = resolve(
|
||||||
|
process.env.NODEDC_ENGINE_SOURCE_ROOT
|
||||||
|
|| join(workspaceRoot, "NODEDC_ENGINE_INFRA"),
|
||||||
|
);
|
||||||
|
const artifactDir = resolve(
|
||||||
|
process.env.NODEDC_DEPLOY_ARTIFACT_DIR
|
||||||
|
|| resolve(scriptDir, "../deploy-artifacts"),
|
||||||
|
);
|
||||||
|
const [
|
||||||
|
patchId = "engine-mcp-registered-execution-profiles-20260724-048",
|
||||||
|
...extra
|
||||||
|
] = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (
|
||||||
|
extra.length
|
||||||
|
|| !/^engine-mcp-registered-execution-profiles-\d{8}-\d{3}$/.test(patchId)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"usage: build-engine-mcp-registered-execution-profiles-artifact.mjs "
|
||||||
|
+ "[engine-mcp-registered-execution-profiles-YYYYMMDD-NNN]",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedSha256 = Object.freeze({
|
||||||
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json":
|
||||||
|
"940510499a9260dbff718b7b1f96f72f8e9ac593b460805af3007731578a7668",
|
||||||
|
"nodedc-source/server/l2ExecutionPlan/catalog.js":
|
||||||
|
"fec56b154ae483ad21bbaaeb4c55707bffce223c27874ac135ce58a9203259a0",
|
||||||
|
"nodedc-source/server/l2ExecutionPlan/registeredProfiles.js":
|
||||||
|
"3c521c1652c61c9d757197c76e053fd3ec602e13ef7f6a4856835ecc1a8a61d1",
|
||||||
|
"nodedc-source/server/routes/engineAgentGateway.js":
|
||||||
|
"8f04edc11251de86b825351c92338be9537207887cf802474b6b6d8c3cce4077",
|
||||||
|
"nodedc-source/services/node-intelligence/activation.json":
|
||||||
|
"3d72709e40b79c01a62a6af4bde911fca4f609d5ac487d284cf97fd8ebd73686",
|
||||||
|
"nodedc-source/server/deployTransitions/registeredExecutionProfilesV2.json":
|
||||||
|
"1db523f035a47c6b00b41b26efe1390ad2d24acece9d30dd039eff56626e934d",
|
||||||
|
});
|
||||||
|
const files = Object.freeze(Object.keys(expectedSha256));
|
||||||
|
const artifact = join(artifactDir, `nodedc-${patchId}.tgz`);
|
||||||
|
const checksum = `${artifact}.sha256`;
|
||||||
|
const stage = await mkdtemp(
|
||||||
|
join(tmpdir(), "nodedc-engine-registered-execution-profiles-"),
|
||||||
|
);
|
||||||
|
|
||||||
|
await assertFresh(artifact);
|
||||||
|
await assertExactSources();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const relativePath of files) {
|
||||||
|
const destination = join(stage, "payload", relativePath);
|
||||||
|
await mkdir(dirname(destination), { recursive: true });
|
||||||
|
await copyFile(join(engineRoot, relativePath), destination);
|
||||||
|
}
|
||||||
|
await writeFile(
|
||||||
|
join(stage, "manifest.env"),
|
||||||
|
`id=${patchId}\ncomponent=engine\ntype=app-overlay\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
await writeFile(join(stage, "files.txt"), `${files.join("\n")}\n`, "utf8");
|
||||||
|
await mkdir(artifactDir, { recursive: true });
|
||||||
|
run("python3", ["-c", canonicalTarScript(), artifact, stage]);
|
||||||
|
const sha256 = digest(await readFile(artifact));
|
||||||
|
await writeFile(
|
||||||
|
checksum,
|
||||||
|
`${sha256} ${artifact.split("/").at(-1)}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
patchId,
|
||||||
|
artifact,
|
||||||
|
checksum,
|
||||||
|
sha256,
|
||||||
|
services: ["nodedc-backend"],
|
||||||
|
mcpVersion: "0.12.0",
|
||||||
|
transition:
|
||||||
|
"gelios-items-envelope-v12-to-registered-profiles-v2-attested",
|
||||||
|
tools: [
|
||||||
|
"engine_list_l2_execution_profiles",
|
||||||
|
"engine_plan_registered_l2_execution",
|
||||||
|
],
|
||||||
|
registeredProfiles: 6,
|
||||||
|
existingMaterializerReused: true,
|
||||||
|
n8nCoreChanged: false,
|
||||||
|
l1Changed: false,
|
||||||
|
l2GraphChanged: false,
|
||||||
|
engineUiChanged: false,
|
||||||
|
databasesChanged: false,
|
||||||
|
credentialsChanged: false,
|
||||||
|
foundryChanged: false,
|
||||||
|
ontologyChanged: false,
|
||||||
|
nodeIntelligenceAttestationChanged: true,
|
||||||
|
nodeIntelligenceImageChanged: false,
|
||||||
|
nodeIntelligenceSourceChanged: false,
|
||||||
|
rawProviderValuesIncluded: false,
|
||||||
|
files,
|
||||||
|
}, null, 2));
|
||||||
|
} finally {
|
||||||
|
await rm(stage, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertExactSources() {
|
||||||
|
for (const [relativePath, expected] of Object.entries(expectedSha256)) {
|
||||||
|
const sourcePath = join(engineRoot, relativePath);
|
||||||
|
const info = await lstat(sourcePath);
|
||||||
|
if (!info.isFile() || info.isSymbolicLink()) {
|
||||||
|
throw new Error(
|
||||||
|
`engine_registered_execution_profiles_source_unsafe:${relativePath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const actual = digest(await readFile(sourcePath));
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(
|
||||||
|
`engine_registered_execution_profiles_target_mismatch:${relativePath}:`
|
||||||
|
+ `expected=${expected}:actual=${actual}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalog = JSON.parse(await readFile(
|
||||||
|
join(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
));
|
||||||
|
const descriptor = JSON.parse(await readFile(
|
||||||
|
join(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/server/deployTransitions/"
|
||||||
|
+ "registeredExecutionProfilesV2.json",
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
));
|
||||||
|
const registered = catalog.packages.flatMap(
|
||||||
|
(providerPackage) => (providerPackage.profiles || []).filter(
|
||||||
|
(profile) => profile.executionPlanTemplate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
registered.length !== 6
|
||||||
|
|| registered.some((profile) => (
|
||||||
|
!profile.dataClass
|
||||||
|
|| !profile.cadence
|
||||||
|
|| profile.executionPlanTemplate?.compilerVersion !== "1.4.0"
|
||||||
|
|| profile.executionPlanTemplate?.connection?.collectionProfileId
|
||||||
|
!== profile.id
|
||||||
|
))
|
||||||
|
|| descriptor?.id !== "engine-mcp-registered-execution-profiles-v2"
|
||||||
|
|| descriptor?.predecessor !== "engine-mcp-gelios-items-envelope-v12"
|
||||||
|
|| descriptor?.mcpVersion !== "0.12.0"
|
||||||
|
|| descriptor?.tools?.apply
|
||||||
|
!== "engine_apply_l2_execution_plan_materialization"
|
||||||
|
|| descriptor?.nodeIntelligenceAttestation?.targetGatewaySha256
|
||||||
|
!== expectedSha256[
|
||||||
|
"nodedc-source/server/routes/engineAgentGateway.js"
|
||||||
|
]
|
||||||
|
|| descriptor?.nodeIntelligenceAttestation?.sidecarImageChanged !== false
|
||||||
|
|| descriptor?.nodeIntelligenceAttestation
|
||||||
|
?.nodeIntelligenceSourceChanged !== false
|
||||||
|
|| descriptor?.boundaries?.nodeIntelligenceAttestationChanged !== true
|
||||||
|
|| Object.entries(descriptor?.boundaries || {}).some(
|
||||||
|
([key, value]) => (
|
||||||
|
key !== "nodeIntelligenceAttestationChanged" && value !== false
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error("engine_registered_execution_profiles_boundary_invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const gateway = await readFile(
|
||||||
|
join(engineRoot, "nodedc-source/server/routes/engineAgentGateway.js"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const resolver = await readFile(
|
||||||
|
join(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/server/l2ExecutionPlan/registeredProfiles.js",
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
const nodeIntelligence = JSON.parse(await readFile(
|
||||||
|
join(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/services/node-intelligence/activation.json",
|
||||||
|
),
|
||||||
|
"utf8",
|
||||||
|
));
|
||||||
|
if (
|
||||||
|
nodeIntelligence?.releaseId !== "2.33.2-974a9fb3492f"
|
||||||
|
|| nodeIntelligence?.source?.gatewaySha256
|
||||||
|
!== expectedSha256[
|
||||||
|
"nodedc-source/server/routes/engineAgentGateway.js"
|
||||||
|
]
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"engine_registered_execution_profiles_attestation_invalid",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const marker of [
|
||||||
|
"engine_list_l2_execution_profiles",
|
||||||
|
"engine_plan_registered_l2_execution",
|
||||||
|
"const ENGINE_AGENT_MCP_VERSION = '0.12.0'",
|
||||||
|
]) {
|
||||||
|
if (!gateway.includes(marker)) {
|
||||||
|
throw new Error(
|
||||||
|
`engine_registered_execution_profiles_gateway_marker_missing:${marker}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!resolver.includes("registered_execution_profile_ref_scope_denied")
|
||||||
|
|| !resolver.includes("targetBoundPlan")
|
||||||
|
|| /gelios|robot2b/i.test(resolver)
|
||||||
|
) {
|
||||||
|
throw new Error("engine_registered_execution_profiles_resolver_invalid");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertFresh(path) {
|
||||||
|
try {
|
||||||
|
await lstat(path);
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code === "ENOENT") return;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error("artifact_already_exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalTarScript() {
|
||||||
|
return [
|
||||||
|
"import gzip,io,pathlib,sys,tarfile",
|
||||||
|
"root=pathlib.Path(sys.argv[2])",
|
||||||
|
"with open(sys.argv[1],'xb') as out:",
|
||||||
|
" with gzip.GzipFile(filename='',mode='wb',fileobj=out,compresslevel=9,mtime=0) as gz:",
|
||||||
|
" with tarfile.open(fileobj=gz,mode='w',format=tarfile.PAX_FORMAT) as tar:",
|
||||||
|
" for top in ('manifest.env','files.txt','payload'):",
|
||||||
|
" p=root/top; paths=[p]+(sorted(p.rglob('*')) if p.is_dir() else [])",
|
||||||
|
" for x in paths:",
|
||||||
|
" info=tar.gettarinfo(str(x),arcname=x.relative_to(root).as_posix())",
|
||||||
|
" info.uid=info.gid=0; info.uname=info.gname='root'; info.mtime=0; "
|
||||||
|
+ "info.mode=0o755 if info.isdir() else 0o644",
|
||||||
|
" with (open(x,'rb') if info.isfile() else io.BytesIO()) as src: "
|
||||||
|
+ "tar.addfile(info,src if info.isfile() else None)",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function digest(value) {
|
||||||
|
return createHash("sha256").update(value).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command, args) {
|
||||||
|
const result = spawnSync(command, args, {
|
||||||
|
encoding: "utf8",
|
||||||
|
maxBuffer: 128 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
if (result.status !== 0) {
|
||||||
|
throw new Error(`${command}_failed:${result.stderr || result.stdout}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,33 @@ import {
|
||||||
geliosTelemetryFieldRegistryV5,
|
geliosTelemetryFieldRegistryV5,
|
||||||
} from "../../packages/external-provider-contract/providers/gelios/v12/index.mjs";
|
} from "../../packages/external-provider-contract/providers/gelios/v12/index.mjs";
|
||||||
|
|
||||||
|
const PUBLIC_PROFILE_METADATA = Object.freeze({
|
||||||
|
"gelios.positions.current.realtime.v7": {
|
||||||
|
cadence: "hot",
|
||||||
|
dataClass: "operational",
|
||||||
|
},
|
||||||
|
"gelios.positions.current.manual.v7": {
|
||||||
|
cadence: "on_demand",
|
||||||
|
dataClass: "operational",
|
||||||
|
},
|
||||||
|
"gelios.geozones.current.realtime.v1": {
|
||||||
|
cadence: "cold",
|
||||||
|
dataClass: "operational",
|
||||||
|
},
|
||||||
|
"gelios.units.profile.cold.v1": {
|
||||||
|
cadence: "cold",
|
||||||
|
dataClass: "operational",
|
||||||
|
},
|
||||||
|
"gelios.units.contacts.cold.v1": {
|
||||||
|
cadence: "cold",
|
||||||
|
dataClass: "restricted",
|
||||||
|
},
|
||||||
|
"gelios.units.identity.warm.v1": {
|
||||||
|
cadence: "warm",
|
||||||
|
dataClass: "restricted",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const engineRoot = resolve(
|
const engineRoot = resolve(
|
||||||
process.env.NODEDC_ENGINE_SOURCE_ROOT || "../NODEDC_ENGINE_INFRA",
|
process.env.NODEDC_ENGINE_SOURCE_ROOT || "../NODEDC_ENGINE_INFRA",
|
||||||
);
|
);
|
||||||
|
|
@ -72,9 +99,14 @@ function executionCatalogEntry() {
|
||||||
: {},
|
: {},
|
||||||
);
|
);
|
||||||
const { packageDigest: _packageDigest, ...artifacts } = plan.artifacts;
|
const { packageDigest: _packageDigest, ...artifacts } = plan.artifacts;
|
||||||
|
const publicMetadata = PUBLIC_PROFILE_METADATA[profile.id];
|
||||||
|
if (!publicMetadata) {
|
||||||
|
throw new Error(`gelios_v12_public_profile_metadata_missing:${profile.id}`);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
dataProductId: profile.dataProductId,
|
dataProductId: profile.dataProductId,
|
||||||
|
...publicMetadata,
|
||||||
capabilityIds: [...profile.capabilityIds],
|
capabilityIds: [...profile.capabilityIds],
|
||||||
stepSignatures: plan.steps.map((step) => ({
|
stepSignatures: plan.steps.map((step) => ({
|
||||||
id: step.id,
|
id: step.id,
|
||||||
|
|
@ -87,6 +119,7 @@ function executionCatalogEntry() {
|
||||||
...(step.config.nodeType ? { nodeType: step.config.nodeType } : {}),
|
...(step.config.nodeType ? { nodeType: step.config.nodeType } : {}),
|
||||||
})),
|
})),
|
||||||
artifacts,
|
artifacts,
|
||||||
|
executionPlanTemplate: plan,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,195 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
import {
|
||||||
|
compileL2ExecutionPlan,
|
||||||
|
instantiateL2Connection,
|
||||||
|
} from "../../packages/external-provider-contract/src/index.mjs";
|
||||||
|
import {
|
||||||
|
GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID,
|
||||||
|
geliosProviderPackageV13,
|
||||||
|
} from "../../packages/external-provider-contract/providers/gelios/v13/index.mjs";
|
||||||
|
|
||||||
|
const engineRoot = resolve(
|
||||||
|
process.env.NODEDC_ENGINE_SOURCE_ROOT || "../NODEDC_ENGINE_INFRA",
|
||||||
|
);
|
||||||
|
const executionCatalogPath = resolve(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
||||||
|
);
|
||||||
|
const securityCatalogPath = resolve(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
||||||
|
);
|
||||||
|
|
||||||
|
const executionCatalog = JSON.parse(
|
||||||
|
await readFile(executionCatalogPath, "utf8"),
|
||||||
|
);
|
||||||
|
for (const value of ["1.5.0"]) {
|
||||||
|
if (!executionCatalog.runtime.compilerVersions.includes(value)) {
|
||||||
|
executionCatalog.runtime.compilerVersions.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const value of ["bounded_response_lookup"]) {
|
||||||
|
if (!executionCatalog.runtime.derivationKinds.includes(value)) {
|
||||||
|
executionCatalog.runtime.derivationKinds.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const value of ["response.lookup.first", "response.lookup.list"]) {
|
||||||
|
if (!executionCatalog.runtime.ruleIds.includes(value)) {
|
||||||
|
executionCatalog.runtime.ruleIds.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
executionCatalog.runtime.compilerVersions.sort();
|
||||||
|
executionCatalog.runtime.derivationKinds.sort();
|
||||||
|
executionCatalog.runtime.ruleIds.sort();
|
||||||
|
executionCatalog.packages = executionCatalog.packages.filter(
|
||||||
|
(providerPackage) => providerPackage.id !== geliosProviderPackageV13.id,
|
||||||
|
);
|
||||||
|
executionCatalog.packages.push(executionCatalogEntry());
|
||||||
|
await writeFile(executionCatalogPath, `${JSON.stringify(executionCatalog, null, 2)}\n`);
|
||||||
|
|
||||||
|
const securityCatalog = JSON.parse(
|
||||||
|
await readFile(securityCatalogPath, "utf8"),
|
||||||
|
);
|
||||||
|
securityCatalog.packages = securityCatalog.packages.filter(
|
||||||
|
(providerPackage) => providerPackage.id !== geliosProviderPackageV13.id,
|
||||||
|
);
|
||||||
|
securityCatalog.packages.push(securityCatalogEntry());
|
||||||
|
await writeFile(securityCatalogPath, `${JSON.stringify(securityCatalog, null, 2)}\n`);
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
providerPackage: geliosProviderPackageV13.id,
|
||||||
|
executionCatalogPath,
|
||||||
|
securityCatalogPath,
|
||||||
|
historicalExecutionPackagePreserved: executionCatalog.packages.some(
|
||||||
|
(providerPackage) => providerPackage.id === "gelios.provider.v12",
|
||||||
|
),
|
||||||
|
historicalSecurityAuthorityPreserved: securityCatalog.packages.some(
|
||||||
|
(providerPackage) => providerPackage.id === "gelios.provider.v12",
|
||||||
|
),
|
||||||
|
activeContactsAuthority: geliosProviderPackageV13.id,
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
function selectedProfiles() {
|
||||||
|
return geliosProviderPackageV13.collectionProfiles.filter(
|
||||||
|
(profile) => profile.dataProductId === GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedCapabilityIds() {
|
||||||
|
return new Set(selectedProfiles().flatMap((profile) => profile.capabilityIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
function executionCatalogEntry() {
|
||||||
|
const profiles = selectedProfiles().map((profile) => {
|
||||||
|
const connection = instantiateL2Connection(geliosProviderPackageV13, {
|
||||||
|
tenantId: "tenant-catalog-build",
|
||||||
|
connectionId: `catalog-${profile.id.replaceAll(".", "-")}`,
|
||||||
|
collectionProfileId: profile.id,
|
||||||
|
providerCredentialRef: "ndc-credref:catalog-build-provider-v13",
|
||||||
|
});
|
||||||
|
const plan = compileL2ExecutionPlan(geliosProviderPackageV13, connection);
|
||||||
|
const { packageDigest: _packageDigest, ...artifacts } = plan.artifacts;
|
||||||
|
return {
|
||||||
|
id: profile.id,
|
||||||
|
dataProductId: profile.dataProductId,
|
||||||
|
capabilityIds: [...profile.capabilityIds],
|
||||||
|
stepSignatures: plan.steps.map((step) => ({
|
||||||
|
id: step.id,
|
||||||
|
kind: step.kind,
|
||||||
|
...(step.config.capabilityId ? { capabilityId: step.config.capabilityId } : {}),
|
||||||
|
...(step.config.mappingContractId ? {
|
||||||
|
mappingContractId: step.config.mappingContractId,
|
||||||
|
} : {}),
|
||||||
|
...(step.config.dataProductId ? { dataProductId: step.config.dataProductId } : {}),
|
||||||
|
...(step.config.nodeType ? { nodeType: step.config.nodeType } : {}),
|
||||||
|
})),
|
||||||
|
artifacts,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const capabilityIds = selectedCapabilityIds();
|
||||||
|
return {
|
||||||
|
id: geliosProviderPackageV13.id,
|
||||||
|
providerId: geliosProviderPackageV13.providerId,
|
||||||
|
version: geliosProviderPackageV13.version,
|
||||||
|
contractDigest: canonicalDigest(geliosProviderPackageV13),
|
||||||
|
providerCredential: {
|
||||||
|
authModeId: "gelios.rest-rotating-bearer.v3",
|
||||||
|
credentialType: "ndcProviderRotatingAccessApi",
|
||||||
|
},
|
||||||
|
publisher: {
|
||||||
|
nodeType: "n8n-nodes-ndc.ndcDataProductPublish",
|
||||||
|
credentialType: "ndcDataProductWriterApi",
|
||||||
|
},
|
||||||
|
capabilities: geliosProviderPackageV13.capabilities
|
||||||
|
.filter((capability) => capabilityIds.has(capability.id))
|
||||||
|
.map((capability) => ({
|
||||||
|
id: capability.id,
|
||||||
|
contractDigest: canonicalDigest(capability),
|
||||||
|
requestDigest: canonicalDigest(capability.request),
|
||||||
|
method: capability.request.method,
|
||||||
|
url: requestUrl(capability.request),
|
||||||
|
})),
|
||||||
|
profiles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function securityCatalogEntry() {
|
||||||
|
const productsByCapability = new Map(
|
||||||
|
geliosProviderPackageV13.capabilities.map((capability) => [capability.id, new Set()]),
|
||||||
|
);
|
||||||
|
for (const profile of selectedProfiles()) {
|
||||||
|
for (const capabilityId of profile.capabilityIds) {
|
||||||
|
productsByCapability.get(capabilityId)?.add(profile.dataProductId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: geliosProviderPackageV13.id,
|
||||||
|
version: geliosProviderPackageV13.version,
|
||||||
|
providerId: geliosProviderPackageV13.providerId,
|
||||||
|
providerCredential: {
|
||||||
|
authModeId: "gelios.rest-rotating-bearer.v3",
|
||||||
|
credentialType: "ndcProviderRotatingAccessApi",
|
||||||
|
},
|
||||||
|
capabilities: geliosProviderPackageV13.capabilities
|
||||||
|
.filter((capability) => productsByCapability.get(capability.id)?.size)
|
||||||
|
.map((capability) => ({
|
||||||
|
id: capability.id,
|
||||||
|
classification: capability.classification,
|
||||||
|
status: capability.status,
|
||||||
|
request: {
|
||||||
|
method: capability.request.method,
|
||||||
|
url: requestUrl(capability.request),
|
||||||
|
},
|
||||||
|
dataProductIds: [...productsByCapability.get(capability.id)].sort(),
|
||||||
|
})),
|
||||||
|
publisher: {
|
||||||
|
nodeType: "n8n-nodes-ndc.ndcDataProductPublish",
|
||||||
|
credentialType: "ndcDataProductWriterApi",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestUrl(request) {
|
||||||
|
const url = new URL(request.path, request.baseUrl);
|
||||||
|
for (const [name, value] of Object.entries(request.query || {})) {
|
||||||
|
url.searchParams.append(name, String(value));
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalDigest(value) {
|
||||||
|
return `sha256:${createHash("sha256").update(JSON.stringify(stableValue(value)), "utf8").digest("hex")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableValue(value) {
|
||||||
|
if (Array.isArray(value)) return value.map(stableValue);
|
||||||
|
if (!value || typeof value !== "object") return value;
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.keys(value).sort().map((key) => [key, stableValue(value[key])]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { readFile, writeFile } from "node:fs/promises";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
import { validateL2ExecutionPlan } from "../../packages/external-provider-contract/src/index.mjs";
|
||||||
|
import { geliosProviderPackageV12 } from "../../packages/external-provider-contract/providers/gelios/v12/index.mjs";
|
||||||
|
|
||||||
|
const PACKAGE_ID = "gelios.provider.v12";
|
||||||
|
const PREDECESSOR_VERSION = "12.0.0";
|
||||||
|
const TARGET_VERSION = "12.0.1";
|
||||||
|
const UNITS_CAPABILITY_ID = "gelios.units.current.read";
|
||||||
|
const engineRoot = resolve(
|
||||||
|
process.env.NODEDC_ENGINE_SOURCE_ROOT || "../NODEDC_ENGINE_INFRA",
|
||||||
|
);
|
||||||
|
const executionCatalogPath = resolve(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/server/assets/execution-plans/v1/catalog.json",
|
||||||
|
);
|
||||||
|
const securityCatalogPath = resolve(
|
||||||
|
engineRoot,
|
||||||
|
"nodedc-source/server/assets/provider-packages/v1/catalog.json",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
geliosProviderPackageV12.id !== PACKAGE_ID
|
||||||
|
|| geliosProviderPackageV12.version !== TARGET_VERSION
|
||||||
|
) {
|
||||||
|
throw new Error("gelios_v12_units_items_source_version_mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceCapabilities = new Map(
|
||||||
|
geliosProviderPackageV12.capabilities.map((capability) => [
|
||||||
|
capability.id,
|
||||||
|
capability,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const unitsCapability = sourceCapabilities.get(UNITS_CAPABILITY_ID);
|
||||||
|
if (
|
||||||
|
!unitsCapability
|
||||||
|
|| unitsCapability.request.response.collectionPaths[0] !== "items"
|
||||||
|
) {
|
||||||
|
throw new Error("gelios_v12_units_items_source_contract_invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionCatalog = JSON.parse(
|
||||||
|
await readFile(executionCatalogPath, "utf8"),
|
||||||
|
);
|
||||||
|
const executionPackage = requirePackage(executionCatalog, "execution");
|
||||||
|
assertMigratableVersion(executionPackage.version, "execution");
|
||||||
|
executionPackage.version = TARGET_VERSION;
|
||||||
|
executionPackage.contractDigest = canonicalDigest(geliosProviderPackageV12);
|
||||||
|
executionPackage.capabilities = geliosProviderPackageV12.capabilities.map(
|
||||||
|
(capability) => ({
|
||||||
|
id: capability.id,
|
||||||
|
contractDigest: canonicalDigest(capability),
|
||||||
|
requestDigest: canonicalDigest(capability.request),
|
||||||
|
method: capability.request.method,
|
||||||
|
url: requestUrl(capability.request),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const profile of executionPackage.profiles) {
|
||||||
|
const plan = profile.executionPlanTemplate;
|
||||||
|
if (!plan) throw new Error(`gelios_v12_registered_plan_missing:${profile.id}`);
|
||||||
|
if (plan.compilerVersion !== "1.4.0") {
|
||||||
|
throw new Error(`gelios_v12_registered_plan_compiler_drift:${profile.id}`);
|
||||||
|
}
|
||||||
|
plan.package.version = TARGET_VERSION;
|
||||||
|
plan.artifacts.packageDigest = canonicalDigest(geliosProviderPackageV12);
|
||||||
|
|
||||||
|
for (const step of plan.steps) {
|
||||||
|
const capabilityId = step.config?.capabilityId;
|
||||||
|
if (!capabilityId) continue;
|
||||||
|
const capability = sourceCapabilities.get(capabilityId);
|
||||||
|
if (!capability) {
|
||||||
|
throw new Error(
|
||||||
|
`gelios_v12_registered_plan_capability_missing:${profile.id}:${capabilityId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
step.config.capabilityDigest = canonicalDigest(capability);
|
||||||
|
if (step.kind === "provider_request") {
|
||||||
|
step.config.request = structuredClone(capability.request);
|
||||||
|
} else if (step.kind === "extract_items") {
|
||||||
|
step.config.response = structuredClone(capability.request.response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepsById = new Map(plan.steps.map((step) => [step.id, step]));
|
||||||
|
for (const node of plan.graphBlueprint.nodes) {
|
||||||
|
const step = stepsById.get(node.stepId);
|
||||||
|
if (!step) {
|
||||||
|
throw new Error(
|
||||||
|
`gelios_v12_registered_plan_blueprint_step_missing:${profile.id}:${node.stepId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
node.configDigest = canonicalDigest({
|
||||||
|
stepConfig: step.config,
|
||||||
|
...(node.adapterConfig ? { adapterConfig: node.adapterConfig } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
plan.graphBlueprint.blueprintDigest = digestWithout(
|
||||||
|
plan.graphBlueprint,
|
||||||
|
"blueprintDigest",
|
||||||
|
);
|
||||||
|
plan.executionPlanDigest = digestWithout(plan, "executionPlanDigest");
|
||||||
|
|
||||||
|
const validation = validateL2ExecutionPlan(plan);
|
||||||
|
if (!validation.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`gelios_v12_registered_plan_invalid:${profile.id}:`
|
||||||
|
+ validation.errors.join(","),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const technicalProfile = executionPackage.profiles.find(
|
||||||
|
(profile) => profile.id === "gelios.units.profile.cold.v1",
|
||||||
|
);
|
||||||
|
const technicalExtract = technicalProfile?.executionPlanTemplate?.steps.find(
|
||||||
|
(step) => (
|
||||||
|
step.kind === "extract_items"
|
||||||
|
&& step.config?.capabilityId === UNITS_CAPABILITY_ID
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (technicalExtract?.config?.response?.collectionPaths?.[0] !== "items") {
|
||||||
|
throw new Error("gelios_v12_technical_profile_items_envelope_missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
const securityCatalog = JSON.parse(
|
||||||
|
await readFile(securityCatalogPath, "utf8"),
|
||||||
|
);
|
||||||
|
const securityPackage = requirePackage(securityCatalog, "security");
|
||||||
|
assertMigratableVersion(securityPackage.version, "security");
|
||||||
|
securityPackage.version = TARGET_VERSION;
|
||||||
|
|
||||||
|
await writeFile(
|
||||||
|
executionCatalogPath,
|
||||||
|
`${JSON.stringify(executionCatalog, null, 2)}\n`,
|
||||||
|
);
|
||||||
|
await writeFile(
|
||||||
|
securityCatalogPath,
|
||||||
|
`${JSON.stringify(securityCatalog, null, 2)}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
packageId: PACKAGE_ID,
|
||||||
|
predecessorVersion: PREDECESSOR_VERSION,
|
||||||
|
targetVersion: TARGET_VERSION,
|
||||||
|
executionCatalogPath,
|
||||||
|
securityCatalogPath,
|
||||||
|
registeredProfiles: executionPackage.profiles.length,
|
||||||
|
technicalProfile: technicalProfile.id,
|
||||||
|
collectionPaths: technicalExtract.config.response.collectionPaths,
|
||||||
|
compilerVersion: technicalProfile.executionPlanTemplate.compilerVersion,
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
|
function requirePackage(catalog, kind) {
|
||||||
|
const matches = catalog.packages.filter(
|
||||||
|
(providerPackage) => providerPackage.id === PACKAGE_ID,
|
||||||
|
);
|
||||||
|
if (matches.length !== 1) {
|
||||||
|
throw new Error(`gelios_v12_${kind}_package_cardinality_invalid`);
|
||||||
|
}
|
||||||
|
return matches[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertMigratableVersion(version, kind) {
|
||||||
|
if (![PREDECESSOR_VERSION, TARGET_VERSION].includes(version)) {
|
||||||
|
throw new Error(
|
||||||
|
`gelios_v12_${kind}_package_version_drift:${String(version)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestUrl(request) {
|
||||||
|
const url = new URL(request.path, request.baseUrl);
|
||||||
|
for (const [name, value] of Object.entries(request.query || {})) {
|
||||||
|
url.searchParams.append(name, String(value));
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function digestWithout(value, key) {
|
||||||
|
const descriptor = structuredClone(value);
|
||||||
|
delete descriptor[key];
|
||||||
|
return canonicalDigest(descriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalDigest(value) {
|
||||||
|
return `sha256:${createHash("sha256").update(
|
||||||
|
JSON.stringify(stableValue(value)),
|
||||||
|
"utf8",
|
||||||
|
).digest("hex")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stableValue(value) {
|
||||||
|
if (Array.isArray(value)) return value.map(stableValue);
|
||||||
|
if (!value || typeof value !== "object") return value;
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.keys(value)
|
||||||
|
.filter((key) => value[key] !== undefined)
|
||||||
|
.sort()
|
||||||
|
.map((key) => [key, stableValue(value[key])]),
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,130 @@
|
||||||
|
import hashlib
|
||||||
|
import importlib.machinery
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||||
|
BUILDER_PATH = SCRIPT_DIR / "build-engine-l1-agent-init-concurrency-artifact.mjs"
|
||||||
|
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||||
|
PATCH_ID = "engine-l1-agent-init-concurrency-20991231-999"
|
||||||
|
SOURCE_COMMIT = "c4077fe0d8d824fa58616eeb986dc78ca43070da"
|
||||||
|
|
||||||
|
|
||||||
|
def load_runner():
|
||||||
|
loader = importlib.machinery.SourceFileLoader(
|
||||||
|
"nodedc_engine_l1_agent_init_concurrency",
|
||||||
|
str(RUNNER_PATH),
|
||||||
|
)
|
||||||
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
RUNNER = load_runner()
|
||||||
|
|
||||||
|
|
||||||
|
class EngineL1AgentInitConcurrencyArtifactTest(unittest.TestCase):
|
||||||
|
def require_target_commit(self):
|
||||||
|
current = subprocess.run(
|
||||||
|
["git", "-C", str(ENGINE_ROOT), "rev-parse", "HEAD"],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
).stdout.strip()
|
||||||
|
if current != SOURCE_COMMIT:
|
||||||
|
self.skipTest("historical Engine L1 agent-init source has advanced")
|
||||||
|
|
||||||
|
def build(self, artifact_dir):
|
||||||
|
self.require_target_commit()
|
||||||
|
environment = os.environ.copy()
|
||||||
|
environment["NODEDC_ENGINE_SOURCE_ROOT"] = str(ENGINE_ROOT)
|
||||||
|
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
||||||
|
completed = subprocess.run(
|
||||||
|
["node", str(BUILDER_PATH), PATCH_ID],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=environment,
|
||||||
|
)
|
||||||
|
return json.loads(completed.stdout)
|
||||||
|
|
||||||
|
def test_artifact_is_exact_deterministic_and_runner_accepted(self):
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="nodedc-engine-l1-agent-init-",
|
||||||
|
) as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
first = self.build(root / "first")
|
||||||
|
second = self.build(root / "second")
|
||||||
|
first_artifact = Path(first["artifact"])
|
||||||
|
second_artifact = Path(second["artifact"])
|
||||||
|
|
||||||
|
self.assertEqual(first["sourceCommit"], SOURCE_COMMIT)
|
||||||
|
self.assertEqual(first_artifact.read_bytes(), second_artifact.read_bytes())
|
||||||
|
self.assertEqual(
|
||||||
|
first["sha256"],
|
||||||
|
hashlib.sha256(first_artifact.read_bytes()).hexdigest(),
|
||||||
|
)
|
||||||
|
self.assertFalse(first["n8nCoreChanged"])
|
||||||
|
|
||||||
|
manifest, entries, payload = RUNNER.load_artifact(
|
||||||
|
first_artifact,
|
||||||
|
root / "loaded",
|
||||||
|
)
|
||||||
|
self.assertEqual(manifest["component"], "engine")
|
||||||
|
self.assertEqual(manifest["type"], "app-overlay")
|
||||||
|
self.assertEqual(tuple(entries), tuple(first["entries"]))
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.component_services("engine", entries),
|
||||||
|
("nodedc-backend", "app"),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.component_healthchecks(
|
||||||
|
"engine",
|
||||||
|
entries,
|
||||||
|
("nodedc-backend", "app"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"http://127.0.0.1:8080/",
|
||||||
|
"http://127.0.0.1:3001/health",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertTrue(RUNNER.component_publish_dist("engine", entries))
|
||||||
|
self.assertEqual(RUNNER.component_builds("engine", entries), ())
|
||||||
|
|
||||||
|
payload_files = {
|
||||||
|
path.relative_to(payload).as_posix()
|
||||||
|
for path in payload.rglob("*")
|
||||||
|
if path.is_file()
|
||||||
|
}
|
||||||
|
self.assertEqual(payload_files, set(first["targetSha256"]))
|
||||||
|
for relative_path, expected in first["targetSha256"].items():
|
||||||
|
self.assertEqual(
|
||||||
|
hashlib.sha256((payload / relative_path).read_bytes()).hexdigest(),
|
||||||
|
expected,
|
||||||
|
)
|
||||||
|
self.assertTrue(all("/server/data/" not in path for path in payload_files))
|
||||||
|
self.assertTrue(all("/tests/" not in path for path in payload_files))
|
||||||
|
self.assertTrue(all(not path.endswith(".env") for path in payload_files))
|
||||||
|
self.assertTrue(all("n8n-data" not in path for path in payload_files))
|
||||||
|
|
||||||
|
with tarfile.open(first_artifact, "r:gz") as archive:
|
||||||
|
for member in archive.getmembers():
|
||||||
|
self.assertIn(member.type, (tarfile.REGTYPE, tarfile.DIRTYPE))
|
||||||
|
self.assertEqual(member.uid, 0)
|
||||||
|
self.assertEqual(member.gid, 0)
|
||||||
|
self.assertEqual(member.mtime, 0)
|
||||||
|
self.assertNotIn(".DS_Store", member.name)
|
||||||
|
self.assertNotIn("__MACOSX", member.name)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
import hashlib
|
||||||
|
import importlib.machinery
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||||
|
BUILDER_PATH = (
|
||||||
|
SCRIPT_DIR
|
||||||
|
/ "build-engine-mcp-gelios-units-items-envelope-artifact.mjs"
|
||||||
|
)
|
||||||
|
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||||
|
PATCH_ID = "engine-mcp-gelios-units-items-envelope-20991231-999"
|
||||||
|
|
||||||
|
|
||||||
|
def load_runner():
|
||||||
|
loader = importlib.machinery.SourceFileLoader(
|
||||||
|
"nodedc_engine_mcp_gelios_units_items",
|
||||||
|
str(RUNNER_PATH),
|
||||||
|
)
|
||||||
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
RUNNER = load_runner()
|
||||||
|
|
||||||
|
|
||||||
|
class EngineMcpGeliosUnitsItemsEnvelopeTest(unittest.TestCase):
|
||||||
|
def build(self, artifact_dir):
|
||||||
|
environment = os.environ.copy()
|
||||||
|
environment["NODEDC_ENGINE_SOURCE_ROOT"] = str(ENGINE_ROOT)
|
||||||
|
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
||||||
|
completed = subprocess.run(
|
||||||
|
["node", str(BUILDER_PATH), PATCH_ID],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=environment,
|
||||||
|
)
|
||||||
|
return json.loads(completed.stdout)
|
||||||
|
|
||||||
|
def test_builder_emits_exact_deterministic_catalog_slice(self):
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="nodedc-engine-gelios-units-items-builder-"
|
||||||
|
) as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
first = self.build(root / "first")
|
||||||
|
second = self.build(root / "second")
|
||||||
|
first_artifact = Path(first["artifact"])
|
||||||
|
second_artifact = Path(second["artifact"])
|
||||||
|
self.assertEqual(
|
||||||
|
first_artifact.read_bytes(),
|
||||||
|
second_artifact.read_bytes(),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
first["sha256"],
|
||||||
|
hashlib.sha256(first_artifact.read_bytes()).hexdigest(),
|
||||||
|
)
|
||||||
|
self.assertEqual(first["services"], ["nodedc-backend"])
|
||||||
|
self.assertEqual(first["mcpVersion"], "0.12.0")
|
||||||
|
self.assertEqual(
|
||||||
|
first["registeredProfile"],
|
||||||
|
"gelios.units.profile.cold.v1",
|
||||||
|
)
|
||||||
|
self.assertEqual(first["responseCollectionPath"], "items")
|
||||||
|
self.assertEqual(first["existingMaterializerReused"], True)
|
||||||
|
self.assertEqual(first["engineRuntimeCodeChanged"], False)
|
||||||
|
self.assertEqual(first["n8nCoreChanged"], False)
|
||||||
|
self.assertEqual(first["l2GraphChanged"], False)
|
||||||
|
|
||||||
|
extract = root / "extract"
|
||||||
|
with tarfile.open(first_artifact, "r:gz") as archive:
|
||||||
|
archive.extractall(extract, filter="data")
|
||||||
|
entries = tuple(
|
||||||
|
(extract / "files.txt").read_text(encoding="utf-8").splitlines()
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
entries,
|
||||||
|
RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_ARTIFACT_ENTRIES,
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
RUNNER.is_engine_mcp_gelios_units_items_slice(
|
||||||
|
"engine",
|
||||||
|
entries,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.component_services("engine", entries),
|
||||||
|
("nodedc-backend",),
|
||||||
|
)
|
||||||
|
RUNNER.validate_engine_mcp_gelios_units_items_slice(
|
||||||
|
extract / "payload",
|
||||||
|
entries,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_preflight_requires_exact_registered_profiles_predecessor(self):
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="nodedc-engine-gelios-units-items-preflight-"
|
||||||
|
) as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
all_live = {
|
||||||
|
**RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256,
|
||||||
|
**RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256,
|
||||||
|
}
|
||||||
|
for relative_path in all_live:
|
||||||
|
path = root / relative_path
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text("live\n", encoding="utf-8")
|
||||||
|
|
||||||
|
def exact_hash(path):
|
||||||
|
relative_path = Path(path).relative_to(root).as_posix()
|
||||||
|
return all_live[relative_path]
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||||
|
mock.patch.object(RUNNER, "sha256_file", side_effect=exact_hash),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"preflight_engine_credential_backend_runtime",
|
||||||
|
return_value={"mode": "verified-derived-retry"},
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = (
|
||||||
|
RUNNER
|
||||||
|
.preflight_engine_mcp_gelios_units_items_predecessor()
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
result["mode"],
|
||||||
|
(
|
||||||
|
"registered-profiles-v2-to-"
|
||||||
|
"gelios-v12.0.1-units-items-envelope"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
result["new_paths"],
|
||||||
|
RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS,
|
||||||
|
)
|
||||||
|
|
||||||
|
changed = next(iter(
|
||||||
|
RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256
|
||||||
|
))
|
||||||
|
with (
|
||||||
|
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"sha256_file",
|
||||||
|
side_effect=lambda path: (
|
||||||
|
"0" * 64
|
||||||
|
if Path(path).relative_to(root).as_posix() == changed
|
||||||
|
else exact_hash(path)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
with self.assertRaises(RUNNER.DeployError):
|
||||||
|
(
|
||||||
|
RUNNER
|
||||||
|
.preflight_engine_mcp_gelios_units_items_predecessor()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_plan_renders_exact_provider_package_patch_boundary(self):
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="nodedc-engine-gelios-units-items-plan-"
|
||||||
|
) as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
built = self.build(root / "artifacts")
|
||||||
|
artifact = Path(built["artifact"])
|
||||||
|
live_root = root / "live"
|
||||||
|
live_root.mkdir()
|
||||||
|
preflight = {
|
||||||
|
"mode": (
|
||||||
|
"registered-profiles-v2-to-"
|
||||||
|
"gelios-v12.0.1-units-items-envelope"
|
||||||
|
),
|
||||||
|
"predecessor_sha256": dict(
|
||||||
|
RUNNER
|
||||||
|
.ENGINE_MCP_GELIOS_UNITS_ITEMS_PREDECESSOR_SHA256
|
||||||
|
),
|
||||||
|
"foundation_sha256": dict(
|
||||||
|
RUNNER
|
||||||
|
.ENGINE_MCP_GELIOS_UNITS_ITEMS_FOUNDATION_SHA256
|
||||||
|
),
|
||||||
|
"target_sha256": dict(
|
||||||
|
RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_TARGET_SHA256
|
||||||
|
),
|
||||||
|
"new_paths": (
|
||||||
|
RUNNER.ENGINE_MCP_GELIOS_UNITS_ITEMS_NEW_PATHS
|
||||||
|
),
|
||||||
|
"backend_mode": "verified-derived-retry",
|
||||||
|
}
|
||||||
|
output = io.StringIO()
|
||||||
|
with (
|
||||||
|
mock.patch.object(RUNNER, "validate_artifact_location"),
|
||||||
|
mock.patch.object(RUNNER, "ensure_layout"),
|
||||||
|
mock.patch.object(RUNNER, "TMP_DIR", root),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"component_root",
|
||||||
|
return_value=live_root,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"component_compose_root",
|
||||||
|
return_value=live_root,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"preflight_engine_mcp_gelios_units_items_predecessor",
|
||||||
|
return_value=preflight,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"preflight_engine_credential_backend_runtime",
|
||||||
|
return_value={"mode": "verified-derived-retry"},
|
||||||
|
),
|
||||||
|
mock.patch.object(RUNNER, "state_has_sha", return_value=False),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"state_has_patch_id",
|
||||||
|
return_value=False,
|
||||||
|
),
|
||||||
|
redirect_stdout(output),
|
||||||
|
):
|
||||||
|
RUNNER.plan_artifact(artifact)
|
||||||
|
plan = output.getvalue()
|
||||||
|
self.assertIn("services=nodedc-backend", plan)
|
||||||
|
self.assertIn("engine_mcp_version=0.12.0", plan)
|
||||||
|
self.assertIn(
|
||||||
|
"engine_mcp_provider_package=gelios.provider.v12@12.0.1",
|
||||||
|
plan,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"engine_mcp_registered_profile=gelios.units.profile.cold.v1",
|
||||||
|
plan,
|
||||||
|
)
|
||||||
|
self.assertIn("engine_mcp_response_collection_path=items", plan)
|
||||||
|
self.assertIn("engine_mcp_existing_materializer=reused", plan)
|
||||||
|
self.assertIn("engine_mcp_runtime_code_changed=no", plan)
|
||||||
|
self.assertIn("engine_mcp_provider_endpoints_changed=no", plan)
|
||||||
|
self.assertIn("l2_graph=untouched", plan)
|
||||||
|
self.assertIn("n8n_core=untouched", plan)
|
||||||
|
self.assertIn("foundry=untouched", plan)
|
||||||
|
self.assertIn("state=new", plan)
|
||||||
|
|
||||||
|
def test_live_acceptance_probes_catalog_and_tools(self):
|
||||||
|
expected_live = (
|
||||||
|
"engine-mcp-gelios-units-items:0.12.0:"
|
||||||
|
"gelios.provider.v12@12.0.1:technical-profile:items"
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
mock.patch.object(RUNNER, "component_root", return_value=ENGINE_ROOT),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"engine_backend_container_id",
|
||||||
|
return_value="backend",
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"run_engine_backend_probe",
|
||||||
|
return_value=expected_live,
|
||||||
|
) as backend_probe,
|
||||||
|
):
|
||||||
|
result = RUNNER.accept_engine_mcp_gelios_units_items_runtime()
|
||||||
|
self.assertEqual(result["live"], expected_live)
|
||||||
|
probe_source = backend_probe.call_args.args[0][-1]
|
||||||
|
self.assertIn("engine_list_l2_execution_profiles", probe_source)
|
||||||
|
self.assertIn("engine_plan_registered_l2_execution", probe_source)
|
||||||
|
self.assertIn("gelios.units.profile.cold.v1", probe_source)
|
||||||
|
self.assertIn("collectionPaths?.[0]!=='items'", probe_source)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
|
|
@ -0,0 +1,316 @@
|
||||||
|
import hashlib
|
||||||
|
import importlib.machinery
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from contextlib import redirect_stdout
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||||
|
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||||
|
BUILDER_PATH = (
|
||||||
|
SCRIPT_DIR
|
||||||
|
/ "build-engine-mcp-registered-execution-profiles-artifact.mjs"
|
||||||
|
)
|
||||||
|
ENGINE_ROOT = SCRIPT_DIR.parent.parent.parent / "NODEDC_ENGINE_INFRA"
|
||||||
|
PATCH_ID = "engine-mcp-registered-execution-profiles-20991231-999"
|
||||||
|
|
||||||
|
|
||||||
|
def load_runner():
|
||||||
|
loader = importlib.machinery.SourceFileLoader(
|
||||||
|
"nodedc_engine_mcp_registered_execution_profiles",
|
||||||
|
str(RUNNER_PATH),
|
||||||
|
)
|
||||||
|
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
RUNNER = load_runner()
|
||||||
|
|
||||||
|
|
||||||
|
class EngineMcpRegisteredExecutionProfilesTest(unittest.TestCase):
|
||||||
|
def require_current_target_source(self):
|
||||||
|
for relative_path, expected in (
|
||||||
|
RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256.items()
|
||||||
|
):
|
||||||
|
path = ENGINE_ROOT / relative_path
|
||||||
|
if (
|
||||||
|
not path.is_file()
|
||||||
|
or hashlib.sha256(path.read_bytes()).hexdigest() != expected
|
||||||
|
):
|
||||||
|
self.skipTest("Registered execution profiles source advanced")
|
||||||
|
|
||||||
|
def build(self, artifact_dir):
|
||||||
|
environment = os.environ.copy()
|
||||||
|
environment["NODEDC_ENGINE_SOURCE_ROOT"] = str(ENGINE_ROOT)
|
||||||
|
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
||||||
|
completed = subprocess.run(
|
||||||
|
["node", str(BUILDER_PATH), PATCH_ID],
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
env=environment,
|
||||||
|
)
|
||||||
|
return json.loads(completed.stdout)
|
||||||
|
|
||||||
|
def test_builder_emits_exact_deterministic_backend_slice(self):
|
||||||
|
self.require_current_target_source()
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="nodedc-engine-registered-profiles-builder-"
|
||||||
|
) as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
first = self.build(root / "first")
|
||||||
|
second = self.build(root / "second")
|
||||||
|
first_artifact = Path(first["artifact"])
|
||||||
|
second_artifact = Path(second["artifact"])
|
||||||
|
self.assertEqual(
|
||||||
|
first_artifact.read_bytes(),
|
||||||
|
second_artifact.read_bytes(),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
first["sha256"],
|
||||||
|
hashlib.sha256(first_artifact.read_bytes()).hexdigest(),
|
||||||
|
)
|
||||||
|
self.assertEqual(first["services"], ["nodedc-backend"])
|
||||||
|
self.assertEqual(first["mcpVersion"], "0.12.0")
|
||||||
|
self.assertEqual(first["registeredProfiles"], 6)
|
||||||
|
self.assertEqual(first["existingMaterializerReused"], True)
|
||||||
|
self.assertEqual(
|
||||||
|
first["nodeIntelligenceAttestationChanged"],
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.assertEqual(first["nodeIntelligenceImageChanged"], False)
|
||||||
|
self.assertEqual(first["nodeIntelligenceSourceChanged"], False)
|
||||||
|
self.assertEqual(first["n8nCoreChanged"], False)
|
||||||
|
self.assertEqual(first["l2GraphChanged"], False)
|
||||||
|
|
||||||
|
extract = root / "extract"
|
||||||
|
with tarfile.open(first_artifact, "r:gz") as archive:
|
||||||
|
archive.extractall(extract, filter="data")
|
||||||
|
entries = tuple(
|
||||||
|
(extract / "files.txt").read_text(encoding="utf-8").splitlines()
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
entries,
|
||||||
|
RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_ARTIFACT_ENTRIES,
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
RUNNER.is_engine_mcp_registered_execution_profiles_slice(
|
||||||
|
"engine",
|
||||||
|
entries,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.component_services("engine", entries),
|
||||||
|
("nodedc-backend",),
|
||||||
|
)
|
||||||
|
RUNNER.validate_engine_mcp_registered_execution_profiles_slice(
|
||||||
|
extract / "payload",
|
||||||
|
entries,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_preflight_requires_exact_046_predecessor_and_absent_new_paths(self):
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="nodedc-engine-registered-profiles-preflight-"
|
||||||
|
) as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
all_live = {
|
||||||
|
**RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_PREDECESSOR_SHA256,
|
||||||
|
**RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256,
|
||||||
|
}
|
||||||
|
for relative_path in all_live:
|
||||||
|
path = root / relative_path
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text("live\n", encoding="utf-8")
|
||||||
|
|
||||||
|
def exact_hash(path):
|
||||||
|
relative_path = Path(path).relative_to(root).as_posix()
|
||||||
|
return all_live[relative_path]
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||||
|
mock.patch.object(RUNNER, "sha256_file", side_effect=exact_hash),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"preflight_engine_credential_backend_runtime",
|
||||||
|
return_value={"mode": "verified-derived-retry"},
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = (
|
||||||
|
RUNNER
|
||||||
|
.preflight_engine_mcp_registered_execution_profiles_predecessor()
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
result["mode"],
|
||||||
|
"gelios-items-envelope-v12-to-registered-profiles-v2-attested",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
result["new_paths"],
|
||||||
|
RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS,
|
||||||
|
)
|
||||||
|
|
||||||
|
new_path = (
|
||||||
|
root
|
||||||
|
/ RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS[0]
|
||||||
|
)
|
||||||
|
new_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
new_path.write_text("unexpected\n", encoding="utf-8")
|
||||||
|
with (
|
||||||
|
mock.patch.object(RUNNER, "component_root", return_value=root),
|
||||||
|
mock.patch.object(RUNNER, "sha256_file", side_effect=exact_hash),
|
||||||
|
):
|
||||||
|
with self.assertRaises(RUNNER.DeployError):
|
||||||
|
(
|
||||||
|
RUNNER
|
||||||
|
.preflight_engine_mcp_registered_execution_profiles_predecessor()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_plan_renders_exact_server_side_planning_boundary(self):
|
||||||
|
self.require_current_target_source()
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="nodedc-engine-registered-profiles-plan-"
|
||||||
|
) as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
built = self.build(root / "artifacts")
|
||||||
|
artifact = Path(built["artifact"])
|
||||||
|
live_root = root / "live"
|
||||||
|
live_root.mkdir()
|
||||||
|
preflight = {
|
||||||
|
"mode":
|
||||||
|
"gelios-items-envelope-v12-to-registered-profiles-v2-attested",
|
||||||
|
"predecessor_sha256": dict(
|
||||||
|
RUNNER
|
||||||
|
.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_PREDECESSOR_SHA256
|
||||||
|
),
|
||||||
|
"foundation_sha256": dict(
|
||||||
|
RUNNER
|
||||||
|
.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_FOUNDATION_SHA256
|
||||||
|
),
|
||||||
|
"target_sha256": dict(
|
||||||
|
RUNNER
|
||||||
|
.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256
|
||||||
|
),
|
||||||
|
"new_paths": (
|
||||||
|
RUNNER
|
||||||
|
.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_NEW_PATHS
|
||||||
|
),
|
||||||
|
"backend_mode": "verified-derived-retry",
|
||||||
|
}
|
||||||
|
output = io.StringIO()
|
||||||
|
with (
|
||||||
|
mock.patch.object(RUNNER, "validate_artifact_location"),
|
||||||
|
mock.patch.object(RUNNER, "ensure_layout"),
|
||||||
|
mock.patch.object(RUNNER, "TMP_DIR", root),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"component_root",
|
||||||
|
return_value=live_root,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"component_compose_root",
|
||||||
|
return_value=live_root,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"preflight_engine_mcp_registered_execution_profiles_predecessor",
|
||||||
|
return_value=preflight,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"preflight_engine_credential_backend_runtime",
|
||||||
|
return_value={"mode": "verified-derived-retry"},
|
||||||
|
),
|
||||||
|
mock.patch.object(RUNNER, "state_has_sha", return_value=False),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"state_has_patch_id",
|
||||||
|
return_value=False,
|
||||||
|
),
|
||||||
|
redirect_stdout(output),
|
||||||
|
):
|
||||||
|
RUNNER.plan_artifact(artifact)
|
||||||
|
plan = output.getvalue()
|
||||||
|
self.assertIn("services=nodedc-backend", plan)
|
||||||
|
self.assertIn("engine_mcp_version=0.12.0", plan)
|
||||||
|
self.assertIn("engine_mcp_registered_profiles=6", plan)
|
||||||
|
self.assertIn("engine_mcp_target_scope=server-derived", plan)
|
||||||
|
self.assertIn("engine_mcp_existing_materializer=reused", plan)
|
||||||
|
self.assertIn("engine_mcp_provider_endpoints_included=no", plan)
|
||||||
|
self.assertIn(
|
||||||
|
"engine_node_intelligence_attestation=updated",
|
||||||
|
plan,
|
||||||
|
)
|
||||||
|
self.assertIn("engine_node_intelligence_image=untouched", plan)
|
||||||
|
self.assertIn("l2_graph=untouched", plan)
|
||||||
|
self.assertIn("n8n_core=untouched", plan)
|
||||||
|
self.assertIn("foundry=untouched", plan)
|
||||||
|
self.assertIn("state=new", plan)
|
||||||
|
|
||||||
|
def test_live_acceptance_probes_tools_catalog_and_target_rebinding(self):
|
||||||
|
self.require_current_target_source()
|
||||||
|
expected_live = (
|
||||||
|
"engine-mcp-registered-execution-profiles:"
|
||||||
|
"0.12.0:profiles-6:target-scope:existing-materializer"
|
||||||
|
)
|
||||||
|
with (
|
||||||
|
mock.patch.object(RUNNER, "component_root", return_value=ENGINE_ROOT),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"engine_backend_container_id",
|
||||||
|
return_value="backend",
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"run_engine_backend_probe",
|
||||||
|
return_value=expected_live,
|
||||||
|
) as backend_probe,
|
||||||
|
):
|
||||||
|
result = (
|
||||||
|
RUNNER.accept_engine_mcp_registered_execution_profiles_runtime()
|
||||||
|
)
|
||||||
|
self.assertEqual(result["live"], expected_live)
|
||||||
|
probe_source = backend_probe.call_args.args[0][-1]
|
||||||
|
self.assertIn("engine_list_l2_execution_profiles", probe_source)
|
||||||
|
self.assertIn("engine_plan_registered_l2_execution", probe_source)
|
||||||
|
self.assertIn("tenant-probe", probe_source)
|
||||||
|
self.assertIn("ndc-credref:engine-target-", probe_source)
|
||||||
|
|
||||||
|
def test_target_gateway_matches_node_intelligence_attestation(self):
|
||||||
|
descriptor_path = (
|
||||||
|
ENGINE_ROOT
|
||||||
|
/ RUNNER.ENGINE_NODE_INTELLIGENCE_DESCRIPTOR_REL
|
||||||
|
)
|
||||||
|
descriptor = RUNNER.read_engine_node_intelligence_descriptor(
|
||||||
|
descriptor_path,
|
||||||
|
"registered profiles target node-intelligence descriptor",
|
||||||
|
)
|
||||||
|
with mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"component_root",
|
||||||
|
return_value=ENGINE_ROOT,
|
||||||
|
):
|
||||||
|
gateway_sha256 = (
|
||||||
|
RUNNER.validate_installed_engine_node_intelligence_source(
|
||||||
|
descriptor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
gateway_sha256,
|
||||||
|
RUNNER.ENGINE_MCP_REGISTERED_EXECUTION_PROFILES_TARGET_SHA256[
|
||||||
|
RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
|
|
@ -147,15 +147,15 @@ class EngineNodeIntelligenceTest(unittest.TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(actual, descriptor)
|
self.assertEqual(actual, descriptor)
|
||||||
|
|
||||||
def test_gateway_accepts_registered_0_11_and_rejects_unknown_successors(self):
|
def test_gateway_accepts_registered_0_12_and_rejects_unknown_successors(self):
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
payload, descriptor = self.make_activation_payload(Path(directory))
|
payload, descriptor = self.make_activation_payload(Path(directory))
|
||||||
gateway = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
gateway = payload / RUNNER.ENGINE_NODE_INTELLIGENCE_GATEWAY_REL
|
||||||
gateway_text = gateway.read_text(encoding="utf-8")
|
gateway_text = gateway.read_text(encoding="utf-8")
|
||||||
self.assertIn("const ENGINE_AGENT_MCP_VERSION = '0.11.0'", gateway_text)
|
self.assertIn("const ENGINE_AGENT_MCP_VERSION = '0.12.0'", gateway_text)
|
||||||
gateway.write_text(
|
gateway.write_text(
|
||||||
gateway_text.replace(
|
gateway_text.replace(
|
||||||
"const ENGINE_AGENT_MCP_VERSION = '0.11.0'",
|
"const ENGINE_AGENT_MCP_VERSION = '0.12.0'",
|
||||||
"const ENGINE_AGENT_MCP_VERSION = '9.9.9'",
|
"const ENGINE_AGENT_MCP_VERSION = '9.9.9'",
|
||||||
),
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ export * from "./v9/index.mjs";
|
||||||
export { geliosProviderPackageV10 } from "./v10/index.mjs";
|
export { geliosProviderPackageV10 } from "./v10/index.mjs";
|
||||||
export { geliosProviderPackageV11 } from "./v11/index.mjs";
|
export { geliosProviderPackageV11 } from "./v11/index.mjs";
|
||||||
export { geliosProviderPackageV12 } from "./v12/index.mjs";
|
export { geliosProviderPackageV12 } from "./v12/index.mjs";
|
||||||
|
export { geliosProviderPackageV13 } from "./v13/index.mjs";
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { geliosProviderPackageV11 } from "../v11/package.mjs";
|
import { geliosProviderPackageV11 } from "../v11/package.mjs";
|
||||||
|
|
||||||
export const GELIOS_PROVIDER_PACKAGE_ID = "gelios.provider.v12";
|
export const GELIOS_PROVIDER_PACKAGE_ID = "gelios.provider.v12";
|
||||||
export const GELIOS_PROVIDER_PACKAGE_VERSION = "12.0.0";
|
export const GELIOS_PROVIDER_PACKAGE_VERSION = "12.0.1";
|
||||||
export const GELIOS_UNIT_IDENTITY_DATA_PRODUCT_ID = "fleet.units.identity.current.v1";
|
export const GELIOS_UNIT_IDENTITY_DATA_PRODUCT_ID = "fleet.units.identity.current.v1";
|
||||||
export const GELIOS_UNIT_IDENTITY_DATA_PRODUCT_VERSION = "1.0.0";
|
export const GELIOS_UNIT_IDENTITY_DATA_PRODUCT_VERSION = "1.0.0";
|
||||||
export const GELIOS_UNIT_IDENTITY_ONTOLOGY_REVISION = "ontology.map.moving_object.v3";
|
export const GELIOS_UNIT_IDENTITY_ONTOLOGY_REVISION = "ontology.map.moving_object.v3";
|
||||||
|
|
||||||
|
const IDENTITY_CONTRACT_VERSION = "12.0.0";
|
||||||
const IDENTITY_CAPABILITY_ID = "gelios.units.identity.read";
|
const IDENTITY_CAPABILITY_ID = "gelios.units.identity.read";
|
||||||
|
const UNITS_CAPABILITY_ID = "gelios.units.current.read";
|
||||||
const IDENTITY_PROFILE_ID = "gelios.units.identity.warm.v1";
|
const IDENTITY_PROFILE_ID = "gelios.units.identity.warm.v1";
|
||||||
const IDENTITY_MAPPING_ID = "gelios.units.to.fleet.units.identity.current.v1";
|
const IDENTITY_MAPPING_ID = "gelios.units.to.fleet.units.identity.current.v1";
|
||||||
const IDENTITY_TEMPLATE_ID = "gelios.units.identity.l2.v1";
|
const IDENTITY_TEMPLATE_ID = "gelios.units.identity.l2.v1";
|
||||||
|
|
@ -24,33 +26,43 @@ const identityCapability = value.capabilities.find(
|
||||||
(capability) => capability.id === IDENTITY_CAPABILITY_ID,
|
(capability) => capability.id === IDENTITY_CAPABILITY_ID,
|
||||||
);
|
);
|
||||||
if (!identityCapability) throw new Error("gelios_v12_identity_capability_missing");
|
if (!identityCapability) throw new Error("gelios_v12_identity_capability_missing");
|
||||||
identityCapability.request.response.collectionPaths = [
|
prependItemsCollectionPath(identityCapability);
|
||||||
"items",
|
|
||||||
...identityCapability.request.response.collectionPaths.filter(
|
const unitsCapability = value.capabilities.find(
|
||||||
(path) => path !== "items",
|
(capability) => capability.id === UNITS_CAPABILITY_ID,
|
||||||
),
|
);
|
||||||
];
|
if (!unitsCapability) throw new Error("gelios_v12_units_capability_missing");
|
||||||
|
prependItemsCollectionPath(unitsCapability);
|
||||||
|
|
||||||
const identityProfile = value.collectionProfiles.find(
|
const identityProfile = value.collectionProfiles.find(
|
||||||
(profile) => profile.id === IDENTITY_PROFILE_ID,
|
(profile) => profile.id === IDENTITY_PROFILE_ID,
|
||||||
);
|
);
|
||||||
if (!identityProfile) throw new Error("gelios_v12_identity_profile_missing");
|
if (!identityProfile) throw new Error("gelios_v12_identity_profile_missing");
|
||||||
identityProfile.version = GELIOS_PROVIDER_PACKAGE_VERSION;
|
identityProfile.version = IDENTITY_CONTRACT_VERSION;
|
||||||
|
|
||||||
const identityMapping = value.mappingContracts.find(
|
const identityMapping = value.mappingContracts.find(
|
||||||
(mapping) => mapping.id === IDENTITY_MAPPING_ID,
|
(mapping) => mapping.id === IDENTITY_MAPPING_ID,
|
||||||
);
|
);
|
||||||
if (!identityMapping) throw new Error("gelios_v12_identity_mapping_missing");
|
if (!identityMapping) throw new Error("gelios_v12_identity_mapping_missing");
|
||||||
identityMapping.version = GELIOS_PROVIDER_PACKAGE_VERSION;
|
identityMapping.version = IDENTITY_CONTRACT_VERSION;
|
||||||
|
|
||||||
const identityTemplate = value.l2Templates.find(
|
const identityTemplate = value.l2Templates.find(
|
||||||
(template) => template.id === IDENTITY_TEMPLATE_ID,
|
(template) => template.id === IDENTITY_TEMPLATE_ID,
|
||||||
);
|
);
|
||||||
if (!identityTemplate) throw new Error("gelios_v12_identity_template_missing");
|
if (!identityTemplate) throw new Error("gelios_v12_identity_template_missing");
|
||||||
identityTemplate.version = GELIOS_PROVIDER_PACKAGE_VERSION;
|
identityTemplate.version = IDENTITY_CONTRACT_VERSION;
|
||||||
|
|
||||||
export const geliosProviderPackageV12 = deepFreeze(value);
|
export const geliosProviderPackageV12 = deepFreeze(value);
|
||||||
|
|
||||||
|
function prependItemsCollectionPath(capability) {
|
||||||
|
capability.request.response.collectionPaths = [
|
||||||
|
"items",
|
||||||
|
...capability.request.response.collectionPaths.filter(
|
||||||
|
(path) => path !== "items",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function deepFreeze(input) {
|
function deepFreeze(input) {
|
||||||
if (!input || typeof input !== "object" || Object.isFrozen(input)) return input;
|
if (!input || typeof input !== "object" || Object.isFrozen(input)) return input;
|
||||||
Object.freeze(input);
|
Object.freeze(input);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export {
|
||||||
|
GELIOS_PROVIDER_PACKAGE_ID,
|
||||||
|
GELIOS_PROVIDER_PACKAGE_VERSION,
|
||||||
|
GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID,
|
||||||
|
GELIOS_UNIT_CONTACTS_DATA_PRODUCT_VERSION,
|
||||||
|
GELIOS_UNIT_CONTACTS_ONTOLOGY_REVISION,
|
||||||
|
geliosProviderPackageV13,
|
||||||
|
} from "./package.mjs";
|
||||||
|
export { geliosTelemetryFieldRegistryV5 } from "../v12/index.mjs";
|
||||||
|
export { geliosCapabilityCatalogV3 } from "../capability-catalog-v3.mjs";
|
||||||
|
|
@ -0,0 +1,318 @@
|
||||||
|
import { geliosProviderPackageV12 } from "../v12/package.mjs";
|
||||||
|
|
||||||
|
export const GELIOS_PROVIDER_PACKAGE_ID = "gelios.provider.v13";
|
||||||
|
export const GELIOS_PROVIDER_PACKAGE_VERSION = "13.0.0";
|
||||||
|
export const GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID = "fleet.units.contacts.current.v2";
|
||||||
|
export const GELIOS_UNIT_CONTACTS_DATA_PRODUCT_VERSION = "2.0.0";
|
||||||
|
export const GELIOS_UNIT_CONTACTS_ONTOLOGY_REVISION = "ontology.map.moving_object.v3";
|
||||||
|
|
||||||
|
const UNITS_CAPABILITY_ID = "gelios.units.identity.read";
|
||||||
|
const USERS_CAPABILITY_ID = "gelios.users.directory.read";
|
||||||
|
const FIELD_POLICY_ID = "gelios.units.contacts.fields.v2";
|
||||||
|
const COLLECTION_PROFILE_ID = "gelios.units.contacts.warm.v2";
|
||||||
|
const MAPPING_ID = "gelios.units.to.fleet.units.contacts.current.v2";
|
||||||
|
const TEMPLATE_ID = "gelios.units.contacts.l2.v2";
|
||||||
|
|
||||||
|
const contactFields = Object.freeze([
|
||||||
|
"available_user_emails",
|
||||||
|
"available_user_ids",
|
||||||
|
"available_user_legal_names",
|
||||||
|
"available_user_logins",
|
||||||
|
"available_user_phones",
|
||||||
|
"device_imei",
|
||||||
|
"device_phone_primary",
|
||||||
|
"device_phone_secondary",
|
||||||
|
"display_name",
|
||||||
|
"provider_creator_email",
|
||||||
|
"provider_creator_id",
|
||||||
|
"provider_creator_legal_name",
|
||||||
|
"provider_creator_login",
|
||||||
|
"provider_creator_phone",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const value = structuredClone(geliosProviderPackageV12);
|
||||||
|
const unitsCapability = value.capabilities.find(
|
||||||
|
(capability) => capability.id === UNITS_CAPABILITY_ID,
|
||||||
|
);
|
||||||
|
const templateBase = value.l2Templates.find(
|
||||||
|
(template) => template.id === "gelios.units.identity.l2.v1",
|
||||||
|
);
|
||||||
|
if (!unitsCapability) throw new Error("gelios_v13_units_capability_missing");
|
||||||
|
if (!templateBase) throw new Error("gelios_v13_template_base_missing");
|
||||||
|
|
||||||
|
value.id = GELIOS_PROVIDER_PACKAGE_ID;
|
||||||
|
value.version = GELIOS_PROVIDER_PACKAGE_VERSION;
|
||||||
|
value.manifest = {
|
||||||
|
...value.manifest,
|
||||||
|
id: "gelios.provider.manifest.v13",
|
||||||
|
version: GELIOS_PROVIDER_PACKAGE_VERSION,
|
||||||
|
capabilityIds: [...value.manifest.capabilityIds, USERS_CAPABILITY_ID],
|
||||||
|
fieldPolicyIds: [...value.manifest.fieldPolicyIds, FIELD_POLICY_ID],
|
||||||
|
collectionProfileIds: [...value.manifest.collectionProfileIds, COLLECTION_PROFILE_ID],
|
||||||
|
dataProductIds: [...value.manifest.dataProductIds, GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID],
|
||||||
|
mappingContractIds: [...value.manifest.mappingContractIds, MAPPING_ID],
|
||||||
|
l2TemplateIds: [...value.manifest.l2TemplateIds, TEMPLATE_ID],
|
||||||
|
};
|
||||||
|
|
||||||
|
value.capabilities.push({
|
||||||
|
...structuredClone(unitsCapability),
|
||||||
|
id: USERS_CAPABILITY_ID,
|
||||||
|
request: {
|
||||||
|
...structuredClone(unitsCapability.request),
|
||||||
|
path: "/api/v1/users",
|
||||||
|
query: {
|
||||||
|
incladmpan: false,
|
||||||
|
inclavfun: false,
|
||||||
|
inclbillinf: false,
|
||||||
|
inclextra: false,
|
||||||
|
inclexs: false,
|
||||||
|
inclmon: false,
|
||||||
|
inclsmtp: false,
|
||||||
|
o1: "userIdAsc",
|
||||||
|
pl: 500,
|
||||||
|
po: 0,
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
collectionPaths: ["items"],
|
||||||
|
pagination: {
|
||||||
|
mode: "offset",
|
||||||
|
limitParameter: "pl",
|
||||||
|
offsetParameter: "po",
|
||||||
|
pageSize: 500,
|
||||||
|
itemsPath: "items",
|
||||||
|
totalPath: "paginationMetadata.totalCount",
|
||||||
|
maxPages: 10,
|
||||||
|
maxItems: 5000,
|
||||||
|
maxResponseBytes: 16 * 1024 * 1024,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
value.fieldPolicies.push({
|
||||||
|
id: FIELD_POLICY_ID,
|
||||||
|
version: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_VERSION,
|
||||||
|
dataProductId: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID,
|
||||||
|
targetFields: [...contactFields],
|
||||||
|
unknownSourceFields: "drop",
|
||||||
|
dynamicSourceFields: "drop_until_classified",
|
||||||
|
restrictedSourcePaths: [
|
||||||
|
"commands",
|
||||||
|
"currentUserAccess",
|
||||||
|
"customFields",
|
||||||
|
"driver",
|
||||||
|
"extraInfo",
|
||||||
|
"hwDecryptKey",
|
||||||
|
"lastMsg",
|
||||||
|
"lastSensorsVal",
|
||||||
|
"maintenancePlan",
|
||||||
|
"preSetCommandGroup",
|
||||||
|
"sensors",
|
||||||
|
"stationaryLat",
|
||||||
|
"stationaryLon",
|
||||||
|
"storageInfo",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
value.collectionProfiles.push({
|
||||||
|
id: COLLECTION_PROFILE_ID,
|
||||||
|
version: GELIOS_PROVIDER_PACKAGE_VERSION,
|
||||||
|
mode: "realtime",
|
||||||
|
schedule: { intervalMs: 5 * 60 * 1000 },
|
||||||
|
capabilityIds: [UNITS_CAPABILITY_ID, USERS_CAPABILITY_ID],
|
||||||
|
dataProductId: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID,
|
||||||
|
mappingContractId: MAPPING_ID,
|
||||||
|
fieldPolicyId: FIELD_POLICY_ID,
|
||||||
|
l2TemplateId: TEMPLATE_ID,
|
||||||
|
entityScope: {
|
||||||
|
mode: "all_visible_to_credential",
|
||||||
|
refresh: "each_collection_run",
|
||||||
|
businessEntityFilter: "forbidden",
|
||||||
|
},
|
||||||
|
batching: { maxFacts: 5000 },
|
||||||
|
cardinality: {
|
||||||
|
maxCurrentEntities: 5000,
|
||||||
|
onExceed: "require_partitioned_data_product",
|
||||||
|
},
|
||||||
|
retry: { maxAttempts: 4, backoff: "fixed_delay", delayMs: 2000 },
|
||||||
|
});
|
||||||
|
|
||||||
|
value.dataProducts.push({
|
||||||
|
id: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID,
|
||||||
|
version: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_VERSION,
|
||||||
|
ontologyRevision: GELIOS_UNIT_CONTACTS_ONTOLOGY_REVISION,
|
||||||
|
deliveryMode: "snapshot+patch",
|
||||||
|
semanticTypes: ["map.moving_object"],
|
||||||
|
fields: [...contactFields],
|
||||||
|
fieldContracts: {
|
||||||
|
available_user_emails: optional("string_array"),
|
||||||
|
available_user_ids: optional("string_array"),
|
||||||
|
available_user_legal_names: optional("string_array"),
|
||||||
|
available_user_logins: optional("string_array"),
|
||||||
|
available_user_phones: optional("string_array"),
|
||||||
|
device_imei: optional("string"),
|
||||||
|
device_phone_primary: optional("string"),
|
||||||
|
device_phone_secondary: optional("string"),
|
||||||
|
display_name: { type: "string", required: true },
|
||||||
|
provider_creator_email: optional("string"),
|
||||||
|
provider_creator_id: optional("string"),
|
||||||
|
provider_creator_legal_name: optional("string"),
|
||||||
|
provider_creator_login: optional("string"),
|
||||||
|
provider_creator_phone: optional("string"),
|
||||||
|
},
|
||||||
|
history: { mode: "none", retentionDays: 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
value.mappingContracts.push({
|
||||||
|
schemaVersion: "nodedc.semantic-mapping/v1",
|
||||||
|
id: MAPPING_ID,
|
||||||
|
version: GELIOS_PROVIDER_PACKAGE_VERSION,
|
||||||
|
sourceCapabilityId: UNITS_CAPABILITY_ID,
|
||||||
|
fieldPolicyId: FIELD_POLICY_ID,
|
||||||
|
target: {
|
||||||
|
dataProductId: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID,
|
||||||
|
version: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_VERSION,
|
||||||
|
ontologyRevision: GELIOS_UNIT_CONTACTS_ONTOLOGY_REVISION,
|
||||||
|
semanticType: "map.moving_object",
|
||||||
|
},
|
||||||
|
derivations: {
|
||||||
|
available_user_emails: responseLookupList("availableToUsers", "email"),
|
||||||
|
available_user_ids: boundedStringList("availableToUsers", "id"),
|
||||||
|
available_user_legal_names: responseLookupList("availableToUsers", "legalName"),
|
||||||
|
available_user_logins: boundedStringList("availableToUsers", "login"),
|
||||||
|
available_user_phones: responseLookupList("availableToUsers", "phone"),
|
||||||
|
provider_creator_email: responseLookupFirst("creator.id", "email"),
|
||||||
|
provider_creator_legal_name: responseLookupFirst("creator.id", "legalName"),
|
||||||
|
provider_creator_phone: responseLookupFirst("creator.id", "phone"),
|
||||||
|
},
|
||||||
|
fact: {
|
||||||
|
sourceId: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["id", "unit_id", "unitId"],
|
||||||
|
coerce: "string",
|
||||||
|
prefix: "gelios-unit-",
|
||||||
|
},
|
||||||
|
semanticType: { constant: "map.moving_object" },
|
||||||
|
observedAt: {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths: ["updatedAt"],
|
||||||
|
coerce: "unix_or_iso_timestamp",
|
||||||
|
fallback: "collection_received_at",
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
available_user_emails: { derive: "available_user_emails" },
|
||||||
|
available_user_ids: { derive: "available_user_ids" },
|
||||||
|
available_user_legal_names: { derive: "available_user_legal_names" },
|
||||||
|
available_user_logins: { derive: "available_user_logins" },
|
||||||
|
available_user_phones: { derive: "available_user_phones" },
|
||||||
|
device_imei: stringField(["imei"]),
|
||||||
|
device_phone_primary: stringField(["phone"]),
|
||||||
|
device_phone_secondary: stringField(["phone2"]),
|
||||||
|
display_name: stringField(["name", "unit_name", "title", "label"], false),
|
||||||
|
provider_creator_email: { derive: "provider_creator_email", omitIfMissing: true },
|
||||||
|
provider_creator_id: stringField(["creator.id"]),
|
||||||
|
provider_creator_legal_name: {
|
||||||
|
derive: "provider_creator_legal_name",
|
||||||
|
omitIfMissing: true,
|
||||||
|
},
|
||||||
|
provider_creator_login: stringField(["creator.login"]),
|
||||||
|
provider_creator_phone: { derive: "provider_creator_phone", omitIfMissing: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
value.l2Templates.push({
|
||||||
|
...structuredClone(templateBase),
|
||||||
|
id: TEMPLATE_ID,
|
||||||
|
version: GELIOS_PROVIDER_PACKAGE_VERSION,
|
||||||
|
steps: [
|
||||||
|
{ id: "collection.trigger", kind: "collection_trigger", collectionProfileDriven: true },
|
||||||
|
{ id: "provider.fetch-units", kind: "provider_request", capabilityId: UNITS_CAPABILITY_ID },
|
||||||
|
{ id: "provider.fetch-users", kind: "provider_request", capabilityId: USERS_CAPABILITY_ID },
|
||||||
|
{ id: "provider.extract-units", kind: "extract_items", capabilityId: UNITS_CAPABILITY_ID },
|
||||||
|
{ id: "ontology.map", kind: "semantic_mapping", mappingContractId: MAPPING_ID },
|
||||||
|
{
|
||||||
|
id: "data-product.publish",
|
||||||
|
kind: "data_product_publish",
|
||||||
|
dataProductId: GELIOS_UNIT_CONTACTS_DATA_PRODUCT_ID,
|
||||||
|
nodeType: "n8n-nodes-ndc.ndcDataProductPublish",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const geliosProviderPackageV13 = deepFreeze(value);
|
||||||
|
|
||||||
|
function optional(type) {
|
||||||
|
return { type, required: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringField(paths, omitIfMissing = true) {
|
||||||
|
return {
|
||||||
|
strategy: "first_non_empty",
|
||||||
|
paths,
|
||||||
|
coerce: "string",
|
||||||
|
...(omitIfMissing ? { omitIfMissing: true } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function boundedStringList(arrayPath, valuePath) {
|
||||||
|
return {
|
||||||
|
kind: "bounded_string_list",
|
||||||
|
rules: ["array.string_values"],
|
||||||
|
default: [],
|
||||||
|
parameters: {
|
||||||
|
arrayPath,
|
||||||
|
valuePath,
|
||||||
|
maxItems: 256,
|
||||||
|
maxItemLength: 512,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseLookupList(sourceArrayPath, valuePath) {
|
||||||
|
return {
|
||||||
|
kind: "bounded_response_lookup",
|
||||||
|
rules: ["response.lookup.list"],
|
||||||
|
default: [],
|
||||||
|
parameters: {
|
||||||
|
responseCapabilityId: USERS_CAPABILITY_ID,
|
||||||
|
responseCollectionPath: "items",
|
||||||
|
responseKeyPath: "id",
|
||||||
|
sourceArrayPath,
|
||||||
|
sourceKeyPath: "id",
|
||||||
|
valuePath,
|
||||||
|
resultMode: "list",
|
||||||
|
maxResponseItems: 5000,
|
||||||
|
maxSourceKeys: 256,
|
||||||
|
maxItems: 256,
|
||||||
|
maxItemLength: 512,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function responseLookupFirst(sourceKeyPath, valuePath) {
|
||||||
|
return {
|
||||||
|
kind: "bounded_response_lookup",
|
||||||
|
rules: ["response.lookup.first"],
|
||||||
|
default: "",
|
||||||
|
parameters: {
|
||||||
|
responseCapabilityId: USERS_CAPABILITY_ID,
|
||||||
|
responseCollectionPath: "items",
|
||||||
|
responseKeyPath: "id",
|
||||||
|
sourceKeyPath,
|
||||||
|
valuePath,
|
||||||
|
resultMode: "first",
|
||||||
|
maxResponseItems: 5000,
|
||||||
|
maxSourceKeys: 1,
|
||||||
|
maxItems: 1,
|
||||||
|
maxItemLength: 512,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepFreeze(input) {
|
||||||
|
if (!input || typeof input !== "object" || Object.isFrozen(input)) return input;
|
||||||
|
Object.freeze(input);
|
||||||
|
for (const child of Object.values(input)) deepFreeze(child);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
@ -9,11 +9,12 @@ export const L2_EXECUTION_PLAN_SCHEMA_VERSION = "nodedc.l2-execution-plan/v1";
|
||||||
export const L2_GRAPH_BLUEPRINT_SCHEMA_VERSION = "nodedc.l2-graph-blueprint/v1";
|
export const L2_GRAPH_BLUEPRINT_SCHEMA_VERSION = "nodedc.l2-graph-blueprint/v1";
|
||||||
export const L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION = "nodedc.l2-materialization-receipt/v1";
|
export const L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION = "nodedc.l2-materialization-receipt/v1";
|
||||||
export const L2_MAPPING_RUNTIME_SCHEMA_VERSION = "nodedc.semantic-mapping-runtime/v1";
|
export const L2_MAPPING_RUNTIME_SCHEMA_VERSION = "nodedc.semantic-mapping-runtime/v1";
|
||||||
export const L2_EXECUTION_PLAN_COMPILER_VERSION = "1.4.0";
|
export const L2_EXECUTION_PLAN_COMPILER_VERSION = "1.5.0";
|
||||||
export const L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS = Object.freeze([
|
export const L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS = Object.freeze([
|
||||||
"1.1.0",
|
"1.1.0",
|
||||||
"1.2.0",
|
"1.2.0",
|
||||||
"1.3.0",
|
"1.3.0",
|
||||||
|
"1.4.0",
|
||||||
L2_EXECUTION_PLAN_COMPILER_VERSION,
|
L2_EXECUTION_PLAN_COMPILER_VERSION,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,14 @@ export function validateProviderPackage(value) {
|
||||||
if (!selectedCapabilityIds.includes(mapping.sourceCapabilityId)) {
|
if (!selectedCapabilityIds.includes(mapping.sourceCapabilityId)) {
|
||||||
errors.push(`collectionProfile.${profile.id}.mapping_source_capability_must_be_selected`);
|
errors.push(`collectionProfile.${profile.id}.mapping_source_capability_must_be_selected`);
|
||||||
}
|
}
|
||||||
|
for (const derivation of Object.values(mapping.derivations || {})) {
|
||||||
|
if (
|
||||||
|
derivation?.kind === "bounded_response_lookup"
|
||||||
|
&& !selectedCapabilityIds.includes(derivation.parameters?.responseCapabilityId)
|
||||||
|
) {
|
||||||
|
errors.push(`collectionProfile.${profile.id}.lookup_response_capability_must_be_selected`);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (mapping.fieldPolicyId !== profile.fieldPolicyId) errors.push(`collectionProfile.${profile.id}.mapping_field_policy_mismatch`);
|
if (mapping.fieldPolicyId !== profile.fieldPolicyId) errors.push(`collectionProfile.${profile.id}.mapping_field_policy_mismatch`);
|
||||||
if (mapping.target?.dataProductId !== profile.dataProductId) errors.push(`collectionProfile.${profile.id}.mapping_data_product_mismatch`);
|
if (mapping.target?.dataProductId !== profile.dataProductId) errors.push(`collectionProfile.${profile.id}.mapping_data_product_mismatch`);
|
||||||
}
|
}
|
||||||
|
|
@ -283,6 +291,16 @@ export function validateProviderPackage(value) {
|
||||||
}
|
}
|
||||||
for (const mapping of mappingContracts) {
|
for (const mapping of mappingContracts) {
|
||||||
assertReference(mapping.sourceCapabilityId, capabilityIds, `mappingContract.${mapping.id}.sourceCapabilityId`, errors);
|
assertReference(mapping.sourceCapabilityId, capabilityIds, `mappingContract.${mapping.id}.sourceCapabilityId`, errors);
|
||||||
|
for (const [derivationId, derivation] of Object.entries(mapping.derivations || {})) {
|
||||||
|
if (derivation?.kind === "bounded_response_lookup") {
|
||||||
|
assertReference(
|
||||||
|
derivation.parameters?.responseCapabilityId,
|
||||||
|
capabilityIds,
|
||||||
|
`mappingContract.${mapping.id}.derivations.${derivationId}.parameters.responseCapabilityId`,
|
||||||
|
errors,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
assertReference(mapping.fieldPolicyId, fieldPolicyIds, `mappingContract.${mapping.id}.fieldPolicyId`, errors);
|
assertReference(mapping.fieldPolicyId, fieldPolicyIds, `mappingContract.${mapping.id}.fieldPolicyId`, errors);
|
||||||
assertReference(mapping.target?.dataProductId, dataProductIds, `mappingContract.${mapping.id}.target.dataProductId`, errors);
|
assertReference(mapping.target?.dataProductId, dataProductIds, `mappingContract.${mapping.id}.target.dataProductId`, errors);
|
||||||
const product = dataProducts.find((item) => item.id === mapping.target?.dataProductId);
|
const product = dataProducts.find((item) => item.id === mapping.target?.dataProductId);
|
||||||
|
|
@ -1049,6 +1067,7 @@ function validateDerivation(value, path, errors) {
|
||||||
"ordered_rules",
|
"ordered_rules",
|
||||||
"flag_set",
|
"flag_set",
|
||||||
"bounded_readings",
|
"bounded_readings",
|
||||||
|
"bounded_response_lookup",
|
||||||
"bounded_string_list",
|
"bounded_string_list",
|
||||||
"bounded_named_values",
|
"bounded_named_values",
|
||||||
]).has(value.kind)) errors.push(`${path}.kind_invalid`);
|
]).has(value.kind)) errors.push(`${path}.kind_invalid`);
|
||||||
|
|
@ -1075,6 +1094,89 @@ function validateDerivation(value, path, errors) {
|
||||||
if (value.kind === "bounded_named_values") {
|
if (value.kind === "bounded_named_values") {
|
||||||
validateBoundedNamedValuesDerivation(value, path, errors);
|
validateBoundedNamedValuesDerivation(value, path, errors);
|
||||||
}
|
}
|
||||||
|
if (value.kind === "bounded_response_lookup") {
|
||||||
|
validateBoundedResponseLookupDerivation(value, path, errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateBoundedResponseLookupDerivation(value, path, errors) {
|
||||||
|
const parameters = isPlainObject(value.parameters) ? value.parameters : {};
|
||||||
|
const allowed = new Set([
|
||||||
|
"responseCapabilityId",
|
||||||
|
"responseCollectionPath",
|
||||||
|
"responseKeyPath",
|
||||||
|
"sourceArrayPath",
|
||||||
|
"sourceKeyPath",
|
||||||
|
"valuePath",
|
||||||
|
"resultMode",
|
||||||
|
"maxResponseItems",
|
||||||
|
"maxSourceKeys",
|
||||||
|
"maxItems",
|
||||||
|
"maxItemLength",
|
||||||
|
]);
|
||||||
|
if (Object.keys(parameters).some((key) => !allowed.has(key))) {
|
||||||
|
errors.push(`${path}.parameters_shape_invalid`);
|
||||||
|
}
|
||||||
|
if (!IDENTIFIER.test(String(parameters.responseCapabilityId || ""))) {
|
||||||
|
errors.push(`${path}.parameters.responseCapabilityId_invalid`);
|
||||||
|
}
|
||||||
|
for (const key of [
|
||||||
|
"responseCollectionPath",
|
||||||
|
"responseKeyPath",
|
||||||
|
"sourceKeyPath",
|
||||||
|
"valuePath",
|
||||||
|
]) {
|
||||||
|
if (!isCanonicalSourcePath(parameters[key])) {
|
||||||
|
errors.push(`${path}.parameters.${key}_must_be_canonical_dot_path`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (SECRET_FIELD_NAME.test(String(parameters.valuePath || ""))) {
|
||||||
|
errors.push(`${path}.parameters.valuePath_secret_field_not_allowed`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
parameters.sourceArrayPath !== undefined
|
||||||
|
&& !isCanonicalSourcePath(parameters.sourceArrayPath)
|
||||||
|
) {
|
||||||
|
errors.push(`${path}.parameters.sourceArrayPath_must_be_canonical_dot_path`);
|
||||||
|
}
|
||||||
|
if (!new Set(["first", "list"]).has(parameters.resultMode)) {
|
||||||
|
errors.push(`${path}.parameters.resultMode_invalid`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!Number.isInteger(parameters.maxResponseItems)
|
||||||
|
|| parameters.maxResponseItems < 1
|
||||||
|
|| parameters.maxResponseItems > 5000
|
||||||
|
) {
|
||||||
|
errors.push(`${path}.parameters.maxResponseItems_invalid`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!Number.isInteger(parameters.maxSourceKeys)
|
||||||
|
|| parameters.maxSourceKeys < 1
|
||||||
|
|| parameters.maxSourceKeys > 256
|
||||||
|
) {
|
||||||
|
errors.push(`${path}.parameters.maxSourceKeys_invalid`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!Number.isInteger(parameters.maxItems)
|
||||||
|
|| parameters.maxItems < 1
|
||||||
|
|| parameters.maxItems > 256
|
||||||
|
) {
|
||||||
|
errors.push(`${path}.parameters.maxItems_invalid`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!Number.isInteger(parameters.maxItemLength)
|
||||||
|
|| parameters.maxItemLength < 1
|
||||||
|
|| parameters.maxItemLength > 512
|
||||||
|
) {
|
||||||
|
errors.push(`${path}.parameters.maxItemLength_invalid`);
|
||||||
|
}
|
||||||
|
if (parameters.resultMode === "list") {
|
||||||
|
if (!Array.isArray(value.default) || value.default.length !== 0) {
|
||||||
|
errors.push(`${path}.default_must_be_empty_array_for_list`);
|
||||||
|
}
|
||||||
|
} else if (value.default !== "") {
|
||||||
|
errors.push(`${path}.default_must_be_empty_string_for_first`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateBoundedStringListDerivation(value, path, errors) {
|
function validateBoundedStringListDerivation(value, path, errors) {
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ const positionsPlan = compileL2ExecutionPlan(
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(positionsPlan.schemaVersion, L2_EXECUTION_PLAN_SCHEMA_VERSION);
|
assert.equal(positionsPlan.schemaVersion, L2_EXECUTION_PLAN_SCHEMA_VERSION);
|
||||||
assert.equal(L2_EXECUTION_PLAN_COMPILER_VERSION, "1.4.0");
|
assert.equal(L2_EXECUTION_PLAN_COMPILER_VERSION, "1.5.0");
|
||||||
assert.deepEqual(L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS, ["1.1.0", "1.2.0", "1.3.0", "1.4.0"]);
|
assert.deepEqual(L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS, ["1.1.0", "1.2.0", "1.3.0", "1.4.0", "1.5.0"]);
|
||||||
assert.equal(positionsPlan.compilerVersion, "1.4.0");
|
assert.equal(positionsPlan.compilerVersion, "1.5.0");
|
||||||
assert.deepEqual(validateL2ExecutionPlan(positionsPlan), { ok: true, errors: [] });
|
assert.deepEqual(validateL2ExecutionPlan(positionsPlan), { ok: true, errors: [] });
|
||||||
assert.match(positionsPlan.executionPlanDigest, /^sha256:[a-f0-9]{64}$/);
|
assert.match(positionsPlan.executionPlanDigest, /^sha256:[a-f0-9]{64}$/);
|
||||||
assert.match(positionsPlan.artifacts.packageDigest, /^sha256:[a-f0-9]{64}$/);
|
assert.match(positionsPlan.artifacts.packageDigest, /^sha256:[a-f0-9]{64}$/);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { geliosProviderPackageV9 } from "../providers/gelios/v9/index.mjs";
|
||||||
import { geliosProviderPackageV10 } from "../providers/gelios/v10/index.mjs";
|
import { geliosProviderPackageV10 } from "../providers/gelios/v10/index.mjs";
|
||||||
import { geliosProviderPackageV11 } from "../providers/gelios/v11/index.mjs";
|
import { geliosProviderPackageV11 } from "../providers/gelios/v11/index.mjs";
|
||||||
import { geliosProviderPackageV12 } from "../providers/gelios/v12/index.mjs";
|
import { geliosProviderPackageV12 } from "../providers/gelios/v12/index.mjs";
|
||||||
|
import { geliosProviderPackageV13 } from "../providers/gelios/v13/index.mjs";
|
||||||
import { normalizeDataProductDefinition } from "../../../services/external-data-plane/src/data-product-policy.mjs";
|
import { normalizeDataProductDefinition } from "../../../services/external-data-plane/src/data-product-policy.mjs";
|
||||||
|
|
||||||
const expectedFields = [
|
const expectedFields = [
|
||||||
|
|
@ -55,6 +56,7 @@ assert.deepEqual(validateProviderPackage(geliosProviderPackageV9), { ok: true, e
|
||||||
assert.deepEqual(validateProviderPackage(geliosProviderPackageV10), { ok: true, errors: [] });
|
assert.deepEqual(validateProviderPackage(geliosProviderPackageV10), { ok: true, errors: [] });
|
||||||
assert.deepEqual(validateProviderPackage(geliosProviderPackageV11), { ok: true, errors: [] });
|
assert.deepEqual(validateProviderPackage(geliosProviderPackageV11), { ok: true, errors: [] });
|
||||||
assert.deepEqual(validateProviderPackage(geliosProviderPackageV12), { ok: true, errors: [] });
|
assert.deepEqual(validateProviderPackage(geliosProviderPackageV12), { ok: true, errors: [] });
|
||||||
|
assert.deepEqual(validateProviderPackage(geliosProviderPackageV13), { ok: true, errors: [] });
|
||||||
assert.equal(geliosProviderPackageV9.id, "gelios.provider.v9");
|
assert.equal(geliosProviderPackageV9.id, "gelios.provider.v9");
|
||||||
assert.equal(geliosProviderPackageV9.version, "9.0.0");
|
assert.equal(geliosProviderPackageV9.version, "9.0.0");
|
||||||
assert.equal(geliosProviderPackageV10.id, "gelios.provider.v10");
|
assert.equal(geliosProviderPackageV10.id, "gelios.provider.v10");
|
||||||
|
|
@ -62,7 +64,9 @@ assert.equal(geliosProviderPackageV10.version, "10.0.0");
|
||||||
assert.equal(geliosProviderPackageV11.id, "gelios.provider.v11");
|
assert.equal(geliosProviderPackageV11.id, "gelios.provider.v11");
|
||||||
assert.equal(geliosProviderPackageV11.version, "11.0.0");
|
assert.equal(geliosProviderPackageV11.version, "11.0.0");
|
||||||
assert.equal(geliosProviderPackageV12.id, "gelios.provider.v12");
|
assert.equal(geliosProviderPackageV12.id, "gelios.provider.v12");
|
||||||
assert.equal(geliosProviderPackageV12.version, "12.0.0");
|
assert.equal(geliosProviderPackageV12.version, "12.0.1");
|
||||||
|
assert.equal(geliosProviderPackageV13.id, "gelios.provider.v13");
|
||||||
|
assert.equal(geliosProviderPackageV13.version, "13.0.0");
|
||||||
assert.equal(geliosProviderPackageV8.id, "gelios.provider.v8");
|
assert.equal(geliosProviderPackageV8.id, "gelios.provider.v8");
|
||||||
|
|
||||||
const profileProduct = geliosProviderPackageV8.dataProducts.find((product) => product.id === "fleet.units.profile.current.v1");
|
const profileProduct = geliosProviderPackageV8.dataProducts.find((product) => product.id === "fleet.units.profile.current.v1");
|
||||||
|
|
@ -127,13 +131,56 @@ assert.deepEqual(identityCapability.request.query, {
|
||||||
const identityCapabilityV12 = geliosProviderPackageV12.capabilities.find(
|
const identityCapabilityV12 = geliosProviderPackageV12.capabilities.find(
|
||||||
(capability) => capability.id === "gelios.units.identity.read",
|
(capability) => capability.id === "gelios.units.identity.read",
|
||||||
);
|
);
|
||||||
|
const unitsCapabilityV12 = geliosProviderPackageV12.capabilities.find(
|
||||||
|
(capability) => capability.id === "gelios.units.current.read",
|
||||||
|
);
|
||||||
assert.equal(identityCapability.request.response.collectionPaths.includes("items"), false);
|
assert.equal(identityCapability.request.response.collectionPaths.includes("items"), false);
|
||||||
assert.deepEqual(identityCapabilityV12.request.response.collectionPaths, [
|
assert.deepEqual(identityCapabilityV12.request.response.collectionPaths, [
|
||||||
"items",
|
"items",
|
||||||
...identityCapability.request.response.collectionPaths,
|
...identityCapability.request.response.collectionPaths,
|
||||||
]);
|
]);
|
||||||
|
assert.deepEqual(unitsCapabilityV12.request.response.collectionPaths, [
|
||||||
|
"items",
|
||||||
|
...geliosProviderPackageV11.capabilities.find(
|
||||||
|
(capability) => capability.id === "gelios.units.current.read",
|
||||||
|
).request.response.collectionPaths,
|
||||||
|
]);
|
||||||
assert.equal(Object.isFrozen(geliosProviderPackageV12), true);
|
assert.equal(Object.isFrozen(geliosProviderPackageV12), true);
|
||||||
assert.equal(Object.isFrozen(identityCapabilityV12.request.response.collectionPaths), true);
|
assert.equal(Object.isFrozen(identityCapabilityV12.request.response.collectionPaths), true);
|
||||||
|
assert.equal(Object.isFrozen(unitsCapabilityV12.request.response.collectionPaths), true);
|
||||||
|
const directoryCapabilityV13 = geliosProviderPackageV13.capabilities.find(
|
||||||
|
(capability) => capability.id === "gelios.users.directory.read",
|
||||||
|
);
|
||||||
|
assert.equal(directoryCapabilityV13.request.path, "/api/v1/users");
|
||||||
|
assert.deepEqual(directoryCapabilityV13.request.response.collectionPaths, ["items"]);
|
||||||
|
assert.equal(directoryCapabilityV13.request.response.pagination.maxItems, 5000);
|
||||||
|
const contactsProductV2 = geliosProviderPackageV13.dataProducts.find(
|
||||||
|
(product) => product.id === "fleet.units.contacts.current.v2",
|
||||||
|
);
|
||||||
|
const registeredContactsProductV2 = JSON.parse(await readFile(new URL(
|
||||||
|
"../../../services/external-data-plane/definitions/fleet.units.contacts.current.v2.json",
|
||||||
|
import.meta.url,
|
||||||
|
), "utf8"));
|
||||||
|
assert.deepEqual(contactsProductV2, registeredContactsProductV2);
|
||||||
|
const contactsProfileV2 = geliosProviderPackageV13.collectionProfiles.find(
|
||||||
|
(profile) => profile.id === "gelios.units.contacts.warm.v2",
|
||||||
|
);
|
||||||
|
assert.deepEqual(contactsProfileV2.capabilityIds, [
|
||||||
|
"gelios.units.identity.read",
|
||||||
|
"gelios.users.directory.read",
|
||||||
|
]);
|
||||||
|
const contactsMappingV2 = geliosProviderPackageV13.mappingContracts.find(
|
||||||
|
(mapping) => mapping.id === "gelios.units.to.fleet.units.contacts.current.v2",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
contactsMappingV2.derivations.available_user_emails.kind,
|
||||||
|
"bounded_response_lookup",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
contactsMappingV2.derivations.provider_creator_email.parameters.resultMode,
|
||||||
|
"first",
|
||||||
|
);
|
||||||
|
assert.equal(Object.isFrozen(geliosProviderPackageV13), true);
|
||||||
const identityMapping = geliosProviderPackageV11.mappingContracts.find(
|
const identityMapping = geliosProviderPackageV11.mappingContracts.find(
|
||||||
(mapping) => mapping.id === "gelios.units.to.fleet.units.identity.current.v1",
|
(mapping) => mapping.id === "gelios.units.to.fleet.units.identity.current.v1",
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"id": "fleet.units.contacts.current.v2",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"ontologyRevision": "ontology.map.moving_object.v3",
|
||||||
|
"deliveryMode": "snapshot+patch",
|
||||||
|
"semanticTypes": [
|
||||||
|
"map.moving_object"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
"available_user_emails",
|
||||||
|
"available_user_ids",
|
||||||
|
"available_user_legal_names",
|
||||||
|
"available_user_logins",
|
||||||
|
"available_user_phones",
|
||||||
|
"device_imei",
|
||||||
|
"device_phone_primary",
|
||||||
|
"device_phone_secondary",
|
||||||
|
"display_name",
|
||||||
|
"provider_creator_email",
|
||||||
|
"provider_creator_id",
|
||||||
|
"provider_creator_legal_name",
|
||||||
|
"provider_creator_login",
|
||||||
|
"provider_creator_phone"
|
||||||
|
],
|
||||||
|
"fieldContracts": {
|
||||||
|
"available_user_emails": { "type": "string_array", "required": false },
|
||||||
|
"available_user_ids": { "type": "string_array", "required": false },
|
||||||
|
"available_user_legal_names": { "type": "string_array", "required": false },
|
||||||
|
"available_user_logins": { "type": "string_array", "required": false },
|
||||||
|
"available_user_phones": { "type": "string_array", "required": false },
|
||||||
|
"device_imei": { "type": "string", "required": false },
|
||||||
|
"device_phone_primary": { "type": "string", "required": false },
|
||||||
|
"device_phone_secondary": { "type": "string", "required": false },
|
||||||
|
"display_name": { "type": "string", "required": true },
|
||||||
|
"provider_creator_email": { "type": "string", "required": false },
|
||||||
|
"provider_creator_id": { "type": "string", "required": false },
|
||||||
|
"provider_creator_legal_name": { "type": "string", "required": false },
|
||||||
|
"provider_creator_login": { "type": "string", "required": false },
|
||||||
|
"provider_creator_phone": { "type": "string", "required": false }
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"mode": "none",
|
||||||
|
"retentionDays": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ assert.deepEqual(bundled.map((definition) => definition.id), [
|
||||||
"fleet.positions.current.v4",
|
"fleet.positions.current.v4",
|
||||||
"fleet.positions.current.v5",
|
"fleet.positions.current.v5",
|
||||||
"fleet.units.contacts.current.v1",
|
"fleet.units.contacts.current.v1",
|
||||||
|
"fleet.units.contacts.current.v2",
|
||||||
"fleet.units.identity.current.v1",
|
"fleet.units.identity.current.v1",
|
||||||
"fleet.units.profile.current.v1",
|
"fleet.units.profile.current.v1",
|
||||||
"map.zones.current.v1",
|
"map.zones.current.v1",
|
||||||
|
|
@ -59,27 +60,33 @@ assert.equal(bundled[5].ontologyRevision, "ontology.map.moving_object.v3");
|
||||||
assert.deepEqual(bundled[5].semanticTypes, ["map.moving_object"]);
|
assert.deepEqual(bundled[5].semanticTypes, ["map.moving_object"]);
|
||||||
assert.equal(bundled[5].history.mode, "none");
|
assert.equal(bundled[5].history.mode, "none");
|
||||||
assert.equal(bundled[5].fieldContracts.device_phone_primary.type, "string");
|
assert.equal(bundled[5].fieldContracts.device_phone_primary.type, "string");
|
||||||
assert.equal(bundled[6].version, "1.0.0");
|
assert.equal(bundled[6].version, "2.0.0");
|
||||||
assert.equal(bundled[6].ontologyRevision, "ontology.map.moving_object.v3");
|
assert.equal(bundled[6].ontologyRevision, "ontology.map.moving_object.v3");
|
||||||
assert.deepEqual(bundled[6].semanticTypes, ["map.moving_object"]);
|
assert.deepEqual(bundled[6].semanticTypes, ["map.moving_object"]);
|
||||||
assert.equal(bundled[6].history.mode, "none");
|
assert.equal(bundled[6].history.mode, "none");
|
||||||
assert.equal(bundled[6].fieldContracts.available_user_logins.type, "string_array");
|
assert.equal(bundled[6].fieldContracts.available_user_emails.type, "string_array");
|
||||||
assert.equal(bundled[6].fieldContracts.device_imei.type, "string");
|
assert.equal(bundled[6].fieldContracts.provider_creator_email.type, "string");
|
||||||
assert.equal(bundled[7].version, "1.0.0");
|
assert.equal(bundled[7].version, "1.0.0");
|
||||||
assert.equal(bundled[7].ontologyRevision, "ontology.map.moving_object.v3");
|
assert.equal(bundled[7].ontologyRevision, "ontology.map.moving_object.v3");
|
||||||
assert.deepEqual(bundled[7].semanticTypes, ["map.moving_object"]);
|
assert.deepEqual(bundled[7].semanticTypes, ["map.moving_object"]);
|
||||||
assert.equal(bundled[7].history.mode, "none");
|
assert.equal(bundled[7].history.mode, "none");
|
||||||
assert.equal(bundled[7].fieldContracts.hardware_port.type, "number");
|
assert.equal(bundled[7].fieldContracts.available_user_logins.type, "string_array");
|
||||||
|
assert.equal(bundled[7].fieldContracts.device_imei.type, "string");
|
||||||
assert.equal(bundled[8].version, "1.0.0");
|
assert.equal(bundled[8].version, "1.0.0");
|
||||||
assert.equal(bundled[8].ontologyRevision, "ontology.map.zone.v1");
|
assert.equal(bundled[8].ontologyRevision, "ontology.map.moving_object.v3");
|
||||||
assert.deepEqual(bundled[8].semanticTypes, ["map.zone"]);
|
assert.deepEqual(bundled[8].semanticTypes, ["map.moving_object"]);
|
||||||
assert.deepEqual(bundled[8].fieldContracts.geometry, { type: "geometry", required: true });
|
assert.equal(bundled[8].history.mode, "none");
|
||||||
assert.equal(bundled[8].fields.includes("dataset_authority"), false);
|
assert.equal(bundled[8].fieldContracts.hardware_port.type, "number");
|
||||||
assert.equal(bundled[9].version, "2.0.0");
|
assert.equal(bundled[9].version, "1.0.0");
|
||||||
assert.equal(bundled[9].ontologyRevision, "ontology.map.zone.v1");
|
assert.equal(bundled[9].ontologyRevision, "ontology.map.zone.v1");
|
||||||
assert.deepEqual(bundled[9].semanticTypes, ["map.zone"]);
|
assert.deepEqual(bundled[9].semanticTypes, ["map.zone"]);
|
||||||
assert.deepEqual(bundled[9].fieldContracts.geometry, { type: "geometry", required: true });
|
assert.deepEqual(bundled[9].fieldContracts.geometry, { type: "geometry", required: true });
|
||||||
assert.deepEqual(bundled[9].fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]);
|
assert.equal(bundled[9].fields.includes("dataset_authority"), false);
|
||||||
|
assert.equal(bundled[10].version, "2.0.0");
|
||||||
|
assert.equal(bundled[10].ontologyRevision, "ontology.map.zone.v1");
|
||||||
|
assert.deepEqual(bundled[10].semanticTypes, ["map.zone"]);
|
||||||
|
assert.deepEqual(bundled[10].fieldContracts.geometry, { type: "geometry", required: true });
|
||||||
|
assert.deepEqual(bundled[10].fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]);
|
||||||
assert.equal(Object.keys(bundled[3].fieldContracts).length, bundled[3].fields.length);
|
assert.equal(Object.keys(bundled[3].fieldContracts).length, bundled[3].fields.length);
|
||||||
assert.equal(Object.keys(bundled[4].fieldContracts).length, bundled[4].fields.length);
|
assert.equal(Object.keys(bundled[4].fieldContracts).length, bundled[4].fields.length);
|
||||||
assert.equal(Object.keys(bundled[5].fieldContracts).length, bundled[5].fields.length);
|
assert.equal(Object.keys(bundled[5].fieldContracts).length, bundled[5].fields.length);
|
||||||
|
|
@ -87,6 +94,7 @@ assert.equal(Object.keys(bundled[6].fieldContracts).length, bundled[6].fields.le
|
||||||
assert.equal(Object.keys(bundled[7].fieldContracts).length, bundled[7].fields.length);
|
assert.equal(Object.keys(bundled[7].fieldContracts).length, bundled[7].fields.length);
|
||||||
assert.equal(Object.keys(bundled[8].fieldContracts).length, bundled[8].fields.length);
|
assert.equal(Object.keys(bundled[8].fieldContracts).length, bundled[8].fields.length);
|
||||||
assert.equal(Object.keys(bundled[9].fieldContracts).length, bundled[9].fields.length);
|
assert.equal(Object.keys(bundled[9].fieldContracts).length, bundled[9].fields.length);
|
||||||
|
assert.equal(Object.keys(bundled[10].fieldContracts).length, bundled[10].fields.length);
|
||||||
|
|
||||||
const directory = await mkdtemp(join(tmpdir(), "nodedc-edp-definitions-"));
|
const directory = await mkdtemp(join(tmpdir(), "nodedc-edp-definitions-"));
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue