mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 15:09:42 +02:00
Merge branch 'master' of https://github.com/Ark0N/Codeman into followups
This commit is contained in:
@@ -68,6 +68,9 @@ export class MockSession extends EventEmitter {
|
||||
this.lastSubmitAt = Date.now();
|
||||
}
|
||||
|
||||
/** Mirrors Session.trackUserInput (the send-key route feeds it around the write path). */
|
||||
trackUserInput(_data: string): void {}
|
||||
|
||||
private _muxName: string | null = null;
|
||||
|
||||
constructor(id: string = 'mock-session-id') {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @fileoverview PUT /api/sessions/:id/name hands the name to the user (#376).
|
||||
*
|
||||
* A rename flips `nameSource` to `manual`, persists it and broadcasts it, so
|
||||
* auto-naming can never overwrite a name a person chose, on this server or
|
||||
* on the one that restores the session after a restart.
|
||||
*
|
||||
* Uses app.inject() — no real HTTP ports needed.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
||||
import { registerSessionRoutes } from '../../src/web/routes/session-routes.js';
|
||||
import { createRouteTestHarness, type RouteTestHarness } from './_route-test-utils.js';
|
||||
import { Session } from '../../src/session.js';
|
||||
import { SseEvent } from '../../src/web/sse-events.js';
|
||||
|
||||
describe('PUT /api/sessions/:id/name', () => {
|
||||
let harness: RouteTestHarness;
|
||||
let session: Session;
|
||||
const updateSessionName = vi.fn(() => true);
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createRouteTestHarness(registerSessionRoutes);
|
||||
// A REAL session, since the ownership flag lives on the class, not the mock.
|
||||
session = new Session({ id: 'name-route-test', workingDir: '/tmp', name: 'w1-demo' });
|
||||
harness.ctx.sessions.set(session.id, session as never);
|
||||
(harness.ctx.mux as Record<string, unknown>).updateSessionName = updateSessionName;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness.app.close();
|
||||
});
|
||||
|
||||
it('flips a placeholder to manual, then persists and broadcasts the ownership', async () => {
|
||||
expect(session.nameSource).toBe('placeholder');
|
||||
|
||||
const res = await harness.app.inject({
|
||||
method: 'PUT',
|
||||
url: `/api/sessions/${session.id}/name`,
|
||||
payload: { name: 'my window' },
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
// The harness registers the bare route; the {success,data} envelope is a server-level hook.
|
||||
expect(res.json()).toMatchObject({ name: 'my window' });
|
||||
expect(session.name).toBe('my window');
|
||||
expect(session.nameSource).toBe('manual');
|
||||
expect(session.applyAutoName('w1-demo: fix it')).toBe(false);
|
||||
expect(session.name).toBe('my window');
|
||||
|
||||
expect(updateSessionName).toHaveBeenCalledWith(session.id, 'my window');
|
||||
expect(harness.ctx.persistSessionState).toHaveBeenCalledWith(session);
|
||||
expect(harness.ctx.broadcast).toHaveBeenCalledWith(
|
||||
SseEvent.SessionUpdated,
|
||||
expect.objectContaining({ id: session.id, name: 'my window', nameSource: 'manual' })
|
||||
);
|
||||
// What the restore path will read back: the persisted state carries the flag.
|
||||
expect(session.toState().nameSource).toBe('manual');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* @fileoverview Auto-naming a session after its first prompt (#376).
|
||||
*
|
||||
* The tracker sits on the raw keystroke stream, so most of these pin the
|
||||
* per-key rules that a review of the first cut found missing: a bare Esc ate
|
||||
* the next prompt's first character, a wheel report mid-word dropped half the
|
||||
* prompt, pasted newlines counted as Enter, and every prompt renamed the tab.
|
||||
*
|
||||
* Port: N/A (no server needed)
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Session } from '../src/session.js';
|
||||
import {
|
||||
SubmittedPromptTracker,
|
||||
deriveAutoSessionName,
|
||||
composeAutoSessionName,
|
||||
isGeneratedSessionName,
|
||||
} from '../src/session-auto-name.js';
|
||||
|
||||
describe('SubmittedPromptTracker', () => {
|
||||
it('reports the draft on Enter across arbitrary chunks, honouring backspace', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
expect(tracker.feed('fix the')).toEqual([]);
|
||||
expect(tracker.feed(' login bugs\x7f')).toEqual([]);
|
||||
expect(tracker.feed('\r')).toEqual(['fix the login bug']);
|
||||
expect(tracker.feed('\r')).toEqual([]);
|
||||
expect(tracker.feed('修复登录跳转\x08问题\r')).toEqual(['修复登录跳问题']);
|
||||
});
|
||||
|
||||
it('treats a bare Esc as the Esc key, not the start of a sequence', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
tracker.feed('\x1b');
|
||||
expect(tracker.feed('fix the login bug\r')).toEqual(['fix the login bug']);
|
||||
tracker.feed('\x1b');
|
||||
expect(tracker.feed('修复登录\r')).toEqual(['修复登录']);
|
||||
// Esc then digits and punctuation used to grow the escape buffer without bound.
|
||||
tracker.feed('\x1b');
|
||||
expect(tracker.feed('12345, ok?\r')).toEqual(['12345, ok?']);
|
||||
// A double Esc is two Esc keys, each its own write (in ONE chunk, `ESC s`
|
||||
// is Alt+s by the terminal's own encoding and stays swallowed).
|
||||
tracker.feed('\x1b');
|
||||
tracker.feed('\x1b');
|
||||
expect(tracker.feed('still here\r')).toEqual(['still here']);
|
||||
});
|
||||
|
||||
it('swallows Alt chords and turns Alt+Enter into a newline in the draft', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
expect(tracker.feed('fix\x1bb the\x1b\rbug\r')).toEqual(['fix the bug']);
|
||||
});
|
||||
|
||||
it('ignores cursor keys, mouse and focus reports, Shift+Tab and Tab', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
expect(tracker.feed('fix the \x1b[<64;10;5M\x1b[<65;10;5mlogin bug\r')).toEqual(['fix the login bug']);
|
||||
expect(tracker.feed('look at @src/ses\tsion.ts and fix it\r')).toEqual(['look at @src/session.ts and fix it']);
|
||||
expect(tracker.feed('typo\x1b[D\x1b[C\x1b[H\x1b[F\x1b[3~\x1b[Z\x1b[I\x1b[O\x1bOC fixed\r')).toEqual(['typo fixed']);
|
||||
expect(tracker.feed('mod\x1b[1;5D\x1b[1;2Cifiers\r')).toEqual(['modifiers']);
|
||||
});
|
||||
|
||||
it('taints the draft on history recall so Enter submits nothing rather than a fragment', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
expect(tracker.feed('old text\x1b[A and more\r')).toEqual([]);
|
||||
expect(tracker.feed('\x1bOB\r')).toEqual([]);
|
||||
expect(tracker.feed('\x1b[1;5A\r')).toEqual([]);
|
||||
expect(tracker.feed('\x10x\r')).toEqual([]);
|
||||
expect(tracker.feed('\x12search\r')).toEqual([]);
|
||||
expect(tracker.feed('fresh prompt\r')).toEqual(['fresh prompt']);
|
||||
// Ctrl+C empties the composer, which also clears the taint.
|
||||
expect(tracker.feed('stale\x1b[A\x03typed after\r')).toEqual(['typed after']);
|
||||
});
|
||||
|
||||
it('keeps bracketed-paste newlines inside the draft', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
expect(tracker.feed('\x1b[200~line one\nline two\r\nline three\x1b[201~ plus typed\r')).toEqual([
|
||||
'line one line two line three plus typed',
|
||||
]);
|
||||
// A paste split across chunks stays a paste.
|
||||
tracker.feed('\x1b[200~first\r');
|
||||
expect(tracker.feed('second\x1b[201~\r')).toEqual(['first second']);
|
||||
});
|
||||
|
||||
it('joins a Shift+Enter / Ctrl+J newline with a space', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
tracker.feed('Fix the login bug');
|
||||
tracker.feed('\n');
|
||||
expect(tracker.feed('Also add tests.\r')).toEqual(['Fix the login bug Also add tests.']);
|
||||
});
|
||||
|
||||
it('mirrors Ctrl+W, Ctrl+U and Ctrl+C', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
expect(tracker.feed('fix the bugs\x17bug\r')).toEqual(['fix the bug']);
|
||||
expect(tracker.feed('discarded\x15kept\r')).toEqual(['kept']);
|
||||
expect(tracker.feed('discarded\x03kept\r')).toEqual(['kept']);
|
||||
});
|
||||
|
||||
it('keeps the HEAD of an over-long draft', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
const [prompt] = tracker.feed(`${'a'.repeat(9000)}\r`);
|
||||
expect(prompt).toHaveLength(8192);
|
||||
// Backspaces past the cap consume the overflow before the kept text.
|
||||
const [again] = tracker.feed(`${'b'.repeat(8200)}${'\x7f'.repeat(10)}\r`);
|
||||
expect(again).toHaveLength(8190);
|
||||
});
|
||||
|
||||
it('abandons a malformed escape without eating the text, and taints on an over-long one', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
expect(tracker.feed('\x1b[修复\r')).toEqual(['修复']);
|
||||
expect(tracker.feed('\x1b]0;window title\x07hello\r')).toEqual(['hello']);
|
||||
// Nothing a terminal sends runs past 64 bytes; the tail is garbage, not a title.
|
||||
expect(tracker.feed(`\x1b]${'x'.repeat(80)}after\r`)).toEqual([]);
|
||||
expect(tracker.feed('next prompt\r')).toEqual(['next prompt']);
|
||||
});
|
||||
|
||||
it('resumes a CSI split across chunks', () => {
|
||||
const tracker = new SubmittedPromptTracker();
|
||||
tracker.feed('abc\x1b[');
|
||||
expect(tracker.feed('Ddef\r')).toEqual(['abcdef']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveAutoSessionName', () => {
|
||||
it('takes the first sentence, drops the full stop, and bounds the length', () => {
|
||||
expect(deriveAutoSessionName('Fix the login bug. Also add tests.')).toBe('Fix the login bug');
|
||||
expect(deriveAutoSessionName(' 修复登录跳转问题。\n不要改数据库')).toBe('修复登录跳转问题');
|
||||
expect(deriveAutoSessionName('Why does this crash? It worked before')).toBe('Why does this crash?');
|
||||
expect(deriveAutoSessionName('Run v2.0 tests. Then deploy')).toBe('Run v2.0 tests');
|
||||
expect(Array.from(deriveAutoSessionName('a'.repeat(200)) ?? '')).toHaveLength(72);
|
||||
const cut = deriveAutoSessionName('word '.repeat(40).trim()) ?? '';
|
||||
expect(cut.endsWith('…')).toBe(true);
|
||||
expect(cut).toMatch(/^(word )+word…$/);
|
||||
});
|
||||
|
||||
it('does not cut on an abbreviation early in the prompt', () => {
|
||||
expect(deriveAutoSessionName('e.g. fix this now')).toBe('e.g. fix this now');
|
||||
expect(deriveAutoSessionName('Ok. Fix the login bug')).toBe('Ok. Fix the login bug');
|
||||
});
|
||||
|
||||
it('returns null for commands and empties, but not for paths', () => {
|
||||
expect(deriveAutoSessionName('/clear')).toBeNull();
|
||||
expect(deriveAutoSessionName('/model opus')).toBeNull();
|
||||
expect(deriveAutoSessionName('/ralph-loop:ralph-loop')).toBeNull();
|
||||
expect(deriveAutoSessionName('! npm test')).toBeNull();
|
||||
expect(deriveAutoSessionName(' ')).toBeNull();
|
||||
expect(deriveAutoSessionName('/home/me/notes.txt what is this')).toBe('/home/me/notes.txt what is this');
|
||||
});
|
||||
|
||||
it('strips control bytes and ANSI before the title is persisted', () => {
|
||||
expect(deriveAutoSessionName('\x1b[31m整理项目文档\x1b[0m')).toBe('整理项目文档');
|
||||
expect(deriveAutoSessionName('a\x00b\tc')).toBe('a b c');
|
||||
});
|
||||
});
|
||||
|
||||
describe('composeAutoSessionName', () => {
|
||||
it('keeps the placeholder as a prefix so the case and the counter survive', () => {
|
||||
expect(composeAutoSessionName('w3-myapp', 'fix the login bug')).toBe('w3-myapp: fix the login bug');
|
||||
expect(composeAutoSessionName('', 'fix the login bug')).toBe('fix the login bug');
|
||||
});
|
||||
|
||||
it('honours the rename cap in UTF-16 units', () => {
|
||||
const name = composeAutoSessionName('w3-myapp', '😀'.repeat(100), 40);
|
||||
expect(name.length).toBeLessThanOrEqual(40);
|
||||
expect(name.startsWith('w3-myapp: ')).toBe(true);
|
||||
expect(name.endsWith('…')).toBe(true);
|
||||
expect(composeAutoSessionName('x'.repeat(127), 'title', 128)).toBe('x'.repeat(127));
|
||||
});
|
||||
|
||||
it('recognises only the generated w/s + number + case form', () => {
|
||||
expect(isGeneratedSessionName('w12-my_case-2')).toBe(true);
|
||||
expect(isGeneratedSessionName('s1-shell')).toBe(true);
|
||||
expect(isGeneratedSessionName('w1-case: fix it')).toBe(false);
|
||||
expect(isGeneratedSessionName('alpha')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session name ownership', () => {
|
||||
it('infers placeholder vs manual from the name and persists the source', () => {
|
||||
const placeholder = new Session({ workingDir: '/tmp', name: 'w1-demo' });
|
||||
expect(placeholder.nameSource).toBe('placeholder');
|
||||
expect(placeholder.toState().nameSource).toBe('placeholder');
|
||||
expect(new Session({ workingDir: '/tmp' }).nameSource).toBe('placeholder');
|
||||
expect(new Session({ workingDir: '/tmp', name: 'my window' }).nameSource).toBe('manual');
|
||||
expect(new Session({ workingDir: '/tmp', name: 'w1-demo: fix it' }).nameSource).toBe('manual');
|
||||
// The boot restore passes the persisted source, which outranks the inference.
|
||||
const recovered = new Session({ workingDir: '/tmp', name: 'w1-demo: fix it', nameSource: 'auto' });
|
||||
expect(recovered.nameSource).toBe('auto');
|
||||
expect(recovered.applyAutoName('w1-demo: other')).toBe(false);
|
||||
});
|
||||
|
||||
it('names once: the first prompt takes it, later prompts and renames do not', () => {
|
||||
const session = new Session({ workingDir: '/tmp', name: 'w1-demo' });
|
||||
expect(session.applyAutoName('w1-demo: fix the login bug')).toBe(true);
|
||||
expect(session.name).toBe('w1-demo: fix the login bug');
|
||||
expect(session.nameSource).toBe('auto');
|
||||
expect(session.applyAutoName('w1-demo: 1')).toBe(false);
|
||||
expect(session.name).toBe('w1-demo: fix the login bug');
|
||||
|
||||
session.name = 'mine';
|
||||
expect(session.nameSource).toBe('manual');
|
||||
expect(session.applyAutoName('other')).toBe(false);
|
||||
expect(session.name).toBe('mine');
|
||||
});
|
||||
|
||||
it('consumes the first prompt even when the composed name is unchanged', () => {
|
||||
const session = new Session({ workingDir: '/tmp', name: 'w1-demo' });
|
||||
expect(session.applyAutoName('w1-demo')).toBe(false);
|
||||
expect(session.nameSource).toBe('auto');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session promptSubmitted', () => {
|
||||
function withFakePty(session: Session): ReturnType<typeof vi.fn> {
|
||||
const write = vi.fn();
|
||||
(session as unknown as { ptyProcess: { write: typeof write } }).ptyProcess = { write };
|
||||
return write;
|
||||
}
|
||||
|
||||
it('emits for user input only, after the bytes reached the PTY', () => {
|
||||
const session = new Session({ workingDir: '/tmp', name: 'w1-demo' });
|
||||
const prompts: string[] = [];
|
||||
session.on('promptSubmitted', (p: string) => prompts.push(p));
|
||||
|
||||
// No PTY yet: the write fails and nothing is reported.
|
||||
expect(session.write('lost\r', { fromUser: true })).toBe(false);
|
||||
expect(prompts).toEqual([]);
|
||||
|
||||
const write = withFakePty(session);
|
||||
expect(session.write('Read @ralph_prompt.md and follow the instructions.\r')).toBe(true);
|
||||
expect(prompts).toEqual([]);
|
||||
expect(session.write('fix the ', { fromUser: true })).toBe(true);
|
||||
expect(session.write('login bug\r', { fromUser: true })).toBe(true);
|
||||
expect(prompts).toEqual(['fix the login bug']);
|
||||
expect(write).toHaveBeenCalledTimes(3);
|
||||
// The pane's last-Enter stamp is kept for EVERY write, user or not.
|
||||
expect(session.lastSubmitAt).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('never feeds the tracker for a shell session', () => {
|
||||
const session = new Session({ workingDir: '/tmp', name: 's1-demo', mode: 'shell' });
|
||||
const prompts: string[] = [];
|
||||
session.on('promptSubmitted', (p: string) => prompts.push(p));
|
||||
withFakePty(session);
|
||||
expect(session.write('ls -la\r', { fromUser: true })).toBe(true);
|
||||
session.trackUserInput('cd src\r');
|
||||
expect(prompts).toEqual([]);
|
||||
});
|
||||
|
||||
it('feeds the send-key line feed so a two-line prompt keeps its separator', () => {
|
||||
const session = new Session({ workingDir: '/tmp', name: 'w1-demo' });
|
||||
const prompts: string[] = [];
|
||||
session.on('promptSubmitted', (p: string) => prompts.push(p));
|
||||
withFakePty(session);
|
||||
session.write('Fix the login bug', { fromUser: true });
|
||||
session.trackUserInput('\n');
|
||||
session.write('Also add tests.\r', { fromUser: true });
|
||||
expect(prompts).toEqual(['Fix the login bug Also add tests.']);
|
||||
});
|
||||
|
||||
it('reports through writeViaMux only when the mux accepted the input', async () => {
|
||||
const session = new Session({ workingDir: '/tmp', name: 'w1-demo' });
|
||||
const prompts: string[] = [];
|
||||
session.on('promptSubmitted', (p: string) => prompts.push(p));
|
||||
const sendInput = vi.fn(async () => false);
|
||||
(session as unknown as { _mux: unknown; _muxSession: unknown })._mux = { sendInput };
|
||||
(session as unknown as { _mux: unknown; _muxSession: unknown })._muxSession = { sessionId: session.id };
|
||||
|
||||
expect(await session.writeViaMux('dropped\r', { fromUser: true })).toBe(false);
|
||||
expect(prompts).toEqual([]);
|
||||
sendInput.mockResolvedValue(true);
|
||||
expect(await session.writeViaMux('delivered\r', { fromUser: true })).toBe(true);
|
||||
expect(prompts).toEqual(['delivered']);
|
||||
expect(await session.writeViaMux('/clear\r')).toBe(true);
|
||||
expect(prompts).toEqual(['delivered']);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { Session } from '../src/session.js';
|
||||
import { createSessionListeners } from '../src/web/session-listener-wiring.js';
|
||||
import { SseEvent } from '../src/web/sse-events.js';
|
||||
|
||||
describe('session listener wiring', () => {
|
||||
it('forwards the attachment request source through registerAttachment', async () => {
|
||||
@@ -20,4 +21,70 @@ describe('session listener wiring', () => {
|
||||
);
|
||||
expect(registerAttachment).toHaveBeenNthCalledWith(2, 'wiring-attach-source-test', '/tmp/report.pdf', 'external');
|
||||
});
|
||||
|
||||
/** The listener reads the setting asynchronously; let its promise chain settle. */
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 5));
|
||||
|
||||
function autoNameDeps(session: Session, enabled: boolean) {
|
||||
const deps = {
|
||||
updateSessionName: vi.fn(() => true),
|
||||
persistSessionState: vi.fn(),
|
||||
broadcast: vi.fn(),
|
||||
getSessionStateWithRespawn: vi.fn(() => session.toState()),
|
||||
isAutoNameEnabled: vi.fn(async () => enabled),
|
||||
};
|
||||
return {
|
||||
deps,
|
||||
refs: createSessionListeners(session, deps as unknown as Parameters<typeof createSessionListeners>[1]),
|
||||
};
|
||||
}
|
||||
|
||||
it('names a placeholder tab after its first real prompt, in the prefix form, once', async () => {
|
||||
const session = new Session({ id: 'wiring-auto-name-test', workingDir: '/tmp', name: 'w1-demo' });
|
||||
const { deps, refs } = autoNameDeps(session, true);
|
||||
|
||||
// A slash command yields no title and leaves the session eligible; the
|
||||
// setting is not even read for it.
|
||||
refs.promptSubmitted('/clear');
|
||||
await flush();
|
||||
expect(deps.isAutoNameEnabled).not.toHaveBeenCalled();
|
||||
expect(session.name).toBe('w1-demo');
|
||||
|
||||
refs.promptSubmitted('整理登录模块并补充测试');
|
||||
await flush();
|
||||
expect(session.name).toBe('w1-demo: 整理登录模块并补充测试');
|
||||
expect(session.nameSource).toBe('auto');
|
||||
expect(deps.updateSessionName).toHaveBeenCalledWith('wiring-auto-name-test', 'w1-demo: 整理登录模块并补充测试');
|
||||
expect(deps.persistSessionState).toHaveBeenCalledWith(session);
|
||||
expect(deps.broadcast).toHaveBeenCalledWith(
|
||||
SseEvent.SessionUpdated,
|
||||
expect.objectContaining({ name: 'w1-demo: 整理登录模块并补充测试', nameSource: 'auto' })
|
||||
);
|
||||
|
||||
// The second prompt never reaches the setting: the tab is named.
|
||||
refs.promptSubmitted('1');
|
||||
await flush();
|
||||
expect(deps.isAutoNameEnabled).toHaveBeenCalledTimes(1);
|
||||
expect(session.name).toBe('w1-demo: 整理登录模块并补充测试');
|
||||
});
|
||||
|
||||
it('leaves the tab alone while the setting is off, and never touches a manual name', async () => {
|
||||
const session = new Session({ id: 'wiring-auto-name-off', workingDir: '/tmp', name: 'w1-demo' });
|
||||
const { deps, refs } = autoNameDeps(session, false);
|
||||
|
||||
refs.promptSubmitted('fix the login bug');
|
||||
await flush();
|
||||
expect(deps.isAutoNameEnabled).toHaveBeenCalledTimes(1);
|
||||
expect(session.name).toBe('w1-demo');
|
||||
// Still a placeholder: flipping the setting on names the NEXT prompt.
|
||||
expect(session.nameSource).toBe('placeholder');
|
||||
expect(deps.updateSessionName).not.toHaveBeenCalled();
|
||||
|
||||
session.name = '人工命名';
|
||||
refs.promptSubmitted('新的任务不能覆盖人工命名');
|
||||
await flush();
|
||||
expect(deps.isAutoNameEnabled).toHaveBeenCalledTimes(1);
|
||||
expect(session.name).toBe('人工命名');
|
||||
expect(deps.persistSessionState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user