mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-19 10:36:26 +02:00
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - The agent Test action builds adapter config from the form.
> - The build-config parser kept plain and secret_ref bindings.
> - It dropped user_secret_ref bindings on the create path.
> - This PR shares one parser that keeps every binding shape.
> - The test path now sends the same env binding set that a real run
sees.
> - The benefit is one fix across every adapter build-config path.
## Linked Issues or Issue Description
**What happened?**
The agent Test action dropped a user-scoped env binding in create mode.
The same agent config worked in a real run. Related public PRs: #10115,
#9321, #9921, #8825.
**Expected behavior**
The Test action should keep user-scoped env bindings and resolve them
like a real run.
**Steps to reproduce**
1. Set a user-scoped env binding on an agent config form.
2. Run Test in create mode.
3. The probe runs without the variable.
**Paperclip version or commit**
c09d2509e3
**Deployment mode**
Local dev (pnpm dev)
**Agent adapter(s) involved**
Not adapter-specific (core bug)
**Database mode**
Embedded PGlite (default — DATABASE_URL unset)
**Additional context**
This change is not Claude-specific.
## What Changed
- Added a shared env binding parser in `@paperclipai/adapter-utils`.
- Replaced the eight adapter build-config copies with the shared helper.
- Kept `plain`, `secret_ref`, and `user_secret_ref` bindings intact in
create mode and edit mode.
- Preserved the runtime merge behavior from the earlier env merge
change.
## Verification
- Author-recorded test run:
`packages/adapter-utils/src/env-bindings.test.ts`
- Author-recorded test run:
`packages/adapters/claude-local/src/ui/build-config.test.ts`
- Author-recorded test run: six adapter build-config test files
- Author-recorded typecheck: `tsc --noEmit` for adapter-utils and the
eight adapter packages
- GitHub checks: all required PR checks pass on PR #10926.
- Greptile review: 5/5 with no open comments.
## Risks
- The change touches adapter config assembly.
- A wrong binding shape would change test-time probe input.
- Tests cover the binding types and the create-mode path.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
OpenAI GPT-5, code execution and repo inspection.
## 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 searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` /
`Closes: #` / `Refs: #` OR (b) described the issue in-PR following the
relevant issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change and contains no internal
Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes or
confirmed no documentation update is needed
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
83 lines
2.9 KiB
TypeScript
83 lines
2.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { buildAdapterEnvConfig, parseEnvBindings, parseEnvVars } from "./env-bindings.js";
|
|
|
|
describe("parseEnvBindings", () => {
|
|
it("keeps plain, company secret, and user-scoped bindings", () => {
|
|
const env = parseEnvBindings({
|
|
PLAIN: { type: "plain", value: "on" },
|
|
LEGACY_STRING: "raw",
|
|
COMPANY: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest" },
|
|
USER: { type: "user_secret_ref", key: "github_token", version: "latest", required: true },
|
|
});
|
|
|
|
expect(env).toEqual({
|
|
PLAIN: { type: "plain", value: "on" },
|
|
LEGACY_STRING: { type: "plain", value: "raw" },
|
|
COMPANY: { type: "secret_ref", secretId: "11111111-1111-1111-1111-111111111111", version: "latest" },
|
|
USER: { type: "user_secret_ref", key: "github_token", version: "latest", required: true },
|
|
});
|
|
});
|
|
|
|
it("keeps a user-scoped binding without an optional version or required flag", () => {
|
|
const env = parseEnvBindings({
|
|
USER: { type: "user_secret_ref", key: "github_token" },
|
|
});
|
|
|
|
expect(env).toEqual({ USER: { type: "user_secret_ref", key: "github_token" } });
|
|
});
|
|
|
|
it("preserves allowMissingOverride when present", () => {
|
|
const env = parseEnvBindings({
|
|
USER: { type: "user_secret_ref", key: "k", allowMissingOverride: true },
|
|
});
|
|
|
|
expect(env.USER).toEqual({ type: "user_secret_ref", key: "k", allowMissingOverride: true });
|
|
});
|
|
|
|
it("drops invalid keys, unknown shapes, and incomplete refs", () => {
|
|
const env = parseEnvBindings({
|
|
"1BAD": { type: "plain", value: "x" },
|
|
MISSING_KEY: { type: "user_secret_ref" },
|
|
MISSING_ID: { type: "secret_ref" },
|
|
UNKNOWN: { type: "mystery", value: "y" },
|
|
});
|
|
|
|
expect(env).toEqual({});
|
|
});
|
|
|
|
it("returns an empty map for non-object input", () => {
|
|
expect(parseEnvBindings(null)).toEqual({});
|
|
expect(parseEnvBindings([])).toEqual({});
|
|
expect(parseEnvBindings("nope")).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("parseEnvVars", () => {
|
|
it("reads KEY=value lines and skips blanks, comments, and invalid keys", () => {
|
|
// The key is trimmed; the value keeps every character after the first "=".
|
|
const env = parseEnvVars("A=1\n# comment\n\nB = two = three\n1BAD=x\nNOEQ");
|
|
|
|
expect(env).toEqual({ A: "1", B: " two = three" });
|
|
});
|
|
});
|
|
|
|
describe("buildAdapterEnvConfig", () => {
|
|
it("lets a structured binding win over a legacy plain-text entry with the same key", () => {
|
|
const env = buildAdapterEnvConfig(
|
|
{ SHARED: { type: "user_secret_ref", key: "token" } },
|
|
"SHARED=legacy\nONLY_LEGACY=x",
|
|
);
|
|
|
|
expect(env).toEqual({
|
|
SHARED: { type: "user_secret_ref", key: "token" },
|
|
ONLY_LEGACY: { type: "plain", value: "x" },
|
|
});
|
|
});
|
|
|
|
it("tolerates missing legacy text", () => {
|
|
expect(buildAdapterEnvConfig({ A: { type: "plain", value: "1" } }, undefined)).toEqual({
|
|
A: { type: "plain", value: "1" },
|
|
});
|
|
});
|
|
});
|