test(core): add P0 beta release tests for queue overflow, /compress, executeCardAction, and multi-workspace handling
generated by llmgit Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
c4ebe6c39f
commit
398dd5da0f
1 file changed
+630
@@ -3776,3 +3776,633 @@ func TestExecuteCardAction_ModeCleansUpWithInteractiveKey(t *testing.T) {
|
||||
t.Error("expected interactive state to be cleaned up after /mode")
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// P0 Beta release tests
|
||||
// ===========================================================================
|
||||
|
||||
// --- 1. Message queue overflow ---
|
||||
|
||||
func TestQueueMessageOverflow_DropsOldestAndReturnsfalse(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
sess := newQueuingSession("qs-overflow")
|
||||
agent := &controllableAgent{nextSession: sess}
|
||||
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
|
||||
|
||||
key := "test:overflow-user"
|
||||
|
||||
state := &interactiveState{
|
||||
agentSession: sess,
|
||||
platform: p,
|
||||
replyCtx: "ctx",
|
||||
}
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[key] = state
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
// Fill the queue to maxQueuedMessages (5).
|
||||
for i := 0; i < maxQueuedMessages; i++ {
|
||||
msg := &Message{SessionKey: key, Content: fmt.Sprintf("msg-%d", i), ReplyCtx: fmt.Sprintf("ctx-%d", i)}
|
||||
ok := e.queueMessageForBusySession(p, msg, key)
|
||||
if !ok {
|
||||
t.Fatalf("expected msg-%d to be queued, got false", i)
|
||||
}
|
||||
}
|
||||
|
||||
state.mu.Lock()
|
||||
if len(state.pendingMessages) != maxQueuedMessages {
|
||||
t.Fatalf("queue depth = %d, want %d", len(state.pendingMessages), maxQueuedMessages)
|
||||
}
|
||||
state.mu.Unlock()
|
||||
|
||||
// The 6th message should be rejected (returns false).
|
||||
overflow := &Message{SessionKey: key, Content: "msg-overflow", ReplyCtx: "ctx-overflow"}
|
||||
ok := e.queueMessageForBusySession(p, overflow, key)
|
||||
if ok {
|
||||
t.Fatal("expected 6th message to be rejected (queue full)")
|
||||
}
|
||||
|
||||
// Queue should still have exactly maxQueuedMessages items (the original 5).
|
||||
state.mu.Lock()
|
||||
if len(state.pendingMessages) != maxQueuedMessages {
|
||||
t.Fatalf("queue depth after overflow = %d, want %d", len(state.pendingMessages), maxQueuedMessages)
|
||||
}
|
||||
// First message should still be msg-0 (FIFO preserved, no silent drop).
|
||||
if state.pendingMessages[0].content != "msg-0" {
|
||||
t.Fatalf("first queued = %q, want msg-0", state.pendingMessages[0].content)
|
||||
}
|
||||
state.mu.Unlock()
|
||||
|
||||
// Platform should have received the MsgMessageQueued replies for the 5 accepted + nothing for rejected.
|
||||
sent := p.getSent()
|
||||
if len(sent) != maxQueuedMessages {
|
||||
t.Fatalf("platform replies = %d, want %d (one per accepted queue)", len(sent), maxQueuedMessages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueMessage_NoState_ReturnsFalse(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
e := newTestEngine()
|
||||
|
||||
msg := &Message{SessionKey: "nonexistent:key", Content: "hello"}
|
||||
ok := e.queueMessageForBusySession(p, msg, "nonexistent:key")
|
||||
if ok {
|
||||
t.Fatal("expected false when no interactive state exists")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueMessage_DeadSession_ReturnsFalse(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
sess := newQueuingSession("dead")
|
||||
sess.alive = false
|
||||
agent := &controllableAgent{nextSession: sess}
|
||||
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
|
||||
|
||||
key := "test:dead-session"
|
||||
state := &interactiveState{
|
||||
agentSession: sess,
|
||||
platform: p,
|
||||
}
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[key] = state
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
msg := &Message{SessionKey: key, Content: "hello"}
|
||||
ok := e.queueMessageForBusySession(p, msg, key)
|
||||
if ok {
|
||||
t.Fatal("expected false for dead session")
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2. /compress flow ---
|
||||
|
||||
type stubCompressorAgent struct {
|
||||
stubAgent
|
||||
cmd string
|
||||
}
|
||||
|
||||
func (a *stubCompressorAgent) CompressCommand() string { return a.cmd }
|
||||
|
||||
func TestCmdCompress_NoCompressor_RepliesNotSupported(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
|
||||
|
||||
msg := &Message{SessionKey: "test:user1", Content: "/compress", ReplyCtx: "ctx"}
|
||||
e.cmdCompress(p, msg)
|
||||
|
||||
sent := p.getSent()
|
||||
if len(sent) == 0 {
|
||||
t.Fatal("expected a reply")
|
||||
}
|
||||
if !strings.Contains(sent[0], e.i18n.T(MsgCompressNotSupported)) {
|
||||
t.Fatalf("expected MsgCompressNotSupported, got %q", sent[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdCompress_NoSession_RepliesNoSession(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
agent := &stubCompressorAgent{cmd: "/compact"}
|
||||
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
|
||||
|
||||
msg := &Message{SessionKey: "test:user1", Content: "/compress", ReplyCtx: "ctx"}
|
||||
e.cmdCompress(p, msg)
|
||||
|
||||
sent := p.getSent()
|
||||
if len(sent) == 0 {
|
||||
t.Fatal("expected a reply")
|
||||
}
|
||||
if !strings.Contains(sent[0], e.i18n.T(MsgCompressNoSession)) {
|
||||
t.Fatalf("expected MsgCompressNoSession, got %q", sent[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdCompress_SessionBusy_RepliesPreviousProcessing(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
sess := newQueuingSession("compress-busy")
|
||||
agent := &stubCompressorAgent{cmd: "/compact"}
|
||||
agent.stubAgent = stubAgent{}
|
||||
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
|
||||
|
||||
key := "test:user1"
|
||||
state := &interactiveState{
|
||||
agentSession: sess,
|
||||
platform: p,
|
||||
}
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[key] = state
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
// Lock the session to simulate busy.
|
||||
session := e.sessions.GetOrCreateActive(key)
|
||||
if !session.TryLock() {
|
||||
t.Fatal("expected TryLock to succeed")
|
||||
}
|
||||
|
||||
msg := &Message{SessionKey: key, Content: "/compress", ReplyCtx: "ctx"}
|
||||
e.cmdCompress(p, msg)
|
||||
|
||||
sent := p.getSent()
|
||||
found := false
|
||||
for _, s := range sent {
|
||||
if strings.Contains(s, e.i18n.T(MsgPreviousProcessing)) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected MsgPreviousProcessing reply, got %v", sent)
|
||||
}
|
||||
session.Unlock()
|
||||
}
|
||||
|
||||
func TestCmdCompress_Success_SendsCompressDone(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
sess := newQueuingSession("compress-ok")
|
||||
agent := &stubCompressorAgent{cmd: "/compact"}
|
||||
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
|
||||
|
||||
key := "test:user1"
|
||||
state := &interactiveState{
|
||||
agentSession: sess,
|
||||
platform: p,
|
||||
replyCtx: "ctx",
|
||||
}
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[key] = state
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
msg := &Message{SessionKey: key, Content: "/compress", ReplyCtx: "ctx"}
|
||||
e.cmdCompress(p, msg)
|
||||
|
||||
// Wait for Send to be called (happens after drainEvents), then inject the result event.
|
||||
deadline := time.After(3 * time.Second)
|
||||
for {
|
||||
sess.sendMu.Lock()
|
||||
n := len(sess.sendCalls)
|
||||
sess.sendMu.Unlock()
|
||||
if n > 0 {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("timed out waiting for compress Send call")
|
||||
default:
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
sess.events <- Event{Type: EventResult, Content: "", Done: true}
|
||||
|
||||
for {
|
||||
sent := p.getSent()
|
||||
foundDone := false
|
||||
for _, s := range sent {
|
||||
if strings.Contains(s, e.i18n.T(MsgCompressDone)) {
|
||||
foundDone = true
|
||||
}
|
||||
}
|
||||
if foundDone {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatalf("timed out waiting for MsgCompressDone, sent = %v", p.getSent())
|
||||
default:
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdCompress_WithText_SendsResult(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
sess := newQueuingSession("compress-text")
|
||||
agent := &stubCompressorAgent{cmd: "/compact"}
|
||||
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
|
||||
|
||||
key := "test:user1"
|
||||
state := &interactiveState{
|
||||
agentSession: sess,
|
||||
platform: p,
|
||||
replyCtx: "ctx",
|
||||
}
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[key] = state
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
msg := &Message{SessionKey: key, Content: "/compress", ReplyCtx: "ctx"}
|
||||
e.cmdCompress(p, msg)
|
||||
|
||||
// Wait for Send to be called (happens after drainEvents).
|
||||
deadline := time.After(3 * time.Second)
|
||||
for {
|
||||
sess.sendMu.Lock()
|
||||
n := len(sess.sendCalls)
|
||||
sess.sendMu.Unlock()
|
||||
if n > 0 {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("timed out waiting for compress Send call")
|
||||
default:
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
sess.events <- Event{Type: EventText, Content: "Compressed to 50%"}
|
||||
sess.events <- Event{Type: EventResult, Content: "Compression complete", Done: true}
|
||||
|
||||
for {
|
||||
sent := p.getSent()
|
||||
foundResult := false
|
||||
for _, s := range sent {
|
||||
if strings.Contains(s, "Compression complete") {
|
||||
foundResult = true
|
||||
}
|
||||
}
|
||||
if foundResult {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatalf("timed out waiting for compress result, sent = %v", p.getSent())
|
||||
default:
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdCompress_DrainsQueueAfterSuccess(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
sess := newQueuingSession("compress-drain")
|
||||
agent := &stubCompressorAgent{cmd: "/compact"}
|
||||
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
|
||||
|
||||
key := "test:user1"
|
||||
state := &interactiveState{
|
||||
agentSession: sess,
|
||||
platform: p,
|
||||
replyCtx: "ctx",
|
||||
pendingMessages: []queuedMessage{
|
||||
{platform: p, replyCtx: "ctx-q1", content: "queued-after-compress"},
|
||||
},
|
||||
}
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[key] = state
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
msg := &Message{SessionKey: key, Content: "/compress", ReplyCtx: "ctx"}
|
||||
e.cmdCompress(p, msg)
|
||||
|
||||
// Complete compress.
|
||||
sess.events <- Event{Type: EventResult, Content: "", Done: true}
|
||||
|
||||
// Wait for Send to be called (drain of queued message).
|
||||
deadline := time.After(3 * time.Second)
|
||||
for {
|
||||
sess.sendMu.Lock()
|
||||
n := len(sess.sendCalls)
|
||||
sess.sendMu.Unlock()
|
||||
if n > 0 {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("timed out waiting for queued message to be sent after compress")
|
||||
default:
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// Provide events for the drained turn so processInteractiveEvents completes.
|
||||
sess.events <- Event{Type: EventResult, Content: "drain-done", Done: true}
|
||||
|
||||
// Verify the queued message was actually sent.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
sess.sendMu.Lock()
|
||||
calls := make([]string, len(sess.sendCalls))
|
||||
copy(calls, sess.sendCalls)
|
||||
sess.sendMu.Unlock()
|
||||
|
||||
if len(calls) == 0 {
|
||||
t.Fatal("expected at least one Send call for the queued message")
|
||||
}
|
||||
found := false
|
||||
for _, c := range calls {
|
||||
if strings.Contains(c, "queued-after-compress") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("queued message not found in send calls: %v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3. executeCardAction routing ---
|
||||
|
||||
func TestExecuteCardAction_CronEnable(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
|
||||
|
||||
store, err := NewCronStore(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = store.Add(&CronJob{ID: "job1", CronExpr: "0 9 * * *", Enabled: false})
|
||||
scheduler := NewCronScheduler(store)
|
||||
e.cronScheduler = scheduler
|
||||
|
||||
e.executeCardAction("/cron", "enable job1", "test:user1")
|
||||
|
||||
job := store.Get("job1")
|
||||
if job == nil {
|
||||
t.Fatal("job not found")
|
||||
}
|
||||
if !job.Enabled {
|
||||
t.Error("expected job to be enabled after card action")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteCardAction_CronDisable(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
|
||||
|
||||
store, err := NewCronStore(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = store.Add(&CronJob{ID: "job1", CronExpr: "0 9 * * *", Enabled: true})
|
||||
scheduler := NewCronScheduler(store)
|
||||
e.cronScheduler = scheduler
|
||||
|
||||
e.executeCardAction("/cron", "disable job1", "test:user1")
|
||||
|
||||
job := store.Get("job1")
|
||||
if job == nil {
|
||||
t.Fatal("job not found")
|
||||
}
|
||||
if job.Enabled {
|
||||
t.Error("expected job to be disabled after card action")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteCardAction_CronDelete(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
|
||||
|
||||
store, err := NewCronStore(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = store.Add(&CronJob{ID: "del-job", CronExpr: "0 9 * * *", Enabled: true})
|
||||
scheduler := NewCronScheduler(store)
|
||||
e.cronScheduler = scheduler
|
||||
|
||||
e.executeCardAction("/cron", "delete del-job", "test:user1")
|
||||
|
||||
job := store.Get("del-job")
|
||||
if job != nil {
|
||||
t.Error("expected job to be deleted after card action")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteCardAction_CronMuteUnmute(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
|
||||
|
||||
store, err := NewCronStore(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = store.Add(&CronJob{ID: "mute-job", CronExpr: "0 9 * * *", Enabled: true})
|
||||
scheduler := NewCronScheduler(store)
|
||||
e.cronScheduler = scheduler
|
||||
|
||||
e.executeCardAction("/cron", "mute mute-job", "test:user1")
|
||||
job := store.Get("mute-job")
|
||||
if job == nil || !job.Mute {
|
||||
t.Error("expected job to be muted")
|
||||
}
|
||||
|
||||
e.executeCardAction("/cron", "unmute mute-job", "test:user1")
|
||||
job = store.Get("mute-job")
|
||||
if job == nil || job.Mute {
|
||||
t.Error("expected job to be unmuted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteCardAction_CronNoScheduler_NoPanic(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
// cronScheduler is nil — should not panic.
|
||||
e.executeCardAction("/cron", "enable job1", "test:user1")
|
||||
}
|
||||
|
||||
func TestExecuteCardAction_CronBadArgs_NoPanic(t *testing.T) {
|
||||
store, _ := NewCronStore(t.TempDir())
|
||||
scheduler := NewCronScheduler(store)
|
||||
e := newTestEngine()
|
||||
e.cronScheduler = scheduler
|
||||
|
||||
// Missing ID.
|
||||
e.executeCardAction("/cron", "enable", "test:user1")
|
||||
// Empty args.
|
||||
e.executeCardAction("/cron", "", "test:user1")
|
||||
}
|
||||
|
||||
func TestExecuteCardAction_StopCleansUp(t *testing.T) {
|
||||
sess := newControllableSession("stop-test")
|
||||
e := newTestEngine()
|
||||
key := "test:user1"
|
||||
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[key] = &interactiveState{agentSession: sess}
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
e.executeCardAction("/stop", "", key)
|
||||
|
||||
e.interactiveMu.Lock()
|
||||
_, exists := e.interactiveStates[key]
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
if exists {
|
||||
t.Error("expected interactive state to be removed after /stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteCardAction_StopPreservesQuiet(t *testing.T) {
|
||||
sess := newControllableSession("stop-quiet")
|
||||
e := newTestEngine()
|
||||
key := "test:user1"
|
||||
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[key] = &interactiveState{agentSession: sess, quiet: true}
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
e.executeCardAction("/stop", "", key)
|
||||
|
||||
e.interactiveMu.Lock()
|
||||
state, exists := e.interactiveStates[key]
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
if !exists {
|
||||
t.Fatal("expected interactive state to persist (quiet mode)")
|
||||
}
|
||||
if !state.quiet {
|
||||
t.Error("expected quiet flag to be preserved")
|
||||
}
|
||||
if state.agentSession != nil {
|
||||
t.Error("expected agentSession to be nil after /stop")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteCardAction_NewCleansUpAndCreatesSession(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
|
||||
key := "test:user1"
|
||||
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[key] = &interactiveState{agentSession: newControllableSession("old")}
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
e.executeCardAction("/new", "", key)
|
||||
|
||||
e.interactiveMu.Lock()
|
||||
_, exists := e.interactiveStates[key]
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
if exists {
|
||||
t.Error("expected old interactive state to be cleaned up after /new")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteCardAction_LangSwitch(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
|
||||
e.executeCardAction("/lang", "zh", "test:user1")
|
||||
if e.i18n.CurrentLang() != LangChinese {
|
||||
t.Errorf("expected LangChinese, got %v", e.i18n.CurrentLang())
|
||||
}
|
||||
|
||||
e.executeCardAction("/lang", "en", "test:user1")
|
||||
if e.i18n.CurrentLang() != LangEnglish {
|
||||
t.Errorf("expected LangEnglish, got %v", e.i18n.CurrentLang())
|
||||
}
|
||||
|
||||
e.executeCardAction("/lang", "ja", "test:user1")
|
||||
if e.i18n.CurrentLang() != LangJapanese {
|
||||
t.Errorf("expected LangJapanese, got %v", e.i18n.CurrentLang())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteCardAction_UnknownCommand_NoPanic(t *testing.T) {
|
||||
e := newTestEngine()
|
||||
// Should not panic for unrecognized commands.
|
||||
e.executeCardAction("/nonexistent", "args", "test:user1")
|
||||
e.executeCardAction("", "", "test:user1")
|
||||
}
|
||||
|
||||
// --- 4. Multi-workspace command handlers use interactiveKey ---
|
||||
|
||||
func TestCmdStatus_UsesInteractiveKeyForMultiWorkspace(t *testing.T) {
|
||||
p := &stubCardPlatform{stubPlatformEngine: stubPlatformEngine{n: "card"}}
|
||||
agent := &stubModelModeAgent{model: "gpt-4.1", mode: "default"}
|
||||
e := NewEngine("test", agent, []Platform{p}, "", LangEnglish)
|
||||
|
||||
wsDir := t.TempDir()
|
||||
rawKey := "feishu:ch1:user1"
|
||||
wsKey := wsDir + ":" + rawKey
|
||||
|
||||
state := &interactiveState{
|
||||
agentSession: newControllableSession("ws-status-test"),
|
||||
platform: p,
|
||||
quiet: true,
|
||||
}
|
||||
iKey := e.interactiveKeyForSessionKey(wsKey)
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[iKey] = state
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
msg := &Message{SessionKey: wsKey, Content: "/status", ReplyCtx: "ctx"}
|
||||
e.cmdStatus(p, msg)
|
||||
|
||||
// The status card should include the quiet state from the correct
|
||||
// interactive key (normalized workspace path), not from a raw lookup.
|
||||
if len(p.repliedCards) == 0 && len(p.sentCards) == 0 {
|
||||
sent := p.getSent()
|
||||
found := false
|
||||
for _, s := range sent {
|
||||
if strings.Contains(s, "Quiet") || strings.Contains(s, "quiet") || strings.Contains(s, "ON") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected status to reflect quiet=true, got %v", sent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdStop_UsesInteractiveKeyForMultiWorkspace(t *testing.T) {
|
||||
p := &stubPlatformEngine{n: "test"}
|
||||
sess := newControllableSession("ws-stop-test")
|
||||
e := NewEngine("test", &stubAgent{}, []Platform{p}, "", LangEnglish)
|
||||
|
||||
wsDir := t.TempDir()
|
||||
rawKey := "feishu:ch1:user1"
|
||||
wsKey := wsDir + ":" + rawKey
|
||||
|
||||
iKey := e.interactiveKeyForSessionKey(wsKey)
|
||||
e.interactiveMu.Lock()
|
||||
e.interactiveStates[iKey] = &interactiveState{agentSession: sess}
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
msg := &Message{SessionKey: wsKey, Content: "/stop", ReplyCtx: "ctx"}
|
||||
e.cmdStop(p, msg)
|
||||
|
||||
e.interactiveMu.Lock()
|
||||
_, exists := e.interactiveStates[iKey]
|
||||
e.interactiveMu.Unlock()
|
||||
|
||||
if exists {
|
||||
t.Error("expected interactive state to be cleaned up by /stop using interactiveKey")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user