Files
PaperClipAI/ui/src/context/FileViewerContext.test.ts
T
DottaandPaperclip f88ac9d078 [codex] Fix local skill, secrets, and file viewer regressions (#8586)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Agents rely on local skills, provider-backed secrets, and workspace
file previews during normal execution.
> - Local skill imports need bounded reference-file inventory so direct
skill discovery stays accurate without accidentally walking too much of
the filesystem.
> - Secrets provider setup needs actionable AWS discovery errors so
operators can recover from IAM/config problems without losing manual
form input.
> - The issue detail file viewer should reopen cleanly after the first
close so users can keep inspecting files during task review.
> - This pull request collects the small fixes and regression tests for
those related operator workflows.
> - The benefit is more predictable local skill imports, clearer secrets
setup failure states, and a less brittle file preview interaction.

## Linked Issues or Issue Description

- No public GitHub issue was found for this extracted local work.
- Related prior sync context: #8536.
- Problem: local skill reference discovery, AWS provider-vault discovery
errors, and issue file preview reopening each had narrow workflow
regressions that made operator recovery harder.
- Expected behavior: skill imports inventory reference files within
bounded local skill directories, AWS discovery failures present safe
actionable guidance while preserving manual values, and closing the
first file preview does not prevent opening another preview.
- Reproduction scope: import a local skill with referenced files,
attempt AWS Secrets Manager discovery with insufficient IAM/list
permissions, and open/close/reopen file previews from an issue detail
page.
- Duplicate search: searched GitHub PRs/issues for `skill inventory
secrets file viewer` and `skill inventory secrets AWS file viewer`; no
matching public duplicate was found.

## What Changed

- Bounded direct local skill file inventory discovery and added
regression coverage for reference file imports.
- Preserved and surfaced safe, actionable AWS Secrets Manager
discovery/import errors in server responses and the secrets UI.
- Kept AWS provider-vault manual form values intact when discovery fails
or returns no candidates.
- Fixed issue file viewer state so closing the first preview still
allows later file previews to open.
- Updated the secrets render test harness to avoid the missing
`React.act` export in the current React package set.

## Verification

- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/company-skills-service.test.ts
server/src/__tests__/secrets-routes.test.ts
server/src/__tests__/secrets-service.test.ts
ui/src/context/FileViewerContext.test.ts
ui/src/pages/Secrets.render.test.tsx`
- Result: 5 test files passed, 116 tests passed.
- Install note: the isolated worktree needed `NODE_ENV=development pnpm
install --frozen-lockfile --prod=false --force` before local
verification because it initially had no dev dependencies installed.

## Risks

- Low-to-medium risk: this touches skill import inventory,
secrets-provider error handling, and file-viewer UI state, but each
change is covered by focused regression tests.
- No migrations.
- No dependency or lockfile changes.
- CI is rerunning on the latest head after review fixes; Greptile is 5/5
with no unresolved Greptile threads.

> 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 Codex coding agent, GPT-5-family model as provided in the
Paperclip run environment, with repository tool use and local command
execution.

## 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 (e.g. `docs/...`, `fix/...`)
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
- [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-06-24 09:17:51 -05:00

194 lines
6.5 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from "vitest";
import {
FILE_VIEWER_NAVIGATE_OPTIONS,
readBrowseStateFromSearch,
readFileViewerStateFromSearch,
writeBrowseStateToSearch,
writeFolderViewerStateToSearch,
writeFileViewerStateToSearch,
} from "./FileViewerContext";
describe("FILE_VIEWER_NAVIGATE_OPTIONS", () => {
it("preserves page scroll when the viewer updates URL search params", () => {
expect(FILE_VIEWER_NAVIGATE_OPTIONS.preventScrollReset).toBe(true);
expect(FILE_VIEWER_NAVIGATE_OPTIONS.replace).toBe(false);
});
});
describe("readFileViewerStateFromSearch", () => {
it("returns null when no file param is present", () => {
expect(readFileViewerStateFromSearch("")).toBeNull();
expect(readFileViewerStateFromSearch("?other=1")).toBeNull();
});
it("reads file, line, column, workspace from the search", () => {
const state = readFileViewerStateFromSearch("?file=ui/src/a.ts&line=42&column=3&workspace=project");
expect(state).toEqual({
path: "ui/src/a.ts",
line: 42,
column: 3,
workspace: "project",
projectId: null,
workspaceId: null,
});
});
it("defaults to auto workspace when param missing", () => {
const state = readFileViewerStateFromSearch("?file=ui/src/a.ts");
expect(state?.workspace).toBe("auto");
});
it("clamps invalid workspace to auto", () => {
const state = readFileViewerStateFromSearch("?file=ui/src/a.ts&workspace=bogus");
expect(state?.workspace).toBe("auto");
});
it("treats invalid line/column as null", () => {
const state = readFileViewerStateFromSearch("?file=x.ts&line=abc&column=-1");
expect(state?.line).toBeNull();
expect(state?.column).toBeNull();
});
});
describe("writeFileViewerStateToSearch", () => {
it("sets all params when opening", () => {
const next = writeFileViewerStateToSearch(
"?existing=1",
{
path: "ui/src/a.ts",
line: 42,
column: 3,
workspace: "project",
projectId: null,
workspaceId: null,
},
);
const params = new URLSearchParams(next);
expect(params.get("file")).toBe("ui/src/a.ts");
expect(params.get("line")).toBe("42");
expect(params.get("column")).toBe("3");
expect(params.get("workspace")).toBe("project");
expect(params.get("existing")).toBe("1");
});
it("omits workspace when auto", () => {
const next = writeFileViewerStateToSearch(
"",
{ path: "a.ts", line: null, column: null, workspace: "auto", projectId: null, workspaceId: null },
);
expect(next.includes("workspace")).toBe(false);
});
it("round-trips explicit target project workspace params", () => {
const targetPath = "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/README.md";
const next = writeFileViewerStateToSearch(
"?existing=1",
{
path: targetPath,
line: 7,
column: null,
workspace: "auto",
projectId: "17acae7d-9d0c-46bf-9c82-be9694ac3461",
workspaceId: "0de5f74f-a7d4-4f73-a9a0-455a2b968cf2",
},
);
const state = readFileViewerStateFromSearch(next);
expect(state).toEqual({
path: targetPath,
line: 7,
column: null,
workspace: "auto",
projectId: "17acae7d-9d0c-46bf-9c82-be9694ac3461",
workspaceId: "0de5f74f-a7d4-4f73-a9a0-455a2b968cf2",
});
});
it("clears viewer params when closing", () => {
const next = writeFileViewerStateToSearch(
"?file=a.ts&line=1&column=2&workspace=project&projectId=project-1&workspaceId=workspace-1&keep=yes",
null,
);
const params = new URLSearchParams(next);
expect(params.get("file")).toBeNull();
expect(params.get("line")).toBeNull();
expect(params.get("column")).toBeNull();
expect(params.get("workspace")).toBeNull();
expect(params.get("projectId")).toBeNull();
expect(params.get("workspaceId")).toBeNull();
expect(params.get("keep")).toBe("yes");
});
it("clears browse-origin viewer params when closing a selected file", () => {
const next = writeFileViewerStateToSearch(
"?tab=thread&browse=1&q=FileViewer&folder=ui/src&file=ui/src/FileViewer.tsx&line=4",
null,
);
const params = new URLSearchParams(next);
expect(params.get("file")).toBeNull();
expect(params.get("line")).toBeNull();
expect(params.get("browse")).toBeNull();
expect(params.get("q")).toBeNull();
expect(params.get("folder")).toBeNull();
expect(params.get("tab")).toBe("thread");
});
it("returns empty string when no params remain", () => {
const next = writeFileViewerStateToSearch("?file=a.ts", null);
expect(next).toBe("");
});
});
describe("folder browse search state", () => {
it("round-trips explicit target folder browse params", () => {
const targetPath = "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/";
const next = writeFolderViewerStateToSearch("?tab=thread", {
path: targetPath,
projectId: "17acae7d-9d0c-46bf-9c82-be9694ac3461",
workspaceId: "0de5f74f-a7d4-4f73-a9a0-455a2b968cf2",
});
expect(readFileViewerStateFromSearch(next)).toBeNull();
expect(readBrowseStateFromSearch(next)).toEqual({
q: null,
folderPath: targetPath,
projectId: "17acae7d-9d0c-46bf-9c82-be9694ac3461",
workspaceId: "0de5f74f-a7d4-4f73-a9a0-455a2b968cf2",
});
});
it("updates browse params without closing the active file preview", () => {
const next = writeBrowseStateToSearch(
"?tab=thread&file=ui/src/components/FileViewerSheet.tsx&line=5&browse=1",
{
q: " FileViewerSheet ",
folderPath: "ui/src/components",
projectId: null,
workspaceId: null,
},
);
const params = new URLSearchParams(next);
expect(params.get("file")).toBe("ui/src/components/FileViewerSheet.tsx");
expect(params.get("line")).toBe("5");
expect(params.get("browse")).toBe("1");
expect(params.get("q")).toBe("FileViewerSheet");
expect(params.get("folder")).toBe("ui/src/components");
expect(params.get("tab")).toBe("thread");
});
it("clears browse params while preserving the file preview", () => {
const next = writeBrowseStateToSearch(
"?file=ui/src/components/FileViewerSheet.tsx&browse=1&q=FileViewerSheet&folder=ui/src/components",
{ q: null, folderPath: null },
);
const params = new URLSearchParams(next);
expect(params.get("file")).toBe("ui/src/components/FileViewerSheet.tsx");
expect(params.get("browse")).toBe("1");
expect(params.get("q")).toBeNull();
expect(params.get("folder")).toBeNull();
});
});