mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-21 14:47:17 +00:00
* feat(feedback): support attaching logs and codebase Add an attachment picker to /feedback (none / logs / logs + codebase). Codebase uploads scan the working directory with sensitive files excluded and are sent through a new multipart upload API on the oauth/node-sdk layers. * fix(feedback): fall back to logs when codebase scan fails * tiny fix * fix(feedback): make diagnostic uploads partial-safe * refactor(feedback): reuse harness session export and normalize upload url types * docs(slash-commands): note optional feedback attachments * refactor(feedback): reorganize feedback upload modules Move the attachment orchestration out of tui/commands/info.ts into a dedicated feedback/feedback-attachments.ts, and split the former codebase-upload/attach.ts into a generic multipart uploader (feedback/upload.ts) and an archive lifecycle module (feedback/archive.ts). Both session and codebase archives now flow through a single upload lifecycle, which also removes the temp-dir leak that occurred when codebase packaging failed. Rename FeedbackCodebaseArchive to FeedbackArchive and the codebase-upload/ directory to codebase/ so module boundaries match their actual responsibilities (scan + package only).
154 lines
4.7 KiB
TypeScript
154 lines
4.7 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
fetchCompleteFeedbackUpload,
|
|
fetchCreateFeedbackUploadUrl,
|
|
kimiCodeFeedbackUploadCompleteUrl,
|
|
kimiCodeFeedbackUploadUrl,
|
|
type CreateFeedbackUploadUrlBody,
|
|
} from '../src/managed-feedback-upload';
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
const SAMPLE_BODY: CreateFeedbackUploadUrlBody = {
|
|
file_hash: 'e4d649659ca70729a510ef58f4cd062890020a1038eead5f411451fce62df415',
|
|
file_name: 'repo.zip',
|
|
file_size: 123,
|
|
feedback_id: 3,
|
|
};
|
|
|
|
describe('kimiCodeFeedbackUploadUrl', () => {
|
|
it('uses the feedback upload_url path', () => {
|
|
expect(kimiCodeFeedbackUploadUrl()).toBe('https://api.kimi.com/coding/v1/feedback/upload_url');
|
|
});
|
|
});
|
|
|
|
describe('kimiCodeFeedbackUploadCompleteUrl', () => {
|
|
it('uses the feedback upload_complete path', () => {
|
|
expect(kimiCodeFeedbackUploadCompleteUrl()).toBe(
|
|
'https://api.kimi.com/coding/v1/feedback/upload_complete',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('fetchCreateFeedbackUploadUrl', () => {
|
|
it('POSTs JSON body with bearer auth and parses upload parts', async () => {
|
|
const fetchMock = vi.fn(async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
code: 0,
|
|
upload: {
|
|
id: 28,
|
|
upload_id: 'tos-multipart-id',
|
|
part_size: 8,
|
|
total_parts: 1,
|
|
parts: [
|
|
{ part_number: 1, url: 'https://example.test/part1', method: 'PUT', size: 123 },
|
|
],
|
|
},
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
const result = await fetchCreateFeedbackUploadUrl('access-token', SAMPLE_BODY);
|
|
|
|
expect(result).toEqual({
|
|
kind: 'ok',
|
|
upload_id: 28,
|
|
parts: [{ part_number: 1, url: 'https://example.test/part1', method: 'PUT', size: 123 }],
|
|
});
|
|
|
|
const calls = fetchMock.mock.calls as unknown as [string, RequestInit?][];
|
|
const [calledUrl, init] = calls[0]!;
|
|
expect(calledUrl).toBe('https://api.kimi.com/coding/v1/feedback/upload_url');
|
|
expect(init?.method).toBe('POST');
|
|
|
|
const headers = new Headers((init?.headers ?? {}) as Record<string, string>);
|
|
expect(headers.get('authorization')).toBe('Bearer access-token');
|
|
expect(JSON.parse(init?.body as string)).toEqual(SAMPLE_BODY);
|
|
});
|
|
|
|
it('returns an error when the response omits parts', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async () =>
|
|
new Response(JSON.stringify({ code: 0, upload: { id: 28 } }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}),
|
|
),
|
|
);
|
|
|
|
const result = await fetchCreateFeedbackUploadUrl('access-token', SAMPLE_BODY);
|
|
|
|
expect(result).toEqual({
|
|
kind: 'error',
|
|
message: 'Feedback upload request failed: missing upload id or parts.',
|
|
});
|
|
});
|
|
|
|
it('returns an error when a part is missing required fields', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async () =>
|
|
new Response(
|
|
JSON.stringify({
|
|
code: 0,
|
|
upload: { id: 28, parts: [{ part_number: 1 }] },
|
|
}),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
),
|
|
),
|
|
);
|
|
|
|
const result = await fetchCreateFeedbackUploadUrl('access-token', SAMPLE_BODY);
|
|
|
|
expect(result).toEqual({
|
|
kind: 'error',
|
|
message: 'Feedback upload request failed: missing upload id or parts.',
|
|
});
|
|
});
|
|
|
|
it('returns an error with status when the server responds 401', async () => {
|
|
vi.stubGlobal('fetch', vi.fn(async () => new Response('', { status: 401 })));
|
|
|
|
const result = await fetchCreateFeedbackUploadUrl('access-token', SAMPLE_BODY);
|
|
|
|
expect(result.kind).toBe('error');
|
|
if (result.kind !== 'error') return;
|
|
expect(result.status).toBe(401);
|
|
expect(result.message).toMatch(/401/);
|
|
});
|
|
});
|
|
|
|
describe('fetchCompleteFeedbackUpload', () => {
|
|
it('POSTs upload_id and parts with bearer auth', async () => {
|
|
const fetchMock = vi.fn(async () => new Response('{}', { status: 200 }));
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
const result = await fetchCompleteFeedbackUpload('access-token', {
|
|
upload_id: 28,
|
|
parts: [
|
|
{ part_number: 1, etag: '"etag-1"' },
|
|
{ part_number: 2, etag: '"etag-2"' },
|
|
],
|
|
});
|
|
|
|
expect(result).toEqual({ kind: 'ok' });
|
|
const calls = fetchMock.mock.calls as unknown as [string, RequestInit?][];
|
|
const [calledUrl, init] = calls[0]!;
|
|
expect(calledUrl).toBe('https://api.kimi.com/coding/v1/feedback/upload_complete');
|
|
expect(JSON.parse(init?.body as string)).toEqual({
|
|
upload_id: 28,
|
|
parts: [
|
|
{ part_number: 1, etag: '"etag-1"' },
|
|
{ part_number: 2, etag: '"etag-2"' },
|
|
],
|
|
});
|
|
});
|
|
});
|