mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
fix(#592): pin the screen-capture branch of last-response for copilot
GET /api/sessions/:id/last-response reads a copilot pane through capturePaneText, since a repainting full-screen TUI streams cursor moves rather than lines, and nothing pinned that at the route: had the branch stopped capturing, copilot would quietly have been segmented off the fused stream again. Three app.inject cases with a stubbed mux: the captured screen is what gets segmented (the stream text never shows), a null capture falls back to the buffer, and a line-streaming mode (opencode) never asks for the screen. The first two fail with the capture switched off. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
d170ad2295
commit
e7a6fb5067
1 file changed
+62
-1
@@ -14,9 +14,11 @@
|
|||||||
* block without it lost the "You" badge on prompts
|
* block without it lost the "You" badge on prompts
|
||||||
* - a pane that has produced no output reports hasContext: false rather than 404ing
|
* - a pane that has produced no output reports hasContext: false rather than 404ing
|
||||||
* - Claude mode still takes the Claude path (regression guard)
|
* - Claude mode still takes the Claude path (regression guard)
|
||||||
|
* - copilot (a repainting full-screen TUI) is read off the pane SCREEN via
|
||||||
|
* capturePaneText, and falls back to the buffer only when there is no screen
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeEach } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import Fastify, { type FastifyInstance } from 'fastify';
|
import Fastify, { type FastifyInstance } from 'fastify';
|
||||||
import fastifyCookie from '@fastify/cookie';
|
import fastifyCookie from '@fastify/cookie';
|
||||||
import { createMockRouteContext, createMockSession, type MockRouteContext } from '../mocks/index.js';
|
import { createMockRouteContext, createMockSession, type MockRouteContext } from '../mocks/index.js';
|
||||||
@@ -154,6 +156,65 @@ describe('GET /api/sessions/:id/last-response — external CLI panes', () => {
|
|||||||
expect(body.data.hasContext).toBe(false);
|
expect(body.data.hasContext).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('copilot reads the pane screen, not the stream', () => {
|
||||||
|
const rule = '─'.repeat(80);
|
||||||
|
const screenOf = (answer: string) =>
|
||||||
|
[
|
||||||
|
' ❯ what changed 11:30',
|
||||||
|
` ● ${answer}`,
|
||||||
|
' /tmp/work Session: 0 AIC used',
|
||||||
|
rule,
|
||||||
|
'❯',
|
||||||
|
rule,
|
||||||
|
' ← open sidebar · Interactive · Manual Approval · / commands · ? help',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
function stubCapture(result: string | null) {
|
||||||
|
const capturePaneText = vi.fn((_muxName: string) => result);
|
||||||
|
(harness.ctx.mux as unknown as { capturePaneText: typeof capturePaneText }).capturePaneText = capturePaneText;
|
||||||
|
return capturePaneText;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('segments the captured screen of the session pane', async () => {
|
||||||
|
session.mode = 'copilot';
|
||||||
|
// What the stream looks like for a repainting TUI once escapes are stripped: rows fused
|
||||||
|
// together, so a reader of the buffer would return this run of text.
|
||||||
|
session.terminalBuffer = 'STREAM-ONLY fused rows that are not the answer';
|
||||||
|
const capture = stubCapture(screenOf('The screen answer.'));
|
||||||
|
|
||||||
|
const { res, body } = await lastResponse();
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(capture).toHaveBeenCalledWith(session.muxName);
|
||||||
|
expect(body.data.text).toBe('The screen answer.');
|
||||||
|
expect(body.data.text).not.toContain('STREAM-ONLY');
|
||||||
|
expect(body.data.hasContext).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the buffer when the pane has no screen to capture', async () => {
|
||||||
|
session.mode = 'copilot';
|
||||||
|
session.terminalBuffer = screenOf('The buffer answer.');
|
||||||
|
const capture = stubCapture(null);
|
||||||
|
|
||||||
|
const { res, body } = await lastResponse();
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect(capture).toHaveBeenCalledWith(session.muxName);
|
||||||
|
expect(body.data.text).toBe('The buffer answer.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never captures the screen for a mode that streams lines', async () => {
|
||||||
|
session.mode = 'opencode';
|
||||||
|
session.terminalBuffer = PANE;
|
||||||
|
const capture = stubCapture(screenOf('must not be read'));
|
||||||
|
|
||||||
|
const { body } = await lastResponse();
|
||||||
|
|
||||||
|
expect(capture).not.toHaveBeenCalled();
|
||||||
|
expect(body.data.text).toContain('Documented the helper in docs/retry.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('leaves Claude mode on the Claude transcript path', async () => {
|
it('leaves Claude mode on the Claude transcript path', async () => {
|
||||||
// Regression guard: a claude pane must NOT be segmented off its terminal
|
// Regression guard: a claude pane must NOT be segmented off its terminal
|
||||||
// buffer, or a real transcript would be shadowed by scraped pane text.
|
// buffer, or a real transcript would be shadowed by scraped pane text.
|
||||||
|
|||||||
Reference in new issue
Block a user