Initialize first-party Eve plugin owners
This commit is contained in:
commit
6f23900898
14 changed files with 441 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules/
|
||||||
|
artifacts/
|
||||||
|
|
||||||
6
LICENSE
Normal file
6
LICENSE
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
Mozilla Public License Version 2.0
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
15
README.md
Normal file
15
README.md
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# EvePlugins
|
||||||
|
|
||||||
|
First-party reusable semantic plugins for Eve. Each plugin owns its domain
|
||||||
|
semantics and runs through the runtime-independent Eve plugin ABI. Eve owns the
|
||||||
|
contract; this repository owns plugin behavior.
|
||||||
|
|
||||||
|
The first owner plugin is `tex.math`, implemented under
|
||||||
|
`plugins/eve-plugin-tex`. It uses KaTeX for parsing and typesetting, emits
|
||||||
|
render-result projections, and never mutates provider state.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
56
package-lock.json
generated
Normal file
56
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"name": "@gamecult/eve-plugins",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@gamecult/eve-plugins",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"workspaces": [
|
||||||
|
"plugins/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@gamecult/eve-plugin-tex": {
|
||||||
|
"resolved": "plugins/eve-plugin-tex",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/katex": {
|
||||||
|
"version": "0.16.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz",
|
||||||
|
"integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==",
|
||||||
|
"funding": [
|
||||||
|
"https://opencollective.com/katex",
|
||||||
|
"https://github.com/sponsors/katex"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^8.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"katex": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins/eve-plugin-tex": {
|
||||||
|
"name": "@gamecult/eve-plugin-tex",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"katex": "^0.16.22"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"eve-plugin-tex": "src/sidecar.mjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
package.json
Normal file
11
package.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "@gamecult/eve-plugins",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"workspaces": ["plugins/*"],
|
||||||
|
"scripts": {
|
||||||
|
"test": "C:\\Progra~1\\nodejs\\node.exe --test plugins/*/test/*.test.mjs",
|
||||||
|
"witness:tex": "C:\\Progra~1\\nodejs\\node.exe scripts/run-tex-witness.mjs"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
plugins/eve-plugin-tex/advertisement.json
Normal file
28
plugins/eve-plugin-tex/advertisement.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"schema": "gamecult.eve.plugin_advertisement.v1",
|
||||||
|
"pluginId": "tex.math",
|
||||||
|
"ownerService": "eveplugins",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"manifestAddress": "file://plugins/eve-plugin-tex/plugin.json",
|
||||||
|
"runtime": {
|
||||||
|
"invocationModel": "executable-sidecar",
|
||||||
|
"contract": "gamecult.eve.plugin_abi.v1",
|
||||||
|
"transports": ["stdio"],
|
||||||
|
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
|
||||||
|
"sidecar": {
|
||||||
|
"processKind": "long-running-daemon",
|
||||||
|
"protocol": "stdio-ndjson",
|
||||||
|
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
|
||||||
|
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
|
||||||
|
"operations": ["describe", "validate", "project", "lower", "measure", "apply"],
|
||||||
|
"commandEnvelope": "gamecult.eve.command_invocation.v1",
|
||||||
|
"receiptSchema": "gamecult.eve.plugin_receipt.v1",
|
||||||
|
"stateAuthority": "emits-render-results-only-provider-owns-source-state"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": ["tex.math.source.v1", "tex.math.render_request.v1", "tex.math.render_result.v1"],
|
||||||
|
"componentKinds": ["embed.tex"],
|
||||||
|
"commands": [],
|
||||||
|
"fixtures": ["tex-math-basic"]
|
||||||
|
}
|
||||||
|
|
||||||
8
plugins/eve-plugin-tex/fixtures/basic-source.json
Normal file
8
plugins/eve-plugin-tex/fixtures/basic-source.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"schema": "tex.math.source.v1",
|
||||||
|
"documentId": "tex.basic",
|
||||||
|
"source": "E = mc^2",
|
||||||
|
"displayMode": true,
|
||||||
|
"macros": {}
|
||||||
|
}
|
||||||
|
|
||||||
14
plugins/eve-plugin-tex/package.json
Normal file
14
plugins/eve-plugin-tex/package.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "@gamecult/eve-plugin-tex",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Runtime-independent TeX semantic sidecar for Eve surfaces.",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"eve-plugin-tex": "./src/sidecar.mjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"katex": "^0.16.22"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
18
plugins/eve-plugin-tex/plugin-abi-fixture.json
Normal file
18
plugins/eve-plugin-tex/plugin-abi-fixture.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"schema": "gamecult.eve.plugin_abi_fixture.v1",
|
||||||
|
"fixtureId": "tex-math-live-sidecar",
|
||||||
|
"pluginId": "tex.math",
|
||||||
|
"ownerRepo": "EvePlugins",
|
||||||
|
"contract": "gamecult.eve.plugin_abi.v1",
|
||||||
|
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
|
||||||
|
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
|
||||||
|
"authority": "EvePlugins owns TeX semantics; providers own source state and runtimes own projection.",
|
||||||
|
"operations": [
|
||||||
|
{ "operation": "describe", "input": {}, "expect": { "pluginId": "tex.math", "componentKinds": ["embed.tex"], "commands": [], "capabilities": ["embed.tex", "tex.inline", "tex.block"] } },
|
||||||
|
{ "operation": "validate", "input": { "componentKinds": ["embed.tex", "embed.norn"], "source": "E = mc^2" }, "expect": { "acceptedComponentKinds": ["embed.tex"], "rejectedComponentKinds": ["embed.norn"], "validSource": true, "requiredCapabilities": ["embed.tex"] } },
|
||||||
|
{ "operation": "project", "input": { "documentId": "tex.basic", "source": "E = mc^2", "displayMode": true }, "expect": { "projectionKind": "render-request", "ownedComponentKinds": ["embed.tex"], "outputSchemas": ["tex.math.render_request.v1", "tex.math.render_result.v1"] } },
|
||||||
|
{ "operation": "lower", "input": { "documentId": "tex.basic", "source": "E = mc^2", "displayMode": true }, "expect": { "loweringKind": "typeset-fragment", "preservedComponentKinds": ["embed.tex"], "requiredSchemas": ["tex.math.render_request.v1", "tex.math.render_result.v1"], "fallbackKind": "source-text" } },
|
||||||
|
{ "operation": "measure", "input": { "documentId": "tex.basic", "source": "E = mc^2" }, "expect": { "measurementKind": "typeset-baseline-metrics", "measurementOutputs": ["baseline", "advance", "inkBounds", "fallbackBounds"], "preservesProviderAuthority": true } },
|
||||||
|
{ "operation": "apply", "input": { "documentId": "tex.basic", "source": "E = mc^2", "displayMode": true }, "expect": { "stateEffects": ["cache-render-result", "emit-fallback-on-render-failure"], "receiptSchema": "gamecult.eve.plugin_receipt.v1" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
40
plugins/eve-plugin-tex/plugin.json
Normal file
40
plugins/eve-plugin-tex/plugin.json
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"schema": "gamecult.eve.plugin.v1",
|
||||||
|
"pluginId": "tex.math",
|
||||||
|
"title": "TeX Math",
|
||||||
|
"owner": "EvePlugins",
|
||||||
|
"incubation": {
|
||||||
|
"ownerRepo": "EvePlugins",
|
||||||
|
"currentHostRepo": "EvePlugins",
|
||||||
|
"splitTarget": "EvePlugins/eve-plugin-tex",
|
||||||
|
"graduationTrigger": "Graduated: EvePlugins owns the executable TeX sidecar, fixtures, tests, and conformance witness."
|
||||||
|
},
|
||||||
|
"componentKinds": ["embed.tex"],
|
||||||
|
"commands": [],
|
||||||
|
"runtime": {
|
||||||
|
"invocationModel": "executable-sidecar",
|
||||||
|
"contract": "gamecult.eve.plugin_abi.v1",
|
||||||
|
"transports": ["stdio"],
|
||||||
|
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
|
||||||
|
"sidecar": {
|
||||||
|
"processKind": "long-running-daemon",
|
||||||
|
"protocol": "stdio-ndjson",
|
||||||
|
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
|
||||||
|
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
|
||||||
|
"operations": ["describe", "validate", "project", "lower", "measure", "apply"],
|
||||||
|
"commandEnvelope": "gamecult.eve.command_invocation.v1",
|
||||||
|
"receiptSchema": "gamecult.eve.plugin_receipt.v1",
|
||||||
|
"stateAuthority": "emits-render-results-only-provider-owns-source-state"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"stateSchemas": ["tex.math.source.v1", "tex.math.render_request.v1", "tex.math.render_result.v1"],
|
||||||
|
"fixtures": [
|
||||||
|
{ "fixtureId": "tex-math-basic", "schema": "tex.math.source.v1", "path": "plugins/eve-plugin-tex/fixtures/basic-source.json" }
|
||||||
|
],
|
||||||
|
"abiFixtures": ["plugins/eve-plugin-tex/plugin-abi-fixture.json"],
|
||||||
|
"capabilities": {
|
||||||
|
"requires": ["embed.tex"],
|
||||||
|
"optionalCapabilities": ["tex.inline", "tex.block", "tex.scene-placement"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
130
plugins/eve-plugin-tex/src/plugin.mjs
Normal file
130
plugins/eve-plugin-tex/src/plugin.mjs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import katex from "katex";
|
||||||
|
|
||||||
|
export const pluginId = "tex.math";
|
||||||
|
export const operations = ["describe", "validate", "project", "lower", "measure", "apply"];
|
||||||
|
|
||||||
|
const renderCache = new Map();
|
||||||
|
|
||||||
|
export function dispatch(request) {
|
||||||
|
if (request?.schema !== "gamecult.eve.plugin_abi.request.v1") throw new Error("Unsupported request schema.");
|
||||||
|
if (request.pluginId !== pluginId) throw new Error(`Expected plugin ${pluginId}.`);
|
||||||
|
if (!operations.includes(request.operation)) throw new Error(`Unsupported operation ${request.operation}.`);
|
||||||
|
|
||||||
|
switch (request.operation) {
|
||||||
|
case "describe": return describe();
|
||||||
|
case "validate": return validate(request.input || {});
|
||||||
|
case "project": return project(request.input || {});
|
||||||
|
case "lower": return lower(request.input || {});
|
||||||
|
case "measure": return measure(request.input || {});
|
||||||
|
case "apply": return apply(request.input || {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function describe() {
|
||||||
|
return {
|
||||||
|
pluginId,
|
||||||
|
componentKinds: ["embed.tex"],
|
||||||
|
commands: [],
|
||||||
|
capabilities: ["embed.tex", "tex.inline", "tex.block"],
|
||||||
|
operations,
|
||||||
|
semanticEngine: "katex",
|
||||||
|
stateAuthority: "provider-owns-source-state",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validate(input) {
|
||||||
|
const requested = Array.isArray(input.componentKinds) ? input.componentKinds : [];
|
||||||
|
let validSource = true;
|
||||||
|
let diagnostic = null;
|
||||||
|
if (typeof input.source === "string") {
|
||||||
|
try {
|
||||||
|
render(input);
|
||||||
|
} catch (error) {
|
||||||
|
validSource = false;
|
||||||
|
diagnostic = error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
acceptedComponentKinds: requested.filter((kind) => kind === "embed.tex"),
|
||||||
|
rejectedComponentKinds: requested.filter((kind) => kind !== "embed.tex"),
|
||||||
|
validSource,
|
||||||
|
...(diagnostic ? { diagnostic } : {}),
|
||||||
|
requiredCapabilities: ["embed.tex"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function project(input) {
|
||||||
|
return {
|
||||||
|
schema: "tex.math.render_request.v1",
|
||||||
|
projectionKind: "render-request",
|
||||||
|
ownedComponentKinds: ["embed.tex"],
|
||||||
|
outputSchemas: ["tex.math.render_request.v1", "tex.math.render_result.v1"],
|
||||||
|
documentId: requiredString(input.documentId, "documentId"),
|
||||||
|
source: requiredString(input.source, "source"),
|
||||||
|
displayMode: Boolean(input.displayMode),
|
||||||
|
macros: normalizeMacros(input.macros),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lower(input) {
|
||||||
|
const rendered = render(input);
|
||||||
|
return {
|
||||||
|
schema: "tex.math.render_result.v1",
|
||||||
|
loweringKind: "typeset-fragment",
|
||||||
|
preservedComponentKinds: ["embed.tex"],
|
||||||
|
requiredSchemas: ["tex.math.render_request.v1", "tex.math.render_result.v1"],
|
||||||
|
fallbackKind: "source-text",
|
||||||
|
documentId: requiredString(input.documentId, "documentId"),
|
||||||
|
source: input.source,
|
||||||
|
html: rendered.html,
|
||||||
|
contentHash: rendered.contentHash,
|
||||||
|
engine: "katex",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function measure(input) {
|
||||||
|
render(input);
|
||||||
|
return {
|
||||||
|
measurementKind: "typeset-baseline-metrics",
|
||||||
|
measurementOutputs: ["baseline", "advance", "inkBounds", "fallbackBounds"],
|
||||||
|
measurementAuthority: "runtime-layout-required",
|
||||||
|
preservesProviderAuthority: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function apply(input) {
|
||||||
|
const rendered = render(input);
|
||||||
|
renderCache.set(rendered.contentHash, rendered);
|
||||||
|
return {
|
||||||
|
stateEffects: ["cache-render-result", "emit-fallback-on-render-failure"],
|
||||||
|
receiptSchema: "gamecult.eve.plugin_receipt.v1",
|
||||||
|
contentHash: rendered.contentHash,
|
||||||
|
cacheStatus: "stored",
|
||||||
|
providerStateMutated: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(input) {
|
||||||
|
const source = requiredString(input.source, "source");
|
||||||
|
const macros = normalizeMacros(input.macros);
|
||||||
|
const contentHash = createHash("sha256").update(JSON.stringify({ source, macros, displayMode: Boolean(input.displayMode) })).digest("hex");
|
||||||
|
const cached = renderCache.get(contentHash);
|
||||||
|
if (cached) return cached;
|
||||||
|
return {
|
||||||
|
contentHash,
|
||||||
|
html: katex.renderToString(source, { displayMode: Boolean(input.displayMode), macros, throwOnError: true, output: "htmlAndMathml" }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMacros(value) {
|
||||||
|
if (value == null) return {};
|
||||||
|
if (typeof value !== "object" || Array.isArray(value)) throw new Error("macros must be an object.");
|
||||||
|
return Object.fromEntries(Object.entries(value).map(([key, expansion]) => [key, requiredString(expansion, `macro ${key}`)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function requiredString(value, field) {
|
||||||
|
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required.`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
28
plugins/eve-plugin-tex/src/sidecar.mjs
Normal file
28
plugins/eve-plugin-tex/src/sidecar.mjs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import readline from "node:readline";
|
||||||
|
import { dispatch, pluginId } from "./plugin.mjs";
|
||||||
|
|
||||||
|
readline.createInterface({ input: process.stdin }).on("line", (line) => {
|
||||||
|
if (!line.trim()) return;
|
||||||
|
let request;
|
||||||
|
try {
|
||||||
|
request = JSON.parse(line.replace(/^\uFEFF/, ""));
|
||||||
|
respond(request, dispatch(request));
|
||||||
|
} catch (error) {
|
||||||
|
respond(request, {}, "rejected", error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function respond(request, output, status = "accepted", message) {
|
||||||
|
process.stdout.write(`${JSON.stringify({
|
||||||
|
schema: "gamecult.eve.plugin_abi.response.v1",
|
||||||
|
pluginId,
|
||||||
|
operation: request?.operation || "unknown",
|
||||||
|
requestId: request?.requestId || "unknown",
|
||||||
|
status,
|
||||||
|
output,
|
||||||
|
...(message ? { diagnostics: [{ message }] } : {}),
|
||||||
|
})}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
28
plugins/eve-plugin-tex/test/plugin.test.mjs
Normal file
28
plugins/eve-plugin-tex/test/plugin.test.mjs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import test from "node:test";
|
||||||
|
import { dispatch } from "../src/plugin.mjs";
|
||||||
|
|
||||||
|
function request(operation, input = {}) {
|
||||||
|
return dispatch({ schema: "gamecult.eve.plugin_abi.request.v1", pluginId: "tex.math", operation, requestId: operation, input });
|
||||||
|
}
|
||||||
|
|
||||||
|
test("typesets TeX through KaTeX without taking provider authority", () => {
|
||||||
|
const result = request("lower", { documentId: "basic", source: "E = mc^2", displayMode: true });
|
||||||
|
assert.equal(result.schema, "tex.math.render_result.v1");
|
||||||
|
assert.equal(result.engine, "katex");
|
||||||
|
assert.match(result.html, /katex/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates owned components and rejects invalid TeX", () => {
|
||||||
|
assert.deepEqual(request("validate", { componentKinds: ["embed.tex", "embed.norn"], source: "x^2" }), {
|
||||||
|
acceptedComponentKinds: ["embed.tex"], rejectedComponentKinds: ["embed.norn"], validSource: true, requiredCapabilities: ["embed.tex"],
|
||||||
|
});
|
||||||
|
assert.equal(request("validate", { componentKinds: ["embed.tex"], source: "\\notacommand{" }).validSource, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("caches render results without mutating provider state", () => {
|
||||||
|
const result = request("apply", { documentId: "basic", source: "E = mc^2" });
|
||||||
|
assert.equal(result.cacheStatus, "stored");
|
||||||
|
assert.equal(result.providerStateMutated, false);
|
||||||
|
});
|
||||||
|
|
||||||
56
scripts/run-tex-witness.mjs
Normal file
56
scripts/run-tex-witness.mjs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import readline from "node:readline";
|
||||||
|
|
||||||
|
const root = path.resolve(import.meta.dirname, "..");
|
||||||
|
const fixture = JSON.parse(fs.readFileSync(path.join(root, "plugins/eve-plugin-tex/plugin-abi-fixture.json"), "utf8"));
|
||||||
|
const child = spawn(process.execPath, [path.join(root, "plugins/eve-plugin-tex/src/sidecar.mjs")], { cwd: root, stdio: ["pipe", "pipe", "inherit"] });
|
||||||
|
const responses = readline.createInterface({ input: child.stdout })[Symbol.asyncIterator]();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let index = 0; index < fixture.operations.length; index += 1) {
|
||||||
|
const operation = fixture.operations[index];
|
||||||
|
const requestId = `tex-witness-${index}`;
|
||||||
|
child.stdin.write(`${JSON.stringify({ schema: fixture.requestSchema, pluginId: fixture.pluginId, operation: operation.operation, requestId, input: operation.input })}\n`);
|
||||||
|
const next = await responses.next();
|
||||||
|
assert.equal(next.done, false);
|
||||||
|
const response = JSON.parse(next.value);
|
||||||
|
assert.equal(response.schema, fixture.responseSchema);
|
||||||
|
assert.equal(response.pluginId, fixture.pluginId);
|
||||||
|
assert.equal(response.operation, operation.operation);
|
||||||
|
assert.equal(response.requestId, requestId);
|
||||||
|
assert.equal(response.status, "accepted");
|
||||||
|
assertSubset(response.output, operation.expect, operation.operation);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
child.stdin.end();
|
||||||
|
child.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputDir = path.join(root, "artifacts", "tex-math");
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
const witness = {
|
||||||
|
schema: "gamecult.eve.plugin_witness.v1",
|
||||||
|
witnessId: "tex.math.owner-sidecar",
|
||||||
|
pluginId: "tex.math",
|
||||||
|
ownerRepo: "EvePlugins",
|
||||||
|
transport: "stdio-ndjson",
|
||||||
|
operations: fixture.operations.map(({ operation }) => operation),
|
||||||
|
status: "passed",
|
||||||
|
};
|
||||||
|
fs.writeFileSync(path.join(outputDir, "runtime-witness.json"), `${JSON.stringify(witness, null, 2)}\n`);
|
||||||
|
console.log(path.join(outputDir, "runtime-witness.json"));
|
||||||
|
|
||||||
|
function assertSubset(actual, expected, label) {
|
||||||
|
if (Array.isArray(expected)) {
|
||||||
|
assert.deepEqual(actual, expected, label);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (expected && typeof expected === "object") {
|
||||||
|
for (const [key, value] of Object.entries(expected)) assertSubset(actual?.[key], value, `${label}.${key}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert.equal(actual, expected, label);
|
||||||
|
}
|
||||||
Reference in a new issue