[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
246
infrastructure/services/adapters/GitHubAdapter.test.ts
Normal file
246
infrastructure/services/adapters/GitHubAdapter.test.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { downloadGistRevision, downloadSyncGist } from './GitHubAdapter.ts';
|
||||
import { SYNC_CONSTANTS } from '../../../domain/sync.ts';
|
||||
|
||||
type FetchCall = {
|
||||
url: string;
|
||||
headers?: HeadersInit;
|
||||
};
|
||||
|
||||
function installFetchMock(
|
||||
handler: (url: string, init?: RequestInit) => Promise<Response> | Response,
|
||||
): { calls: FetchCall[]; restore: () => void } {
|
||||
const calls: FetchCall[] = [];
|
||||
const original = globalThis.fetch;
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
||||
calls.push({ url, headers: init?.headers });
|
||||
return handler(url, init);
|
||||
}) as typeof fetch;
|
||||
return {
|
||||
calls,
|
||||
restore: () => {
|
||||
globalThis.fetch = original;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const FULL_SYNCED_FILE = {
|
||||
meta: {
|
||||
version: 3,
|
||||
updatedAt: 1_700_000_000_000,
|
||||
deviceId: 'device-1',
|
||||
deviceName: 'Test',
|
||||
},
|
||||
payload: `ENC:${'A'.repeat(50_000)}`,
|
||||
};
|
||||
|
||||
const FULL_CONTENT = JSON.stringify(FULL_SYNCED_FILE, null, 2);
|
||||
// Reproduce #2643: Gist API embeds a truncated mid-string payload (~900 KiB cut).
|
||||
const TRUNCATED_CONTENT = FULL_CONTENT.slice(0, 900);
|
||||
const utf8ByteLength = (value: string): number =>
|
||||
new TextEncoder().encode(value).byteLength;
|
||||
|
||||
test('downloadSyncGist fetches raw_url when Gist API marks the file truncated', async () => {
|
||||
const rawUrl = 'https://gist.githubusercontent.com/u/abc/raw/netcatty-vault.json';
|
||||
const { calls, restore } = installFetchMock((url) => {
|
||||
if (url.includes('/gists/') && !url.includes('raw')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'gist-1',
|
||||
description: SYNC_CONSTANTS.GIST_DESCRIPTION,
|
||||
files: {
|
||||
[SYNC_CONSTANTS.SYNC_FILE_NAME]: {
|
||||
filename: SYNC_CONSTANTS.SYNC_FILE_NAME,
|
||||
content: TRUNCATED_CONTENT,
|
||||
truncated: true,
|
||||
raw_url: rawUrl,
|
||||
size: utf8ByteLength(FULL_CONTENT),
|
||||
},
|
||||
},
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
if (url === rawUrl) {
|
||||
return new Response(FULL_CONTENT, { status: 200 });
|
||||
}
|
||||
return new Response('not found', { status: 404 });
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await downloadSyncGist('token-xyz', 'gist-1');
|
||||
assert.deepEqual(result, FULL_SYNCED_FILE);
|
||||
const rawCall = calls.find((c) => c.url === rawUrl);
|
||||
assert.ok(rawCall, 'must fetch raw_url for truncated gist');
|
||||
assert.deepEqual(rawCall.headers, {
|
||||
Authorization: 'Bearer token-xyz',
|
||||
Accept: 'application/vnd.github.raw',
|
||||
});
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('downloadSyncGist falls back to raw_url when embedded content is incomplete JSON', async () => {
|
||||
const rawUrl = 'https://gist.githubusercontent.com/u/abc/raw/netcatty-vault.json';
|
||||
const { restore } = installFetchMock((url) => {
|
||||
if (url.includes('/gists/') && !url.includes('raw')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'gist-1',
|
||||
description: SYNC_CONSTANTS.GIST_DESCRIPTION,
|
||||
files: {
|
||||
[SYNC_CONSTANTS.SYNC_FILE_NAME]: {
|
||||
filename: SYNC_CONSTANTS.SYNC_FILE_NAME,
|
||||
// truncated flag missing (edge case) but content is cut mid-string
|
||||
content: TRUNCATED_CONTENT,
|
||||
raw_url: rawUrl,
|
||||
size: utf8ByteLength(FULL_CONTENT),
|
||||
},
|
||||
},
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
if (url === rawUrl) {
|
||||
return new Response(FULL_CONTENT, { status: 200 });
|
||||
}
|
||||
return new Response('not found', { status: 404 });
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await downloadSyncGist('token-xyz', 'gist-1');
|
||||
assert.deepEqual(result, FULL_SYNCED_FILE);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('downloadSyncGist uses embedded content when file is not truncated', async () => {
|
||||
const small = { meta: { version: 1 }, payload: 'small' };
|
||||
const body = JSON.stringify(small);
|
||||
let rawFetches = 0;
|
||||
const { restore } = installFetchMock((url) => {
|
||||
if (url.includes('/gists/')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'gist-1',
|
||||
description: SYNC_CONSTANTS.GIST_DESCRIPTION,
|
||||
files: {
|
||||
[SYNC_CONSTANTS.SYNC_FILE_NAME]: {
|
||||
filename: SYNC_CONSTANTS.SYNC_FILE_NAME,
|
||||
content: body,
|
||||
truncated: false,
|
||||
raw_url: 'https://gist.githubusercontent.com/u/abc/raw/unused',
|
||||
size: utf8ByteLength(body),
|
||||
},
|
||||
},
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
rawFetches += 1;
|
||||
return new Response('should not fetch', { status: 500 });
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await downloadSyncGist('token-xyz', 'gist-1');
|
||||
assert.deepEqual(result, small);
|
||||
assert.equal(rawFetches, 0);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('downloadSyncGist keeps embedded multibyte content without raw fetch', async () => {
|
||||
const small = {
|
||||
meta: {
|
||||
version: 1,
|
||||
deviceName: '我的电脑',
|
||||
},
|
||||
payload: 'small',
|
||||
};
|
||||
const body = JSON.stringify(small);
|
||||
// GitHub reports UTF-8 bytes; string.length is smaller for CJK, which must not
|
||||
// be treated as truncation.
|
||||
assert.ok(utf8ByteLength(body) > body.length);
|
||||
let rawFetches = 0;
|
||||
const { restore } = installFetchMock((url) => {
|
||||
if (url.includes('/gists/')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'gist-1',
|
||||
description: SYNC_CONSTANTS.GIST_DESCRIPTION,
|
||||
files: {
|
||||
[SYNC_CONSTANTS.SYNC_FILE_NAME]: {
|
||||
filename: SYNC_CONSTANTS.SYNC_FILE_NAME,
|
||||
content: body,
|
||||
truncated: false,
|
||||
raw_url: 'https://gist.githubusercontent.com/u/abc/raw/unused',
|
||||
size: utf8ByteLength(body),
|
||||
},
|
||||
},
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
rawFetches += 1;
|
||||
return new Response('should not fetch', { status: 500 });
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await downloadSyncGist('token-xyz', 'gist-1');
|
||||
assert.deepEqual(result, small);
|
||||
assert.equal(rawFetches, 0);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('downloadGistRevision also recovers truncated content via raw_url', async () => {
|
||||
const rawUrl = 'https://gist.githubusercontent.com/u/abc/raw/rev/netcatty-vault.json';
|
||||
const { restore } = installFetchMock((url) => {
|
||||
if (url.includes('/gists/gist-1/deadbeef')) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: 'gist-1',
|
||||
description: SYNC_CONSTANTS.GIST_DESCRIPTION,
|
||||
files: {
|
||||
[SYNC_CONSTANTS.SYNC_FILE_NAME]: {
|
||||
filename: SYNC_CONSTANTS.SYNC_FILE_NAME,
|
||||
content: TRUNCATED_CONTENT,
|
||||
truncated: true,
|
||||
raw_url: rawUrl,
|
||||
size: utf8ByteLength(FULL_CONTENT),
|
||||
},
|
||||
},
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
updated_at: '2026-01-01T00:00:00Z',
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
if (url === rawUrl) {
|
||||
return new Response(FULL_CONTENT, { status: 200 });
|
||||
}
|
||||
return new Response('not found', { status: 404 });
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await downloadGistRevision('token-xyz', 'gist-1', 'deadbeef');
|
||||
assert.deepEqual(result, FULL_SYNCED_FILE);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user