Built-in design generation now runs as an agentic MCP tool-loop (reusing the agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the Settings experimental toggle; the orchestrator stays the default. - design-agent system prompt + in-process design toolset (parity-locked with the MCP surface) + flag-gated Intent::Design routing - spawn_agents execution as sequential sub-loops + live creation-mode badges (per-agent glow + 'N/M designing...' header) - new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state, export_nodes, spawn_agents; style-guide local audit - #27 AI panel restyle: rounded tool cards + green check-rings, gray user bubbles, model-pill bottom toolbar, header, empty-state pills, the PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown - multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI (switch / close / + / Cmd+T) with each run bound to its tab Large checkpoint commit spanning the working tree (Rust shell crates).
654 lines
23 KiB
TypeScript
654 lines
23 KiB
TypeScript
// apps/desktop/git/__tests__/git-iso.test.ts
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import {
|
|
initSingleFile,
|
|
openRepo,
|
|
commitFile,
|
|
readBlobAtCommit,
|
|
logForRef,
|
|
restoreFileFromCommit,
|
|
listBranches,
|
|
createBranch,
|
|
deleteBranch,
|
|
switchBranch,
|
|
getCurrentBranch,
|
|
setRef,
|
|
readBlobOidAt,
|
|
findMergeBase,
|
|
} from '../git-iso';
|
|
import { detectRepo } from '../repo-detector';
|
|
import { mkTempDir, writeOpFile } from './test-helpers';
|
|
|
|
describe('git-iso', () => {
|
|
let temp: { dir: string; dispose: () => Promise<void> };
|
|
|
|
beforeEach(async () => {
|
|
temp = await mkTempDir();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await temp.dispose();
|
|
});
|
|
|
|
describe('initSingleFile', () => {
|
|
it('creates .op-history/<basename>.git/ with HEAD pointing at refs/heads/main', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op');
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
|
|
expect(handle.mode).toBe('single-file');
|
|
expect(handle.dir).toBe(temp.dir);
|
|
expect(handle.gitdir).toBe(join(temp.dir, '.op-history', 'login.op.git'));
|
|
expect(existsSync(join(handle.gitdir, 'HEAD'))).toBe(true);
|
|
});
|
|
|
|
it('is idempotent: re-running init on an existing repo returns the same handle without erroring', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op');
|
|
const first = await initSingleFile({ filePath: opFile });
|
|
const second = await initSingleFile({ filePath: opFile });
|
|
expect(second.gitdir).toBe(first.gitdir);
|
|
expect(second.dir).toBe(first.dir);
|
|
expect(existsSync(join(second.gitdir, 'HEAD'))).toBe(true);
|
|
});
|
|
|
|
it('respects the defaultBranch option', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op');
|
|
const handle = await initSingleFile({ filePath: opFile, defaultBranch: 'trunk' });
|
|
// isomorphic-git writes HEAD as a symbolic ref string
|
|
const fs = await import('node:fs/promises');
|
|
const head = await fs.readFile(join(handle.gitdir, 'HEAD'), 'utf-8');
|
|
expect(head.trim()).toBe('ref: refs/heads/trunk');
|
|
});
|
|
|
|
it('writes core.worktree = ../.. so terminal git can inspect the repo', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op');
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
// Read the on-disk config and verify both core.worktree and core.bare.
|
|
const fsp = await import('node:fs/promises');
|
|
const config = await fsp.readFile(join(handle.gitdir, 'config'), 'utf-8');
|
|
expect(config).toMatch(/worktree\s*=\s*\.\.\/\.\./);
|
|
expect(config).toMatch(/bare\s*=\s*false/);
|
|
});
|
|
});
|
|
|
|
describe('openRepo', () => {
|
|
it('opens an existing single-file repo via a successful detection', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op');
|
|
await initSingleFile({ filePath: opFile });
|
|
|
|
const detection = await detectRepo(opFile);
|
|
expect(detection.mode).toBe('single-file');
|
|
if (detection.mode !== 'single-file') throw new Error('detection failed');
|
|
|
|
const handle = await openRepo(detection);
|
|
expect(handle.mode).toBe('single-file');
|
|
expect(handle.dir).toBe(temp.dir);
|
|
});
|
|
});
|
|
|
|
describe('commitFile + readBlobAtCommit', () => {
|
|
it('creates a commit on the given ref and the blob round-trips', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
|
|
const { hash } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'initial',
|
|
author: { name: 'tester', email: 'tester@example.com' },
|
|
});
|
|
expect(hash).toMatch(/^[a-f0-9]{40}$/);
|
|
|
|
const content = await readBlobAtCommit({
|
|
handle,
|
|
filepath: 'login.op',
|
|
commitHash: hash,
|
|
});
|
|
const parsed = JSON.parse(content);
|
|
expect(parsed.children[0].id).toBe('r1');
|
|
});
|
|
|
|
it('throws commit-empty when committing the same content twice', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op');
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await expect(
|
|
commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'second',
|
|
author: { name: 't', email: 't@example.com' },
|
|
}),
|
|
).rejects.toMatchObject({ name: 'GitError', code: 'commit-empty' });
|
|
});
|
|
|
|
it('chains commits: each commit has the previous as parent', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
|
|
const { hash: h1 } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
|
|
// Mutate the file then commit again.
|
|
await writeOpFile(temp.dir, 'login.op', { version: '1.0.0', children: [{ id: 'r2' }] });
|
|
const { hash: h2 } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'second',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
|
|
expect(h2).not.toBe(h1);
|
|
// Verify h1 content is recoverable
|
|
const c1 = await readBlobAtCommit({ handle, filepath: 'login.op', commitHash: h1 });
|
|
expect(JSON.parse(c1).children[0].id).toBe('r1');
|
|
// Verify h2 content
|
|
const c2 = await readBlobAtCommit({ handle, filepath: 'login.op', commitHash: h2 });
|
|
expect(JSON.parse(c2).children[0].id).toBe('r2');
|
|
});
|
|
});
|
|
|
|
describe('logForRef', () => {
|
|
it('returns commits in reverse chronological order (newest first)', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
|
|
const { hash: h1 } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await writeOpFile(temp.dir, 'login.op', { version: '1.0.0', children: [{ id: 'r2' }] });
|
|
const { hash: h2 } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'second',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
|
|
const log = await logForRef({ handle, ref: 'refs/heads/main', depth: 10 });
|
|
expect(log).toHaveLength(2);
|
|
expect(log[0].hash).toBe(h2);
|
|
expect(log[0].message).toBe('second\n');
|
|
expect(log[1].hash).toBe(h1);
|
|
expect(log[1].message).toBe('first\n');
|
|
});
|
|
|
|
it('returns an empty array for a ref that does not exist', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op');
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
const log = await logForRef({
|
|
handle,
|
|
ref: 'refs/openpencil/autosaves/main',
|
|
depth: 10,
|
|
});
|
|
expect(log).toEqual([]);
|
|
});
|
|
|
|
it('respects the depth parameter', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
// Make 3 commits.
|
|
for (let i = 0; i < 3; i++) {
|
|
await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: `r${i}` }],
|
|
});
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: `commit-${i}`,
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
}
|
|
const log = await logForRef({ handle, ref: 'refs/heads/main', depth: 2 });
|
|
expect(log).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe('restoreFileFromCommit', () => {
|
|
it("writes a previous commit's content to the working tree", async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
const { hash: h1 } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
// Mutate
|
|
await writeOpFile(temp.dir, 'login.op', { version: '1.0.0', children: [{ id: 'r2' }] });
|
|
// Restore
|
|
await restoreFileFromCommit({ handle, filepath: 'login.op', commitHash: h1 });
|
|
// Verify the working tree file was overwritten
|
|
const fsp = await import('node:fs/promises');
|
|
const restored = await fsp.readFile(opFile, 'utf-8');
|
|
expect(JSON.parse(restored).children[0].id).toBe('r1');
|
|
});
|
|
|
|
it('does NOT create a new commit (the caller is responsible)', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
const { hash: h1 } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await writeOpFile(temp.dir, 'login.op', { version: '1.0.0', children: [{ id: 'r2' }] });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'second',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
const logBefore = await logForRef({ handle, ref: 'refs/heads/main', depth: 10 });
|
|
|
|
await restoreFileFromCommit({ handle, filepath: 'login.op', commitHash: h1 });
|
|
const logAfter = await logForRef({ handle, ref: 'refs/heads/main', depth: 10 });
|
|
expect(logAfter).toHaveLength(logBefore.length); // unchanged
|
|
});
|
|
});
|
|
|
|
describe('branch operations', () => {
|
|
it('listBranches returns empty for a fresh repo with no commits', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op');
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
const branches = await listBranches({ handle });
|
|
expect(branches).toEqual([]); // no commits = no branches
|
|
});
|
|
|
|
it('listBranches returns the branch after a commit creates it', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
const branches = await listBranches({ handle });
|
|
expect(branches).toContain('main');
|
|
});
|
|
|
|
it('createBranch from current HEAD adds a new branch', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await createBranch({ handle, name: 'feature-x' });
|
|
const branches = await listBranches({ handle });
|
|
expect(branches).toContain('feature-x');
|
|
expect(branches).toContain('main');
|
|
});
|
|
|
|
it('createBranch throws branch-exists when the name is taken', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await createBranch({ handle, name: 'feature-x' });
|
|
await expect(createBranch({ handle, name: 'feature-x' })).rejects.toMatchObject({
|
|
name: 'GitError',
|
|
code: 'branch-exists',
|
|
});
|
|
});
|
|
|
|
it('deleteBranch throws branch-current when deleting the active branch', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await expect(deleteBranch({ handle, name: 'main' })).rejects.toMatchObject({
|
|
name: 'GitError',
|
|
code: 'branch-current',
|
|
});
|
|
});
|
|
|
|
it('deleteBranch removes a non-active branch', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await createBranch({ handle, name: 'feature-x' });
|
|
await deleteBranch({ handle, name: 'feature-x' });
|
|
const branches = await listBranches({ handle });
|
|
expect(branches).not.toContain('feature-x');
|
|
expect(branches).toContain('main');
|
|
});
|
|
|
|
it('deleteBranch throws branch-unmerged for an unmerged branch without force', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'base',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await createBranch({ handle, name: 'feature-x' });
|
|
await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r2' }],
|
|
});
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/feature-x',
|
|
message: 'feature only',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await expect(deleteBranch({ handle, name: 'feature-x' })).rejects.toMatchObject({
|
|
name: 'GitError',
|
|
code: 'branch-unmerged',
|
|
});
|
|
});
|
|
|
|
it('deleteBranch removes an unmerged branch when force=true', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'base',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await createBranch({ handle, name: 'feature-x' });
|
|
await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r2' }],
|
|
});
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/feature-x',
|
|
message: 'feature only',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await deleteBranch({ handle, name: 'feature-x', force: true });
|
|
const branches = await listBranches({ handle });
|
|
expect(branches).toEqual(['main']);
|
|
});
|
|
|
|
it('deleteBranch treats a fast-forward-merged branch as merged (equal OID tips)', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'base',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
// Branch off main with no additional commits: feature-ff tip === main tip.
|
|
await createBranch({ handle, name: 'feature-ff' });
|
|
// Without the equal-OID short-circuit in isBranchMergedAnywhere this would
|
|
// throw branch-unmerged because isomorphic-git's isDescendent returns
|
|
// false when oid === ancestor.
|
|
await deleteBranch({ handle, name: 'feature-ff' });
|
|
const branches = await listBranches({ handle });
|
|
expect(branches).toEqual(['main']);
|
|
});
|
|
|
|
it('getCurrentBranch returns the active branch name', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
const current = await getCurrentBranch({ handle });
|
|
expect(current).toBe('main');
|
|
});
|
|
|
|
it('switchBranch updates the working tree file to the target branch tip', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
// Branch off, then commit something different on the new branch.
|
|
await createBranch({ handle, name: 'feature-x' });
|
|
await writeOpFile(temp.dir, 'login.op', { version: '1.0.0', children: [{ id: 'r2' }] });
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/feature-x',
|
|
message: 'feature change',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
// Switch back to main and verify the file content reverts.
|
|
await switchBranch({ handle, name: 'main', filepath: 'login.op' });
|
|
const fsp = await import('node:fs/promises');
|
|
const content = await fsp.readFile(opFile, 'utf-8');
|
|
expect(JSON.parse(content).children[0].id).toBe('r1');
|
|
});
|
|
});
|
|
|
|
describe('setRef + readBlobOidAt', () => {
|
|
it('setRef creates a new ref pointing at the given commit and force-overwrites an existing one', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
const { hash: h1 } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
|
|
// Create a brand-new ref pointing at h1.
|
|
await setRef({ handle, ref: 'refs/openpencil/autosaves/main', value: h1 });
|
|
const log1 = await logForRef({
|
|
handle,
|
|
ref: 'refs/openpencil/autosaves/main',
|
|
depth: 10,
|
|
});
|
|
expect(log1).toHaveLength(1);
|
|
expect(log1[0].hash).toBe(h1);
|
|
|
|
// Make a second milestone, then force the autosave ref to it (overwrite).
|
|
await writeOpFile(temp.dir, 'login.op', { version: '1.0.0', children: [{ id: 'r2' }] });
|
|
const { hash: h2 } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'second',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
await setRef({ handle, ref: 'refs/openpencil/autosaves/main', value: h2 });
|
|
const log2 = await logForRef({
|
|
handle,
|
|
ref: 'refs/openpencil/autosaves/main',
|
|
depth: 10,
|
|
});
|
|
// The autosave ref now jumps to h2 — its log walks h2 → h1 (parent chain).
|
|
expect(log2[0].hash).toBe(h2);
|
|
});
|
|
|
|
it('readBlobOidAt returns the blob OID at the ref tip and null for missing ref/file', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
|
|
// Before any commit, the ref doesn't exist → null.
|
|
const before = await readBlobOidAt({
|
|
handle,
|
|
ref: 'refs/heads/main',
|
|
filepath: 'login.op',
|
|
});
|
|
expect(before).toBeNull();
|
|
|
|
await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'first',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
|
|
// After commit, the OID is a 40-char hex string.
|
|
const after = await readBlobOidAt({
|
|
handle,
|
|
ref: 'refs/heads/main',
|
|
filepath: 'login.op',
|
|
});
|
|
expect(after).toMatch(/^[a-f0-9]{40}$/);
|
|
|
|
// Asking for a file that's not in the commit's tree → null.
|
|
const missing = await readBlobOidAt({
|
|
handle,
|
|
ref: 'refs/heads/main',
|
|
filepath: 'does-not-exist.op',
|
|
});
|
|
expect(missing).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('findMergeBase', () => {
|
|
it('returns the common ancestor of two divergent branches', async () => {
|
|
const opFile = await writeOpFile(temp.dir, 'login.op', {
|
|
version: '1.0.0',
|
|
children: [{ id: 'r1' }],
|
|
});
|
|
const handle = await initSingleFile({ filePath: opFile });
|
|
|
|
const { hash: base } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/main',
|
|
message: 'base',
|
|
author: { name: 't', email: 't@example.com' },
|
|
});
|
|
|
|
// Branch off and commit on each branch.
|
|
await writeOpFile(temp.dir, 'login.op', { version: '1.0.0', children: [{ id: 'r2' }] });
|
|
const { hash: ours } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/feature-a',
|
|
message: 'ours',
|
|
author: { name: 't', email: 't@example.com' },
|
|
parents: [base],
|
|
});
|
|
await writeOpFile(temp.dir, 'login.op', { version: '1.0.0', children: [{ id: 'r3' }] });
|
|
const { hash: theirs } = await commitFile({
|
|
handle,
|
|
filepath: 'login.op',
|
|
ref: 'refs/heads/feature-b',
|
|
message: 'theirs',
|
|
author: { name: 't', email: 't@example.com' },
|
|
parents: [base],
|
|
});
|
|
|
|
const mergeBase = await findMergeBase({ handle, oid1: ours, oid2: theirs });
|
|
expect(mergeBase).toBe(base);
|
|
});
|
|
});
|
|
});
|