mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Adapters expose a Test action that probes the configured runtime —
install, resolvability, hello — to give operators a fast yes/no on
whether an environment is healthy
> - The Codex test path was running its hello probe directly without
going through the managed-runtime preparation that production runs use,
so a healthy production setup could still report a probe failure
> - The plugin worker manager wasn't surfacing terminated workers
cleanly, leaving the runtime probe waiting on a dead worker until the
request timed out
> - This pull request routes the Codex test probe through
`prepareAdapterExecutionTargetRuntime` (so it sees the same managed
Codex home production sees), exposes `commandCwd` on
`createCommandManagedRuntimeClient` so callers can target a per-probe
directory without leaking the workspace `remoteCwd`, and propagates
plugin-worker termination as a usable error instead of a hang
> - The benefit is the Codex Test action mirrors production behavior
end-to-end, and probes against a terminated plugin worker fail fast
instead of timing out
## What Changed
- `packages/adapter-utils/src/command-managed-runtime.ts`: rename the
`remoteCwd` knob to `commandCwd` so callers can target a per-probe
directory without inheriting the workspace cwd; matching test coverage
in `command-managed-runtime.test.ts`
- `packages/adapter-utils/src/sandbox-callback-bridge.{ts,test.ts}`:
small fixes to keep callback bridge stop semantics deterministic
- `packages/adapters/codex-local/src/server/test.ts`: thread the Codex
hello probe through `prepareAdapterExecutionTargetRuntime` +
`prepareManagedCodexHome` so the probe sees the same managed home
production sees; new `test.remote.test.ts` covers the remote probe path
- `packages/adapters/cursor-local/src/server/execute.ts`: small
probe-side cleanup that aligns with the new commandCwd contract
- `server/src/services/plugin-worker-manager.ts`: surface plugin-worker
termination as a structured error so callers fail fast; new
`plugin-worker-terminated.cjs` fixture and
`plugin-worker-manager.test.ts` cases pin the behavior
## Verification
- `pnpm vitest run --no-coverage --project @paperclipai/adapter-utils
--project @paperclipai/adapter-codex-local --project
@paperclipai/adapter-cursor-local --project @paperclipai/server` —
1749/1750 passing (1 unrelated skip)
- `pnpm typecheck` clean
## Risks
Low–medium. The `remoteCwd → commandCwd` rename is a parameter renaming
on an internal helper used only by adapter test/execute paths in this
repo. The plugin-worker-terminated path was previously a hang; failing
fast may surface latent timeouts as explicit termination errors in
callers that already expected them.
## Model Used
Claude Opus 4.7 (1M context)
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable — new tests cover
commandCwd, plugin-worker termination, and Codex remote test path
- [x] If this change affects the UI, I have included before/after
screenshots — N/A (no UI)
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
---
> **Stacked PR.** Sits on top of #5444 which adds the per-run runtime
API surface this PR builds on. Cumulative diff against `master` includes
that PR's content; the files touched by *this* PR's commit are listed
under "What Changed" above. Will rebase onto `master` and force-push
once #5444 merges.
114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import type { PaperclipPluginManifestV1 } from "@paperclipai/shared";
|
|
import {
|
|
JsonRpcCallError,
|
|
type HostToWorkerMethods,
|
|
} from "@paperclipai/plugin-sdk";
|
|
import {
|
|
appendStderrExcerpt,
|
|
createPluginWorkerHandle,
|
|
formatWorkerFailureMessage,
|
|
} from "../services/plugin-worker-manager.js";
|
|
|
|
const FIXTURES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "fixtures");
|
|
const TERMINATED_WORKER_ENTRYPOINT = path.join(FIXTURES_DIR, "plugin-worker-terminated.cjs");
|
|
|
|
const TEST_MANIFEST: PaperclipPluginManifestV1 = {
|
|
id: "test.plugin",
|
|
apiVersion: 1,
|
|
version: "1.0.0",
|
|
displayName: "Test plugin",
|
|
description: "Test plugin",
|
|
author: "Paperclip",
|
|
categories: ["automation"],
|
|
capabilities: [],
|
|
entrypoints: { worker: "dist/worker.js" },
|
|
};
|
|
|
|
describe("plugin-worker-manager stderr failure context", () => {
|
|
it("appends worker stderr context to failure messages", () => {
|
|
expect(
|
|
formatWorkerFailureMessage(
|
|
"Worker process exited (code=1, signal=null)",
|
|
"TypeError: Unknown file extension \".ts\"",
|
|
),
|
|
).toBe(
|
|
"Worker process exited (code=1, signal=null)\n\nWorker stderr:\nTypeError: Unknown file extension \".ts\"",
|
|
);
|
|
});
|
|
|
|
it("does not duplicate stderr that is already present", () => {
|
|
const message = [
|
|
"Worker process exited (code=1, signal=null)",
|
|
"",
|
|
"Worker stderr:",
|
|
"TypeError: Unknown file extension \".ts\"",
|
|
].join("\n");
|
|
|
|
expect(
|
|
formatWorkerFailureMessage(message, "TypeError: Unknown file extension \".ts\""),
|
|
).toBe(message);
|
|
});
|
|
|
|
it("keeps only the latest stderr excerpt", () => {
|
|
let excerpt = "";
|
|
excerpt = appendStderrExcerpt(excerpt, "first line");
|
|
excerpt = appendStderrExcerpt(excerpt, "second line");
|
|
|
|
expect(excerpt).toContain("first line");
|
|
expect(excerpt).toContain("second line");
|
|
|
|
excerpt = appendStderrExcerpt(excerpt, "x".repeat(9_000));
|
|
|
|
expect(excerpt).not.toContain("first line");
|
|
expect(excerpt).not.toContain("second line");
|
|
expect(excerpt.length).toBeLessThanOrEqual(8_000);
|
|
});
|
|
|
|
it("does not emit an unhandled rejection when a plugin responds with terminated before callers attach handlers", async () => {
|
|
const unhandledRejection = vi.fn();
|
|
process.on("unhandledRejection", unhandledRejection);
|
|
|
|
const handle = createPluginWorkerHandle("test.plugin", {
|
|
entrypointPath: TERMINATED_WORKER_ENTRYPOINT,
|
|
manifest: TEST_MANIFEST,
|
|
config: {},
|
|
instanceInfo: {
|
|
instanceId: "instance-1",
|
|
hostVersion: "1.0.0",
|
|
},
|
|
apiVersion: 1,
|
|
hostHandlers: {},
|
|
});
|
|
|
|
try {
|
|
await handle.start();
|
|
|
|
const pendingCall = handle.call(
|
|
"environmentExecute" as keyof HostToWorkerMethods,
|
|
{
|
|
driverKey: "e2b",
|
|
companyId: "company-1",
|
|
environmentId: "environment-1",
|
|
config: {},
|
|
lease: { providerLeaseId: "lease-1" },
|
|
command: "echo",
|
|
} as HostToWorkerMethods[keyof HostToWorkerMethods][0],
|
|
);
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
await expect(pendingCall).rejects.toBeInstanceOf(JsonRpcCallError);
|
|
await expect(pendingCall).rejects.toMatchObject({
|
|
message: expect.stringContaining("terminated"),
|
|
});
|
|
expect(unhandledRejection).not.toHaveBeenCalled();
|
|
} finally {
|
|
process.off("unhandledRejection", unhandledRejection);
|
|
await handle.stop().catch(() => undefined);
|
|
}
|
|
});
|
|
});
|