fix(ext/sw): model isomorphic-git PushResult correctly; test the real push-rejection throw
PushResult has a singular `error: string | null` and a `refs` map — there
is no top-level `errors: string[]`. Replace the fabricated type and the
`errors`-based rejection guard with a correct PushResultRuntime type and a
belt-and-suspenders refsOk check. The real rejection path is a thrown
GitPushError (isomorphic-git throws, never returns, when a hook declines);
replace the single fiction-testing test with (a) a throw-path test that mocks
push to reject with a GitPushError and (b) a defensive non-throwing test that
mocks push to resolve { ok: false, error: 'unpack failed' }.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
This commit is contained in:
@@ -147,14 +147,32 @@ describe('commitSignedPush engine', () => {
|
||||
expect(args.onAuth()).toEqual(AUTH);
|
||||
});
|
||||
|
||||
test('5 — push rejection throws with server detail', async () => {
|
||||
test('5a — push rejection throw path (real): GitPushError thrown by isomorphic-git propagates', async () => {
|
||||
// isomorphic-git THROWS GitPushError (never returns) when a pre-receive hook declines.
|
||||
// This is the real rejection path; commitSignedPush must not swallow it.
|
||||
const hookError = Object.assign(
|
||||
new Error('One or more branches were not updated: pre-receive hook declined'),
|
||||
{ code: 'GitPushError' },
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
vi.mocked(git.push).mockResolvedValueOnce({ ok: false, errors: ['pre-receive hook declined'] } as any);
|
||||
vi.mocked(git.push).mockRejectedValueOnce(hookError);
|
||||
const files: CommitFile[] = [{ path: 'vault/item.enc', content: new Uint8Array([1]) }];
|
||||
|
||||
await expect(
|
||||
commitSignedPush({ gitUrl: GIT_URL, branch: BRANCH, auth: AUTH, files, message: 'test', signer: makeSigner() }),
|
||||
).rejects.toThrow('pre-receive hook declined');
|
||||
).rejects.toThrow('not updated');
|
||||
});
|
||||
|
||||
test('5b — push rejection defensive path: non-throwing ok:false triggers guard', async () => {
|
||||
// Belt-and-suspenders: if push() somehow resolves with ok:false (non-standard),
|
||||
// the guard must still throw with the error detail from the result.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
vi.mocked(git.push).mockResolvedValueOnce({ ok: false, error: 'unpack failed', refs: {} } as any);
|
||||
const files: CommitFile[] = [{ path: 'vault/item.enc', content: new Uint8Array([1]) }];
|
||||
|
||||
await expect(
|
||||
commitSignedPush({ gitUrl: GIT_URL, branch: BRANCH, auth: AUTH, files, message: 'test', signer: makeSigner() }),
|
||||
).rejects.toThrow('unpack failed');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -23,10 +23,9 @@ import { createMemFs } from './mem-fs';
|
||||
import { buildSshSig } from './sshsig';
|
||||
import type { CommitFile, CommitSigner } from './git-host';
|
||||
|
||||
// Runtime push results from isomorphic-git include an `errors` array that is
|
||||
// not reflected in the TypeScript PushResult type. Intersect it here so the
|
||||
// rejection check compiles under strict mode.
|
||||
type PushResultRuntime = { ok: boolean; errors?: string[] };
|
||||
// isomorphic-git PushResult: { ok: boolean; error: string | null; refs: {[ref]: {ok, error}} }
|
||||
// There is NO top-level `errors: string[]`; the type below reflects reality.
|
||||
type PushResultRuntime = { ok: boolean; error?: string | null; refs?: Record<string, { ok: boolean; error?: string | null }> };
|
||||
|
||||
/**
|
||||
* Clone `gitUrl` at depth 1, write `files`, sign the commit with SSHSIG, and
|
||||
@@ -93,8 +92,7 @@ export async function commitSignedPush(opts: {
|
||||
}),
|
||||
});
|
||||
|
||||
// Push over git-receive-pack. The runtime result includes `errors[]` when a
|
||||
// pre-receive hook declines — cast to PushResultRuntime to access it.
|
||||
// Push over git-receive-pack.
|
||||
const res = await git.push({
|
||||
fs, http, dir,
|
||||
url: opts.gitUrl,
|
||||
@@ -103,11 +101,13 @@ export async function commitSignedPush(opts: {
|
||||
onAuth,
|
||||
}) as unknown as PushResultRuntime;
|
||||
|
||||
const ok = res?.ok && (!res.errors || res.errors.length === 0);
|
||||
if (!ok) {
|
||||
throw new Error(
|
||||
`org push rejected by server/hook: ${JSON.stringify(res?.errors ?? res)}`,
|
||||
);
|
||||
// isomorphic-git THROWS GitPushError when the server/pre-receive hook declines
|
||||
// a ref (the normal org-write rejection path); that error propagates to the
|
||||
// caller unchanged. This guard is defensive belt-and-suspenders for any
|
||||
// non-throwing failure: push() only returns when ok && every ref ok.
|
||||
const refsOk = !res?.refs || Object.values(res.refs).every((r) => r.ok);
|
||||
if (!res || res.ok !== true || !refsOk) {
|
||||
throw new Error(`org push rejected by server/hook: ${res?.error ?? JSON.stringify(res)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user