This commit is contained in:
Nathan Flurry 2026-03-14 20:28:41 -07:00
parent 3263d4f5e1
commit 0fbea6ce61
166 changed files with 6675 additions and 7105 deletions

View file

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

View file

@ -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();
});
});

View file

@ -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: {},
},

View file

@ -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`,
}),

View file

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

View file

@ -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);
});
});

View file

@ -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");

View file

@ -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" },
},

View file

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

View file

@ -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);
});
});