Skip to content

Commit 9f24822

Browse files
committed
Add tests for agent output mode (NXF_AGENT)
Tests for AI agent-friendly output mode via env vars. All tests marked @PendingFeature until implementation. Signed-off-by: Edmund Miller <edmund.miller@seqera.io>
1 parent ea8fea4 commit 9f24822

File tree

2 files changed

+257
-0
lines changed

2 files changed

+257
-0
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/*
2+
* Copyright 2013-2024, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package nextflow.trace
18+
19+
import java.nio.file.Paths
20+
21+
import nextflow.Session
22+
import nextflow.processor.TaskHandler
23+
import nextflow.processor.TaskRun
24+
import nextflow.trace.event.TaskEvent
25+
import spock.lang.PendingFeature
26+
import spock.lang.Specification
27+
28+
/**
29+
* Tests for AgentLogObserver
30+
*
31+
* @author Edmund Miller <edmund.miller@utdallas.edu>
32+
*/
33+
class AgentLogObserverTest extends Specification {
34+
35+
@PendingFeature
36+
def 'should output pipeline info on flow begin'() {
37+
given:
38+
def output = []
39+
def observer = new TestAgentLogObserver(output)
40+
def session = Stub(Session) {
41+
getManifest() >> [name: 'nf-core/rnaseq', version: '3.14.0']
42+
getScriptName() >> 'main.nf'
43+
getProfile() >> 'test,docker'
44+
getWorkDir() >> Paths.get('/tmp/work')
45+
}
46+
47+
when:
48+
observer.onFlowCreate(session)
49+
observer.onFlowBegin()
50+
51+
then:
52+
output[0] == '[PIPELINE] nf-core/rnaseq 3.14.0 | profile=test,docker'
53+
output[1].startsWith('[WORKDIR]')
54+
output[1].contains('/tmp/work')
55+
}
56+
57+
@PendingFeature
58+
def 'should deduplicate warnings'() {
59+
given:
60+
def output = []
61+
def observer = new TestAgentLogObserver(output)
62+
63+
when:
64+
observer.appendWarning('Task runtime metrics are not reported when using macOS')
65+
observer.appendWarning('Task runtime metrics are not reported when using macOS')
66+
observer.appendWarning('Different warning message')
67+
68+
then:
69+
output.size() == 2
70+
output[0] == '[WARN] Task runtime metrics are not reported when using macOS'
71+
output[1] == '[WARN] Different warning message'
72+
}
73+
74+
@PendingFeature
75+
def 'should output error with context'() {
76+
given:
77+
def output = []
78+
def observer = new TestAgentLogObserver(output)
79+
def task = Stub(TaskRun) {
80+
getName() >> 'FASTQC (sample_1)'
81+
getExitStatus() >> 127
82+
getScript() >> 'fastqc --version'
83+
getStderr() >> 'bash: fastqc: command not found'
84+
getStdout() >> null
85+
getWorkDir() >> Paths.get('/tmp/work/ab/123456')
86+
}
87+
88+
when:
89+
observer.printTaskError(task)
90+
91+
then:
92+
output[0] == '[ERROR] FASTQC (sample_1)'
93+
output[1] == 'exit: 127'
94+
output[2] == 'cmd: fastqc --version'
95+
output[3] == 'stderr: bash: fastqc: command not found'
96+
output[4].startsWith('workdir:')
97+
output[4].contains('/tmp/work/ab/123456')
98+
}
99+
100+
@PendingFeature
101+
def 'should output summary on flow complete'() {
102+
given:
103+
def output = []
104+
def stats = new WorkflowStats()
105+
stats.succeededCount = 10
106+
stats.failedCount = 0
107+
stats.cachedCount = 5
108+
def statsObserver = Stub(WorkflowStatsObserver) {
109+
getStats() >> stats
110+
}
111+
def observer = new TestAgentLogObserver(output)
112+
observer.setStatsObserver(statsObserver)
113+
114+
when:
115+
observer.onFlowComplete()
116+
117+
then:
118+
output[0] == '\n[SUCCESS] completed=10 failed=0 cached=5'
119+
}
120+
121+
@PendingFeature
122+
def 'should output failed summary when tasks fail'() {
123+
given:
124+
def output = []
125+
def stats = new WorkflowStats()
126+
stats.succeededCount = 5
127+
stats.failedCount = 2
128+
stats.cachedCount = 0
129+
def statsObserver = Stub(WorkflowStatsObserver) {
130+
getStats() >> stats
131+
}
132+
def observer = new TestAgentLogObserver(output)
133+
observer.setStatsObserver(statsObserver)
134+
135+
when:
136+
observer.onFlowComplete()
137+
138+
then:
139+
output[0] == '\n[FAILED] completed=7 failed=2 cached=0'
140+
}
141+
142+
@PendingFeature
143+
def 'should handle task complete for failed task'() {
144+
given:
145+
def output = []
146+
def observer = new TestAgentLogObserver(output)
147+
def task = Stub(TaskRun) {
148+
getName() >> 'TEST_PROC'
149+
getExitStatus() >> 1
150+
getScript() >> 'exit 1'
151+
getStderr() >> 'error'
152+
getStdout() >> null
153+
getWorkDir() >> Paths.get('/work/xx/yy')
154+
isFailed() >> true
155+
}
156+
def handler = Stub(TaskHandler) {
157+
getTask() >> task
158+
}
159+
def event = Stub(TaskEvent) {
160+
getHandler() >> handler
161+
}
162+
163+
when:
164+
observer.onTaskComplete(event)
165+
166+
then:
167+
output.size() > 0
168+
output[0] == '[ERROR] TEST_PROC'
169+
}
170+
171+
@PendingFeature
172+
def 'should not output for successful task'() {
173+
given:
174+
def output = []
175+
def observer = new TestAgentLogObserver(output)
176+
def task = Stub(TaskRun) {
177+
isFailed() >> false
178+
}
179+
def handler = Stub(TaskHandler) {
180+
getTask() >> task
181+
}
182+
def event = Stub(TaskEvent) {
183+
getHandler() >> handler
184+
}
185+
186+
when:
187+
observer.onTaskComplete(event)
188+
189+
then:
190+
output.size() == 0
191+
}
192+
193+
@PendingFeature
194+
def 'should truncate long command'() {
195+
given:
196+
def output = []
197+
def observer = new TestAgentLogObserver(output)
198+
def longCommand = 'x' * 300
199+
def task = Stub(TaskRun) {
200+
getName() >> 'PROC'
201+
getExitStatus() >> 1
202+
getScript() >> longCommand
203+
getStderr() >> null
204+
getStdout() >> null
205+
getWorkDir() >> Paths.get('/work')
206+
}
207+
208+
when:
209+
observer.printTaskError(task)
210+
211+
then:
212+
def cmdLine = output.find { it.startsWith('cmd:') }
213+
cmdLine != null
214+
cmdLine.length() < 250
215+
cmdLine.endsWith('...')
216+
}
217+
218+
/**
219+
* Test subclass that captures output
220+
*/
221+
static class TestAgentLogObserver extends AgentLogObserver {
222+
private List<String> output
223+
224+
TestAgentLogObserver(List<String> output) {
225+
this.output = output
226+
}
227+
228+
@Override
229+
protected void println(String line) {
230+
output << line
231+
}
232+
}
233+
}

modules/nf-commons/src/test/nextflow/SysEnvTest.groovy

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package nextflow
1919

20+
import spock.lang.PendingFeature
2021
import spock.lang.Specification
2122
import spock.lang.Unroll
2223

@@ -120,4 +121,27 @@ class SysEnvTest extends Specification {
120121
[FOO:'0'] | 1 | 0
121122
[FOO:'100'] | 1 | 100
122123
}
124+
125+
@PendingFeature
126+
@Unroll
127+
def 'should detect agent mode' () {
128+
given:
129+
SysEnv.push(STATE)
130+
131+
expect:
132+
SysEnv.isAgentMode() == EXPECTED
133+
134+
cleanup:
135+
SysEnv.pop()
136+
137+
where:
138+
STATE | EXPECTED
139+
[:] | false
140+
[NXF_AGENT:'true'] | true
141+
[NXF_AGENT:'false'] | false
142+
[AGENT:'true'] | true
143+
[CLAUDECODE:'true'] | true
144+
// Multiple can be set, any true triggers agent mode
145+
[NXF_AGENT:'true', AGENT:'false'] | true
146+
}
123147
}

0 commit comments

Comments
 (0)