mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-19 21:56:00 +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).
208 lines
6 KiB
TypeScript
208 lines
6 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
fetchSubmitFeedback,
|
|
kimiCodeFeedbackUrl,
|
|
type SubmitFeedbackBody,
|
|
} from '../src/managed-feedback';
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
const SAMPLE_BODY: SubmitFeedbackBody = {
|
|
session_id: 'sess-123',
|
|
content: 'great tool',
|
|
version: 'kimi-code-0.1.1',
|
|
os: 'Darwin 25.3.0',
|
|
model: 'kimi-code/kimi-for-coding',
|
|
contact: 'test@example.com',
|
|
info: { tool: 'kimi-code-cli', env: 'test' },
|
|
};
|
|
|
|
describe('kimiCodeFeedbackUrl', () => {
|
|
it('appends /feedback to the default base URL', () => {
|
|
expect(kimiCodeFeedbackUrl()).toBe('https://api.kimi.com/coding/v1/feedback');
|
|
});
|
|
|
|
it('honours KIMI_CODE_BASE_URL and trims trailing slashes', () => {
|
|
vi.stubEnv('KIMI_CODE_BASE_URL', 'https://example.test/v9///');
|
|
expect(kimiCodeFeedbackUrl()).toBe('https://example.test/v9/feedback');
|
|
});
|
|
});
|
|
|
|
describe('fetchSubmitFeedback', () => {
|
|
it('POSTs JSON body with bearer auth and returns feedback_id on 200', async () => {
|
|
const fetchMock = vi.fn(async () =>
|
|
new Response(JSON.stringify({ feedback_id: 3 }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
const result = await fetchSubmitFeedback(
|
|
'https://api.example/feedback',
|
|
'access-token',
|
|
SAMPLE_BODY,
|
|
);
|
|
|
|
expect(result).toEqual({ kind: 'ok', feedbackId: 3 });
|
|
|
|
const calls = fetchMock.mock.calls as unknown as [string, RequestInit?][];
|
|
const [calledUrl, init] = calls[0]!;
|
|
expect(calledUrl).toBe('https://api.example/feedback');
|
|
expect(init?.method).toBe('POST');
|
|
|
|
const headers = new Headers((init?.headers ?? {}) as Record<string, string>);
|
|
expect(headers.get('authorization')).toBe('Bearer access-token');
|
|
expect(headers.get('content-type')).toBe('application/json');
|
|
expect(headers.get('accept')).toBe('application/json');
|
|
|
|
expect(JSON.parse(init?.body as string)).toEqual(SAMPLE_BODY);
|
|
});
|
|
|
|
it('returns an error when the server omits feedback_id', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async () =>
|
|
new Response(JSON.stringify({ ok: true }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}),
|
|
),
|
|
);
|
|
|
|
const result = await fetchSubmitFeedback('https://api.example/feedback', 'access-token', SAMPLE_BODY);
|
|
|
|
expect(result).toEqual({
|
|
kind: 'error',
|
|
message: 'Failed to submit feedback: missing feedback_id.',
|
|
});
|
|
});
|
|
|
|
it('preserves the kimi-code- version prefix in the request body', async () => {
|
|
const fetchMock = vi.fn(async () =>
|
|
new Response(JSON.stringify({ feedback_id: 3 }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}),
|
|
);
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
await fetchSubmitFeedback('https://api.example/feedback', 'tok', SAMPLE_BODY);
|
|
|
|
const calls = fetchMock.mock.calls as unknown as [string, RequestInit?][];
|
|
const sent = JSON.parse(calls[0]?.[1]?.body as string) as Record<string, unknown>;
|
|
expect(sent['version']).toBe('kimi-code-0.1.1');
|
|
});
|
|
|
|
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 fetchSubmitFeedback(
|
|
'https://api.example/feedback',
|
|
'access-token',
|
|
SAMPLE_BODY,
|
|
);
|
|
|
|
expect(result.kind).toBe('error');
|
|
if (result.kind !== 'error') return;
|
|
expect(result.status).toBe(401);
|
|
expect(result.message).toMatch(/401/);
|
|
});
|
|
|
|
it('surfaces API error messages from failed submissions', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(
|
|
async () =>
|
|
new Response(JSON.stringify({ error: { message: 'feedback rejected' } }), {
|
|
status: 400,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
}),
|
|
),
|
|
);
|
|
|
|
const result = await fetchSubmitFeedback(
|
|
'https://api.example/feedback',
|
|
'access-token',
|
|
SAMPLE_BODY,
|
|
);
|
|
|
|
expect(result.kind).toBe('error');
|
|
if (result.kind !== 'error') return;
|
|
expect(result.status).toBe(400);
|
|
expect(result.message).toBe('feedback rejected');
|
|
});
|
|
|
|
it('returns an error with status when the server responds 500', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async () => new Response('', { status: 500 })),
|
|
);
|
|
|
|
const result = await fetchSubmitFeedback(
|
|
'https://api.example/feedback',
|
|
'access-token',
|
|
SAMPLE_BODY,
|
|
);
|
|
|
|
expect(result.kind).toBe('error');
|
|
if (result.kind !== 'error') return;
|
|
expect(result.status).toBe(500);
|
|
expect(result.message).toBe('Failed to submit feedback: HTTP 500');
|
|
});
|
|
|
|
it('returns a timeout error when the request aborts', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(
|
|
(_url: string, init?: RequestInit) =>
|
|
new Promise<Response>((_, reject) => {
|
|
init?.signal?.addEventListener('abort', () => {
|
|
const err = new Error('aborted');
|
|
err.name = 'AbortError';
|
|
reject(err);
|
|
});
|
|
}),
|
|
),
|
|
);
|
|
|
|
const result = await fetchSubmitFeedback(
|
|
'https://api.example/feedback',
|
|
'access-token',
|
|
SAMPLE_BODY,
|
|
{ timeoutMs: 5 },
|
|
);
|
|
|
|
expect(result.kind).toBe('error');
|
|
if (result.kind !== 'error') return;
|
|
expect(result.status).toBeUndefined();
|
|
expect(result.message).toMatch(/timed out/);
|
|
});
|
|
|
|
it('returns a generic error message on network failure', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn(async () => {
|
|
throw new TypeError('network down');
|
|
}),
|
|
);
|
|
|
|
const result = await fetchSubmitFeedback(
|
|
'https://api.example/feedback',
|
|
'access-token',
|
|
SAMPLE_BODY,
|
|
);
|
|
|
|
expect(result.kind).toBe('error');
|
|
if (result.kind !== 'error') return;
|
|
expect(result.status).toBeUndefined();
|
|
expect(result.message).toMatch(/network down/);
|
|
});
|
|
});
|