mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(plugin-sdk): make API baseline module-mergeable (#121842)
This commit is contained in:
committed by
GitHub
parent
2cc11aff38
commit
3cfb344f9f
@@ -7,7 +7,7 @@ snapshots remain local inspection artifacts.
|
||||
|
||||
- `config-baseline.sha256` — hashes of config baseline JSON artifacts.
|
||||
- `config-baseline.counts.json` — maximum entry counts for each config baseline kind.
|
||||
- `plugin-sdk-api-baseline.jsonl` — one content-derived record per Plugin SDK module and export.
|
||||
- `plugin-sdk-api-baseline.jsonl` — one content hash per Plugin SDK module.
|
||||
- `sqlite-session-transcript-schema-baseline.sha256` — hash of the sessions/transcripts SQLite schema baseline.
|
||||
|
||||
**Local only (gitignored):**
|
||||
@@ -24,11 +24,11 @@ Do not edit any of these files by hand.
|
||||
- Validate Plugin SDK API contract manifest: `pnpm plugin-sdk:api:check`
|
||||
|
||||
The Plugin SDK contract sorts modules by import specifier and exports by kind
|
||||
then name. Its line-delimited records keep concurrent changes to separate
|
||||
exports mergeable. Export records carry the normalized `declaration` and an
|
||||
explicit `closureHash` for surface-reachable repo declarations. Committed
|
||||
records omit source paths so file moves do not change the contract; origin
|
||||
`source` fields remain available in the local pretty JSON snapshot.
|
||||
then name. Each line hashes one module's normalized declarations and the
|
||||
surface-reachable declaration closure hashes, so changes to different modules
|
||||
do not rewrite shared aggregate records. Committed records omit source paths so
|
||||
file moves do not change the contract; origin `source` fields remain available
|
||||
in the local pretty JSON snapshot.
|
||||
|
||||
- Regenerate SQLite sessions/transcripts schema baseline: `pnpm sqlite:sessions-schema:gen`
|
||||
- Validate SQLite sessions/transcripts schema baseline: `pnpm sqlite:sessions-schema:check`
|
||||
|
||||
+152
-5018
File diff suppressed because one or more lines are too long
@@ -16,25 +16,13 @@ function describeContractLine(line: string): { identity: string | null; label: s
|
||||
try {
|
||||
const record = JSON.parse(line) as {
|
||||
entrypoint?: unknown;
|
||||
exportName?: unknown;
|
||||
recordType?: unknown;
|
||||
};
|
||||
if (typeof record.entrypoint === "string" && record.recordType === "module") {
|
||||
if (typeof record.entrypoint === "string") {
|
||||
return {
|
||||
identity: `module\0${record.entrypoint}`,
|
||||
label: `entrypoint=${record.entrypoint}`,
|
||||
};
|
||||
}
|
||||
if (
|
||||
typeof record.entrypoint === "string" &&
|
||||
typeof record.exportName === "string" &&
|
||||
record.recordType === "export"
|
||||
) {
|
||||
return {
|
||||
identity: `export\0${record.entrypoint}\0${record.exportName}`,
|
||||
label: `entrypoint=${record.entrypoint} exportName=${record.exportName}`,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Invalid committed JSONL still appears in the bounded raw-line diff.
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Tests the plugin SDK public API baseline.
|
||||
*/
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import ts from "typescript";
|
||||
@@ -307,7 +309,8 @@ describe("Plugin SDK API baseline", () => {
|
||||
expect(rendered.json).toContain('"source": {');
|
||||
expect(rendered.jsonl).not.toContain('"sourceLine":');
|
||||
expect(rendered.jsonl).not.toContain('"sourcePath":');
|
||||
expect(rendered.jsonl).toContain('"closureHash":"');
|
||||
expect(rendered.jsonl).toContain('"contentHash":"');
|
||||
expect(rendered.jsonl).not.toContain('"closureHash":"');
|
||||
expect(rendered.jsonl).not.toContain("// declaration closure:");
|
||||
});
|
||||
|
||||
@@ -318,7 +321,7 @@ describe("Plugin SDK API baseline", () => {
|
||||
expect(reverse.jsonl).toBe(rendered.jsonl);
|
||||
});
|
||||
|
||||
it("keeps unrelated JSONL records byte-identical when one export changes", () => {
|
||||
it("keeps unrelated module hashes byte-identical when one export changes", () => {
|
||||
const target = rendered.baseline.modules[0];
|
||||
expect(target?.exports.length).toBeGreaterThan(0);
|
||||
const changed = renderPluginSdkApiBaselineModules(
|
||||
@@ -338,8 +341,68 @@ describe("Plugin SDK API baseline", () => {
|
||||
const before = rendered.jsonl.split("\n");
|
||||
const after = changed.jsonl.split("\n");
|
||||
|
||||
expect(after[1]).not.toBe(before[1]);
|
||||
expect(after.slice(2)).toEqual(before.slice(2));
|
||||
expect(after[0]).not.toBe(before[0]);
|
||||
expect(after.slice(1)).toEqual(before.slice(1));
|
||||
});
|
||||
|
||||
it("writes one line per module and merges disjoint module edits without conflicts", () => {
|
||||
const modules = rendered.baseline.modules;
|
||||
const left = modules[0];
|
||||
const right = modules.at(-1);
|
||||
expect(left?.exports.length).toBeGreaterThan(0);
|
||||
expect(right?.exports.length).toBeGreaterThan(0);
|
||||
expect(left?.entrypoint).not.toBe(right?.entrypoint);
|
||||
|
||||
const editModule = (target: typeof left, suffix: string) =>
|
||||
renderPluginSdkApiBaselineModules(
|
||||
modules.map((moduleSurface) =>
|
||||
moduleSurface === target
|
||||
? {
|
||||
...moduleSurface,
|
||||
exports: moduleSurface.exports.map((exportSurface, index) =>
|
||||
index === 0
|
||||
? {
|
||||
...exportSurface,
|
||||
declaration: `${exportSurface.declaration ?? ""} ${suffix}`,
|
||||
}
|
||||
: exportSurface,
|
||||
),
|
||||
}
|
||||
: moduleSurface,
|
||||
),
|
||||
);
|
||||
const ours = editModule(left, "left edit");
|
||||
const theirs = editModule(right, "right edit");
|
||||
const lines = rendered.jsonl
|
||||
.trimEnd()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
expect(lines).toHaveLength(modules.length);
|
||||
expect(lines.map((line) => line.importSpecifier)).toEqual(
|
||||
modules.map((moduleSurface) => moduleSurface.importSpecifier),
|
||||
);
|
||||
expect(lines).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ contentHash: expect.stringMatching(/^[a-f0-9]{64}$/u) }),
|
||||
]),
|
||||
);
|
||||
|
||||
const mergeDir = tempDirs.make("openclaw-plugin-sdk-api-merge-");
|
||||
const basePath = path.join(mergeDir, "base.jsonl");
|
||||
const oursPath = path.join(mergeDir, "ours.jsonl");
|
||||
const theirsPath = path.join(mergeDir, "theirs.jsonl");
|
||||
fs.writeFileSync(basePath, rendered.jsonl);
|
||||
fs.writeFileSync(oursPath, ours.jsonl);
|
||||
fs.writeFileSync(theirsPath, theirs.jsonl);
|
||||
|
||||
const merge = spawnSync("git", ["merge-file", "--stdout", oursPath, basePath, theirsPath], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
|
||||
expect(merge.status, merge.stderr).toBe(0);
|
||||
expect(merge.stdout).toContain(ours.jsonl.trimEnd().split("\n")[0]);
|
||||
expect(merge.stdout).toContain(theirs.jsonl.trimEnd().split("\n").at(-1));
|
||||
});
|
||||
|
||||
it("renders byte-identical JSONL deterministically", async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// API baseline helpers render public SDK exports for contract drift checks.
|
||||
import { createHash } from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -349,35 +350,29 @@ function buildModuleSurface(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function sha256(content: string): string {
|
||||
return createHash("sha256").update(content, "utf8").digest("hex");
|
||||
}
|
||||
|
||||
function buildJsonlLines(baseline: PluginSdkApiBaseline): string[] {
|
||||
const lines: string[] = [];
|
||||
|
||||
for (const moduleSurface of baseline.modules) {
|
||||
lines.push(
|
||||
JSON.stringify({
|
||||
category: moduleSurface.category,
|
||||
entrypoint: moduleSurface.entrypoint,
|
||||
importSpecifier: moduleSurface.importSpecifier,
|
||||
recordType: "module",
|
||||
}),
|
||||
);
|
||||
|
||||
for (const exportSurface of moduleSurface.exports) {
|
||||
lines.push(
|
||||
JSON.stringify({
|
||||
closureHash: exportSurface.closureHash,
|
||||
declaration: exportSurface.declaration,
|
||||
entrypoint: moduleSurface.entrypoint,
|
||||
exportName: exportSurface.exportName,
|
||||
importSpecifier: moduleSurface.importSpecifier,
|
||||
kind: exportSurface.kind,
|
||||
recordType: "export",
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return lines;
|
||||
return baseline.modules.map((moduleSurface) => {
|
||||
const contractSurface = {
|
||||
category: moduleSurface.category,
|
||||
entrypoint: moduleSurface.entrypoint,
|
||||
exports: moduleSurface.exports.map((exportSurface) => ({
|
||||
closureHash: exportSurface.closureHash,
|
||||
declaration: exportSurface.declaration,
|
||||
exportName: exportSurface.exportName,
|
||||
kind: exportSurface.kind,
|
||||
})),
|
||||
importSpecifier: moduleSurface.importSpecifier,
|
||||
};
|
||||
return JSON.stringify({
|
||||
contentHash: sha256(JSON.stringify(contractSurface)),
|
||||
entrypoint: moduleSurface.entrypoint,
|
||||
importSpecifier: moduleSurface.importSpecifier,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Render the current public SDK API baseline without writing generated artifacts. */
|
||||
|
||||
Reference in New Issue
Block a user