mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 21:05:21 +02:00
feat: add opt-in chat provider and data foundation (#13100)
Add dormant provider contracts, qualified patched adapters, tenant-scoped persistence and lifecycle ownership without activating chat routes. Preserve the experimental integration as dependent PR #13038. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
7d84b183fb
commit
6abeb67334
143 files changed
+789391
-162
No files matched your search
@@ -49,6 +49,64 @@ const claudeAcpPatch = await readFile(
|
||||
"utf8",
|
||||
);
|
||||
|
||||
for (const version of ["0.12.0", "0.13.1"]) {
|
||||
test(`ACPX ${version} release patch uses portable generated unified hunks`, async () => {
|
||||
const patch = await readFile(
|
||||
new URL(`../patches/acpx@${version}.patch`, import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const lines = patch.split("\n");
|
||||
let hunkCount = 0;
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const header = lines[index].match(
|
||||
/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/,
|
||||
);
|
||||
if (!header) continue;
|
||||
hunkCount += 1;
|
||||
const body = [];
|
||||
let oldLines = 0;
|
||||
let newLines = 0;
|
||||
while (index + 1 < lines.length && (oldLines < Number(header[2] ?? 1) || newLines < Number(header[4] ?? 1))) {
|
||||
const line = lines[++index];
|
||||
// Unified diff's EOF marker is metadata, not a source/destination
|
||||
// line, and may occur between the removed and added final lines.
|
||||
if (line === "\\ No newline at end of file") continue;
|
||||
// Git accepts an empty context line with its optional space omitted.
|
||||
assert.ok(line === "" || /^[ +\-]/.test(line), `invalid unified hunk line: ${line}`);
|
||||
const normalized = line === "" ? " " : line;
|
||||
body.push(normalized);
|
||||
if (!normalized.startsWith("+")) oldLines += 1;
|
||||
if (!normalized.startsWith("-")) newLines += 1;
|
||||
}
|
||||
assert.equal(
|
||||
body.filter((line) => !line.startsWith("+")).length,
|
||||
Number(header[2] ?? 1),
|
||||
);
|
||||
assert.equal(
|
||||
body.filter((line) => !line.startsWith("-")).length,
|
||||
Number(header[4] ?? 1),
|
||||
);
|
||||
const prefix = body.findIndex((line) => !line.startsWith(" "));
|
||||
const suffix = body
|
||||
.slice()
|
||||
.reverse()
|
||||
.findIndex((line) => !line.startsWith(" "));
|
||||
// pnpm patch-commit emits three context lines. Hand-added asymmetric
|
||||
// context can force GNU patch's locate_hunk() to require EOF even when
|
||||
// BSD patch and git apply accept the same source and hunk.
|
||||
assert.ok(
|
||||
prefix >= 0 && prefix <= 3,
|
||||
`regenerate ${version} hunk at old line ${header[1]} with pnpm patch-commit (prefix ${prefix})`,
|
||||
);
|
||||
assert.ok(
|
||||
suffix >= 0 && suffix <= 3,
|
||||
`regenerate ${version} hunk at old line ${header[1]} with pnpm patch-commit (suffix ${suffix})`,
|
||||
);
|
||||
}
|
||||
assert.ok(hunkCount > 0);
|
||||
});
|
||||
}
|
||||
|
||||
test("published packages preserve the patched ACPX runtime", () => {
|
||||
assert.equal(
|
||||
rootPackage.pnpm.patchedDependencies["acpx@0.12.0"],
|
||||
@@ -61,7 +119,7 @@ test("published packages preserve the patched ACPX runtime", () => {
|
||||
assert.equal(adapterUtilsPackage.dependencies.acpx, "0.12.0");
|
||||
assert.deepEqual(adapterUtilsPackage.bundleDependencies, ["acpx"]);
|
||||
assert.equal(serverPackage.dependencies.acpx, "0.13.1");
|
||||
assert.deepEqual(serverPackage.bundleDependencies, ["acpx"]);
|
||||
assert.ok(serverPackage.bundleDependencies.includes("acpx"));
|
||||
assert.equal(bundledCliNpmDependencies.has("acpx"), true);
|
||||
assert.equal(cliEsbuildConfig.external.includes("acpx"), false);
|
||||
});
|
||||
@@ -226,7 +284,7 @@ test("bundled package patch selection rejects an unpatched installed version", (
|
||||
);
|
||||
});
|
||||
|
||||
test("server package staging bundles and patches the vendored runner's acpx runtime", (t) => {
|
||||
test("server package staging applies every bundled runtime patch and preserves the vendored runner", (t) => {
|
||||
const fixtureDir = mkdtempSync(join(tmpdir(), "paperclip-bundled-stage-"));
|
||||
const sourceDir = join(fixtureDir, "source");
|
||||
const destinationDir = join(fixtureDir, "destination");
|
||||
@@ -263,7 +321,7 @@ mkdir -p "$destination/node_modules/.pnpm"
|
||||
set -euo pipefail
|
||||
printf 'npm %s\\n' "$*" >> "$FAKE_CALL_LOG"
|
||||
[ "$*" = "install --omit=dev --ignore-scripts --no-audit --no-fund" ]
|
||||
node -e 'const pkg = require("./package.json"); if ("devDependencies" in pkg) process.exit(1)'
|
||||
node -e 'const fs = require("node:fs"); const pkg = require("./package.json"); if ("devDependencies" in pkg) process.exit(1); for (const [name, version] of Object.entries(pkg.dependencies)) { const dir = "node_modules/" + name; fs.mkdirSync(dir + "/dist", { recursive: true }); fs.writeFileSync(dir + "/package.json", JSON.stringify({ name, version })); }'
|
||||
mkdir -p node_modules/acpx/dist
|
||||
printf 'unpatched runtime\\n' > node_modules/acpx/dist/runtime.js
|
||||
printf '{"name":"acpx","version":"0.13.1"}\\n' > node_modules/acpx/package.json
|
||||
@@ -284,6 +342,10 @@ while [ "$#" -gt 0 ]; do
|
||||
fi
|
||||
done
|
||||
patch_input="$(cat)"
|
||||
printf '%s\\n' "$patch_input" > "$target/applied.patch"
|
||||
if [[ "$target" != */acpx ]]; then
|
||||
exit 0
|
||||
fi
|
||||
grep -q spawnEnvironment <<< "$patch_input"
|
||||
grep -q spawnAgent <<< "$patch_input"
|
||||
grep -q onAgentStderr <<< "$patch_input"
|
||||
@@ -293,7 +355,11 @@ printf 'patched spawnEnvironment runtime\\n' > "$target/dist/runtime.js"
|
||||
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
[new URL("./prepare-bundled-package.mjs", import.meta.url).pathname, sourceDir, destinationDir],
|
||||
[
|
||||
new URL("./prepare-bundled-package.mjs", import.meta.url).pathname,
|
||||
sourceDir,
|
||||
destinationDir,
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -318,9 +384,23 @@ printf 'patched spawnEnvironment runtime\\n' > "$target/dist/runtime.js"
|
||||
/patch -p1 --forward -d .*node_modules\/acpx/,
|
||||
);
|
||||
assert.equal(
|
||||
readFileSync(callLog, "utf8").split("\n").filter((line) => line.startsWith("patch ")).length,
|
||||
1,
|
||||
readFileSync(callLog, "utf8")
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("patch ")).length,
|
||||
serverPackage.bundleDependencies.length,
|
||||
);
|
||||
for (const name of serverPackage.bundleDependencies) {
|
||||
const specifier = `${name}@${serverPackage.dependencies[name]}`;
|
||||
const patchPath = rootPackage.pnpm.patchedDependencies[specifier];
|
||||
assert.equal(
|
||||
readFileSync(
|
||||
join(destinationDir, "node_modules", name, "applied.patch"),
|
||||
"utf8",
|
||||
),
|
||||
`${readFileSync(new URL(`../${patchPath}`, import.meta.url), "utf8").trimEnd()}\n`,
|
||||
`${specifier} receives its own full configured patch`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("bundled package dry runs preview without querying published versions", () => {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import {
|
||||
createBundledInstallManifest,
|
||||
materializePublishManifest,
|
||||
selectBundledDependencyPatches,
|
||||
} from "./prepare-bundled-package.mjs";
|
||||
|
||||
const repoRoot = fileURLToPath(new URL("..", import.meta.url));
|
||||
const rootPackage = JSON.parse(
|
||||
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
||||
);
|
||||
const serverPackage = JSON.parse(
|
||||
readFileSync(new URL("../server/package.json", import.meta.url), "utf8"),
|
||||
);
|
||||
const workspace = readFileSync(
|
||||
new URL("../pnpm-workspace.yaml", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const required = [
|
||||
["@chat-adapter/discord", "4.39.0"],
|
||||
["@chat-adapter/github", "4.39.0"],
|
||||
["@chat-adapter/slack", "4.39.0"],
|
||||
["@chat-adapter/teams", "4.39.0"],
|
||||
["@chat-adapter/telegram", "4.39.0"],
|
||||
["@discordjs/ws", "1.2.3"],
|
||||
];
|
||||
|
||||
for (const [name, version] of required) {
|
||||
test(`published server retains the patched ${name}@${version} runtime`, () => {
|
||||
const published = materializePublishManifest(serverPackage);
|
||||
const installed = createBundledInstallManifest(
|
||||
published,
|
||||
serverPackage.bundleDependencies,
|
||||
);
|
||||
assert.equal(
|
||||
serverPackage.dependencies[name],
|
||||
version,
|
||||
"the patched runtime must have an exact direct version",
|
||||
);
|
||||
assert.ok(
|
||||
serverPackage.bundleDependencies.includes(name),
|
||||
"npm consumers cannot apply this repository's pnpm patches",
|
||||
);
|
||||
assert.equal(
|
||||
installed.dependencies[name],
|
||||
version,
|
||||
"release staging must install the runtime before patching",
|
||||
);
|
||||
assert.ok(
|
||||
published.bundleDependencies.includes(name),
|
||||
"the patched runtime must remain in the published tarball",
|
||||
);
|
||||
const specifier = `${name}@${version}`;
|
||||
const patchPath = rootPackage.pnpm.patchedDependencies[specifier];
|
||||
assert.equal(typeof patchPath, "string");
|
||||
assert.ok(
|
||||
workspace.includes(`"${specifier}": ${patchPath}`),
|
||||
"both supported pnpm configuration paths must match",
|
||||
);
|
||||
// Parse the actual full patch, not merely its configured filename. This is
|
||||
// read-only; clean-package materialization is a separate qualification.
|
||||
execFileSync("git", ["apply", "--stat", patchPath], {
|
||||
cwd: repoRoot,
|
||||
stdio: "pipe",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test("release selection includes every chat runtime patch and the existing ACPX patch", (t) => {
|
||||
const destination = mkdtempSync(
|
||||
join(tmpdir(), "paperclip-chat-release-contract-"),
|
||||
);
|
||||
t.after(() => rmSync(destination, { recursive: true, force: true }));
|
||||
for (const [name, version] of [...required, ["acpx", "0.13.1"]]) {
|
||||
const directory = join(destination, "node_modules", name);
|
||||
mkdirSync(directory, { recursive: true });
|
||||
writeFileSync(
|
||||
join(directory, "package.json"),
|
||||
JSON.stringify({ name, version }),
|
||||
);
|
||||
}
|
||||
const selected = selectBundledDependencyPatches(
|
||||
destination,
|
||||
serverPackage.bundleDependencies,
|
||||
rootPackage.pnpm.patchedDependencies,
|
||||
);
|
||||
assert.deepEqual(
|
||||
selected.map(({ specifier }) => specifier).sort(),
|
||||
[
|
||||
...required.map(([name, version]) => `${name}@${version}`),
|
||||
"acpx@0.13.1",
|
||||
].sort(),
|
||||
);
|
||||
writeFileSync(
|
||||
join(destination, "node_modules", "@discordjs/ws", "package.json"),
|
||||
JSON.stringify({
|
||||
name: "@discordjs/ws",
|
||||
version: "1.2.4",
|
||||
}),
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
selectBundledDependencyPatches(
|
||||
destination,
|
||||
serverPackage.bundleDependencies,
|
||||
rootPackage.pnpm.patchedDependencies,
|
||||
),
|
||||
/installed @discordjs\/ws@1\.2\.4/,
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user