Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ jobs:
ARCHITECTURE_BASE: ${{ github.event.pull_request.base.sha || github.event.before || 'HEAD^' }}
run: node scripts/check-architecture.mjs

- name: Unit-test the Pi patch checks
run: node --test scripts/pi-patch-hash.test.mjs

- name: Unit tests
run: pnpm -r --if-present test

Expand Down
4 changes: 2 additions & 2 deletions packages/agent-runtime/src/native-pi-session.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { basename, join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AgentSession, ModelRuntime, SessionManager } from "@earendil-works/pi-coding-agent";
import { createAssistantMessageEventStream, type AssistantMessage } from "@earendil-works/pi-ai";
Expand Down Expand Up @@ -799,7 +799,7 @@ describe("native fork children", () => {
expect(failure?.message).not.toContain(f.group);
expect(existsSync(foreignPath)).toBe(true);
expect(readFileSync(foreignPath, "utf8")).toContain("collision");
expect(groupEntries(f.group).sort()).toEqual(before.concat([foreignPath.split("/").at(-1)!]).sort());
expect(groupEntries(f.group).sort()).toEqual(before.concat([basename(foreignPath)]).sort());
expect(readFileSync(f.file, "utf8")).toBe(parentBytes);
} finally { service.disposeAll(); }
} finally {
Expand Down
26 changes: 24 additions & 2 deletions scripts/check-pi-dependencies.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
#!/usr/bin/env node
/**
* Require every pinned Pi package to be installed at the target version, and
* the ones we patch to be installed as pnpm's patched instance.
*
* Usage:
* node scripts/check-pi-dependencies.mjs
* node scripts/check-pi-dependencies.mjs --root <dir>
* pnpm check:pi-dependencies
*/
import { existsSync, readFileSync, realpathSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { installedPatchHashes } from "./pi-patch-hash.mjs";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
function parseArgs(argv) {
let out = resolve(dirname(fileURLToPath(import.meta.url)), "..");
for (let i = 0; i < argv.length; i += 1) {
if (argv[i] === "--root" && argv[i + 1]) {
out = resolve(argv[i + 1]);
i += 1;
}
}
return out;
}

const root = parseArgs(process.argv.slice(2));
const targetVersion = "1.0.1";

function readJson(path) {
Expand All @@ -25,7 +47,7 @@ function assertInstalled(packagePath, expectedName, { patched = false } = {}) {
if (manifest.name !== expectedName || manifest.version !== targetVersion) {
throw new Error(`${packagePath} resolves to ${manifest.name}@${manifest.version}, expected ${expectedName}@${targetVersion}`);
}
if (patched && !resolved.includes("patch_hash=")) {
if (patched && installedPatchHashes(root, expectedName, targetVersion).length === 0) {
throw new Error(`${packagePath} does not resolve to pnpm's patched package instance`);
}
}
Expand Down
44 changes: 38 additions & 6 deletions scripts/check-pi-patches.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
import { existsSync, readFileSync, realpathSync } from "node:fs";
#!/usr/bin/env node
/**
* Require every Pi patch to be mapped in pnpm-workspace.yaml, installed by
* pnpm, recorded in pnpm-lock.yaml, and to still carry the audited contracts.
*
* Usage:
* node scripts/check-pi-patches.mjs
* node scripts/check-pi-patches.mjs --root <dir>
* pnpm check:pi-patches
*/
import { existsSync, readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { installedPatchHashes } from "./pi-patch-hash.mjs";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
function parseArgs(argv) {
let out = resolve(dirname(fileURLToPath(import.meta.url)), "..");
for (let i = 0; i < argv.length; i += 1) {
if (argv[i] === "--root" && argv[i + 1]) {
out = resolve(argv[i + 1]);
i += 1;
}
}
return out;
}

const root = parseArgs(process.argv.slice(2));
const targetVersion = "1.0.1";
const entries = [
{
Expand Down Expand Up @@ -39,10 +61,20 @@ for (const entry of entries) {
const workspaceMapping = `'${entry.name}@${targetVersion}': ${entry.patch}`;
if (!workspace.includes(workspaceMapping)) throw new Error(`pnpm-workspace.yaml does not map ${entry.name} to ${entry.patch}`);
const packagePath = join(root, entry.packagePath);
const resolved = realpathSync(packagePath);
const patchHash = resolved.match(/patch_hash=([a-f0-9]+)/)?.[1];
if (!patchHash || !lockfile.includes(`${entry.name}@${targetVersion}(patch_hash=${patchHash}`)) {
throw new Error(`${entry.name}@${targetVersion} installed patch hash is absent from pnpm-lock.yaml`);
if (!existsSync(packagePath)) {
throw new Error(`Install dependencies before this check; missing ${entry.packagePath}`);
}
const patchHashes = installedPatchHashes(root, entry.name, targetVersion);
if (patchHashes.length === 0) {
throw new Error(`${entry.name}@${targetVersion} is not installed as a patched instance`);
}
const unlocked = patchHashes.filter(
(patchHash) => !lockfile.includes(`${entry.name}@${targetVersion}(patch_hash=${patchHash}`),
);
if (unlocked.length > 0) {
throw new Error(
`${entry.name}@${targetVersion} installed patch hash is absent from pnpm-lock.yaml: ${unlocked.join(", ")}`,
);
}
}

Expand Down
36 changes: 36 additions & 0 deletions scripts/pi-patch-hash.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";

/**
* pnpm copies the lockfile it actually installed from into the virtual
* store, next to the store entries it describes.
*/
export const VIRTUAL_STORE_LOCKFILE = join("node_modules", ".pnpm", "lock.yaml");

function escapeForRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

/**
* Return the patch hashes pnpm recorded for `name@version` in a lockfile.
*
* Every patched snapshot is keyed `'name@version(patch_hash=<hex>)...`
* (single-quoted for scoped names, bare otherwise), so the snapshot keys
* carry the full hash even when the matching `.pnpm` directory name has
* been shortened.
*/
export function patchHashesIn(lockText, name, version) {
const snapshotKey = new RegExp(
`'?${escapeForRegExp(name)}@${escapeForRegExp(version)}\\(patch_hash=([a-f0-9]+)\\)`,
"g",
);
return [...new Set([...lockText.matchAll(snapshotKey)].map((match) => match[1]))].sort();
}

/**
* Patch hashes of the instances pnpm actually installed, read from the
* virtual store's own lockfile.
*/
export function installedPatchHashes(root, name, version) {
return patchHashesIn(readFileSync(join(root, VIRTUAL_STORE_LOCKFILE), "utf8"), name, version);
}
226 changes: 226 additions & 0 deletions scripts/pi-patch-hash.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, relative } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { patchHashesIn } from "./pi-patch-hash.mjs";

const here = dirname(fileURLToPath(import.meta.url));
const patchDependencyCheck = join(here, "check-pi-dependencies.mjs");
const patchCheck = join(here, "check-pi-patches.mjs");

const TARGET = "1.0.1";
const PATCHED = [
{
name: "@earendil-works/pi-agent-core",
markers: ["hosted_search_update", "localRequestErrorDetails"],
extra: "",
},
{
name: "@earendil-works/pi-ai",
markers: ["hostedSearch", "withLocalRequestErrors", "AnthropicOAuthTokenError", "Retry-After"],
extra: [
'+{"openai-completions":{"chat:deepseek-flash":{"baseUrl":"https://api.deepseek.com","compat":{"supportsMidConvoSystemMessages":true}}}}',
"diff --git a/dist/index.d.ts b/dist/index.d.ts",
"diff --git a/dist/types.d.ts b/dist/types.d.ts",
"diff --git a/dist/utils/assistant-message-frame.d.ts b/dist/utils/assistant-message-frame.d.ts",
"diff --git a/dist/utils/estimate.d.ts b/dist/utils/estimate.d.ts",
"diff --git a/dist/utils/hosted-search.d.ts b/dist/utils/hosted-search.d.ts",
"diff --git a/dist/utils/local-request-error.d.ts b/dist/utils/local-request-error.d.ts",
"diff --git a/dist/utils/local-request-stream.d.ts b/dist/utils/local-request-stream.d.ts",
].join("\n"),
},
{
name: "@earendil-works/pi-coding-agent",
markers: ["hostedSearchReplayProjection", "estimateProjectedContextTokens"],
extra: "+export declare function estimateProjectedContextTokens(",
},
];
const RELEASE_AGE_EXCLUDE = [
"chord",
"pi-agent-core",
"pi-ai",
"pi-codemode",
"pi-coding-agent",
"pi-mcp",
"pi-telemetry",
"pi-tui",
].map((name) => `@earendil-works/${name}@${TARGET}`);
const HASH = `${"0".repeat(63)}1`;

function patchPath(name) {
return `patches/${name.replace("@earendil-works/", "@earendil-works__")}@${TARGET}.patch`;
}

/**
* A `.pnpm` entry name as pnpm writes it once it has to shorten the virtual
* store directory on a long path: the `patch_hash=` segment is gone even
* though the install really is patched. Both checks used to read the hash out
* of the resolved symlink, so they called this install unpatched.
*/
function storeEntryDir(name, hash, shorten) {
const bare = name.replace("@earendil-works/", "");
return shorten
? `node_modules/.pnpm/${bare}@${TARGET}_${hash.slice(0, 12)}`
: `node_modules/.pnpm/${bare}@${TARGET}_patch_hash=${hash}`;
}

function run(script, root) {
try {
const stdout = execFileSync("node", [script, "--root", root], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
return { code: 0, stdout, stderr: "" };
} catch (error) {
return {
code: error.status ?? 1,
stdout: error.stdout?.toString() ?? "",
stderr: error.stderr?.toString() ?? "",
};
}
}

function write(root, path, contents) {
const file = join(root, path);
mkdirSync(dirname(file), { recursive: true });
writeFileSync(file, contents);
}

function snapshotSection(root, names, hash) {
return `lockfileVersion: '9.0'\n\nsnapshots:\n${
names.map((name) => ` '${name}@${TARGET}(patch_hash=${hash})': {}`).join("\n")
}\n`;
}

/**
* Build a workspace that satisfies both checks: correct pins, every patch
* mapped and locked, and the Pi packages installed as patched instances.
*/
function workspace({
shorten = true,
storeNames = PATCHED.map((entry) => entry.name),
storeHash = HASH,
lockHash = HASH,
} = {}) {
const dir = mkdtempSync(join(tmpdir(), "pi-desktop-patches-"));

write(
dir,
"packages/agent-runtime/package.json",
JSON.stringify({
dependencies: Object.fromEntries([...PATCHED.map((e) => e.name), "@earendil-works/pi-mcp"].map((n) => [n, TARGET])),
}),
);
write(
dir,
"apps/desktop/package.json",
JSON.stringify({
devDependencies: { "@earendil-works/pi-ai": TARGET, "@earendil-works/pi-mcp": TARGET },
}),
);
write(dir, "pnpm-lock.yaml", snapshotSection(dir, PATCHED.map((entry) => entry.name), lockHash));
write(dir, "node_modules/.pnpm/lock.yaml", snapshotSection(dir, storeNames, storeHash));

for (const entry of PATCHED) {
write(dir, patchPath(entry.name), [entry.markers.join("\n"), entry.extra].filter(Boolean).join("\n"));
}
// pnpm-workspace.yaml maps each patch, which is what check-pi-patches reads.
const workspaceYaml = PATCHED.map(
(entry) => `'${entry.name}@${TARGET}': ${patchPath(entry.name)}`,
).join("\n");
write(
dir,
"pnpm-workspace.yaml",
`${workspaceYaml}\nminimumReleaseAgeExclude:\n${RELEASE_AGE_EXCLUDE.map((name) => ` - '${name}'`).join("\n")}\n`,
);

const link = (from, to) => {
mkdirSync(dirname(from), { recursive: true });
symlinkSync(relative(dirname(from), to), from, "dir");
};
for (const name of PATCHED.map((entry) => entry.name)) {
const entryDir = join(dir, storeEntryDir(name, storeHash, shorten), "node_modules", name);
write(dir, relative(dir, join(entryDir, "package.json")), JSON.stringify({ name, version: TARGET }));
link(join(dir, "packages/agent-runtime/node_modules", name), entryDir);
}
for (const [scope, name] of [
["apps/desktop", "@earendil-works/pi-ai"],
["apps/desktop", "@earendil-works/pi-mcp"],
]) {
write(dir, `${scope}/node_modules/${name}/package.json`, JSON.stringify({ name, version: TARGET }));
}
return dir;
}

test("patchHashesIn reads scoped and unscoped snapshot keys, and ignores other versions", () => {
const lock = [
" '@scope/pkg@2.0.0(patch_hash=abc123)': {}",
" bare@2.0.0(patch_hash=def456)': {}",
" '@scope/pkg@1.0.0(patch_hash=aaa111)(peer@1)': {}",
" '@scope/pkg@1.0.0(patch_hash=aaa111)(peer@2)': {}",
" '@scope/other@1.0.0(patch_hash=bbb222)': {}",
].join("\n");
assert.deepEqual(patchHashesIn(lock, "@scope/pkg", "1.0.0"), ["aaa111"]);
assert.deepEqual(patchHashesIn(lock, "bare", "2.0.0"), ["def456"]);
assert.deepEqual(patchHashesIn(lock, "@scope/pkg", "9.9.9"), []);
});

test("patchHashesIn treats the version literally", () => {
assert.deepEqual(patchHashesIn(" 'pkg@1x0.0(patch_hash=aaa111)': {}", "pkg", "1.0.0"), []);
});

test("check-pi-dependencies accepts a shortened virtual store entry name", () => {
const dir = workspace({ shorten: true });
try {
const result = run(patchDependencyCheck, dir);
assert.equal(result.code, 0, result.stderr);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

test("check-pi-patches accepts a shortened virtual store entry name", () => {
const dir = workspace({ shorten: true });
try {
const result = run(patchCheck, dir);
assert.equal(result.code, 0, result.stderr);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

test("check-pi-dependencies still rejects an install with no patched snapshot", () => {
const dir = workspace({ storeNames: [] });
try {
const result = run(patchDependencyCheck, dir);
assert.equal(result.code, 1);
assert.match(result.stderr, /does not resolve to pnpm's patched package instance/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

test("check-pi-patches rejects an installed patch hash that pnpm-lock.yaml does not record", () => {
const dir = workspace({ storeHash: HASH, lockHash: "f".repeat(64) });
try {
const result = run(patchCheck, dir);
assert.equal(result.code, 1);
assert.match(result.stderr, /absent from pnpm-lock\.yaml/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

test("check-pi-patches rejects a package that is not installed as a patched instance", () => {
const dir = workspace({ storeNames: [] });
try {
const result = run(patchCheck, dir);
assert.equal(result.code, 1);
assert.match(result.stderr, /is not installed as a patched instance/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
Loading