WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)
1import { describe, expect, it } from 'vitest';
2import { isTangledRemote, parseTangledRemote } from '../../src/utils/git.js';
3
4describe('Git Utilities', () => {
5 describe('isTangledRemote', () => {
6 it('should detect SSH tangled remotes', () => {
7 expect(isTangledRemote('git@tangled.org:did:plc:abc123/repo.git')).toBe(true);
8 expect(isTangledRemote('ssh://git@tangled.org/did:plc:abc123/repo.git')).toBe(true);
9 });
10
11 it('should detect HTTPS tangled remotes', () => {
12 expect(isTangledRemote('https://tangled.org/markbennett.ca/tangled-cli')).toBe(true);
13 expect(isTangledRemote('https://tangled.org/user.bsky.social/repo')).toBe(true);
14 });
15
16 it('should reject non-tangled remotes', () => {
17 expect(isTangledRemote('git@github.com:user/repo.git')).toBe(false);
18 expect(isTangledRemote('https://github.com/user/repo')).toBe(false);
19 expect(isTangledRemote('https://gitlab.com/user/repo')).toBe(false);
20 expect(isTangledRemote('')).toBe(false);
21 });
22 });
23
24 describe('parseTangledRemote', () => {
25 describe('SSH URLs with DIDs', () => {
26 it('should parse git@tangled.org:did:plc:xxx/repo.git format', () => {
27 const result = parseTangledRemote(
28 'git@tangled.org:did:plc:b2mcbcamkwyznc5fkplwlxbf/tangled-cli.git'
29 );
30 expect(result).toEqual({
31 owner: 'did:plc:b2mcbcamkwyznc5fkplwlxbf',
32 ownerType: 'did',
33 name: 'tangled-cli',
34 protocol: 'ssh',
35 });
36 });
37
38 it('should parse ssh://git@tangled.org/did:plc:xxx/repo.git format', () => {
39 const result = parseTangledRemote(
40 'ssh://git@tangled.org/did:plc:b2mcbcamkwyznc5fkplwlxbf/tangled-cli.git'
41 );
42 expect(result).toEqual({
43 owner: 'did:plc:b2mcbcamkwyznc5fkplwlxbf',
44 ownerType: 'did',
45 name: 'tangled-cli',
46 protocol: 'ssh',
47 });
48 });
49
50 it('should handle SSH URLs without .git extension', () => {
51 const result = parseTangledRemote('git@tangled.org:did:plc:abc123/repo');
52 expect(result).toEqual({
53 owner: 'did:plc:abc123',
54 ownerType: 'did',
55 name: 'repo',
56 protocol: 'ssh',
57 });
58 });
59 });
60
61 describe('SSH URLs with handles', () => {
62 it('should parse SSH URL with handle instead of DID', () => {
63 const result = parseTangledRemote('git@tangled.org:markbennett.ca/tangled-cli.git');
64 expect(result).toEqual({
65 owner: 'markbennett.ca',
66 ownerType: 'handle',
67 name: 'tangled-cli',
68 protocol: 'ssh',
69 });
70 });
71 });
72
73 describe('HTTPS URLs with handles', () => {
74 it('should parse https://tangled.org/handle/repo format', () => {
75 const result = parseTangledRemote('https://tangled.org/markbennett.ca/tangled-cli');
76 expect(result).toEqual({
77 owner: 'markbennett.ca',
78 ownerType: 'handle',
79 name: 'tangled-cli',
80 protocol: 'https',
81 });
82 });
83
84 it('should parse HTTPS with user.bsky.social handle', () => {
85 const result = parseTangledRemote('https://tangled.org/user.bsky.social/repo');
86 expect(result).toEqual({
87 owner: 'user.bsky.social',
88 ownerType: 'handle',
89 name: 'repo',
90 protocol: 'https',
91 });
92 });
93
94 it('should handle HTTPS URLs without .git extension', () => {
95 const result = parseTangledRemote('https://tangled.org/markbennett.ca/repo');
96 expect(result?.name).toBe('repo');
97 });
98 });
99
100 describe('HTTPS URLs with DIDs', () => {
101 it('should parse HTTPS URL with DID instead of handle', () => {
102 const result = parseTangledRemote(
103 'https://tangled.org/did:plc:b2mcbcamkwyznc5fkplwlxbf/repo'
104 );
105 expect(result).toEqual({
106 owner: 'did:plc:b2mcbcamkwyznc5fkplwlxbf',
107 ownerType: 'did',
108 name: 'repo',
109 protocol: 'https',
110 });
111 });
112 });
113
114 describe('edge cases', () => {
115 it('should handle trailing slashes', () => {
116 const result = parseTangledRemote('https://tangled.org/markbennett.ca/repo/');
117 expect(result?.name).toBe('repo');
118 });
119
120 it('should handle .git extension in various positions', () => {
121 const result1 = parseTangledRemote('git@tangled.org:did:plc:abc123/repo.git');
122 const result2 = parseTangledRemote('https://tangled.org/markbennett.ca/repo.git');
123 expect(result1?.name).toBe('repo');
124 expect(result2?.name).toBe('repo');
125 });
126
127 it('should return null for invalid DID format', () => {
128 const result = parseTangledRemote('git@tangled.org:did:plc:INVALID/repo.git');
129 expect(result).toBeNull();
130 });
131
132 it('should return null for invalid handle format', () => {
133 const result = parseTangledRemote('https://tangled.org/invalid/repo');
134 expect(result).toBeNull();
135 });
136
137 it('should return null for missing repo name', () => {
138 const result = parseTangledRemote('git@tangled.org:did:plc:abc123');
139 expect(result).toBeNull();
140 });
141
142 it('should return null for non-tangled URLs', () => {
143 expect(parseTangledRemote('git@github.com:user/repo.git')).toBeNull();
144 expect(parseTangledRemote('https://github.com/user/repo')).toBeNull();
145 });
146 });
147 });
148});