-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathoperations.test.ts
More file actions
193 lines (159 loc) · 8.11 KB
/
Copy pathoperations.test.ts
File metadata and controls
193 lines (159 loc) · 8.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import * as assert from 'assert';
import * as sinon from 'sinon';
import type { Cache } from '@gitlens/git/cache.js';
import type { GitServiceContext } from '@gitlens/git/context.js';
import { CheckoutError, FetchError, PullError, PushError, ResetError } from '@gitlens/git/errors.js';
import type { GitBranchReference } from '@gitlens/git/models/reference.js';
import type { GitResult, GitRunOptions } from '@gitlens/git/run.types.js';
import type { CliGitProviderInternal } from '../../cliGitProvider.js';
import { RunError } from '../../exec/exec.errors.js';
import type { Git } from '../../exec/git.js';
import { defaultExceptionHandler, GitError } from '../../exec/git.js';
import { OperationsGitSubProvider } from '../operations.js';
suite('OperationsGitSubProvider Test Suite', () => {
let sandbox: sinon.SinonSandbox;
let operations: OperationsGitSubProvider;
let gitStub: sinon.SinonStubbedInstance<Git>;
const repoPath = '/repo';
function branchRef(): GitBranchReference {
return {
refType: 'branch',
name: 'feature',
ref: 'feature',
remote: false,
upstream: { name: 'origin/feature', missing: false },
repoPath: repoPath,
};
}
function successResult(): GitResult {
return { stdout: '', stderr: undefined, exitCode: 0, cancelled: false };
}
/**
* Stubs `git.run` to mirror the real {@link Git.runCore} error contract for a failing command:
* with `errors: 'throw'` the rejection is thrown; otherwise it is routed through the real
* {@link defaultExceptionHandler}, which swallows `GitWarnings` matches (resolving as success)
* and rethrows everything else. This reproduces the production swallow behavior without spawning
* a real git process, so the tests genuinely distinguish a push that surfaces a rejection from
* one that silently swallows it.
*/
function stubRunFailure(stderr: string): void {
(gitStub.run as sinon.SinonStub).callsFake(
async (options: GitRunOptions, ...args: readonly (string | undefined)[]) => {
const ex = new GitError(
new RunError(
{ message: stderr, cmd: `git ${args.filter(a => a != null).join(' ')}`, code: 1 },
'',
stderr,
),
);
if (options.errors === 'throw') throw ex;
// Mirror Git.runCore: let the default handler decide fatal vs. non-fatal
defaultExceptionHandler(ex, options.cwd);
return successResult();
},
);
}
setup(() => {
sandbox = sinon.createSandbox();
class MockGit {
supports(_feature: string) {
return Promise.resolve(true);
}
run(..._args: any[]) {
return Promise.resolve(successResult());
}
}
gitStub = sandbox.createStubInstance(MockGit) as unknown as sinon.SinonStubbedInstance<Git>;
(gitStub.run as sinon.SinonStub).resolves(successResult());
const context = {} as unknown as GitServiceContext;
const cache = {} as unknown as Cache;
const provider = {} as unknown as CliGitProviderInternal;
operations = new OperationsGitSubProvider(context, gitStub, cache, provider);
});
teardown(() => {
sandbox.restore();
});
test('push passes `errors: throw` so rejections are not swallowed by the default handler', async () => {
await operations.push(repoPath, { reference: branchRef() });
const pushCall = (gitStub.run as sinon.SinonStub).getCalls().find(c => c.args.includes('push'));
assert.ok(pushCall, 'expected git.run to be invoked with a push command');
assert.strictEqual((pushCall.args[0] as GitRunOptions).errors, 'throw');
});
test('push surfaces a non-fast-forward (tipBehind) rejection as PushError', async () => {
stubRunFailure(
'To origin\n ! [rejected] feature -> feature (non-fast-forward)\n' +
"error: failed to push some refs to 'origin'\n" +
'hint: Updates were rejected because the tip of your current branch is behind\n' +
'hint: its remote counterpart.',
);
await assert.rejects(operations.push(repoPath, { reference: branchRef() }), (ex: unknown) =>
PushError.is(ex, 'tipBehind'),
);
});
test('push surfaces a remoteAhead rejection as PushError', async () => {
stubRunFailure(
"error: failed to push some refs to 'origin'\n" +
'hint: Updates were rejected because the remote contains work that you do not have locally.',
);
await assert.rejects(operations.push(repoPath, { reference: branchRef() }), (ex: unknown) =>
PushError.is(ex, 'remoteAhead'),
);
});
test('fetch surfaces an unreachable-remote failure as FetchError', async () => {
// `remoteConnectionError` is a GitWarning, so without `errors: 'throw'` the default handler
// swallows it and the fetch resolves as if it succeeded.
stubRunFailure('fatal: Could not read from remote repository.');
await assert.rejects(operations.fetch(repoPath), (ex: unknown) => FetchError.is(ex, 'remoteConnectionFailed'));
});
test('pull surfaces an unreachable-remote failure as PullError', async () => {
// `remoteConnectionError` is a GitWarning, so without `errors: 'throw'` the default handler
// swallows it and the pull resolves as if it succeeded.
stubRunFailure('fatal: Could not read from remote repository.');
await assert.rejects(operations.pull(repoPath), (ex: unknown) => PullError.is(ex, 'remoteConnectionFailed'));
});
test('reset surfaces an invalid-revision failure as ResetError', async () => {
// `unknownRevision` is a GitWarning, so without `errors: 'throw'` the default handler swallows
// it and the reset resolves as if it succeeded (without resetting).
stubRunFailure("fatal: ambiguous argument 'badref': unknown revision or path not in the working tree.");
await assert.rejects(operations.reset(repoPath, 'badref'), (ex: unknown) =>
ResetError.is(ex, 'ambiguousArgument'),
);
});
test('checkout with createBranch passes -b and ref', async () => {
await operations.checkout(repoPath, 'origin/main', { createBranch: 'feature/foo' });
const call = (gitStub.run as sinon.SinonStub).getCalls().find(c => c.args.includes('checkout'));
assert.ok(call, 'expected git.run to be invoked with checkout');
const gitArgs = call.args.filter((a): a is string => typeof a === 'string');
assert.deepStrictEqual(gitArgs, ['checkout', '-b', 'feature/foo', 'origin/main', '--']);
});
test('checkout with createBranch and noTracking passes --no-track', async () => {
await operations.checkout(repoPath, 'origin/main', { createBranch: 'feature/foo', noTracking: true });
const call = (gitStub.run as sinon.SinonStub).getCalls().find(c => c.args.includes('checkout'));
assert.ok(call, 'expected git.run to be invoked with checkout');
const gitArgs = call.args.filter((a): a is string => typeof a === 'string');
assert.deepStrictEqual(gitArgs, ['checkout', '-b', 'feature/foo', '--no-track', 'origin/main', '--']);
});
test('checkout with createBranch and noTracking=false omits --no-track', async () => {
await operations.checkout(repoPath, 'origin/main', { createBranch: 'feature/foo', noTracking: false });
const call = (gitStub.run as sinon.SinonStub).getCalls().find(c => c.args.includes('checkout'));
assert.ok(call, 'expected git.run to be invoked with checkout');
const gitArgs = call.args.filter((a): a is string => typeof a === 'string');
assert.deepStrictEqual(gitArgs, ['checkout', '-b', 'feature/foo', 'origin/main', '--']);
});
test('checkout surfaces an invalid-ref failure as CheckoutError', async () => {
// `unknownRevision` is a GitWarning, so without `errors: 'throw'` the default handler swallows
// it and the checkout resolves as if it succeeded.
stubRunFailure("fatal: ambiguous argument 'badref': unknown revision or path not in the working tree.");
await assert.rejects(operations.checkout(repoPath, 'badref'), (ex: unknown) =>
CheckoutError.is(ex, 'pathspecNotFound'),
);
});
test('restore surfaces an invalid-ref failure as CheckoutError', async () => {
// restore is implemented via `git checkout`; `unknownRevision` is a GitWarning, so without
// `errors: 'throw'` the default handler swallows it and the restore resolves as if it succeeded.
stubRunFailure("fatal: ambiguous argument 'badref': unknown revision or path not in the working tree.");
await assert.rejects(operations.restore(repoPath, 'file.ts', { ref: 'badref' }), (ex: unknown) =>
CheckoutError.is(ex, 'pathspecNotFound'),
);
});
});