mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-17 00:04:54 +00:00
wip
This commit is contained in:
parent
3263d4f5e1
commit
0fbea6ce61
166 changed files with 6675 additions and 7105 deletions
|
|
@ -1,129 +0,0 @@
|
|||
import { chmodSync, mkdtempSync, writeFileSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { gitSpiceAvailable, gitSpiceListStack, gitSpiceRestackSubtree } from "../src/integrations/git-spice/index.js";
|
||||
|
||||
function makeTempDir(prefix: string): string {
|
||||
return mkdtempSync(join(tmpdir(), prefix));
|
||||
}
|
||||
|
||||
function writeScript(path: string, body: string): void {
|
||||
writeFileSync(path, body, "utf8");
|
||||
chmodSync(path, 0o755);
|
||||
}
|
||||
|
||||
async function withEnv<T>(updates: Record<string, string | undefined>, fn: () => Promise<T>): Promise<T> {
|
||||
const previous = new Map<string, string | undefined>();
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
previous.set(key, process.env[key]);
|
||||
if (value == null) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
for (const [key, value] of previous) {
|
||||
if (value == null) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe("git-spice integration", () => {
|
||||
it("parses stack rows from mixed/malformed json output", async () => {
|
||||
const repoPath = makeTempDir("hf-git-spice-parse-");
|
||||
const scriptPath = join(repoPath, "fake-git-spice.sh");
|
||||
writeScript(
|
||||
scriptPath,
|
||||
[
|
||||
"#!/bin/sh",
|
||||
'if [ \"$1\" = \"--help\" ]; then',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [ \"$1\" = \"log\" ]; then',
|
||||
" echo 'noise line'",
|
||||
' echo \'{"branch":"feature/a","parent":"main"}\'',
|
||||
" echo '{bad json'",
|
||||
' echo \'{"name":"feature/b","parentBranch":"feature/a"}\'',
|
||||
' echo \'{"name":"feature/a","parent":"main"}\'',
|
||||
" exit 0",
|
||||
"fi",
|
||||
"exit 1",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
await withEnv({ HF_GIT_SPICE_BIN: scriptPath }, async () => {
|
||||
const rows = await gitSpiceListStack(repoPath);
|
||||
expect(rows).toEqual([
|
||||
{ branchName: "feature/a", parentBranch: "main" },
|
||||
{ branchName: "feature/b", parentBranch: "feature/a" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back across versioned subtree restack command variants", async () => {
|
||||
const repoPath = makeTempDir("hf-git-spice-fallback-");
|
||||
const scriptPath = join(repoPath, "fake-git-spice.sh");
|
||||
const logPath = join(repoPath, "calls.log");
|
||||
writeScript(
|
||||
scriptPath,
|
||||
[
|
||||
"#!/bin/sh",
|
||||
'echo \"$*\" >> \"$SPICE_LOG_PATH\"',
|
||||
'if [ \"$1\" = \"--help\" ]; then',
|
||||
" exit 0",
|
||||
"fi",
|
||||
'if [ \"$1\" = \"upstack\" ] && [ \"$2\" = \"restack\" ]; then',
|
||||
" exit 1",
|
||||
"fi",
|
||||
'if [ \"$1\" = \"branch\" ] && [ \"$2\" = \"restack\" ] && [ \"$5\" = \"--no-prompt\" ]; then',
|
||||
" exit 0",
|
||||
"fi",
|
||||
"exit 1",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
await withEnv(
|
||||
{
|
||||
HF_GIT_SPICE_BIN: scriptPath,
|
||||
SPICE_LOG_PATH: logPath,
|
||||
},
|
||||
async () => {
|
||||
await gitSpiceRestackSubtree(repoPath, "feature/a");
|
||||
},
|
||||
);
|
||||
|
||||
const lines = readFileSync(logPath, "utf8")
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line.trim().length > 0);
|
||||
|
||||
expect(lines).toContain("upstack restack --branch feature/a --no-prompt");
|
||||
expect(lines).toContain("upstack restack --branch feature/a");
|
||||
expect(lines).toContain("branch restack --branch feature/a --no-prompt");
|
||||
expect(lines).not.toContain("branch restack --branch feature/a");
|
||||
});
|
||||
|
||||
it("reports unavailable when explicit binary and PATH are missing", async () => {
|
||||
const repoPath = makeTempDir("hf-git-spice-missing-");
|
||||
|
||||
await withEnv(
|
||||
{
|
||||
HF_GIT_SPICE_BIN: "/non-existent/hf-git-spice-binary",
|
||||
PATH: "/non-existent/bin",
|
||||
},
|
||||
async () => {
|
||||
const available = await gitSpiceAvailable(repoPath);
|
||||
expect(available).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { execFile } from "node:child_process";
|
||||
import { validateRemote } from "../src/integrations/git/index.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
describe("validateRemote", () => {
|
||||
const originalCwd = process.cwd();
|
||||
|
||||
beforeEach(() => {
|
||||
process.chdir(originalCwd);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(originalCwd);
|
||||
});
|
||||
|
||||
test("ignores broken worktree gitdir in current directory", async () => {
|
||||
const sandboxDir = mkdtempSync(join(tmpdir(), "validate-remote-cwd-"));
|
||||
const brokenRepoDir = resolve(sandboxDir, "broken-worktree");
|
||||
const remoteRepoDir = resolve(sandboxDir, "remote");
|
||||
|
||||
mkdirSync(brokenRepoDir, { recursive: true });
|
||||
writeFileSync(resolve(brokenRepoDir, ".git"), "gitdir: /definitely/missing/worktree\n", "utf8");
|
||||
await execFileAsync("git", ["init", remoteRepoDir]);
|
||||
await execFileAsync("git", ["-C", remoteRepoDir, "config", "user.name", "Foundry Test"]);
|
||||
await execFileAsync("git", ["-C", remoteRepoDir, "config", "user.email", "test@example.com"]);
|
||||
writeFileSync(resolve(remoteRepoDir, "README.md"), "# test\n", "utf8");
|
||||
await execFileAsync("git", ["-C", remoteRepoDir, "add", "README.md"]);
|
||||
await execFileAsync("git", ["-C", remoteRepoDir, "commit", "-m", "init"]);
|
||||
|
||||
process.chdir(brokenRepoDir);
|
||||
|
||||
await expect(validateRemote(remoteRepoDir)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -9,7 +9,7 @@ export function createTestConfig(overrides?: Partial<AppConfig>): AppConfig {
|
|||
return ConfigSchema.parse({
|
||||
auto_submit: true,
|
||||
notify: ["terminal" as const],
|
||||
workspace: { default: "default" },
|
||||
organization: { default: "default" },
|
||||
backend: {
|
||||
host: "127.0.0.1",
|
||||
port: 7741,
|
||||
|
|
@ -19,7 +19,7 @@ export function createTestConfig(overrides?: Partial<AppConfig>): AppConfig {
|
|||
backup_interval_secs: 3600,
|
||||
backup_retention_days: 7,
|
||||
},
|
||||
providers: {
|
||||
sandboxProviders: {
|
||||
local: {},
|
||||
e2b: {},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,48 +1,15 @@
|
|||
import type { BackendDriver, GitDriver, GithubDriver, StackDriver, TmuxDriver } from "../../src/driver.js";
|
||||
import type { BackendDriver, GithubDriver, TmuxDriver } from "../../src/driver.js";
|
||||
|
||||
export function createTestDriver(overrides?: Partial<BackendDriver>): BackendDriver {
|
||||
return {
|
||||
git: overrides?.git ?? createTestGitDriver(),
|
||||
stack: overrides?.stack ?? createTestStackDriver(),
|
||||
github: overrides?.github ?? createTestGithubDriver(),
|
||||
tmux: overrides?.tmux ?? createTestTmuxDriver(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestGitDriver(overrides?: Partial<GitDriver>): GitDriver {
|
||||
return {
|
||||
validateRemote: async () => {},
|
||||
ensureCloned: async () => {},
|
||||
fetch: async () => {},
|
||||
listRemoteBranches: async () => [],
|
||||
listLocalRemoteRefs: async () => [],
|
||||
remoteDefaultBaseRef: async () => "origin/main",
|
||||
revParse: async () => "abc1234567890",
|
||||
ensureRemoteBranch: async () => {},
|
||||
diffStatForBranch: async () => "+0/-0",
|
||||
conflictsWithMain: async () => false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestStackDriver(overrides?: Partial<StackDriver>): StackDriver {
|
||||
return {
|
||||
available: async () => false,
|
||||
listStack: async () => [],
|
||||
syncRepo: async () => {},
|
||||
restackRepo: async () => {},
|
||||
restackSubtree: async () => {},
|
||||
rebaseBranch: async () => {},
|
||||
reparentBranch: async () => {},
|
||||
trackBranch: async () => {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestGithubDriver(overrides?: Partial<GithubDriver>): GithubDriver {
|
||||
return {
|
||||
listPullRequests: async () => [],
|
||||
createPr: async (_repoPath, _headBranch, _title) => ({
|
||||
createPr: async (_repoFullName, _headBranch, _title) => ({
|
||||
number: 1,
|
||||
url: `https://github.com/test/repo/pull/1`,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { githubDataKey, historyKey, projectBranchSyncKey, projectKey, taskKey, taskSandboxKey, workspaceKey } from "../src/actors/keys.js";
|
||||
import { githubDataKey, historyKey, organizationKey, repositoryKey, taskKey, taskSandboxKey } from "../src/actors/keys.js";
|
||||
|
||||
describe("actor keys", () => {
|
||||
it("prefixes every key with workspace namespace", () => {
|
||||
it("prefixes every key with organization namespace", () => {
|
||||
const keys = [
|
||||
workspaceKey("default"),
|
||||
projectKey("default", "repo"),
|
||||
organizationKey("default"),
|
||||
repositoryKey("default", "repo"),
|
||||
taskKey("default", "repo", "task"),
|
||||
taskSandboxKey("default", "sbx"),
|
||||
historyKey("default", "repo"),
|
||||
githubDataKey("default"),
|
||||
projectBranchSyncKey("default", "repo"),
|
||||
];
|
||||
|
||||
for (const key of keys) {
|
||||
expect(key[0]).toBe("ws");
|
||||
expect(key[0]).toBe("org");
|
||||
expect(key[1]).toBe("default");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ import { execFileSync } from "node:child_process";
|
|||
import { setTimeout as delay } from "node:timers/promises";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { setupTest } from "rivetkit/test";
|
||||
import { workspaceKey } from "../src/actors/keys.js";
|
||||
import { organizationKey } from "../src/actors/keys.js";
|
||||
import { registry } from "../src/actors/index.js";
|
||||
import { repoIdFromRemote } from "../src/services/repo.js";
|
||||
import { createTestDriver } from "./helpers/test-driver.js";
|
||||
import { createTestRuntimeContext } from "./helpers/test-context.js";
|
||||
|
||||
|
|
@ -24,59 +25,60 @@ function createRepo(): { repoPath: string } {
|
|||
return { repoPath };
|
||||
}
|
||||
|
||||
async function waitForWorkspaceRows(ws: any, workspaceId: string, expectedCount: number) {
|
||||
async function waitForOrganizationRows(ws: any, organizationId: string, expectedCount: number) {
|
||||
for (let attempt = 0; attempt < 40; attempt += 1) {
|
||||
const rows = await ws.listTasks({ workspaceId });
|
||||
const rows = await ws.listTasks({ organizationId });
|
||||
if (rows.length >= expectedCount) {
|
||||
return rows;
|
||||
}
|
||||
await delay(50);
|
||||
}
|
||||
return ws.listTasks({ workspaceId });
|
||||
return ws.listTasks({ organizationId });
|
||||
}
|
||||
|
||||
describe("workspace isolation", () => {
|
||||
it.skipIf(!runActorIntegration)("keeps task lists isolated by workspace", async (t) => {
|
||||
describe("organization isolation", () => {
|
||||
it.skipIf(!runActorIntegration)("keeps task lists isolated by organization", async (t) => {
|
||||
const testDriver = createTestDriver();
|
||||
createTestRuntimeContext(testDriver);
|
||||
|
||||
const { client } = await setupTest(t, registry);
|
||||
const wsA = await client.workspace.getOrCreate(workspaceKey("alpha"), {
|
||||
const wsA = await client.organization.getOrCreate(organizationKey("alpha"), {
|
||||
createWithInput: "alpha",
|
||||
});
|
||||
const wsB = await client.workspace.getOrCreate(workspaceKey("beta"), {
|
||||
const wsB = await client.organization.getOrCreate(organizationKey("beta"), {
|
||||
createWithInput: "beta",
|
||||
});
|
||||
|
||||
const { repoPath } = createRepo();
|
||||
const repoA = await wsA.addRepo({ workspaceId: "alpha", remoteUrl: repoPath });
|
||||
const repoB = await wsB.addRepo({ workspaceId: "beta", remoteUrl: repoPath });
|
||||
const repoId = repoIdFromRemote(repoPath);
|
||||
await wsA.applyGithubRepositoryProjection({ repoId, remoteUrl: repoPath });
|
||||
await wsB.applyGithubRepositoryProjection({ repoId, remoteUrl: repoPath });
|
||||
|
||||
await wsA.createTask({
|
||||
workspaceId: "alpha",
|
||||
repoId: repoA.repoId,
|
||||
organizationId: "alpha",
|
||||
repoId,
|
||||
task: "task A",
|
||||
providerId: "local",
|
||||
sandboxProviderId: "local",
|
||||
explicitBranchName: "feature/a",
|
||||
explicitTitle: "A",
|
||||
});
|
||||
|
||||
await wsB.createTask({
|
||||
workspaceId: "beta",
|
||||
repoId: repoB.repoId,
|
||||
organizationId: "beta",
|
||||
repoId,
|
||||
task: "task B",
|
||||
providerId: "local",
|
||||
sandboxProviderId: "local",
|
||||
explicitBranchName: "feature/b",
|
||||
explicitTitle: "B",
|
||||
});
|
||||
|
||||
const aRows = await waitForWorkspaceRows(wsA, "alpha", 1);
|
||||
const bRows = await waitForWorkspaceRows(wsB, "beta", 1);
|
||||
const aRows = await waitForOrganizationRows(wsA, "alpha", 1);
|
||||
const bRows = await waitForOrganizationRows(wsB, "beta", 1);
|
||||
|
||||
expect(aRows.length).toBe(1);
|
||||
expect(bRows.length).toBe(1);
|
||||
expect(aRows[0]?.workspaceId).toBe("alpha");
|
||||
expect(bRows[0]?.workspaceId).toBe("beta");
|
||||
expect(aRows[0]?.organizationId).toBe("alpha");
|
||||
expect(bRows[0]?.organizationId).toBe("beta");
|
||||
expect(aRows[0]?.taskId).not.toBe(bRows[0]?.taskId);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
// @ts-nocheck
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { setupTest } from "rivetkit/test";
|
||||
import { workspaceKey } from "../src/actors/keys.js";
|
||||
import { organizationKey } from "../src/actors/keys.js";
|
||||
import { registry } from "../src/actors/index.js";
|
||||
import { createTestDriver } from "./helpers/test-driver.js";
|
||||
import { createTestRuntimeContext } from "./helpers/test-context.js";
|
||||
|
||||
const runActorIntegration = process.env.HF_ENABLE_ACTOR_INTEGRATION_TESTS === "1";
|
||||
|
||||
describe("workspace star sandbox agent repo", () => {
|
||||
describe("organization star sandbox agent repo", () => {
|
||||
it.skipIf(!runActorIntegration)("stars the sandbox agent repo through the github driver", async (t) => {
|
||||
const calls: string[] = [];
|
||||
const testDriver = createTestDriver({
|
||||
|
|
@ -26,11 +26,11 @@ describe("workspace star sandbox agent repo", () => {
|
|||
createTestRuntimeContext(testDriver);
|
||||
|
||||
const { client } = await setupTest(t, registry);
|
||||
const ws = await client.workspace.getOrCreate(workspaceKey("alpha"), {
|
||||
const ws = await client.organization.getOrCreate(organizationKey("alpha"), {
|
||||
createWithInput: "alpha",
|
||||
});
|
||||
|
||||
const result = await ws.starSandboxAgentRepo({ workspaceId: "alpha" });
|
||||
const result = await ws.starSandboxAgentRepo({ organizationId: "alpha" });
|
||||
|
||||
expect(calls).toEqual(["rivet-dev/sandbox-agent"]);
|
||||
expect(result.repo).toBe("rivet-dev/sandbox-agent");
|
||||
|
|
@ -6,7 +6,7 @@ function makeConfig(overrides?: Partial<AppConfig>): AppConfig {
|
|||
return ConfigSchema.parse({
|
||||
auto_submit: true,
|
||||
notify: ["terminal"],
|
||||
workspace: { default: "default" },
|
||||
organization: { default: "default" },
|
||||
backend: {
|
||||
host: "127.0.0.1",
|
||||
port: 7741,
|
||||
|
|
@ -16,7 +16,7 @@ function makeConfig(overrides?: Partial<AppConfig>): AppConfig {
|
|||
backup_interval_secs: 3600,
|
||||
backup_retention_days: 7,
|
||||
},
|
||||
providers: {
|
||||
sandboxProviders: {
|
||||
local: {},
|
||||
e2b: {},
|
||||
},
|
||||
|
|
@ -33,7 +33,7 @@ describe("sandbox config", () => {
|
|||
|
||||
it("prefers e2b when an api key is configured", () => {
|
||||
const config = makeConfig({
|
||||
providers: {
|
||||
sandboxProviders: {
|
||||
local: {},
|
||||
e2b: { apiKey: "test-token" },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeParentBranch, parentLookupFromStack, sortBranchesForOverview } from "../src/actors/project/stack-model.js";
|
||||
|
||||
describe("stack-model", () => {
|
||||
it("normalizes self-parent references to null", () => {
|
||||
expect(normalizeParentBranch("feature/a", "feature/a")).toBeNull();
|
||||
expect(normalizeParentBranch("feature/a", "main")).toBe("main");
|
||||
expect(normalizeParentBranch("feature/a", null)).toBeNull();
|
||||
});
|
||||
|
||||
it("builds parent lookup with sanitized entries", () => {
|
||||
const lookup = parentLookupFromStack([
|
||||
{ branchName: "feature/a", parentBranch: "main" },
|
||||
{ branchName: "feature/b", parentBranch: "feature/b" },
|
||||
{ branchName: " ", parentBranch: "main" },
|
||||
]);
|
||||
|
||||
expect(lookup.get("feature/a")).toBe("main");
|
||||
expect(lookup.get("feature/b")).toBeNull();
|
||||
expect(lookup.has(" ")).toBe(false);
|
||||
});
|
||||
|
||||
it("orders branches by graph depth and handles cycles safely", () => {
|
||||
const rows = sortBranchesForOverview([
|
||||
{ branchName: "feature/b", parentBranch: "feature/a", updatedAt: 200 },
|
||||
{ branchName: "feature/a", parentBranch: "main", updatedAt: 100 },
|
||||
{ branchName: "main", parentBranch: null, updatedAt: 50 },
|
||||
{ branchName: "cycle-a", parentBranch: "cycle-b", updatedAt: 300 },
|
||||
{ branchName: "cycle-b", parentBranch: "cycle-a", updatedAt: 250 },
|
||||
]);
|
||||
|
||||
expect(rows.map((row) => row.branchName)).toEqual(["main", "feature/a", "feature/b", "cycle-a", "cycle-b"]);
|
||||
});
|
||||
});
|
||||
|
|
@ -59,8 +59,8 @@ describe("workbench model changes", () => {
|
|||
});
|
||||
|
||||
describe("workbench send readiness", () => {
|
||||
it("rejects unknown tabs", () => {
|
||||
expect(() => requireSendableSessionMeta(null, "tab-1")).toThrow("Unknown workbench tab: tab-1");
|
||||
it("rejects unknown sessions", () => {
|
||||
expect(() => requireSendableSessionMeta(null, "session-1")).toThrow("Unknown workbench session: session-1");
|
||||
});
|
||||
|
||||
it("rejects pending sessions", () => {
|
||||
|
|
@ -70,7 +70,7 @@ describe("workbench send readiness", () => {
|
|||
status: "pending_session_create",
|
||||
sandboxSessionId: null,
|
||||
},
|
||||
"tab-2",
|
||||
"session-2",
|
||||
),
|
||||
).toThrow("Session is not ready (status: pending_session_create). Wait for session provisioning to complete.");
|
||||
});
|
||||
|
|
@ -81,6 +81,6 @@ describe("workbench send readiness", () => {
|
|||
sandboxSessionId: "session-1",
|
||||
};
|
||||
|
||||
expect(requireSendableSessionMeta(meta, "tab-3")).toBe(meta);
|
||||
expect(requireSendableSessionMeta(meta, "session-3")).toBe(meta);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue