github-issue-tower-defence-management 1.156.3 → 1.157.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/CHANGELOG.md +14 -0
- package/README.md +6 -6
- package/bin/adapter/entry-points/cli/index.js +6 -1
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +6 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleDataDelivery.js +3 -10
- package/bin/adapter/entry-points/console/consoleDataDelivery.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleTabNames.js +32 -0
- package/bin/adapter/entry-points/console/consoleTabNames.js.map +1 -0
- package/bin/adapter/entry-points/console/webServer.js +1 -1
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/FileSystemConsoleTabsRepository.js +82 -0
- package/bin/adapter/entry-points/handlers/FileSystemConsoleTabsRepository.js.map +1 -0
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js +2 -10
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js +1 -19
- package/bin/adapter/repositories/FileSystemSubAgentLivenessResolver.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +49 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/ConsoleTabsRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/ConsoleTabsRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +8 -0
- package/src/adapter/entry-points/cli/projectConfig.test.ts +56 -0
- package/src/adapter/entry-points/cli/projectConfig.ts +8 -0
- package/src/adapter/entry-points/console/consoleDataDelivery.ts +3 -10
- package/src/adapter/entry-points/console/consoleTabNames.test.ts +51 -0
- package/src/adapter/entry-points/console/consoleTabNames.ts +37 -0
- package/src/adapter/entry-points/console/webServer.ts +1 -1
- package/src/adapter/entry-points/handlers/FileSystemConsoleTabsRepository.test.ts +217 -0
- package/src/adapter/entry-points/handlers/FileSystemConsoleTabsRepository.ts +113 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.ts +2 -12
- package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.test.ts +12 -4
- package/src/adapter/repositories/FileSystemSubAgentLivenessResolver.ts +1 -21
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +159 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +59 -0
- package/src/domain/usecases/adapter-interfaces/ConsoleTabsRepository.ts +12 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleDataDelivery.d.ts +2 -1
- package/types/adapter/entry-points/console/consoleDataDelivery.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleTabNames.d.ts +4 -0
- package/types/adapter/entry-points/console/consoleTabNames.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/FileSystemConsoleTabsRepository.d.ts +13 -0
- package/types/adapter/entry-points/handlers/FileSystemConsoleTabsRepository.d.ts.map +1 -0
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSubAgentLivenessResolver.d.ts +0 -1
- package/types/adapter/repositories/FileSystemSubAgentLivenessResolver.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/ConsoleTabsRepository.d.ts +9 -0
- package/types/domain/usecases/adapter-interfaces/ConsoleTabsRepository.d.ts.map +1 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { FileSystemConsoleTabsRepository } from './FileSystemConsoleTabsRepository';
|
|
5
|
+
import { readDoneProjectItemIds } from '../console/consoleDoneStore';
|
|
6
|
+
import type { ConsoleListItem } from '../../../domain/usecases/console/GenerateConsoleListsUseCase';
|
|
7
|
+
|
|
8
|
+
const makeItem = (
|
|
9
|
+
overrides: Partial<ConsoleListItem> = {},
|
|
10
|
+
): ConsoleListItem => ({
|
|
11
|
+
number: 1,
|
|
12
|
+
title: 'Test Issue',
|
|
13
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
14
|
+
repo: 'user/repo',
|
|
15
|
+
nameWithOwner: 'user/repo',
|
|
16
|
+
projectItemId: 'item-1',
|
|
17
|
+
itemId: 'item-1',
|
|
18
|
+
isPr: false,
|
|
19
|
+
story: 'Story A',
|
|
20
|
+
status: 'Awaiting Quality Check',
|
|
21
|
+
nextActionDate: null,
|
|
22
|
+
nextActionHour: null,
|
|
23
|
+
dependedIssueUrls: [],
|
|
24
|
+
labels: [],
|
|
25
|
+
createdAt: new Date('2024-01-01T00:00:00Z').toISOString(),
|
|
26
|
+
relatedOpenPullRequestUrls: [],
|
|
27
|
+
...overrides,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const makeStatusTab = (
|
|
31
|
+
pjcode: string,
|
|
32
|
+
items: ConsoleListItem[],
|
|
33
|
+
storyOrder: string[] = ['Story A', 'Story B'],
|
|
34
|
+
) => ({
|
|
35
|
+
pjcode,
|
|
36
|
+
generatedAt: '2026-08-16T04:19:32Z',
|
|
37
|
+
statusOptions: [],
|
|
38
|
+
storyOrder,
|
|
39
|
+
storyColors: {},
|
|
40
|
+
items,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('FileSystemConsoleTabsRepository', () => {
|
|
44
|
+
let dir: string;
|
|
45
|
+
const PJCODE = 'test-project';
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
dir = fs.mkdtempSync(
|
|
49
|
+
path.join(os.tmpdir(), 'console-tabs-repository-test-'),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const writeTabFile = (tab: string, data: unknown): void => {
|
|
58
|
+
const tabDir = path.join(dir, PJCODE, tab);
|
|
59
|
+
fs.mkdirSync(tabDir, { recursive: true });
|
|
60
|
+
fs.writeFileSync(path.join(tabDir, 'list.json'), JSON.stringify(data));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const readTabFile = (tab: string): unknown => {
|
|
64
|
+
const filePath = path.join(dir, PJCODE, tab, 'list.json');
|
|
65
|
+
const data: unknown = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
66
|
+
return data;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
it('inserts the item into the prs tab when transitioning to Awaiting Quality Check', () => {
|
|
70
|
+
writeTabFile('prs', makeStatusTab(PJCODE, []));
|
|
71
|
+
const repo = new FileSystemConsoleTabsRepository(dir, PJCODE);
|
|
72
|
+
const item = makeItem({ status: 'Awaiting Quality Check' });
|
|
73
|
+
|
|
74
|
+
repo.patchIssueTabTransition({
|
|
75
|
+
projectItemId: item.projectItemId,
|
|
76
|
+
item,
|
|
77
|
+
targetTabName: 'prs',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(readTabFile('prs')).toMatchObject({
|
|
81
|
+
items: [{ projectItemId: 'item-1' }],
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('preserves generatedAt in the patched tab file', () => {
|
|
86
|
+
writeTabFile('prs', makeStatusTab(PJCODE, []));
|
|
87
|
+
const repo = new FileSystemConsoleTabsRepository(dir, PJCODE);
|
|
88
|
+
const item = makeItem();
|
|
89
|
+
|
|
90
|
+
repo.patchIssueTabTransition({
|
|
91
|
+
projectItemId: item.projectItemId,
|
|
92
|
+
item,
|
|
93
|
+
targetTabName: 'prs',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(readTabFile('prs')).toHaveProperty(
|
|
97
|
+
'generatedAt',
|
|
98
|
+
'2026-08-16T04:19:32Z',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('removes the item from any tab it previously appeared in', () => {
|
|
103
|
+
const existingItem = makeItem({ projectItemId: 'item-1' });
|
|
104
|
+
writeTabFile('todo-by-human', makeStatusTab(PJCODE, [existingItem]));
|
|
105
|
+
writeTabFile('prs', makeStatusTab(PJCODE, []));
|
|
106
|
+
const repo = new FileSystemConsoleTabsRepository(dir, PJCODE);
|
|
107
|
+
const item = makeItem({ status: 'Awaiting Quality Check' });
|
|
108
|
+
|
|
109
|
+
repo.patchIssueTabTransition({
|
|
110
|
+
projectItemId: 'item-1',
|
|
111
|
+
item,
|
|
112
|
+
targetTabName: 'prs',
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(readTabFile('todo-by-human')).toMatchObject({ items: [] });
|
|
116
|
+
expect(readTabFile('prs')).toMatchObject({
|
|
117
|
+
items: [{ projectItemId: 'item-1' }],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('removes the projectItemId from the target tab .done.json', () => {
|
|
122
|
+
writeTabFile('prs', makeStatusTab(PJCODE, []));
|
|
123
|
+
const doneFilePath = path.join(dir, PJCODE, 'prs', '.done.json');
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
doneFilePath,
|
|
126
|
+
JSON.stringify({ projectItemIds: ['item-1', 'item-2'] }),
|
|
127
|
+
);
|
|
128
|
+
const repo = new FileSystemConsoleTabsRepository(dir, PJCODE);
|
|
129
|
+
const item = makeItem();
|
|
130
|
+
|
|
131
|
+
repo.patchIssueTabTransition({
|
|
132
|
+
projectItemId: 'item-1',
|
|
133
|
+
item,
|
|
134
|
+
targetTabName: 'prs',
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const ids = readDoneProjectItemIds(dir, PJCODE, 'prs');
|
|
138
|
+
expect(ids).not.toContain('item-1');
|
|
139
|
+
expect(ids).toContain('item-2');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('is a no-op when the target tab file does not exist', () => {
|
|
143
|
+
const repo = new FileSystemConsoleTabsRepository(dir, PJCODE);
|
|
144
|
+
const item = makeItem();
|
|
145
|
+
|
|
146
|
+
expect(() =>
|
|
147
|
+
repo.patchIssueTabTransition({
|
|
148
|
+
projectItemId: item.projectItemId,
|
|
149
|
+
item,
|
|
150
|
+
targetTabName: 'prs',
|
|
151
|
+
}),
|
|
152
|
+
).not.toThrow();
|
|
153
|
+
expect(fs.existsSync(path.join(dir, PJCODE, 'prs', 'list.json'))).toBe(
|
|
154
|
+
false,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('removes the item from any tab and writes nothing when targetTabName is null', () => {
|
|
159
|
+
const existingItem = makeItem({ projectItemId: 'item-1' });
|
|
160
|
+
writeTabFile('prs', makeStatusTab(PJCODE, [existingItem]));
|
|
161
|
+
const repo = new FileSystemConsoleTabsRepository(dir, PJCODE);
|
|
162
|
+
const item = makeItem({ status: 'Awaiting Workspace' });
|
|
163
|
+
|
|
164
|
+
repo.patchIssueTabTransition({
|
|
165
|
+
projectItemId: 'item-1',
|
|
166
|
+
item,
|
|
167
|
+
targetTabName: null,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(readTabFile('prs')).toMatchObject({ items: [] });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('maintains story order after inserting the new item', () => {
|
|
174
|
+
const itemInStoryB = makeItem({
|
|
175
|
+
projectItemId: 'item-2',
|
|
176
|
+
story: 'Story B',
|
|
177
|
+
});
|
|
178
|
+
writeTabFile('prs', makeStatusTab(PJCODE, [itemInStoryB]));
|
|
179
|
+
const repo = new FileSystemConsoleTabsRepository(dir, PJCODE);
|
|
180
|
+
const item = makeItem({
|
|
181
|
+
projectItemId: 'item-1',
|
|
182
|
+
story: 'Story A',
|
|
183
|
+
status: 'Awaiting Quality Check',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
repo.patchIssueTabTransition({
|
|
187
|
+
projectItemId: 'item-1',
|
|
188
|
+
item,
|
|
189
|
+
targetTabName: 'prs',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(readTabFile('prs')).toMatchObject({
|
|
193
|
+
items: [{ story: 'Story A' }, { story: 'Story B' }],
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('does not write to tab files that do not contain the item when targetTabName is null', () => {
|
|
198
|
+
const otherItem = makeItem({ projectItemId: 'item-99' });
|
|
199
|
+
writeTabFile('prs', makeStatusTab(PJCODE, [otherItem]));
|
|
200
|
+
const mtimeBefore = fs.statSync(
|
|
201
|
+
path.join(dir, PJCODE, 'prs', 'list.json'),
|
|
202
|
+
).mtimeMs;
|
|
203
|
+
const repo = new FileSystemConsoleTabsRepository(dir, PJCODE);
|
|
204
|
+
const item = makeItem({ projectItemId: 'item-1' });
|
|
205
|
+
|
|
206
|
+
repo.patchIssueTabTransition({
|
|
207
|
+
projectItemId: 'item-1',
|
|
208
|
+
item,
|
|
209
|
+
targetTabName: null,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const mtimeAfter = fs.statSync(
|
|
213
|
+
path.join(dir, PJCODE, 'prs', 'list.json'),
|
|
214
|
+
).mtimeMs;
|
|
215
|
+
expect(mtimeAfter).toBe(mtimeBefore);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type {
|
|
4
|
+
ConsoleListItem,
|
|
5
|
+
ConsoleTabName,
|
|
6
|
+
} from '../../../domain/usecases/console/GenerateConsoleListsUseCase';
|
|
7
|
+
import type { ConsoleTabsRepository } from '../../../domain/usecases/adapter-interfaces/ConsoleTabsRepository';
|
|
8
|
+
import {
|
|
9
|
+
CONSOLE_LIST_TAB_NAMES,
|
|
10
|
+
sortByStoryOrder,
|
|
11
|
+
} from '../console/consoleTabNames';
|
|
12
|
+
|
|
13
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
14
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
|
+
|
|
16
|
+
const writeJsonAtomic = (filePath: string, data: unknown): void => {
|
|
17
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
18
|
+
const tmpPath = `${filePath}.tmp`;
|
|
19
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data));
|
|
20
|
+
fs.renameSync(tmpPath, filePath);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const readTabListJson = (filePath: string): Record<string, unknown> | null => {
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
26
|
+
const parsed: unknown = JSON.parse(raw);
|
|
27
|
+
if (!isRecord(parsed) || !Array.isArray(parsed.items)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const clearProjectItemIdFromDoneJson = (
|
|
37
|
+
doneFilePath: string,
|
|
38
|
+
projectItemId: string,
|
|
39
|
+
): void => {
|
|
40
|
+
let existingIds: string[] = [];
|
|
41
|
+
try {
|
|
42
|
+
const raw = fs.readFileSync(doneFilePath, 'utf-8');
|
|
43
|
+
const parsed: unknown = JSON.parse(raw);
|
|
44
|
+
if (isRecord(parsed)) {
|
|
45
|
+
const rawIds = parsed.projectItemIds;
|
|
46
|
+
if (Array.isArray(rawIds)) {
|
|
47
|
+
existingIds = rawIds.filter(
|
|
48
|
+
(id): id is string => typeof id === 'string',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// File does not exist; start with empty list
|
|
54
|
+
}
|
|
55
|
+
writeJsonAtomic(doneFilePath, {
|
|
56
|
+
projectItemIds: existingIds.filter((id) => id !== projectItemId),
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export class FileSystemConsoleTabsRepository implements ConsoleTabsRepository {
|
|
61
|
+
constructor(
|
|
62
|
+
private readonly consoleDataOutputDir: string,
|
|
63
|
+
private readonly pjcode: string,
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
patchIssueTabTransition(params: {
|
|
67
|
+
projectItemId: string;
|
|
68
|
+
item: ConsoleListItem;
|
|
69
|
+
targetTabName: ConsoleTabName | null;
|
|
70
|
+
}): void {
|
|
71
|
+
const { projectItemId, item, targetTabName } = params;
|
|
72
|
+
|
|
73
|
+
for (const tabName of CONSOLE_LIST_TAB_NAMES) {
|
|
74
|
+
const filePath = path.join(
|
|
75
|
+
this.consoleDataOutputDir,
|
|
76
|
+
this.pjcode,
|
|
77
|
+
tabName,
|
|
78
|
+
'list.json',
|
|
79
|
+
);
|
|
80
|
+
const existing = readTabListJson(filePath);
|
|
81
|
+
if (existing === null) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const rawItems = existing.items;
|
|
86
|
+
const items: unknown[] = Array.isArray(rawItems) ? rawItems : [];
|
|
87
|
+
const withoutThisItem = items.filter(
|
|
88
|
+
(i) => !(isRecord(i) && i.projectItemId === projectItemId),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (tabName === targetTabName) {
|
|
92
|
+
const rawStoryOrder = existing.storyOrder;
|
|
93
|
+
const storyOrder = Array.isArray(rawStoryOrder)
|
|
94
|
+
? rawStoryOrder.filter((s): s is string => typeof s === 'string')
|
|
95
|
+
: [];
|
|
96
|
+
const newItems = sortByStoryOrder(
|
|
97
|
+
[...withoutThisItem, item],
|
|
98
|
+
storyOrder,
|
|
99
|
+
);
|
|
100
|
+
writeJsonAtomic(filePath, { ...existing, items: newItems });
|
|
101
|
+
const doneFilePath = path.join(
|
|
102
|
+
this.consoleDataOutputDir,
|
|
103
|
+
this.pjcode,
|
|
104
|
+
tabName,
|
|
105
|
+
'.done.json',
|
|
106
|
+
);
|
|
107
|
+
clearProjectItemIdFromDoneJson(doneFilePath, projectItemId);
|
|
108
|
+
} else if (withoutThisItem.length !== items.length) {
|
|
109
|
+
writeJsonAtomic(filePath, { ...existing, items: withoutThisItem });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -4,10 +4,10 @@ import type { Issue } from '../../../domain/entities/Issue';
|
|
|
4
4
|
import type { Project } from '../../../domain/entities/Project';
|
|
5
5
|
import {
|
|
6
6
|
ConsoleLists,
|
|
7
|
-
ConsoleTabName,
|
|
8
7
|
GenerateConsoleListsUseCase,
|
|
9
8
|
} from '../../../domain/usecases/console/GenerateConsoleListsUseCase';
|
|
10
9
|
import { resetDoneProjectItemIdsAcrossTabs } from '../console/consoleDoneStore';
|
|
10
|
+
import { CONSOLE_LIST_TAB_NAMES } from '../console/consoleTabNames';
|
|
11
11
|
|
|
12
12
|
export type ConsoleListsWriterParams = {
|
|
13
13
|
consoleDataOutputDir: string | null | undefined;
|
|
@@ -19,16 +19,6 @@ export type ConsoleListsWriterParams = {
|
|
|
19
19
|
generatedAt?: string;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
const CONSOLE_TAB_NAMES: ConsoleTabName[] = [
|
|
23
|
-
'workflow-blocker',
|
|
24
|
-
'prs',
|
|
25
|
-
'triage',
|
|
26
|
-
'unread',
|
|
27
|
-
'failed-preparation',
|
|
28
|
-
'todo-by-human',
|
|
29
|
-
'todo-by-agent',
|
|
30
|
-
];
|
|
31
|
-
|
|
32
22
|
export const formatConsoleGeneratedAt = (date: Date): string =>
|
|
33
23
|
date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
34
24
|
|
|
@@ -57,7 +47,7 @@ export const writeConsoleLists = (params: ConsoleListsWriterParams): void => {
|
|
|
57
47
|
workflowBlockerStoryName: params.workflowBlockerStoryName ?? null,
|
|
58
48
|
});
|
|
59
49
|
|
|
60
|
-
for (const tab of
|
|
50
|
+
for (const tab of CONSOLE_LIST_TAB_NAMES) {
|
|
61
51
|
writeJsonAtomic(
|
|
62
52
|
path.join(consoleDataOutputDir, pjcode, tab, 'list.json'),
|
|
63
53
|
lists[tab],
|
|
@@ -69,7 +69,7 @@ describe('FileSystemSubAgentLivenessResolver', () => {
|
|
|
69
69
|
expect(result).toEqual(new Set());
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it('returns
|
|
72
|
+
it('returns null when the runtime root exists and the session has no running file', async () => {
|
|
73
73
|
const resolver = new FileSystemSubAgentLivenessResolver(runtimeRoot);
|
|
74
74
|
|
|
75
75
|
const result = await resolver.resolveLiveSubAgentIds({
|
|
@@ -77,7 +77,7 @@ describe('FileSystemSubAgentLivenessResolver', () => {
|
|
|
77
77
|
mainTranscriptPath,
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
expect(result).
|
|
80
|
+
expect(result).toBeNull();
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
it('returns null when the configured runtime root directory does not exist', async () => {
|
|
@@ -192,7 +192,7 @@ describe('running-subagents record agent id contract with TranscriptSessionSubAg
|
|
|
192
192
|
]);
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
it('
|
|
195
|
+
it('detects a working sub-agent when the runtime root exists and the session has no running-subagents record', async () => {
|
|
196
196
|
const subAgentsDirectory = path.join(
|
|
197
197
|
transcriptRoot,
|
|
198
198
|
cwdSlug,
|
|
@@ -224,6 +224,14 @@ describe('running-subagents record agent id contract with TranscriptSessionSubAg
|
|
|
224
224
|
new Map([[sessionName, mainTranscriptPath]]),
|
|
225
225
|
);
|
|
226
226
|
|
|
227
|
-
expect(result.get(sessionName)).
|
|
227
|
+
expect(result.get(sessionName)).toEqual([
|
|
228
|
+
{
|
|
229
|
+
label: `agent-${liveAgentId}`,
|
|
230
|
+
silentSeconds: 0,
|
|
231
|
+
runningSeconds: 694800,
|
|
232
|
+
waitingOnExternalProcess: false,
|
|
233
|
+
finishedResultUnconsumed: false,
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
228
236
|
});
|
|
229
237
|
});
|
|
@@ -4,12 +4,6 @@ import { SubAgentLivenessResolver } from '../../domain/usecases/adapter-interfac
|
|
|
4
4
|
|
|
5
5
|
const RUNNING_SUBAGENTS_FILE_NAME = 'running-subagents.txt';
|
|
6
6
|
|
|
7
|
-
const isMissingFileError = (error: unknown): boolean =>
|
|
8
|
-
typeof error === 'object' &&
|
|
9
|
-
error !== null &&
|
|
10
|
-
'code' in error &&
|
|
11
|
-
error.code === 'ENOENT';
|
|
12
|
-
|
|
13
7
|
export class FileSystemSubAgentLivenessResolver implements SubAgentLivenessResolver {
|
|
14
8
|
constructor(private readonly runtimeRootDirectory: string | null) {}
|
|
15
9
|
|
|
@@ -26,10 +20,7 @@ export class FileSystemSubAgentLivenessResolver implements SubAgentLivenessResol
|
|
|
26
20
|
let content: string;
|
|
27
21
|
try {
|
|
28
22
|
content = fs.readFileSync(runningFilePath, 'utf8');
|
|
29
|
-
} catch
|
|
30
|
-
if (isMissingFileError(error) && this.hasReadableRuntimeRootDirectory()) {
|
|
31
|
-
return new Set();
|
|
32
|
-
}
|
|
23
|
+
} catch {
|
|
33
24
|
return null;
|
|
34
25
|
}
|
|
35
26
|
const liveIds = new Set<string>();
|
|
@@ -42,17 +33,6 @@ export class FileSystemSubAgentLivenessResolver implements SubAgentLivenessResol
|
|
|
42
33
|
return liveIds;
|
|
43
34
|
};
|
|
44
35
|
|
|
45
|
-
private hasReadableRuntimeRootDirectory = (): boolean => {
|
|
46
|
-
if (this.runtimeRootDirectory === null) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
return fs.statSync(this.runtimeRootDirectory).isDirectory();
|
|
51
|
-
} catch {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
36
|
private resolveRunningSubAgentsFilePath = (
|
|
57
37
|
mainTranscriptPath: string | null,
|
|
58
38
|
): string | null => {
|
|
@@ -3363,4 +3363,163 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
3363
3363
|
expect(mockIssueRepository.approvePullRequest).not.toHaveBeenCalled();
|
|
3364
3364
|
});
|
|
3365
3365
|
});
|
|
3366
|
+
|
|
3367
|
+
describe('consoleTabsRepository integration', () => {
|
|
3368
|
+
let mockConsoleTabsRepository: {
|
|
3369
|
+
patchIssueTabTransition: jest.Mock;
|
|
3370
|
+
};
|
|
3371
|
+
|
|
3372
|
+
beforeEach(() => {
|
|
3373
|
+
mockConsoleTabsRepository = {
|
|
3374
|
+
patchIssueTabTransition: jest.fn(),
|
|
3375
|
+
};
|
|
3376
|
+
useCase = new NotifyFinishedIssuePreparationUseCase(
|
|
3377
|
+
mockProjectRepository,
|
|
3378
|
+
mockIssueRepository,
|
|
3379
|
+
mockIssueCommentRepository,
|
|
3380
|
+
mockWebhookRepository,
|
|
3381
|
+
mockConsoleTabsRepository,
|
|
3382
|
+
);
|
|
3383
|
+
});
|
|
3384
|
+
|
|
3385
|
+
it('calls patchIssueTabTransition with prs tab when issue transitions to Awaiting Quality Check', async () => {
|
|
3386
|
+
const issue = createMockIssue({
|
|
3387
|
+
status: 'Preparation',
|
|
3388
|
+
itemId: 'item-1',
|
|
3389
|
+
});
|
|
3390
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
3391
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
3392
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
3393
|
+
createMockComment({ content: 'From: :robot: report' }),
|
|
3394
|
+
]);
|
|
3395
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
3396
|
+
{
|
|
3397
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
3398
|
+
isConflicted: false,
|
|
3399
|
+
isPassedAllCiJob: true,
|
|
3400
|
+
isCiStateSuccess: true,
|
|
3401
|
+
isResolvedAllReviewComments: true,
|
|
3402
|
+
isBranchOutOfDate: false,
|
|
3403
|
+
missingRequiredCheckNames: [],
|
|
3404
|
+
},
|
|
3405
|
+
]);
|
|
3406
|
+
|
|
3407
|
+
await useCase.run({
|
|
3408
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
3409
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
3410
|
+
thresholdForAutoReject: 3,
|
|
3411
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
3412
|
+
allowedIssueAuthors: null,
|
|
3413
|
+
});
|
|
3414
|
+
|
|
3415
|
+
expect(
|
|
3416
|
+
mockConsoleTabsRepository.patchIssueTabTransition,
|
|
3417
|
+
).toHaveBeenCalledWith(
|
|
3418
|
+
expect.objectContaining({
|
|
3419
|
+
projectItemId: 'item-1',
|
|
3420
|
+
targetTabName: 'prs',
|
|
3421
|
+
}),
|
|
3422
|
+
);
|
|
3423
|
+
});
|
|
3424
|
+
|
|
3425
|
+
it('calls patchIssueTabTransition with null tab when issue transitions to Awaiting Workspace', async () => {
|
|
3426
|
+
const issue = createMockIssue({
|
|
3427
|
+
status: 'Preparation',
|
|
3428
|
+
itemId: 'item-2',
|
|
3429
|
+
dependedIssueUrls: ['https://github.com/user/repo/issues/99'],
|
|
3430
|
+
});
|
|
3431
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
3432
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
3433
|
+
|
|
3434
|
+
await useCase.run({
|
|
3435
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
3436
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
3437
|
+
thresholdForAutoReject: 3,
|
|
3438
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
3439
|
+
allowedIssueAuthors: null,
|
|
3440
|
+
});
|
|
3441
|
+
|
|
3442
|
+
expect(
|
|
3443
|
+
mockConsoleTabsRepository.patchIssueTabTransition,
|
|
3444
|
+
).toHaveBeenCalledWith(
|
|
3445
|
+
expect.objectContaining({
|
|
3446
|
+
projectItemId: 'item-2',
|
|
3447
|
+
targetTabName: null,
|
|
3448
|
+
}),
|
|
3449
|
+
);
|
|
3450
|
+
});
|
|
3451
|
+
|
|
3452
|
+
it('passes relatedOpenPullRequestUrls from findRelatedOpenPRs into the prs tab item', async () => {
|
|
3453
|
+
const issue = createMockIssue({
|
|
3454
|
+
status: 'Preparation',
|
|
3455
|
+
itemId: 'item-1',
|
|
3456
|
+
isPr: false,
|
|
3457
|
+
});
|
|
3458
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
3459
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
3460
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
3461
|
+
createMockComment({ content: 'From: :robot: report' }),
|
|
3462
|
+
]);
|
|
3463
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
3464
|
+
{
|
|
3465
|
+
url: 'https://github.com/user/repo/pull/42',
|
|
3466
|
+
isConflicted: false,
|
|
3467
|
+
isPassedAllCiJob: true,
|
|
3468
|
+
isCiStateSuccess: true,
|
|
3469
|
+
isResolvedAllReviewComments: true,
|
|
3470
|
+
isBranchOutOfDate: false,
|
|
3471
|
+
missingRequiredCheckNames: [],
|
|
3472
|
+
},
|
|
3473
|
+
]);
|
|
3474
|
+
let capturedArg: unknown = undefined;
|
|
3475
|
+
mockConsoleTabsRepository.patchIssueTabTransition.mockImplementation(
|
|
3476
|
+
(arg: unknown) => {
|
|
3477
|
+
capturedArg = arg;
|
|
3478
|
+
},
|
|
3479
|
+
);
|
|
3480
|
+
|
|
3481
|
+
await useCase.run({
|
|
3482
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
3483
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
3484
|
+
thresholdForAutoReject: 3,
|
|
3485
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
3486
|
+
allowedIssueAuthors: null,
|
|
3487
|
+
});
|
|
3488
|
+
|
|
3489
|
+
expect(capturedArg).toMatchObject({
|
|
3490
|
+
targetTabName: 'prs',
|
|
3491
|
+
item: {
|
|
3492
|
+
relatedOpenPullRequestUrls: ['https://github.com/user/repo/pull/42'],
|
|
3493
|
+
},
|
|
3494
|
+
});
|
|
3495
|
+
});
|
|
3496
|
+
|
|
3497
|
+
it('does not call patchIssueTabTransition when consoleTabsRepository is not provided', async () => {
|
|
3498
|
+
useCase = new NotifyFinishedIssuePreparationUseCase(
|
|
3499
|
+
mockProjectRepository,
|
|
3500
|
+
mockIssueRepository,
|
|
3501
|
+
mockIssueCommentRepository,
|
|
3502
|
+
mockWebhookRepository,
|
|
3503
|
+
);
|
|
3504
|
+
const issue = createMockIssue({
|
|
3505
|
+
status: 'Preparation',
|
|
3506
|
+
dependedIssueUrls: ['https://github.com/user/repo/issues/99'],
|
|
3507
|
+
});
|
|
3508
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
3509
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
3510
|
+
|
|
3511
|
+
await expect(
|
|
3512
|
+
useCase.run({
|
|
3513
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
3514
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
3515
|
+
thresholdForAutoReject: 3,
|
|
3516
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
3517
|
+
allowedIssueAuthors: null,
|
|
3518
|
+
}),
|
|
3519
|
+
).resolves.not.toThrow();
|
|
3520
|
+
expect(
|
|
3521
|
+
mockConsoleTabsRepository.patchIssueTabTransition,
|
|
3522
|
+
).not.toHaveBeenCalled();
|
|
3523
|
+
});
|
|
3524
|
+
});
|
|
3366
3525
|
});
|