mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
fix: fail closed on unusable Codex auth during sandbox auth merge
When reusing a Codex sandbox, the workspace-restore step now performs an auth-merge check before adopting the sandbox's auth.json. The merge fails closed when: - host or sandbox Codex auth is unusable (missing token or account) - the subscription payload shapes differ between host and sandbox - subscription account identities do not match Additional hardening: node absence on PATH during the embedded decision script now exits non-zero instead of silently falling through. Co-change comments link parseAuth (the embedded script) to hasUsableAuthPayload (codex-home.ts) so a future auth-format change cannot silently diverge between the two implementations. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
98df214917
3422 files changed
+1614966
No files matched your search
@@ -0,0 +1,193 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildFilePatch,
|
||||
buildFilePatches,
|
||||
diffSummary,
|
||||
initialExpandedFileSet,
|
||||
LONG_DIFF_LINE_THRESHOLD,
|
||||
nextExpandedFileSet,
|
||||
statusLabel,
|
||||
toFileViewModels,
|
||||
} from "../src/diff-model.js";
|
||||
import { changedFile, diffResponse } from "./fixtures.js";
|
||||
|
||||
describe("workspace diff UI model", () => {
|
||||
it("summarizes changed files and line counts", () => {
|
||||
const diff = diffResponse();
|
||||
|
||||
expect(diffSummary(diff)).toMatchObject({
|
||||
changedLabel: "1 file",
|
||||
lineLabel: "+1 / -1",
|
||||
warningCount: 0,
|
||||
truncated: false,
|
||||
});
|
||||
expect(toFileViewModels(diff)[0]).toMatchObject({
|
||||
path: "src/app.ts",
|
||||
status: "modified",
|
||||
patchKinds: ["unstaged"],
|
||||
lineCount: 7,
|
||||
longDiff: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("represents empty workspace diffs", () => {
|
||||
const diff = diffResponse({ files: [] });
|
||||
|
||||
expect(toFileViewModels(diff)).toEqual([]);
|
||||
expect(diffSummary(diff).changedLabel).toBe("0 files");
|
||||
});
|
||||
|
||||
it("surfaces truncation and file warnings", () => {
|
||||
const warning = { code: "patch_truncated" as const, message: "Patch was truncated.", path: "src/app.ts" };
|
||||
const file = changedFile({
|
||||
truncated: true,
|
||||
warnings: [warning],
|
||||
patches: [],
|
||||
});
|
||||
const diff = diffResponse({ files: [file], truncated: true, warnings: [warning] });
|
||||
|
||||
expect(buildFilePatch(file)).toBeNull();
|
||||
expect(toFileViewModels(diff)[0]?.warnings).toEqual([warning]);
|
||||
expect(diffSummary(diff)).toMatchObject({
|
||||
warningCount: 1,
|
||||
truncated: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not duplicate aggregated patch warnings", () => {
|
||||
const warning = { code: "patch_truncated" as const, message: "Patch was truncated.", path: "src/app.ts" };
|
||||
const file = changedFile({
|
||||
warnings: [warning],
|
||||
patches: [
|
||||
{
|
||||
kind: "unstaged",
|
||||
patch: null,
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
binary: false,
|
||||
oversized: false,
|
||||
truncated: true,
|
||||
warnings: [warning],
|
||||
},
|
||||
],
|
||||
});
|
||||
const diff = diffResponse({ files: [file], warnings: [warning] });
|
||||
|
||||
expect(toFileViewModels(diff)[0]?.warnings).toEqual([warning]);
|
||||
expect(diffSummary(diff).warningCount).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps staged and unstaged patches renderable as separate single-file diffs", () => {
|
||||
const stagedPatch = [
|
||||
"diff --git a/src/app.ts b/src/app.ts",
|
||||
"index 1111111..2222222 100644",
|
||||
"--- a/src/app.ts",
|
||||
"+++ b/src/app.ts",
|
||||
"@@ -1 +1 @@",
|
||||
"-export const value = 1;",
|
||||
"+export const value = 2;",
|
||||
"",
|
||||
].join("\n");
|
||||
const unstagedPatch = [
|
||||
"diff --git a/src/app.ts b/src/app.ts",
|
||||
"index 2222222..3333333 100644",
|
||||
"--- a/src/app.ts",
|
||||
"+++ b/src/app.ts",
|
||||
"@@ -3 +3 @@",
|
||||
"-export const label = 'old';",
|
||||
"+export const label = 'new';",
|
||||
"",
|
||||
].join("\n");
|
||||
const file = changedFile({
|
||||
staged: true,
|
||||
unstaged: true,
|
||||
patches: [
|
||||
{
|
||||
kind: "staged",
|
||||
patch: stagedPatch,
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
binary: false,
|
||||
oversized: false,
|
||||
truncated: false,
|
||||
warnings: [],
|
||||
},
|
||||
{
|
||||
kind: "unstaged",
|
||||
patch: unstagedPatch,
|
||||
additions: 1,
|
||||
deletions: 1,
|
||||
binary: false,
|
||||
oversized: false,
|
||||
truncated: false,
|
||||
warnings: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const patches = buildFilePatches(file);
|
||||
const viewModel = toFileViewModels(diffResponse({ files: [file] }))[0];
|
||||
|
||||
expect(buildFilePatch(file)).toBe(stagedPatch.trimEnd());
|
||||
expect(patches.map((patch) => patch.kind)).toEqual(["staged", "unstaged"]);
|
||||
expect(patches.map((patch) => patch.patch?.match(/^diff --git/gm)?.length ?? 0)).toEqual([1, 1]);
|
||||
expect(viewModel?.patches).toHaveLength(2);
|
||||
expect(viewModel?.patchKinds).toEqual(["staged", "unstaged"]);
|
||||
});
|
||||
|
||||
it("marks long text diffs so the UI can fold them by default", () => {
|
||||
const longPatch = [
|
||||
"diff --git a/src/large.ts b/src/large.ts",
|
||||
"index 1111111..2222222 100644",
|
||||
"--- a/src/large.ts",
|
||||
"+++ b/src/large.ts",
|
||||
"@@ -1,1 +1,1 @@",
|
||||
...Array.from({ length: LONG_DIFF_LINE_THRESHOLD }, (_, index) => `+export const value${index} = ${index};`),
|
||||
"",
|
||||
].join("\n");
|
||||
const files = toFileViewModels(diffResponse({
|
||||
files: [
|
||||
changedFile({ path: "src/small.ts" }),
|
||||
changedFile({
|
||||
path: "src/large.ts",
|
||||
additions: LONG_DIFF_LINE_THRESHOLD,
|
||||
deletions: 0,
|
||||
patches: [
|
||||
{
|
||||
kind: "unstaged",
|
||||
patch: longPatch,
|
||||
additions: LONG_DIFF_LINE_THRESHOLD,
|
||||
deletions: 0,
|
||||
binary: false,
|
||||
oversized: false,
|
||||
truncated: false,
|
||||
warnings: [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
}));
|
||||
const longFile = files.find((file) => file.path === "src/large.ts");
|
||||
const defaultExpanded = initialExpandedFileSet(files);
|
||||
|
||||
expect(longFile?.lineCount).toBeGreaterThan(LONG_DIFF_LINE_THRESHOLD);
|
||||
expect(longFile?.longDiff).toBe(true);
|
||||
expect(defaultExpanded.has("src/small.ts")).toBe(true);
|
||||
expect(defaultExpanded.has("src/large.ts")).toBe(false);
|
||||
});
|
||||
|
||||
it("toggles expanded file state without mutating the current set", () => {
|
||||
const current = new Set(["a.ts"]);
|
||||
const collapsed = nextExpandedFileSet(current, "a.ts");
|
||||
const expanded = nextExpandedFileSet(current, "b.ts");
|
||||
|
||||
expect(current.has("a.ts")).toBe(true);
|
||||
expect(collapsed.has("a.ts")).toBe(false);
|
||||
expect(expanded.has("b.ts")).toBe(true);
|
||||
});
|
||||
|
||||
it("labels file statuses for the sidebar", () => {
|
||||
expect(statusLabel("untracked")).toBe("Untracked");
|
||||
expect(statusLabel("type_changed")).toBe("Type changed");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user