Skip to content

Commit e5c07eb

Browse files
test: address CodeRabbit review on PR #8
- Add runWhoami dry-run regression for whitespace-only TESTSPRITE_API_URL - Add commitBundle rollback-rename failure coverage (documents known gap) - Clean up bundle.commit temp dirs after each test
1 parent 91e2988 commit e5c07eb

2 files changed

Lines changed: 88 additions & 36 deletions

File tree

src/commands/auth.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,21 @@ describe('runWhoami', () => {
643643
expect(printed).toEqual(sampleMe);
644644
});
645645

646+
it('dry-run: whitespace-only TESTSPRITE_API_URL falls through to prod default endpoint', async () => {
647+
const { capture, deps } = makeCapture();
648+
await runWhoami(
649+
{ profile: 'default', output: 'text', debug: false, dryRun: true },
650+
{
651+
...deps,
652+
env: { TESTSPRITE_API_URL: ' ' },
653+
credentialsPath,
654+
fetchImpl: makeFetch(meResponse()),
655+
},
656+
);
657+
const out = capture.stdout.join('\n');
658+
expect(out).toContain('endpoint: https://api.testsprite.com');
659+
});
660+
646661
it('L1788: text output includes the resolved endpoint URL', async () => {
647662
writeProfile(
648663
'default',

src/lib/bundle.commit.test.ts

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import type * as NodeFsPromises from 'node:fs/promises';
2-
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2+
import {
3+
existsSync,
4+
mkdirSync,
5+
mkdtempSync,
6+
readdirSync,
7+
readFileSync,
8+
writeFileSync,
9+
} from 'node:fs';
310
import { tmpdir } from 'node:os';
411
import { join } from 'node:path';
512
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -35,8 +42,16 @@ describe('commitBundle', () => {
3542
rmMock.mockReset();
3643
});
3744

38-
it('rolls back to the prior complete bundle when the staging swap fails', async () => {
45+
async function withTempParent(run: (parent: string) => Promise<void>): Promise<void> {
3946
const parent = mkdtempSync(join(tmpdir(), 'bundle-commit-parent-'));
47+
try {
48+
await run(parent);
49+
} finally {
50+
await realRm(parent, { recursive: true, force: true }).catch(() => undefined);
51+
}
52+
}
53+
54+
function seedBundleDirs(parent: string): { dir: string; tmpDir: string } {
4055
const dir = join(parent, 'bundle');
4156
const tmpDir = join(dir, '.tmp');
4257

@@ -49,48 +64,70 @@ describe('commitBundle', () => {
4964
writeFileSync(join(tmpDir, 'result.json'), '{}\n', 'utf8');
5065
writeFileSync(join(tmpDir, 'steps', '01-evidence.json'), '{"step":9}\n', 'utf8');
5166

52-
renameMock.mockImplementation(async (oldPath, newPath) => {
53-
const src = String(oldPath);
54-
const dest = String(newPath);
55-
if (src.includes('.staging.') && !dest.includes('.prior.')) {
56-
throw Object.assign(new Error('simulated atomic swap failure'), { code: 'EACCES' });
57-
}
58-
return realRename(oldPath, newPath);
59-
});
60-
61-
await expect(commitBundle(tmpDir, dir)).rejects.toThrow('simulated atomic swap failure');
67+
return { dir, tmpDir };
68+
}
6269

63-
expect(readFileSync(join(dir, 'meta.json'), 'utf8')).toBe('{"snapshotId":"snap_old"}\n');
64-
expect(readFileSync(join(dir, 'steps', '01-evidence.json'), 'utf8')).toBe('{"step":1}\n');
65-
const leftovers = readdirSync(parent).filter(
66-
name => name.includes('.staging.') || name.includes('.prior.'),
67-
);
68-
expect(leftovers).toEqual([]);
70+
it('rolls back to the prior complete bundle when the staging swap fails', async () => {
71+
await withTempParent(async parent => {
72+
const { dir, tmpDir } = seedBundleDirs(parent);
73+
74+
renameMock.mockImplementation(async (oldPath, newPath) => {
75+
const src = String(oldPath);
76+
const dest = String(newPath);
77+
if (src.includes('.staging.') && !dest.includes('.prior.')) {
78+
throw Object.assign(new Error('simulated atomic swap failure'), { code: 'EACCES' });
79+
}
80+
return realRename(oldPath, newPath);
81+
});
82+
83+
await expect(commitBundle(tmpDir, dir)).rejects.toThrow('simulated atomic swap failure');
84+
85+
expect(readFileSync(join(dir, 'meta.json'), 'utf8')).toBe('{"snapshotId":"snap_old"}\n');
86+
expect(readFileSync(join(dir, 'steps', '01-evidence.json'), 'utf8')).toBe('{"step":1}\n');
87+
const leftovers = readdirSync(parent).filter(
88+
name => name.includes('.staging.') || name.includes('.prior.'),
89+
);
90+
expect(leftovers).toEqual([]);
91+
});
6992
});
7093

7194
it('keeps the new bundle when post-swap prior-dir cleanup fails', async () => {
72-
const parent = mkdtempSync(join(tmpdir(), 'bundle-commit-parent-'));
73-
const dir = join(parent, 'bundle');
74-
const tmpDir = join(dir, '.tmp');
95+
await withTempParent(async parent => {
96+
const { dir, tmpDir } = seedBundleDirs(parent);
7597

76-
mkdirSync(join(dir, 'steps'), { recursive: true });
77-
writeFileSync(join(dir, 'meta.json'), '{"snapshotId":"snap_old"}\n', 'utf8');
78-
writeFileSync(join(dir, 'steps', '01-evidence.json'), '{"step":1}\n', 'utf8');
98+
rmMock.mockImplementation(async (path, options) => {
99+
if (String(path).includes('.prior.')) {
100+
throw Object.assign(new Error('simulated prior cleanup failure'), { code: 'EACCES' });
101+
}
102+
return realRm(path, options);
103+
});
79104

80-
mkdirSync(join(tmpDir, 'steps'), { recursive: true });
81-
writeFileSync(join(tmpDir, 'meta.json'), '{"snapshotId":"snap_new"}\n', 'utf8');
82-
writeFileSync(join(tmpDir, 'steps', '01-evidence.json'), '{"step":9}\n', 'utf8');
105+
await expect(commitBundle(tmpDir, dir)).resolves.toBeUndefined();
83106

84-
rmMock.mockImplementation(async (path, options) => {
85-
if (String(path).includes('.prior.')) {
86-
throw Object.assign(new Error('simulated prior cleanup failure'), { code: 'EACCES' });
87-
}
88-
return realRm(path, options);
107+
expect(readFileSync(join(dir, 'meta.json'), 'utf8')).toBe('{"snapshotId":"snap_new"}\n');
108+
expect(readFileSync(join(dir, 'steps', '01-evidence.json'), 'utf8')).toBe('{"step":9}\n');
89109
});
110+
});
90111

91-
await expect(commitBundle(tmpDir, dir)).resolves.toBeUndefined();
92-
93-
expect(readFileSync(join(dir, 'meta.json'), 'utf8')).toBe('{"snapshotId":"snap_new"}\n');
94-
expect(readFileSync(join(dir, 'steps', '01-evidence.json'), 'utf8')).toBe('{"step":9}\n');
112+
it('propagates when rollback rename fails after a staging swap error', async () => {
113+
await withTempParent(async parent => {
114+
const { dir, tmpDir } = seedBundleDirs(parent);
115+
116+
renameMock.mockImplementation(async (oldPath, newPath) => {
117+
const src = String(oldPath);
118+
const dest = String(newPath);
119+
if (src.includes('.staging.') && !dest.includes('.prior.')) {
120+
throw Object.assign(new Error('simulated atomic swap failure'), { code: 'EACCES' });
121+
}
122+
if (src.includes('.prior.') && dest === dir) {
123+
throw Object.assign(new Error('simulated rollback rename failure'), { code: 'EACCES' });
124+
}
125+
return realRename(oldPath, newPath);
126+
});
127+
128+
await expect(commitBundle(tmpDir, dir)).rejects.toThrow('simulated rollback rename failure');
129+
// Known gap: double-failure can leave `dir` absent until manual recovery.
130+
expect(existsSync(dir)).toBe(false);
131+
});
95132
});
96133
});

0 commit comments

Comments
 (0)