Files
clide/test/builtin/claude/claude_composer_test.dart
jpmschweitzerandClaude Fable 5 a15b3fd098 feat(claude): reach bypass via Ctrl/Cmd+Shift+M + shift-click (T-510)
Restores the original intent that T-226's refinement wording lost: the
shift modifier is the explicit opt-in for bypassPermissions, not a
separate confirm flow. Plain Ctrl/Cmd+M keeps cycling the safe trio;
Ctrl/Cmd+Shift+M cycles the full list. The composer menu's bypass row
was permanently disabled, deferring to "the cockpit's confirmed path" —
but that roster is ghost-fed (T-396), so bypass was unreachable from
the primary session's UI entirely. The row now no-ops on a plain click
(menu stays open) and selects on shift-click, with a hint naming the
gesture. Roster badge and /permissions paths unchanged.

lib/test_app.dart is a format-only follow-up to the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 12:54:11 +02:00

606 lines
25 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// Tests for the native Claude input composer (T-138): Enter submits,
/// Shift+Enter does not, blank input is ignored, and submitted text is
/// PTY-encoded for `pane.write`.
library;
import 'package:clide/builtin/claude/src/claude_composer.dart';
import 'package:clide/builtin/claude/src/clipboard_paste.dart';
import 'package:clide/builtin/claude/src/slash_commands.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
import '../../helpers/widget_harness.dart';
void main() {
group('encodeClaudeInput', () {
test('single-line input gets a trailing carriage return', () {
expect(encodeClaudeInput('hello claude'), 'hello claude\r');
});
test('multi-line input is wrapped in bracketed-paste markers', () {
expect(encodeClaudeInput('line one\nline two'), '\x1b[200~line one\nline two\x1b[201~\r');
});
});
group('ClaudeComposer', () {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() => f.dispose());
Future<List<String>> pump(WidgetTester tester, {bool enabled = true}) async {
final submitted = <String>[];
await tester.pumpWidget(harness(f, ClaudeComposer(enabled: enabled, onSubmit: submitted.add)));
return submitted;
}
testWidgets('Enter submits the text and clears the field', (tester) async {
final submitted = await pump(tester);
await tester.enterText(find.byType(EditableText), 'hey there');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(submitted, ['hey there']);
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, isEmpty);
});
testWidgets('Shift+Enter does not submit', (tester) async {
final submitted = await pump(tester);
await tester.enterText(find.byType(EditableText), 'draft');
await tester.pump();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pump();
expect(submitted, isEmpty);
});
testWidgets('blank input is ignored', (tester) async {
final submitted = await pump(tester);
await tester.enterText(find.byType(EditableText), ' ');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(submitted, isEmpty);
});
testWidgets('disabled composer is read-only', (tester) async {
await pump(tester, enabled: false);
expect(tester.widget<EditableText>(find.byType(EditableText)).readOnly, isTrue);
});
Future<void> pasteAttachment(WidgetTester tester) async {
tester.widget<EditableText>(find.byType(EditableText)).focusNode.requestFocus();
await tester.pump();
await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
await tester.sendKeyEvent(LogicalKeyboardKey.keyV);
await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
await tester.pumpAndSettle();
}
testWidgets('Ctrl+V of a file shows a chip and leaves the text field empty', (tester) async {
// WidgetsApp provides DefaultTextEditingShortcuts in the real app;
// the bare harness doesn't, so wrap explicitly to map Ctrl+V ->
// PasteTextIntent, which the composer's Actions override intercepts.
await tester.pumpWidget(
harness(
f,
DefaultTextEditingShortcuts(
child: ClaudeComposer(
onSubmit: (_) {},
pasteResolver: () async => const [ComposerAttachment(path: '/tmp/notes.txt', isImage: false)],
),
),
),
);
await pasteAttachment(tester);
expect(find.text('notes.txt'), findsOneWidget);
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, isEmpty);
});
testWidgets('submit appends attachment @path tokens to the message', (tester) async {
final submitted = <String>[];
await tester.pumpWidget(
harness(
f,
DefaultTextEditingShortcuts(
child: ClaudeComposer(
onSubmit: submitted.add,
pasteResolver: () async => const [ComposerAttachment(path: '/tmp/notes.txt', isImage: false)],
),
),
),
);
await tester.enterText(find.byType(EditableText), 'look at this');
await pasteAttachment(tester);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
expect(submitted, ['look at this @/tmp/notes.txt']);
// Chip cleared after send.
expect(find.text('notes.txt'), findsNothing);
});
Future<List<String>> pumpWithCommands(WidgetTester tester, List<String> commands) async {
final submitted = <String>[];
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: submitted.add, slashCommandsResolver: () => commands)));
return submitted;
}
testWidgets('typing a slash opens the typeahead with matching commands', (tester) async {
await pumpWithCommands(tester, ['model', 'memory', 'clear']);
await tester.enterText(find.byType(EditableText), '/m');
await tester.pump();
await tester.pump(); // ClideTypeahead inserts the popover post-frame
expect(find.text('/model'), findsOneWidget);
expect(find.text('/memory'), findsOneWidget);
expect(find.text('/clear'), findsNothing);
});
testWidgets('an inline slash (mid-message) also opens the typeahead', (tester) async {
await pumpWithCommands(tester, ['clear', 'compact']);
await tester.enterText(find.byType(EditableText), 'hey /cl');
await tester.pump();
await tester.pump(); // ClideTypeahead inserts the popover post-frame
expect(find.text('/clear'), findsOneWidget);
});
testWidgets('arrow-down + Enter completes the selected command (no submit)', (tester) async {
// The composer unions kClideOwnedCommands onto the resolver's list
// (T-162), so '/m' yields [mcp, memory, model] — 'mcp' joined the owned
// set in T-413. Two arrow-downs reach 'model'.
final submitted = await pumpWithCommands(tester, ['model', 'memory']);
await tester.enterText(find.byType(EditableText), '/m');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // → 'memory'
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // → 'model'
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
await tester.pump(); // popover closes post-frame after completion
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, '/model ');
expect(submitted, isEmpty, reason: 'Enter completes, it does not submit, while the popup is open');
expect(find.text('/memory'), findsNothing, reason: 'popup closes after completion');
});
testWidgets('Tab completes the top suggestion', (tester) async {
await pumpWithCommands(tester, ['clear', 'compact']);
await tester.enterText(find.byType(EditableText), '/cle');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, '/clear ');
});
testWidgets('a hyphenated command keeps filtering through the dash and Tab completes it (T-278)', (tester) async {
await pumpWithCommands(tester, ['add-dir', 'add-context', 'model']);
await tester.enterText(find.byType(EditableText), '/add-');
await tester.pump();
await tester.pump(); // ClideTypeahead inserts the popover post-frame
expect(find.text('/add-dir'), findsOneWidget);
expect(find.text('/add-context'), findsOneWidget);
expect(find.text('/model'), findsNothing);
// Typing further through the hyphen narrows the list (it does not empty it).
await tester.enterText(find.byType(EditableText), '/add-d');
await tester.pump();
await tester.pump();
expect(find.text('/add-dir'), findsOneWidget);
expect(find.text('/add-context'), findsNothing);
// Tab reliably accepts the highlighted suggestion.
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, '/add-dir ');
});
testWidgets('Escape dismisses the typeahead', (tester) async {
await pumpWithCommands(tester, ['model']);
await tester.enterText(find.byType(EditableText), '/mo');
await tester.pump();
await tester.pump(); // ClideTypeahead inserts the popover post-frame
expect(find.text('/model'), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
await tester.pump(); // popover closes post-frame
expect(find.text('/model'), findsNothing);
});
testWidgets('with the popup closed, Enter still submits', (tester) async {
final submitted = await pumpWithCommands(tester, ['model']);
await tester.enterText(find.byType(EditableText), 'plain message');
await tester.pump();
expect(find.text('/model'), findsNothing);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(submitted, ['plain message']);
});
testWidgets('remove × cancels the attachment before send', (tester) async {
final submitted = <String>[];
await tester.pumpWidget(
harness(
f,
DefaultTextEditingShortcuts(
child: ClaudeComposer(
onSubmit: submitted.add,
pasteResolver: () async => const [ComposerAttachment(path: '/tmp/notes.txt', isImage: false)],
),
),
),
);
await pasteAttachment(tester);
expect(find.text('notes.txt'), findsOneWidget);
await tester.tap(find.byKey(const ValueKey('composer-remove-/tmp/notes.txt')));
await tester.pumpAndSettle();
expect(find.text('notes.txt'), findsNothing);
await tester.enterText(find.byType(EditableText), 'just text');
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
expect(submitted, ['just text']);
});
testWidgets('Escape interrupts when the typeahead is closed', (tester) async {
var interrupts = 0;
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onInterrupt: () => interrupts++)));
tester.widget<EditableText>(find.byType(EditableText)).focusNode.requestFocus();
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(interrupts, 1);
});
testWidgets('Escape closes the typeahead before it interrupts', (tester) async {
var interrupts = 0;
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onInterrupt: () => interrupts++, slashCommandsResolver: () => ['model'])));
await tester.enterText(find.byType(EditableText), '/mo');
await tester.pump();
await tester.pump(); // ClideTypeahead inserts the popover post-frame
expect(find.text('/model'), findsOneWidget);
// First Escape only dismisses the popup; it does not interrupt.
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
await tester.pump(); // popover closes post-frame
expect(find.text('/model'), findsNothing);
expect(interrupts, 0);
// A second Escape, now with the popup closed, interrupts.
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(interrupts, 1);
});
testWidgets('the Stop button shows when busy and interrupts on tap', (tester) async {
var interrupts = 0;
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, busy: true, onInterrupt: () => interrupts++)));
expect(find.text('Stop ⎋'), findsOneWidget);
await tester.tap(find.text('Stop ⎋'));
await tester.pump();
expect(interrupts, 1);
});
testWidgets('no Stop button when idle', (tester) async {
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onInterrupt: () {})));
expect(find.text('Stop ⎋'), findsNothing);
});
// T-162: clide-owned commands surface in typeahead even when absent from the
// CLI probe (slashCommandsResolver).
testWidgets('clide-owned /resume surfaces even when absent from the probe list', (tester) async {
// The probe list (CLI-sourced) only has 'model' — no 'resume' or 'fork'.
await pumpWithCommands(tester, ['model']);
await tester.enterText(find.byType(EditableText), '/res');
await tester.pump();
await tester.pump(); // ClideTypeahead inserts the popover post-frame
// /resume must appear (sourced from kClideOwnedCommands).
expect(find.text('/resume'), findsOneWidget);
// /model must not appear (doesn't match '/res').
expect(find.text('/model'), findsNothing);
});
testWidgets('clide-owned /clear surfaces without duplicate when also in probe', (tester) async {
// 'clear' is in both the probe list AND kClideOwnedCommands.
await pumpWithCommands(tester, ['clear', 'model']);
await tester.enterText(find.byType(EditableText), '/cl');
await tester.pump();
await tester.pump(); // ClideTypeahead inserts the popover post-frame
// /clear must appear exactly once (filterSlashCommands de-dupes via seen set).
expect(find.text('/clear'), findsOneWidget);
});
testWidgets('clide-owned commands are reachable via the default resolver', (tester) async {
// No slashCommandsResolver → default path; kClideOwnedCommands must be included.
final submitted = <String>[];
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: submitted.add)));
await tester.enterText(find.byType(EditableText), '/fo');
await tester.pump();
await tester.pump(); // ClideTypeahead inserts the popover post-frame
// /fork is a kClideOwnedCommands member; it must appear without a probe.
expect(find.text('/fork'), findsOneWidget);
// Sanity: kClideOwnedCommands is the source (not a coincidence).
expect(kClideOwnedCommands, contains('fork'));
});
});
group('ClaudeComposer mode cycle (T-226)', () {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() => f.dispose());
testWidgets('Ctrl+M fires onCycleMode and is consumed', (tester) async {
var cycles = 0;
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onCycleMode: () => cycles++)));
await tester.tap(find.byType(EditableText));
await tester.pump();
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyM);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(cycles, 1);
// The chord didn't type an 'm' into the field.
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, isEmpty);
});
testWidgets('plain m types normally (no modifier, no cycle)', (tester) async {
var cycles = 0;
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onCycleMode: () => cycles++)));
await tester.enterText(find.byType(EditableText), 'm');
await tester.pump();
expect(cycles, 0);
});
testWidgets('Ctrl+Shift+M fires onCycleModeFull, not onCycleMode (T-510)', (tester) async {
var safe = 0, full = 0;
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onCycleMode: () => safe++, onCycleModeFull: () => full++)));
await tester.tap(find.byType(EditableText));
await tester.pump();
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyM);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(full, 1);
expect(safe, 0);
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, isEmpty);
});
testWidgets('plain Ctrl+M still cycles the safe trio only', (tester) async {
var safe = 0, full = 0;
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onCycleMode: () => safe++, onCycleModeFull: () => full++)));
await tester.tap(find.byType(EditableText));
await tester.pump();
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyM);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(safe, 1);
expect(full, 0);
});
});
group('ClaudeComposer external focus node (T-227)', () {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() => f.dispose());
testWidgets('uses a supplied focus node and focuses the input on request', (tester) async {
final node = FocusNode();
addTearDown(node.dispose);
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, focusNode: node)));
expect(tester.widget<EditableText>(find.byType(EditableText)).focusNode, same(node));
node.requestFocus(); // what the pane does on a background tap
await tester.pump();
expect(node.hasFocus, isTrue);
});
testWidgets('key handling still works through the supplied node', (tester) async {
final submitted = <String>[];
final node = FocusNode();
addTearDown(node.dispose);
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: submitted.add, focusNode: node)));
await tester.enterText(find.byType(EditableText), 'via external node');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(submitted, ['via external node']);
});
});
group('ClaudeComposer prompt history (T-163)', () {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() => f.dispose());
String text(WidgetTester tester) => tester.widget<EditableText>(find.byType(EditableText)).controller.text;
Future<void> pumpWithHistory(WidgetTester tester, {required List<String> history, ValueChanged<TextEditingValue>? onDraftChanged}) async {
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, history: history, onDraftChanged: onDraftChanged)));
await tester.tap(find.byType(EditableText));
await tester.pump();
}
testWidgets('Up walks back through history, Down walks forward', (tester) async {
await pumpWithHistory(tester, history: ['first', 'second', 'third']);
await tester.enterText(find.byType(EditableText), 'wip');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(text(tester), 'third'); // newest first
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(text(tester), 'second');
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(text(tester), 'third');
});
testWidgets('Down past the newest entry restores the in-progress draft', (tester) async {
await pumpWithHistory(tester, history: ['old']);
await tester.enterText(find.byType(EditableText), 'my draft');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); // → 'old'
await tester.pump();
expect(text(tester), 'old');
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); // past newest → restore
await tester.pump();
expect(text(tester), 'my draft');
});
testWidgets('Up does nothing when there is no history', (tester) async {
await pumpWithHistory(tester, history: const []);
await tester.enterText(find.byType(EditableText), 'solo');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(text(tester), 'solo');
});
testWidgets('Up off the first line does not recall (multiline edits first)', (tester) async {
await pumpWithHistory(tester, history: ['recalled']);
// Caret ends up on the last line of a two-line draft.
await tester.enterText(find.byType(EditableText), 'line one\nline two');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(text(tester), 'line one\nline two'); // unchanged — no recall
});
testWidgets('previewing history does not overwrite the persisted draft', (tester) async {
final drafts = <TextEditingValue>[];
await pumpWithHistory(tester, history: ['past'], onDraftChanged: drafts.add);
await tester.enterText(find.byType(EditableText), 'keep me');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); // preview 'past'
await tester.pump();
// The last *persisted* draft is still the user's text, not the preview.
expect(drafts.last.text, 'keep me');
});
});
group('ClaudeComposer draft persistence (T-228)', () {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() => f.dispose());
testWidgets('seeds the field from initialValue on mount', (tester) async {
await tester.pumpWidget(
harness(
f,
ClaudeComposer(
onSubmit: (_) {},
initialValue: const TextEditingValue(text: 'half-typed', selection: TextSelection.collapsed(offset: 4)),
),
),
);
final controller = tester.widget<EditableText>(find.byType(EditableText)).controller;
expect(controller.text, 'half-typed');
expect(controller.selection.baseOffset, 4); // caret restored too
});
testWidgets('reports draft changes (text + caret) via onDraftChanged', (tester) async {
final drafts = <TextEditingValue>[];
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onDraftChanged: drafts.add)));
await tester.enterText(find.byType(EditableText), 'draft text');
await tester.pump();
expect(drafts.last.text, 'draft text');
});
testWidgets('reports an empty draft when submitting clears the field', (tester) async {
final drafts = <TextEditingValue>[];
await tester.pumpWidget(harness(f, ClaudeComposer(onSubmit: (_) {}, onDraftChanged: drafts.add)));
await tester.enterText(find.byType(EditableText), 'send me');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
// The last reported value is empty — owners drop the draft so a sent
// message doesn't come back.
expect(drafts.last.text, isEmpty);
});
testWidgets('round-trips a draft across an interaction-zone swap', (tester) async {
// Mirrors the pane's pattern: a host holds the draft and shows either
// the prompt (composer gone) or the composer seeded from that draft.
final showPrompt = ValueNotifier(false);
addTearDown(showPrompt.dispose);
await tester.pumpWidget(harness(f, _DraftSwapHost(showPrompt: showPrompt)));
await tester.enterText(find.byType(EditableText), 'survived the prompt');
await tester.pump();
// Prompt arrives — the composer is torn down.
showPrompt.value = true;
await tester.pump();
expect(find.byType(EditableText), findsNothing);
// Prompt resolved — composer remounts and restores the draft.
showPrompt.value = false;
await tester.pump();
expect(tester.widget<EditableText>(find.byType(EditableText)).controller.text, 'survived the prompt');
});
});
}
/// Mimics the pane's draft handling (T-228): holds a per-host draft and
/// renders either a prompt placeholder (composer torn down) or the
/// composer seeded from that draft.
class _DraftSwapHost extends StatefulWidget {
const _DraftSwapHost({required this.showPrompt});
final ValueListenable<bool> showPrompt;
@override
State<_DraftSwapHost> createState() => _DraftSwapHostState();
}
class _DraftSwapHostState extends State<_DraftSwapHost> {
TextEditingValue? _draft;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<bool>(
valueListenable: widget.showPrompt,
builder: (context, prompt, _) =>
prompt ? const SizedBox.shrink() : ClaudeComposer(onSubmit: (_) {}, initialValue: _draft, onDraftChanged: (v) => _draft = v),
);
}
}