fix(cli): terminal 출력 테스트를 안정화한다
This commit is contained in:
parent
baa1c0a890
commit
28f68a6880
6 changed files with 145 additions and 27 deletions
|
|
@ -1068,7 +1068,7 @@ echo '{"id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{}}}'
|
|||
Command: scriptPath,
|
||||
Args: []string{},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
proc, err := startCodexAppServerProc(ctx, profile, workspace)
|
||||
|
|
@ -1078,7 +1078,7 @@ echo '{"id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{}}}'
|
|||
defer proc.close()
|
||||
|
||||
markerPath := filepath.Join(workspace, "cwd.txt")
|
||||
cwdBytes := readFileEventually(t, markerPath, 2*time.Second)
|
||||
cwdBytes := readFileEventually(t, markerPath, 5*time.Second)
|
||||
cwd := strings.TrimSpace(string(cwdBytes))
|
||||
|
||||
resolvedWorkspace, err := os.Readlink(workspace)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ func TestCLIStartPersistentReturnsErrorWhenProcessExitsDuringStartup(t *testing.
|
|||
Args: []string{"-c", "exit 2"},
|
||||
Persistent: true,
|
||||
Terminal: true,
|
||||
StartupIdleTimeoutMS: 50,
|
||||
StartupIdleTimeoutMS: 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ func TestCLIStartCleansUpStartedProfilesWhenLaterProfileFails(t *testing.T) {
|
|||
Args: []string{"-c", "exit 2"},
|
||||
Persistent: true,
|
||||
Terminal: true,
|
||||
StartupIdleTimeoutMS: 50,
|
||||
StartupIdleTimeoutMS: 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -106,7 +106,7 @@ func TestCLIStartDeterministicOrder(t *testing.T) {
|
|||
Args: []string{"-c", "exit 2"},
|
||||
Persistent: true,
|
||||
Terminal: true,
|
||||
StartupIdleTimeoutMS: 50,
|
||||
StartupIdleTimeoutMS: 200,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,17 +183,19 @@ func (e *persistentExecutor) Execute(ctx context.Context, spec runtime.Execution
|
|||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
if idleTimer == nil {
|
||||
idleTimer = time.NewTimer(idleTimeout)
|
||||
idleC = idleTimer.C
|
||||
} else {
|
||||
if !idleTimer.Stop() {
|
||||
select {
|
||||
case <-idleTimer.C:
|
||||
default:
|
||||
if waitForFilteredMessage || outputFilter.HasOutput() || !profile.Terminal {
|
||||
if idleTimer == nil {
|
||||
idleTimer = time.NewTimer(idleTimeout)
|
||||
idleC = idleTimer.C
|
||||
} else {
|
||||
if !idleTimer.Stop() {
|
||||
select {
|
||||
case <-idleTimer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
idleTimer.Reset(idleTimeout)
|
||||
}
|
||||
idleTimer.Reset(idleTimeout)
|
||||
}
|
||||
case <-idleC:
|
||||
if delta := outputFilter.Flush(); delta != "" {
|
||||
|
|
|
|||
|
|
@ -251,7 +251,7 @@ func TestCLIExecutePersistentTerminalAcceptsClaudeBypassWarning(t *testing.T) {
|
|||
Persistent: true,
|
||||
Terminal: true,
|
||||
ResponseIdleTimeoutMS: 100,
|
||||
StartupIdleTimeoutMS: 50,
|
||||
StartupIdleTimeoutMS: 250,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -263,7 +263,7 @@ func TestCLIExecutePersistentTerminalAcceptsClaudeBypassWarning(t *testing.T) {
|
|||
}
|
||||
defer func() { _ = c.Stop(ctx) }()
|
||||
|
||||
execCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
execCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
sink := &testutil.FakeSink{}
|
||||
|
|
@ -297,7 +297,7 @@ func TestCLIExecutePersistentTerminalAcceptsClaudeWorkspaceTrustAndBypassWarning
|
|||
Persistent: true,
|
||||
Terminal: true,
|
||||
ResponseIdleTimeoutMS: 100,
|
||||
StartupIdleTimeoutMS: 50,
|
||||
StartupIdleTimeoutMS: 250,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -309,7 +309,7 @@ func TestCLIExecutePersistentTerminalAcceptsClaudeWorkspaceTrustAndBypassWarning
|
|||
}
|
||||
defer func() { _ = c.Stop(ctx) }()
|
||||
|
||||
execCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
execCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
sink := &testutil.FakeSink{}
|
||||
|
|
@ -355,7 +355,7 @@ func TestCLIExecutePersistentClaudeTUICancelsSessionLimitUpgradePrompt(t *testin
|
|||
}
|
||||
defer func() { _ = c.Stop(ctx) }()
|
||||
|
||||
execCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
execCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
sink := &testutil.FakeSink{}
|
||||
|
|
@ -409,7 +409,7 @@ func TestCLIExecutePersistentClaudeTUIReplaysPromptAfterStartupReadyRace(t *test
|
|||
}
|
||||
defer func() { _ = c.Stop(ctx) }()
|
||||
|
||||
execCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
execCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
sink := &testutil.FakeSink{}
|
||||
|
|
|
|||
|
|
@ -17,21 +17,26 @@ type persistentOutputFilter interface {
|
|||
CompletionMessage() string
|
||||
}
|
||||
|
||||
type passthroughOutputFilter struct{}
|
||||
type passthroughOutputFilter struct {
|
||||
emitted bool
|
||||
}
|
||||
|
||||
func (passthroughOutputFilter) Filter(chunk string) string {
|
||||
func (f *passthroughOutputFilter) Filter(chunk string) string {
|
||||
if chunk != "" {
|
||||
f.emitted = true
|
||||
}
|
||||
return chunk
|
||||
}
|
||||
|
||||
func (passthroughOutputFilter) Flush() string {
|
||||
func (f *passthroughOutputFilter) Flush() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (passthroughOutputFilter) HasOutput() bool {
|
||||
return false
|
||||
func (f *passthroughOutputFilter) HasOutput() bool {
|
||||
return f.emitted
|
||||
}
|
||||
|
||||
func (passthroughOutputFilter) CompletionMessage() string {
|
||||
func (f *passthroughOutputFilter) CompletionMessage() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +44,10 @@ func newPersistentOutputFilter(target string, profile config.CLIProfileConf, pro
|
|||
if profile.Terminal && isClaudeTerminalProfile(target, profile) {
|
||||
return newClaudeTUIOutputFilter(prompt, baselineAssistant)
|
||||
}
|
||||
return passthroughOutputFilter{}
|
||||
if profile.Terminal {
|
||||
return newTerminalPassthroughOutputFilter(prompt)
|
||||
}
|
||||
return &passthroughOutputFilter{}
|
||||
}
|
||||
|
||||
func isClaudeTerminalProfile(target string, profile config.CLIProfileConf) bool {
|
||||
|
|
@ -57,6 +65,80 @@ type claudeTUIOutputFilter struct {
|
|||
seenAssistant bool
|
||||
}
|
||||
|
||||
type terminalPassthroughOutputFilter struct {
|
||||
prompt string
|
||||
promptBuf strings.Builder
|
||||
promptDone bool
|
||||
suppressLineEnd bool
|
||||
emitted bool
|
||||
}
|
||||
|
||||
func newTerminalPassthroughOutputFilter(prompt string) *terminalPassthroughOutputFilter {
|
||||
return &terminalPassthroughOutputFilter{prompt: strings.TrimRight(prompt, "\r\n")}
|
||||
}
|
||||
|
||||
func (f *terminalPassthroughOutputFilter) Filter(chunk string) string {
|
||||
if chunk == "" {
|
||||
return ""
|
||||
}
|
||||
if !f.promptDone && f.prompt != "" {
|
||||
f.promptBuf.WriteString(chunk)
|
||||
raw := f.promptBuf.String()
|
||||
if strings.HasPrefix(f.prompt, raw) {
|
||||
if raw == f.prompt {
|
||||
f.promptDone = true
|
||||
f.suppressLineEnd = true
|
||||
f.promptBuf.Reset()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(raw, f.prompt) {
|
||||
rest := raw[len(f.prompt):]
|
||||
if rest == "" {
|
||||
f.promptDone = true
|
||||
f.suppressLineEnd = true
|
||||
f.promptBuf.Reset()
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(rest, "\r") || strings.HasPrefix(rest, "\n") {
|
||||
f.promptDone = true
|
||||
f.promptBuf.Reset()
|
||||
return f.emit(strings.TrimLeft(rest, "\r\n"))
|
||||
}
|
||||
}
|
||||
f.promptDone = true
|
||||
f.promptBuf.Reset()
|
||||
return f.emit(raw)
|
||||
}
|
||||
if f.suppressLineEnd {
|
||||
f.suppressLineEnd = false
|
||||
chunk = strings.TrimLeft(chunk, "\r\n")
|
||||
if chunk == "" {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return f.emit(chunk)
|
||||
}
|
||||
|
||||
func (f *terminalPassthroughOutputFilter) Flush() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *terminalPassthroughOutputFilter) HasOutput() bool {
|
||||
return f.emitted
|
||||
}
|
||||
|
||||
func (f *terminalPassthroughOutputFilter) CompletionMessage() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f *terminalPassthroughOutputFilter) emit(chunk string) string {
|
||||
if chunk != "" {
|
||||
f.emitted = true
|
||||
}
|
||||
return chunk
|
||||
}
|
||||
|
||||
type claudeMessageSnapshot struct {
|
||||
text string
|
||||
closed bool
|
||||
|
|
|
|||
|
|
@ -84,6 +84,40 @@ func TestPassthroughOutputFilterKeepsNonClaudeTerminalOutput(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTerminalPassthroughOutputFilterSuppressesPromptEcho(t *testing.T) {
|
||||
filter := newPersistentOutputFilter("raw-terminal", testProfile("sh", true), "hello", "")
|
||||
|
||||
if got := filter.Filter("he"); got != "" {
|
||||
t.Fatalf("first prompt echo Filter() = %q, want empty", got)
|
||||
}
|
||||
if filter.HasOutput() {
|
||||
t.Fatalf("HasOutput() = true while only prompt echo was seen")
|
||||
}
|
||||
if got := filter.Filter("llo\r\n"); got != "" {
|
||||
t.Fatalf("completed prompt echo Filter() = %q, want empty", got)
|
||||
}
|
||||
if filter.HasOutput() {
|
||||
t.Fatalf("HasOutput() = true after only prompt echo was seen")
|
||||
}
|
||||
|
||||
got := filter.Filter("reply:hello\r\n")
|
||||
if got != "reply:hello\r\n" {
|
||||
t.Fatalf("reply Filter() = %q, want reply", got)
|
||||
}
|
||||
if !filter.HasOutput() {
|
||||
t.Fatalf("HasOutput() = false after reply")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminalPassthroughOutputFilterSuppressesPromptEchoBeforeReplyInSameChunk(t *testing.T) {
|
||||
filter := newPersistentOutputFilter("raw-terminal", testProfile("sh", true), "hello", "")
|
||||
|
||||
got := filter.Filter("hello\r\nreply:hello\r\n")
|
||||
if got != "reply:hello\r\n" {
|
||||
t.Fatalf("Filter() = %q, want reply without echoed prompt", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeTUIOutputFilterSuppressesStartupNoiseBeforeAssistantMarker(t *testing.T) {
|
||||
filter := newClaudeTUIOutputFilter("hello")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue