@elyun/bylane 1.29.0 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +32 -8
- package/commands/bylane-cleanup.md +5 -0
- package/commands/bylane-code-agent.md +18 -0
- package/commands/bylane-issue-agent.md +46 -1
- package/commands/bylane-orchestrator.md +43 -1
- package/commands/bylane-setup.md +23 -5
- package/hooks/bylane-session-cleanup.js +94 -0
- package/package.json +1 -1
- package/src/cleanup.js +58 -8
- package/src/cli.js +22 -3
- package/src/config.js +4 -0
- package/src/pipeline.js +195 -0
- package/src/queue-utils.js +119 -0
- package/src/respond-loop.js +24 -2
- package/src/review-loop.js +24 -2
- package/tests/pipeline.test.js +171 -0
- package/tests/queue-utils.test.js +141 -0
- package/.bylane/bylane.json +0 -37
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { reconcileQueue, expireStaleItems, gcQueue, maintainQueue, QUEUE_TTL_MS, GC_AGE_MS } from '../src/queue-utils.js'
|
|
3
|
+
|
|
4
|
+
describe('reconcileQueue', () => {
|
|
5
|
+
it('활성 PR 목록에 없는 pending 항목을 resolved로 전환한다', () => {
|
|
6
|
+
const queue = [
|
|
7
|
+
{ number: 1, status: 'pending', detectedAt: new Date().toISOString() },
|
|
8
|
+
{ number: 2, status: 'pending', detectedAt: new Date().toISOString() },
|
|
9
|
+
{ number: 3, status: 'responded', detectedAt: new Date().toISOString() }
|
|
10
|
+
]
|
|
11
|
+
const activePrs = new Set([2])
|
|
12
|
+
const { queue: result, resolvedCount } = reconcileQueue(queue, activePrs)
|
|
13
|
+
|
|
14
|
+
expect(resolvedCount).toBe(1)
|
|
15
|
+
expect(result[0].status).toBe('resolved')
|
|
16
|
+
expect(result[0].reason).toBe('no_longer_actionable')
|
|
17
|
+
expect(result[0].resolvedAt).toBeTruthy()
|
|
18
|
+
expect(result[1].status).toBe('pending')
|
|
19
|
+
// responded 항목은 건드리지 않음
|
|
20
|
+
expect(result[2].status).toBe('responded')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('모든 항목이 활성이면 변경 없음', () => {
|
|
24
|
+
const queue = [
|
|
25
|
+
{ number: 1, status: 'pending' },
|
|
26
|
+
{ number: 2, status: 'pending' }
|
|
27
|
+
]
|
|
28
|
+
const { queue: result, resolvedCount } = reconcileQueue(queue, new Set([1, 2]))
|
|
29
|
+
expect(resolvedCount).toBe(0)
|
|
30
|
+
expect(result[0].status).toBe('pending')
|
|
31
|
+
expect(result[1].status).toBe('pending')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('빈 큐에서 동작한다', () => {
|
|
35
|
+
const { queue, resolvedCount } = reconcileQueue([], new Set([1]))
|
|
36
|
+
expect(queue).toEqual([])
|
|
37
|
+
expect(resolvedCount).toBe(0)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('expireStaleItems', () => {
|
|
42
|
+
it('TTL 초과 pending 항목을 expired로 전환한다', () => {
|
|
43
|
+
const old = new Date(Date.now() - QUEUE_TTL_MS - 1000).toISOString()
|
|
44
|
+
const fresh = new Date().toISOString()
|
|
45
|
+
const queue = [
|
|
46
|
+
{ number: 1, status: 'pending', detectedAt: old },
|
|
47
|
+
{ number: 2, status: 'pending', detectedAt: fresh },
|
|
48
|
+
{ number: 3, status: 'resolved', detectedAt: old }
|
|
49
|
+
]
|
|
50
|
+
const { queue: result, expiredCount } = expireStaleItems(queue)
|
|
51
|
+
|
|
52
|
+
expect(expiredCount).toBe(1)
|
|
53
|
+
expect(result[0].status).toBe('expired')
|
|
54
|
+
expect(result[0].expiredAt).toBeTruthy()
|
|
55
|
+
expect(result[1].status).toBe('pending')
|
|
56
|
+
// resolved는 건드리지 않음
|
|
57
|
+
expect(result[2].status).toBe('resolved')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('커스텀 TTL을 적용할 수 있다', () => {
|
|
61
|
+
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString()
|
|
62
|
+
const queue = [{ number: 1, status: 'pending', detectedAt: twoHoursAgo }]
|
|
63
|
+
const { expiredCount } = expireStaleItems(queue, 1 * 60 * 60 * 1000)
|
|
64
|
+
expect(expiredCount).toBe(1)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('detectedAt 없는 항목은 건너뛴다', () => {
|
|
68
|
+
const queue = [{ number: 1, status: 'pending' }]
|
|
69
|
+
const { expiredCount } = expireStaleItems(queue)
|
|
70
|
+
expect(expiredCount).toBe(0)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('gcQueue', () => {
|
|
75
|
+
it('GC 기준 초과한 resolved/expired 항목을 제거한다', () => {
|
|
76
|
+
const oldTs = new Date(Date.now() - GC_AGE_MS - 1000).toISOString()
|
|
77
|
+
const freshTs = new Date().toISOString()
|
|
78
|
+
const queue = [
|
|
79
|
+
{ number: 1, status: 'resolved', resolvedAt: oldTs },
|
|
80
|
+
{ number: 2, status: 'expired', expiredAt: oldTs },
|
|
81
|
+
{ number: 3, status: 'resolved', resolvedAt: freshTs },
|
|
82
|
+
{ number: 4, status: 'pending', detectedAt: oldTs }
|
|
83
|
+
]
|
|
84
|
+
const { queue: result, removedCount } = gcQueue(queue)
|
|
85
|
+
|
|
86
|
+
expect(removedCount).toBe(2)
|
|
87
|
+
expect(result).toHaveLength(2)
|
|
88
|
+
expect(result[0].number).toBe(3)
|
|
89
|
+
expect(result[1].number).toBe(4)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('GC 기준 미달이면 유지한다', () => {
|
|
93
|
+
const freshTs = new Date().toISOString()
|
|
94
|
+
const queue = [
|
|
95
|
+
{ number: 1, status: 'resolved', resolvedAt: freshTs },
|
|
96
|
+
{ number: 2, status: 'expired', expiredAt: freshTs }
|
|
97
|
+
]
|
|
98
|
+
const { removedCount } = gcQueue(queue)
|
|
99
|
+
expect(removedCount).toBe(0)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('maintainQueue', () => {
|
|
104
|
+
it('reconcile + expire + GC를 한 번에 실행한다', () => {
|
|
105
|
+
const oldTs = new Date(Date.now() - GC_AGE_MS - 1000).toISOString()
|
|
106
|
+
const staleTs = new Date(Date.now() - QUEUE_TTL_MS - 1000).toISOString()
|
|
107
|
+
const freshTs = new Date().toISOString()
|
|
108
|
+
|
|
109
|
+
const queue = [
|
|
110
|
+
{ number: 1, status: 'pending', detectedAt: freshTs }, // active → stays
|
|
111
|
+
{ number: 2, status: 'pending', detectedAt: freshTs }, // not active → resolved
|
|
112
|
+
{ number: 3, status: 'pending', detectedAt: staleTs }, // TTL 초과 → expired
|
|
113
|
+
{ number: 4, status: 'resolved', resolvedAt: oldTs }, // GC 대상
|
|
114
|
+
{ number: 5, status: 'responded', detectedAt: freshTs } // 유지
|
|
115
|
+
]
|
|
116
|
+
const activePrs = new Set([1])
|
|
117
|
+
|
|
118
|
+
const result = maintainQueue(queue, activePrs)
|
|
119
|
+
|
|
120
|
+
// #2, #3 모두 active에 없으므로 reconcile에서 resolved (expire보다 먼저 실행)
|
|
121
|
+
expect(result.resolvedCount).toBe(2) // #2, #3
|
|
122
|
+
expect(result.expiredCount).toBe(0) // reconcile이 먼저 resolved 처리
|
|
123
|
+
expect(result.removedCount).toBe(1) // #4
|
|
124
|
+
|
|
125
|
+
// 남은 항목 확인
|
|
126
|
+
expect(result.queue).toHaveLength(4)
|
|
127
|
+
expect(result.queue.find(q => q.number === 1).status).toBe('pending')
|
|
128
|
+
expect(result.queue.find(q => q.number === 2).status).toBe('resolved')
|
|
129
|
+
expect(result.queue.find(q => q.number === 3).status).toBe('resolved')
|
|
130
|
+
expect(result.queue.find(q => q.number === 5).status).toBe('responded')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('activePrNumbers가 null이면 reconcile을 생략한다', () => {
|
|
134
|
+
const queue = [
|
|
135
|
+
{ number: 1, status: 'pending', detectedAt: new Date().toISOString() }
|
|
136
|
+
]
|
|
137
|
+
const result = maintainQueue(queue, null)
|
|
138
|
+
expect(result.resolvedCount).toBe(0)
|
|
139
|
+
expect(result.queue[0].status).toBe('pending')
|
|
140
|
+
})
|
|
141
|
+
})
|
package/.bylane/bylane.json
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"trackers": {
|
|
3
|
-
"primary": "github",
|
|
4
|
-
"linear": {
|
|
5
|
-
"enabled": false
|
|
6
|
-
}
|
|
7
|
-
},
|
|
8
|
-
"notifications": {
|
|
9
|
-
"telegram": {
|
|
10
|
-
"enabled": true,
|
|
11
|
-
"chatIdEnv": "TELEGRAM_CHAT_ID",
|
|
12
|
-
"botTokenEnv": "TELEGRAM_BOT_TOKEN"
|
|
13
|
-
},
|
|
14
|
-
"slack": {
|
|
15
|
-
"enabled": false
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
"team": {
|
|
19
|
-
"enabled": true,
|
|
20
|
-
"members": [],
|
|
21
|
-
"reviewAssignment": "round-robin"
|
|
22
|
-
},
|
|
23
|
-
"permissions": {
|
|
24
|
-
"scope": "write"
|
|
25
|
-
},
|
|
26
|
-
"maxRetries": 3,
|
|
27
|
-
"loopTimeoutMinutes": 30,
|
|
28
|
-
"figma": {
|
|
29
|
-
"enabled": false
|
|
30
|
-
},
|
|
31
|
-
"branch": {
|
|
32
|
-
"pattern": "{tracker}-{issue-number}",
|
|
33
|
-
"defaults": {
|
|
34
|
-
"tracker": "issues"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|