Files
PaperClipAI/packages/adapter-utils/src/env-bindings.test.ts
T
Nicky LeachandPaperclip f5e9ca3e89 fix(adapters): keep user-scoped env bindings on the agent Test action (#10926)
## 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>
2026-08-05 17:18:41 -07:00

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" },
});
});
});