mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 04:25:52 +01:00
Give tasks durable company-scoped workspace bindings independent of project assignment, channel defaults and authorized repository preparation. Reuse verified input seeds and sparse output recovery while preserving trust, privacy and controller provisioning boundaries.
Full CI and E2E, Superagent, legacy checks and Greptile Apex 5/5 passed on reviewed head 753680b61c. All review findings are resolved; the PR records focused verification and historical acceptance limits.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
445 lines
22 KiB
TypeScript
445 lines
22 KiB
TypeScript
import { execFile as execFileCallback } from "node:child_process";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { pathToFileURL } from "node:url";
|
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
import { ensureManagedProjectWorkspace, prepareProjectRepositoryWorkspaces } from "../services/heartbeat.ts";
|
|
import { buildGitAuthInvocation, GIT_CREDENTIAL_TOKEN_ENV_KEY } from "../services/git-credentials.ts";
|
|
import { sanitizeRuntimeServiceBaseEnv } from "../services/workspace-runtime.ts";
|
|
import { resolveManagedProjectWorkspaceDir } from "../home-paths.ts";
|
|
import { readGitConnectionFailure } from "../services/git-connection-failure.js";
|
|
|
|
const execFile = promisify(execFileCallback);
|
|
|
|
let tempHome: string;
|
|
let originalHome: string | undefined;
|
|
|
|
beforeAll(async () => {
|
|
originalHome = process.env.PAPERCLIP_HOME;
|
|
tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-managed-clone-"));
|
|
process.env.PAPERCLIP_HOME = tempHome;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (originalHome === undefined) delete process.env.PAPERCLIP_HOME;
|
|
else process.env.PAPERCLIP_HOME = originalHome;
|
|
await fs.rm(tempHome, { recursive: true, force: true });
|
|
});
|
|
|
|
async function createLocalSourceRepo() {
|
|
const sourceRepo = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-clone-source-"));
|
|
await execFile("git", ["init"], { cwd: sourceRepo });
|
|
await execFile("git", ["config", "user.email", "paperclip@example.com"], { cwd: sourceRepo });
|
|
await execFile("git", ["config", "user.name", "Paperclip Test"], { cwd: sourceRepo });
|
|
await fs.writeFile(path.join(sourceRepo, "README.md"), "hello\n", "utf8");
|
|
await execFile("git", ["add", "README.md"], { cwd: sourceRepo });
|
|
await execFile("git", ["commit", "-m", "init"], { cwd: sourceRepo });
|
|
return sourceRepo;
|
|
}
|
|
|
|
describe("ensureManagedProjectWorkspace clone credentials", () => {
|
|
it.each([
|
|
["authentication_failed", "fatal: Authentication failed for 'https://example.test/team/repo.git/'"],
|
|
["dns_failure", "ssh: Could not resolve hostname repository-alias.example.test: Name or service not known\nfatal: Could not read from remote repository."],
|
|
[null, "fatal: Authentication failed for 'https://example.test/team/repo.git/'\nfatal: bad config line 1 in file /tmp/config"],
|
|
[null, "fatal: corrupt loose object"],
|
|
] as const)("preserves only proven outbound clone classification through the actionable wrapper: %s", async (reason, stderr) => {
|
|
const bin = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-fake-clone-"));
|
|
await fs.writeFile(path.join(bin, "git"), `#!/bin/sh
|
|
printf '%s\\n' "$FIXTURE_CLONE_STDERR" >&2
|
|
exit 128
|
|
`, { mode: 0o755 });
|
|
try {
|
|
const failure = await ensureManagedProjectWorkspace({
|
|
companyId: "remote-clone-fixture", projectId: path.basename(bin),
|
|
repoUrl: "https://fixture-user:fixture-password@example.test/team/repo.git",
|
|
resolveGitAuth: async () => ({
|
|
configArgs: [], env: { PATH: bin, FIXTURE_CLONE_STDERR: stderr },
|
|
source: "company_secret", secretName: "GITHUB_TOKEN",
|
|
}),
|
|
}).catch((error: unknown) => error);
|
|
expect(failure).toBeInstanceOf(Error);
|
|
expect((failure as Error).message).toContain("Failed to prepare managed checkout");
|
|
expect((failure as Error).message).not.toContain("fixture-password");
|
|
expect(readGitConnectionFailure(failure)).toEqual(reason ? {
|
|
schemaVersion: 1, provider: "git", operation: "clone", reason,
|
|
} : null);
|
|
const homeEntries = await fs.readdir(path.dirname(resolveManagedProjectWorkspaceDir({
|
|
companyId: "remote-clone-fixture", projectId: path.basename(bin), repoName: "repo",
|
|
})));
|
|
expect(homeEntries.some((name) => name.includes(".clone-"))).toBe(false);
|
|
} finally {
|
|
await fs.rm(bin, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("does not classify a credential-provider exception that resembles a Git failure", async () => {
|
|
const failure = Object.assign(new Error("fatal: Authentication failed for 'https://example.test/repo.git'"), {
|
|
code: 128, stderr: "fatal: Authentication failed for 'https://example.test/repo.git'",
|
|
});
|
|
const caught = await ensureManagedProjectWorkspace({
|
|
companyId: "credential-provider-fixture", projectId: "project", repoUrl: "https://example.test/repo.git",
|
|
resolveGitAuth: async () => { throw failure; },
|
|
}).catch((error: unknown) => error);
|
|
expect(caught).toBe(failure);
|
|
expect(readGitConnectionFailure(caught)).toBeNull();
|
|
});
|
|
|
|
it("does not transfer clone classification to a later local checkout failure", async () => {
|
|
const bin = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-fake-checkout-"));
|
|
const previousPath = process.env.PATH;
|
|
await fs.writeFile(path.join(bin, "git"), `#!/bin/sh
|
|
if [ "$1" = clone ]; then
|
|
for argument in "$@"; do destination="$argument"; done
|
|
/bin/mkdir -p "$destination/.git"
|
|
exit 0
|
|
fi
|
|
printf '%s\\n' "fatal: Authentication failed for 'https://example.test/repo.git'" >&2
|
|
exit 128
|
|
`, { mode: 0o755 });
|
|
try {
|
|
process.env.PATH = bin;
|
|
const failure = await ensureManagedProjectWorkspace({
|
|
companyId: "checkout-failure-fixture", projectId: "project", repoUrl: "https://example.test/repo.git", repoRef: "missing-branch",
|
|
}).catch((error: unknown) => error);
|
|
expect(failure).toBeInstanceOf(Error);
|
|
expect((failure as Error).message).toContain("Authentication failed");
|
|
expect(readGitConnectionFailure(failure)).toBeNull();
|
|
} finally {
|
|
if (previousPath === undefined) delete process.env.PATH; else process.env.PATH = previousPath;
|
|
await fs.rm(bin, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("materializes every repository-only project row inside the task workspace and reuses local edits", async () => {
|
|
const first = await createLocalSourceRepo();
|
|
const second = await createLocalSourceRepo();
|
|
try {
|
|
const anchor = await ensureManagedProjectWorkspace({ companyId: "repo-only", projectId: "two", repoUrl: first });
|
|
await execFile("git", ["checkout", "-b", "project-branch"], { cwd: second });
|
|
const resolveGitAuth = vi.fn(async () => null);
|
|
const input = {
|
|
cwd: anchor.cwd, anchorRepoUrl: first, resolveGitAuth,
|
|
workspaces: [
|
|
{ id: "first", repoUrl: first, repoRef: null },
|
|
{ id: "duplicate-first", repoUrl: first, repoRef: null },
|
|
{ id: "second", repoUrl: second, repoRef: "project-branch" },
|
|
],
|
|
};
|
|
const additional = await prepareProjectRepositoryWorkspaces(input);
|
|
expect(additional).toHaveLength(1);
|
|
expect(additional[0]!.workspaceId).toBe("second");
|
|
expect(path.relative(anchor.cwd, additional[0]!.cwd)).toMatch(/^\.paperclip-repositories\//);
|
|
expect((await execFile("git", ["branch", "--show-current"], { cwd: additional[0]!.cwd })).stdout.trim()).toBe("project-branch");
|
|
expect(resolveGitAuth).toHaveBeenCalledWith(second);
|
|
await fs.writeFile(path.join(additional[0]!.cwd, "README.md"), "work in progress");
|
|
expect(await prepareProjectRepositoryWorkspaces(input)).toEqual(additional);
|
|
expect(await fs.readFile(path.join(additional[0]!.cwd, "README.md"), "utf8")).toBe("work in progress");
|
|
expect((await execFile("git", ["status", "--porcelain"], { cwd: anchor.cwd })).stdout).toBe("");
|
|
} finally {
|
|
await Promise.all([first, second].map((cwd) => fs.rm(cwd, { recursive: true, force: true })));
|
|
}
|
|
});
|
|
|
|
it("clones a remote sibling with a missing configured cwd and rechecks destination access on reuse", async () => {
|
|
const source = await createLocalSourceRepo();
|
|
const task = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-remote-sibling-"));
|
|
try {
|
|
const remote = "https://example.test/remote.git";
|
|
const resolveGitAuth = vi.fn(async () => ({
|
|
configArgs: ["-c", `url.${pathToFileURL(source).href}.insteadOf=${remote}`],
|
|
env: {}, source: "company_secret" as const, secretName: "fixture",
|
|
}));
|
|
const prepareLocalSource = vi.fn(async (cwd: string) => {
|
|
await fs.realpath(cwd);
|
|
return async () => {};
|
|
});
|
|
let destinationReadable = true;
|
|
const beforePublish = vi.fn(async () => {
|
|
if (!destinationReadable) throw new Error("Retained private destination is not authorized");
|
|
});
|
|
const input = {
|
|
cwd: task, anchorRepoUrl: null, resolveGitAuth, prepareLocalSource, beforePublish,
|
|
workspaces: [{ id: "remote", cwd: path.join(task, "missing"), repoUrl: remote, repoRef: null }],
|
|
};
|
|
const [repo] = await prepareProjectRepositoryWorkspaces(input);
|
|
expect(await fs.readFile(path.join(repo!.cwd, "README.md"), "utf8")).toBe("hello\n");
|
|
expect(prepareLocalSource).not.toHaveBeenCalled();
|
|
expect(resolveGitAuth).toHaveBeenCalledTimes(1);
|
|
await fs.writeFile(path.join(repo!.cwd, "README.md"), "retained task edits");
|
|
beforePublish.mockClear();
|
|
expect(await prepareProjectRepositoryWorkspaces(input)).toEqual([repo]);
|
|
expect(beforePublish).toHaveBeenCalledTimes(1);
|
|
expect(resolveGitAuth).toHaveBeenCalledTimes(1);
|
|
expect(await fs.readFile(path.join(repo!.cwd, "README.md"), "utf8")).toBe("retained task edits");
|
|
destinationReadable = false;
|
|
await expect(prepareProjectRepositoryWorkspaces(input)).rejects.toThrow("Retained private destination is not authorized");
|
|
expect(prepareLocalSource).not.toHaveBeenCalled();
|
|
expect(await fs.readFile(path.join(repo!.cwd, "README.md"), "utf8")).toBe("retained task edits");
|
|
} finally {
|
|
await Promise.all([source, task].map((cwd) => fs.rm(cwd, { recursive: true, force: true })));
|
|
}
|
|
});
|
|
|
|
it.each(["file_url", "absolute_path"] as const)("authorizes the actual %s fallback when the configured cwd is missing", async (transport) => {
|
|
const source = await createLocalSourceRepo();
|
|
const task = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-local-sibling-"));
|
|
try {
|
|
let sourceReadable = false;
|
|
const recheck = vi.fn(async () => {});
|
|
const prepareLocalSource = vi.fn(async (cwd: string) => {
|
|
expect(cwd).toBe(source);
|
|
if (!sourceReadable) throw new Error("Private source is not authorized");
|
|
return recheck;
|
|
});
|
|
const input = {
|
|
cwd: task, anchorRepoUrl: null, prepareLocalSource,
|
|
workspaces: [{ id: "local", cwd: path.join(task, "missing"),
|
|
repoUrl: transport === "file_url" ? pathToFileURL(source).href : source, repoRef: null }],
|
|
};
|
|
await expect(prepareProjectRepositoryWorkspaces(input)).rejects.toThrow("Private source is not authorized");
|
|
expect(await fs.readdir(path.join(task, ".paperclip-repositories"))).toEqual([]);
|
|
sourceReadable = true;
|
|
await fs.writeFile(path.join(source, "README.md"), "uncommitted source edits");
|
|
const [repo] = await prepareProjectRepositoryWorkspaces(input);
|
|
expect(await fs.readFile(path.join(repo!.cwd, "README.md"), "utf8")).toBe("hello\n");
|
|
expect(recheck).toHaveBeenCalled();
|
|
sourceReadable = false;
|
|
await expect(prepareProjectRepositoryWorkspaces(input)).rejects.toThrow("Private source is not authorized");
|
|
} finally {
|
|
await Promise.all([source, task].map((cwd) => fs.rm(cwd, { recursive: true, force: true })));
|
|
}
|
|
});
|
|
|
|
it("fails task preparation when any attached repository cannot be cloned", async () => {
|
|
const first = await createLocalSourceRepo();
|
|
try {
|
|
const anchor = await ensureManagedProjectWorkspace({ companyId: "repo-failure", projectId: "two", repoUrl: first });
|
|
await expect(prepareProjectRepositoryWorkspaces({
|
|
cwd: anchor.cwd, anchorRepoUrl: first,
|
|
workspaces: [{ id: "missing", repoUrl: path.join(first, "missing.git"), repoRef: null }],
|
|
})).rejects.toThrow("Failed to prepare managed checkout");
|
|
} finally {
|
|
await fs.rm(first, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("seeds a configured second local checkout with its uncommitted work and ignores", async () => {
|
|
const first = await createLocalSourceRepo();
|
|
const second = await createLocalSourceRepo();
|
|
try {
|
|
const anchor = await ensureManagedProjectWorkspace({ companyId: "local-project", projectId: "two", repoUrl: first });
|
|
await fs.writeFile(path.join(second, "README.md"), "local changes");
|
|
await fs.writeFile(path.join(second, ".gitignore"), "secret.txt\n");
|
|
await fs.writeFile(path.join(second, "secret.txt"), "private");
|
|
const [repo] = await prepareProjectRepositoryWorkspaces({
|
|
cwd: anchor.cwd, anchorRepoUrl: first,
|
|
workspaces: [{ id: "second", cwd: second, repoUrl: "https://github.com/example/backend.git", repoRef: null }],
|
|
});
|
|
expect(await fs.readFile(path.join(repo!.cwd, "README.md"), "utf8")).toBe("local changes");
|
|
await expect(fs.stat(path.join(repo!.cwd, "secret.txt"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
expect((await execFile("git", ["remote", "get-url", "origin"], { cwd: repo!.cwd })).stdout.trim()).toBe("https://github.com/example/backend.git");
|
|
} finally {
|
|
await Promise.all([first, second].map((cwd) => fs.rm(cwd, { recursive: true, force: true })));
|
|
}
|
|
});
|
|
it("keeps different repositories with the same name separate within one project", async () => {
|
|
const first = await createLocalSourceRepo();
|
|
const second = await createLocalSourceRepo();
|
|
try {
|
|
const results = await Promise.all([first, second].map((repoUrl) => ensureManagedProjectWorkspace({
|
|
companyId: "company-multiple", projectId: "project-multiple", repoUrl,
|
|
})));
|
|
expect(results[0]!.cwd).not.toBe(results[1]!.cwd);
|
|
for (let i = 0; i < results.length; i++) {
|
|
const origin = await execFile("git", ["remote", "get-url", "origin"], { cwd: results[i]!.cwd });
|
|
expect(origin.stdout.trim()).toBe([first, second][i]);
|
|
}
|
|
} finally {
|
|
await Promise.all([first, second].map((cwd) => fs.rm(cwd, { recursive: true, force: true })));
|
|
}
|
|
});
|
|
it("rechecks the repository when another process wins the checkout rename", async () => {
|
|
const first = await createLocalSourceRepo();
|
|
const second = await createLocalSourceRepo();
|
|
const companyId = "cross-process-race";
|
|
const projectId = "same-name";
|
|
const sharedCwd = resolveManagedProjectWorkspaceDir({ companyId, projectId });
|
|
try {
|
|
// A different process does not share managedCheckoutMaterializations. Publish
|
|
// its completed checkout after this caller chose its destination, before rename.
|
|
const resolveGitAuth = vi.fn(async () => {
|
|
if (!(await fs.stat(sharedCwd).catch(() => null))) {
|
|
await execFile("git", ["clone", second, sharedCwd]);
|
|
}
|
|
return null;
|
|
});
|
|
const result = await ensureManagedProjectWorkspace({ companyId, projectId, repoUrl: first, resolveGitAuth });
|
|
expect((await execFile("git", ["remote", "get-url", "origin"], { cwd: result.cwd })).stdout.trim()).toBe(first);
|
|
expect((await execFile("git", ["remote", "get-url", "origin"], { cwd: sharedCwd })).stdout.trim()).toBe(second);
|
|
expect(result.cwd).not.toBe(sharedCwd);
|
|
} finally {
|
|
await Promise.all([first, second].map((cwd) => fs.rm(cwd, { recursive: true, force: true })));
|
|
}
|
|
});
|
|
|
|
it("clones exactly as before when no auth provider is configured", async () => {
|
|
const sourceRepo = await createLocalSourceRepo();
|
|
try {
|
|
const result = await ensureManagedProjectWorkspace({
|
|
companyId: "company-noauth",
|
|
projectId: "project-1",
|
|
repoUrl: sourceRepo,
|
|
});
|
|
expect(result.warning).toBeNull();
|
|
const gitDir = await fs.stat(path.join(result.cwd, ".git"));
|
|
expect(gitDir.isDirectory()).toBe(true);
|
|
} finally {
|
|
await fs.rm(sourceRepo, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("consults the provider with the repo URL and clones normally when it returns null", async () => {
|
|
const sourceRepo = await createLocalSourceRepo();
|
|
const resolveGitAuth = vi.fn(async () => null);
|
|
try {
|
|
const result = await ensureManagedProjectWorkspace({
|
|
companyId: "company-nullauth",
|
|
projectId: "project-1",
|
|
repoUrl: sourceRepo,
|
|
resolveGitAuth,
|
|
});
|
|
expect(resolveGitAuth).toHaveBeenCalledWith(sourceRepo);
|
|
const gitDir = await fs.stat(path.join(result.cwd, ".git"));
|
|
expect(gitDir.isDirectory()).toBe(true);
|
|
} finally {
|
|
await fs.rm(sourceRepo, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("does not blame the credential when an authenticated clone fails for non-auth reasons", async () => {
|
|
// The failure here is a missing local path, not an auth rejection — the error must not
|
|
// claim the credential "was rejected". Attribution for genuinely auth-shaped failures is
|
|
// covered by the describeGitAuthFailure unit tests in git-credentials.test.ts.
|
|
const missingRepo = path.join(os.tmpdir(), "paperclip-definitely-missing", "repo.git");
|
|
const resolveGitAuth = vi.fn(async () => ({
|
|
configArgs: [],
|
|
env: { [GIT_CREDENTIAL_TOKEN_ENV_KEY]: "token", GIT_TERMINAL_PROMPT: "0" },
|
|
source: "company_secret" as const,
|
|
secretName: "GH_TOKEN",
|
|
}));
|
|
const error = await ensureManagedProjectWorkspace({
|
|
companyId: "company-authfail",
|
|
projectId: "project-1",
|
|
repoUrl: missingRepo,
|
|
resolveGitAuth,
|
|
}).then(
|
|
() => { throw new Error("expected the clone to fail"); },
|
|
(err: unknown) => err as Error,
|
|
);
|
|
expect(error.message).toContain("Failed to prepare managed checkout");
|
|
expect(error.message).not.toContain("GH_TOKEN company-secret GitHub credential");
|
|
});
|
|
|
|
it("serializes concurrent materializations of the same managed checkout", async () => {
|
|
const sourceRepo = await createLocalSourceRepo();
|
|
try {
|
|
const [first, second] = await Promise.all([
|
|
ensureManagedProjectWorkspace({
|
|
companyId: "company-concurrent",
|
|
projectId: "project-1",
|
|
repoUrl: sourceRepo,
|
|
}),
|
|
ensureManagedProjectWorkspace({
|
|
companyId: "company-concurrent",
|
|
projectId: "project-1",
|
|
repoUrl: sourceRepo,
|
|
}),
|
|
]);
|
|
expect(first.cwd).toBe(second.cwd);
|
|
expect(first.warning).toBeNull();
|
|
expect(second.warning).toBeNull();
|
|
const gitDir = await fs.stat(path.join(first.cwd, ".git"));
|
|
expect(gitDir.isDirectory()).toBe(true);
|
|
// No temp clone directories left behind next to the target.
|
|
const siblings = await fs.readdir(path.dirname(first.cwd));
|
|
expect(siblings.filter((name) => name.includes(".clone-"))).toEqual([]);
|
|
} finally {
|
|
await fs.rm(sourceRepo, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("does not mention credentials when an unauthenticated clone fails for non-auth reasons", async () => {
|
|
// The Settings → Secrets hint is reserved for auth-shaped failures (covered in
|
|
// git-credentials.test.ts); a plain missing-repo failure must not suggest credentials.
|
|
const missingRepo = path.join(os.tmpdir(), "paperclip-definitely-missing", "repo.git");
|
|
const error = await ensureManagedProjectWorkspace({
|
|
companyId: "company-noauthfail",
|
|
projectId: "project-1",
|
|
repoUrl: missingRepo,
|
|
}).then(
|
|
() => { throw new Error("expected the clone to fail"); },
|
|
(err: unknown) => err as Error,
|
|
);
|
|
expect(error.message).toContain("Failed to prepare managed checkout");
|
|
expect(error.message).not.toContain("company secret");
|
|
});
|
|
|
|
it("leaves neither the target nor temp directories behind when the clone fails", async () => {
|
|
const missingRepo = path.join(os.tmpdir(), "paperclip-definitely-missing", "repo.git");
|
|
const companyId = "company-cleanup";
|
|
const projectId = "project-1";
|
|
await expect(ensureManagedProjectWorkspace({
|
|
companyId,
|
|
projectId,
|
|
repoUrl: missingRepo,
|
|
})).rejects.toThrow();
|
|
// Filesystem-path repo "URLs" derive no repo name, so the managed dir is the _default slot.
|
|
const cwd = resolveManagedProjectWorkspaceDir({ companyId, projectId });
|
|
await expect(fs.stat(cwd)).rejects.toMatchObject({ code: "ENOENT" });
|
|
const siblings = await fs.readdir(path.dirname(cwd));
|
|
expect(siblings.filter((name) => name.includes(".clone-"))).toEqual([]);
|
|
});
|
|
|
|
it("keeps using a pre-existing non-git directory as-is without attempting a clone", async () => {
|
|
const companyId = "company-existing";
|
|
const projectId = "project-1";
|
|
const sourceRepo = await createLocalSourceRepo();
|
|
try {
|
|
const cwd = resolveManagedProjectWorkspaceDir({ companyId, projectId });
|
|
await fs.mkdir(cwd, { recursive: true });
|
|
await fs.writeFile(path.join(cwd, "keep.txt"), "operator data\n", "utf8");
|
|
const result = await ensureManagedProjectWorkspace({
|
|
companyId,
|
|
projectId,
|
|
repoUrl: sourceRepo,
|
|
});
|
|
expect(result.cwd).toBe(cwd);
|
|
expect(result.warning).toContain("Using it as-is");
|
|
await expect(fs.readFile(path.join(cwd, "keep.txt"), "utf8")).resolves.toBe("operator data\n");
|
|
} finally {
|
|
await fs.rm(sourceRepo, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("keeps the credential env alive through the sanitizer spread order", () => {
|
|
// The clone env is `{ ...sanitize(process.env), GIT_TERMINAL_PROMPT, ...auth.env }`. The
|
|
// sanitizer strips every PAPERCLIP_* key, so the token env must be spread after it.
|
|
const invocation = buildGitAuthInvocation({
|
|
token: "tok",
|
|
source: "company_secret",
|
|
secretName: "GITHUB_TOKEN",
|
|
});
|
|
const cloneEnv = {
|
|
...sanitizeRuntimeServiceBaseEnv({ ...process.env, [GIT_CREDENTIAL_TOKEN_ENV_KEY]: "stale" }),
|
|
GIT_TERMINAL_PROMPT: "0",
|
|
...invocation.env,
|
|
};
|
|
expect(cloneEnv[GIT_CREDENTIAL_TOKEN_ENV_KEY]).toBe("tok");
|
|
expect(sanitizeRuntimeServiceBaseEnv({ [GIT_CREDENTIAL_TOKEN_ENV_KEY]: "stale" })[GIT_CREDENTIAL_TOKEN_ENV_KEY])
|
|
.toBeUndefined();
|
|
});
|
|
});
|