hungry-ghost-hive 0.43.2 → 0.45.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/dist/agents/base-agent.d.ts +1 -0
- package/dist/agents/base-agent.d.ts.map +1 -1
- package/dist/agents/base-agent.js +4 -0
- package/dist/agents/base-agent.js.map +1 -1
- package/dist/agents/intermediate.js +2 -2
- package/dist/agents/intermediate.js.map +1 -1
- package/dist/agents/junior.js +2 -2
- package/dist/agents/junior.js.map +1 -1
- package/dist/agents/qa.d.ts.map +1 -1
- package/dist/agents/qa.js +5 -5
- package/dist/agents/qa.js.map +1 -1
- package/dist/agents/senior.d.ts.map +1 -1
- package/dist/agents/senior.js +5 -5
- package/dist/agents/senior.js.map +1 -1
- package/dist/agents/tech-lead.d.ts.map +1 -1
- package/dist/agents/tech-lead.js +8 -3
- package/dist/agents/tech-lead.js.map +1 -1
- package/dist/cli/commands/assign.d.ts.map +1 -1
- package/dist/cli/commands/assign.js +4 -2
- package/dist/cli/commands/assign.js.map +1 -1
- package/dist/cli/commands/assign.test.js +5 -0
- package/dist/cli/commands/assign.test.js.map +1 -1
- package/dist/cli/commands/manager/handoff-recovery.d.ts.map +1 -1
- package/dist/cli/commands/manager/handoff-recovery.js +4 -2
- package/dist/cli/commands/manager/handoff-recovery.js.map +1 -1
- package/dist/cli/commands/manager/index.d.ts.map +1 -1
- package/dist/cli/commands/manager/index.js +16 -12
- package/dist/cli/commands/manager/index.js.map +1 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.d.ts.map +1 -1
- package/dist/cli/commands/manager/tech-lead-lifecycle.js +8 -3
- package/dist/cli/commands/manager/tech-lead-lifecycle.js.map +1 -1
- package/dist/cli/commands/msg.d.ts.map +1 -1
- package/dist/cli/commands/msg.js +8 -7
- package/dist/cli/commands/msg.js.map +1 -1
- package/dist/cli/commands/my-stories.js +3 -3
- package/dist/cli/commands/my-stories.js.map +1 -1
- package/dist/cli/commands/nuke.d.ts.map +1 -1
- package/dist/cli/commands/nuke.js +18 -7
- package/dist/cli/commands/nuke.js.map +1 -1
- package/dist/cli/commands/nuke.test.js +24 -0
- package/dist/cli/commands/nuke.test.js.map +1 -1
- package/dist/cli/commands/pr.js +5 -0
- package/dist/cli/commands/pr.js.map +1 -1
- package/dist/cli/commands/pr.test.js +43 -1
- package/dist/cli/commands/pr.test.js.map +1 -1
- package/dist/cli/commands/req.d.ts +1 -1
- package/dist/cli/commands/req.d.ts.map +1 -1
- package/dist/cli/commands/req.js +9 -6
- package/dist/cli/commands/req.js.map +1 -1
- package/dist/cli/commands/resume.d.ts.map +1 -1
- package/dist/cli/commands/resume.js +4 -1
- package/dist/cli/commands/resume.js.map +1 -1
- package/dist/cli/commands/stories.js +3 -3
- package/dist/cli/commands/stories.js.map +1 -1
- package/dist/cli/dashboard/panels/agents.d.ts.map +1 -1
- package/dist/cli/dashboard/panels/agents.js +7 -3
- package/dist/cli/dashboard/panels/agents.js.map +1 -1
- package/dist/cli-runtimes/chrome.d.ts +17 -0
- package/dist/cli-runtimes/chrome.d.ts.map +1 -0
- package/dist/cli-runtimes/chrome.js +36 -0
- package/dist/cli-runtimes/chrome.js.map +1 -0
- package/dist/cli-runtimes/claude.d.ts +3 -3
- package/dist/cli-runtimes/claude.d.ts.map +1 -1
- package/dist/cli-runtimes/claude.js +14 -8
- package/dist/cli-runtimes/claude.js.map +1 -1
- package/dist/cli-runtimes/codex.d.ts +3 -3
- package/dist/cli-runtimes/codex.d.ts.map +1 -1
- package/dist/cli-runtimes/codex.js +2 -2
- package/dist/cli-runtimes/codex.js.map +1 -1
- package/dist/cli-runtimes/gemini.d.ts +3 -3
- package/dist/cli-runtimes/gemini.d.ts.map +1 -1
- package/dist/cli-runtimes/gemini.js +2 -2
- package/dist/cli-runtimes/gemini.js.map +1 -1
- package/dist/cli-runtimes/index.d.ts +3 -2
- package/dist/cli-runtimes/index.d.ts.map +1 -1
- package/dist/cli-runtimes/index.js +1 -0
- package/dist/cli-runtimes/index.js.map +1 -1
- package/dist/cli-runtimes/index.test.js +133 -1
- package/dist/cli-runtimes/index.test.js.map +1 -1
- package/dist/cli-runtimes/types.d.ts +9 -2
- package/dist/cli-runtimes/types.d.ts.map +1 -1
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +6 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/context-files/generator.d.ts +1 -1
- package/dist/context-files/generator.d.ts.map +1 -1
- package/dist/context-files/generator.js +3 -2
- package/dist/context-files/generator.js.map +1 -1
- package/dist/context-files/index.test.js +2 -0
- package/dist/context-files/index.test.js.map +1 -1
- package/dist/db/client.d.ts +1 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +6 -0
- package/dist/db/client.js.map +1 -1
- package/dist/db/migrations/015-add-story-markdown-path.sql +5 -0
- package/dist/db/queries/stories.d.ts +3 -3
- package/dist/db/queries/stories.d.ts.map +1 -1
- package/dist/db/queries/stories.js +23 -5
- package/dist/db/queries/stories.js.map +1 -1
- package/dist/db/queries/test-helpers.d.ts.map +1 -1
- package/dist/db/queries/test-helpers.js +1 -0
- package/dist/db/queries/test-helpers.js.map +1 -1
- package/dist/git/worktree.d.ts.map +1 -1
- package/dist/git/worktree.js +7 -0
- package/dist/git/worktree.js.map +1 -1
- package/dist/git/worktree.test.js +30 -0
- package/dist/git/worktree.test.js.map +1 -1
- package/dist/orchestrator/prompt-templates.d.ts +3 -1
- package/dist/orchestrator/prompt-templates.d.ts.map +1 -1
- package/dist/orchestrator/prompt-templates.js +16 -8
- package/dist/orchestrator/prompt-templates.js.map +1 -1
- package/dist/orchestrator/prompt-templates.test.js +4 -0
- package/dist/orchestrator/prompt-templates.test.js.map +1 -1
- package/dist/orchestrator/scheduler.d.ts.map +1 -1
- package/dist/orchestrator/scheduler.js +23 -12
- package/dist/orchestrator/scheduler.js.map +1 -1
- package/dist/orchestrator/scheduler.test.js +1 -0
- package/dist/orchestrator/scheduler.test.js.map +1 -1
- package/dist/tmux/manager.d.ts +7 -6
- package/dist/tmux/manager.d.ts.map +1 -1
- package/dist/tmux/manager.js +29 -13
- package/dist/tmux/manager.js.map +1 -1
- package/dist/utils/auto-merge.d.ts.map +1 -1
- package/dist/utils/auto-merge.js +66 -5
- package/dist/utils/auto-merge.js.map +1 -1
- package/dist/utils/auto-merge.test.js +62 -0
- package/dist/utils/auto-merge.test.js.map +1 -1
- package/dist/utils/instance.d.ts +32 -0
- package/dist/utils/instance.d.ts.map +1 -0
- package/dist/utils/instance.js +82 -0
- package/dist/utils/instance.js.map +1 -0
- package/dist/utils/instance.test.d.ts +2 -0
- package/dist/utils/instance.test.d.ts.map +1 -0
- package/dist/utils/instance.test.js +103 -0
- package/dist/utils/instance.test.js.map +1 -0
- package/dist/utils/paths.d.ts +2 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +2 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/paths.test.js +6 -0
- package/dist/utils/paths.test.js.map +1 -1
- package/dist/utils/story-markdown.d.ts +16 -0
- package/dist/utils/story-markdown.d.ts.map +1 -0
- package/dist/utils/story-markdown.js +82 -0
- package/dist/utils/story-markdown.js.map +1 -0
- package/dist/utils/story-markdown.test.d.ts +2 -0
- package/dist/utils/story-markdown.test.d.ts.map +1 -0
- package/dist/utils/story-markdown.test.js +143 -0
- package/dist/utils/story-markdown.test.js.map +1 -0
- package/package.json +1 -1
- package/src/agents/base-agent.ts +5 -0
- package/src/agents/intermediate.ts +2 -2
- package/src/agents/junior.ts +2 -2
- package/src/agents/qa.ts +13 -8
- package/src/agents/senior.ts +21 -11
- package/src/agents/tech-lead.ts +28 -13
- package/src/cli/commands/assign.test.ts +5 -0
- package/src/cli/commands/assign.ts +4 -2
- package/src/cli/commands/manager/handoff-recovery.ts +4 -2
- package/src/cli/commands/manager/index.ts +16 -11
- package/src/cli/commands/manager/tech-lead-lifecycle.ts +9 -3
- package/src/cli/commands/msg.ts +8 -7
- package/src/cli/commands/my-stories.ts +22 -13
- package/src/cli/commands/nuke.test.ts +31 -0
- package/src/cli/commands/nuke.ts +18 -7
- package/src/cli/commands/pr.test.ts +77 -1
- package/src/cli/commands/pr.ts +5 -0
- package/src/cli/commands/req.ts +13 -6
- package/src/cli/commands/resume.ts +4 -1
- package/src/cli/commands/stories.ts +22 -13
- package/src/cli/dashboard/panels/agents.ts +7 -3
- package/src/cli-runtimes/chrome.ts +43 -0
- package/src/cli-runtimes/claude.ts +26 -9
- package/src/cli-runtimes/codex.ts +12 -3
- package/src/cli-runtimes/gemini.ts +12 -3
- package/src/cli-runtimes/index.test.ts +158 -0
- package/src/cli-runtimes/index.ts +3 -2
- package/src/cli-runtimes/types.ts +19 -2
- package/src/config/schema.ts +6 -0
- package/src/context-files/generator.ts +3 -2
- package/src/context-files/index.test.ts +2 -0
- package/src/db/client.ts +7 -0
- package/src/db/migrations/015-add-story-markdown-path.sql +5 -0
- package/src/db/queries/stories.ts +29 -5
- package/src/db/queries/test-helpers.ts +1 -0
- package/src/git/worktree.test.ts +43 -0
- package/src/git/worktree.ts +10 -0
- package/src/orchestrator/prompt-templates.test.ts +4 -0
- package/src/orchestrator/prompt-templates.ts +20 -8
- package/src/orchestrator/scheduler.test.ts +1 -0
- package/src/orchestrator/scheduler.ts +33 -12
- package/src/tmux/manager.ts +42 -13
- package/src/utils/auto-merge.test.ts +81 -0
- package/src/utils/auto-merge.ts +78 -5
- package/src/utils/instance.test.ts +129 -0
- package/src/utils/instance.ts +95 -0
- package/src/utils/paths.test.ts +8 -0
- package/src/utils/paths.ts +3 -0
- package/src/utils/story-markdown.test.ts +176 -0
- package/src/utils/story-markdown.ts +94 -0
|
@@ -21,6 +21,11 @@ vi.mock('./paths.js', () => ({
|
|
|
21
21
|
getHivePaths: vi.fn(() => ({ hiveDir: '/mock/hive' })),
|
|
22
22
|
}));
|
|
23
23
|
|
|
24
|
+
vi.mock('../connectors/project-management/operations.js', () => ({
|
|
25
|
+
postLifecycleComment: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
syncStatusForStory: vi.fn(),
|
|
27
|
+
}));
|
|
28
|
+
|
|
24
29
|
import { loadConfig } from '../config/loader.js';
|
|
25
30
|
import { autoMergeApprovedPRs } from './auto-merge.js';
|
|
26
31
|
|
|
@@ -266,6 +271,82 @@ describe('auto-merge functionality', () => {
|
|
|
266
271
|
expect(mockLoadConfig).toHaveBeenCalledWith('/mock/hive');
|
|
267
272
|
});
|
|
268
273
|
|
|
274
|
+
it('should keep PR as queued when auto-merge is pending (PR still open after gh pr merge --auto)', async () => {
|
|
275
|
+
const pr = createPullRequest(db, {
|
|
276
|
+
storyId,
|
|
277
|
+
teamId,
|
|
278
|
+
branchName: 'feature/auto-merge-pending',
|
|
279
|
+
githubPrNumber: 456,
|
|
280
|
+
});
|
|
281
|
+
updatePullRequest(db, pr.id, { status: 'approved' });
|
|
282
|
+
|
|
283
|
+
mockLoadConfig.mockReturnValue({
|
|
284
|
+
integrations: {
|
|
285
|
+
autonomy: { level: 'full' },
|
|
286
|
+
source_control: { provider: 'github' },
|
|
287
|
+
project_management: { provider: 'none' },
|
|
288
|
+
},
|
|
289
|
+
} as any);
|
|
290
|
+
|
|
291
|
+
// Mock execSync: first call returns OPEN+MERGEABLE, merge command succeeds,
|
|
292
|
+
// second call (post-merge check) returns OPEN (auto-merge pending)
|
|
293
|
+
const mockExecSync = vi.fn();
|
|
294
|
+
mockExecSync
|
|
295
|
+
.mockReturnValueOnce(
|
|
296
|
+
JSON.stringify({ state: 'OPEN', mergeable: 'MERGEABLE', mergeStateStatus: 'CLEAN' })
|
|
297
|
+
)
|
|
298
|
+
.mockReturnValueOnce(undefined) // gh pr merge --auto
|
|
299
|
+
.mockReturnValueOnce(
|
|
300
|
+
JSON.stringify({ state: 'OPEN', mergeable: 'MERGEABLE', mergeStateStatus: 'BLOCKED' })
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
vi.doMock('child_process', () => ({ execSync: mockExecSync }));
|
|
304
|
+
|
|
305
|
+
const dbClient = { db, save: vi.fn(), close: vi.fn(), runMigrations: vi.fn() };
|
|
306
|
+
const result = await autoMergeApprovedPRs('/mock/root', dbClient);
|
|
307
|
+
|
|
308
|
+
// Should return 0 because the PR was not actually merged yet
|
|
309
|
+
expect(result).toBe(0);
|
|
310
|
+
// PR should remain 'queued' (not rolled back to 'approved' or advanced to 'merged')
|
|
311
|
+
expect(getPullRequestById(db, pr.id)?.status).toBe('queued');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should reset stale branch PR to approved after updating behind branch', async () => {
|
|
315
|
+
const pr = createPullRequest(db, {
|
|
316
|
+
storyId,
|
|
317
|
+
teamId,
|
|
318
|
+
branchName: 'feature/stale-branch',
|
|
319
|
+
githubPrNumber: 789,
|
|
320
|
+
});
|
|
321
|
+
updatePullRequest(db, pr.id, { status: 'approved' });
|
|
322
|
+
|
|
323
|
+
mockLoadConfig.mockReturnValue({
|
|
324
|
+
integrations: {
|
|
325
|
+
autonomy: { level: 'full' },
|
|
326
|
+
source_control: { provider: 'github' },
|
|
327
|
+
project_management: { provider: 'none' },
|
|
328
|
+
},
|
|
329
|
+
} as any);
|
|
330
|
+
|
|
331
|
+
// Mock execSync: PR state shows BEHIND, then gh pr update-branch succeeds
|
|
332
|
+
const mockExecSync = vi.fn();
|
|
333
|
+
mockExecSync
|
|
334
|
+
.mockReturnValueOnce(
|
|
335
|
+
JSON.stringify({ state: 'OPEN', mergeable: 'MERGEABLE', mergeStateStatus: 'BEHIND' })
|
|
336
|
+
)
|
|
337
|
+
.mockReturnValueOnce(undefined); // gh pr update-branch
|
|
338
|
+
|
|
339
|
+
vi.doMock('child_process', () => ({ execSync: mockExecSync }));
|
|
340
|
+
|
|
341
|
+
const dbClient = { db, save: vi.fn(), close: vi.fn(), runMigrations: vi.fn() };
|
|
342
|
+
const result = await autoMergeApprovedPRs('/mock/root', dbClient);
|
|
343
|
+
|
|
344
|
+
// Should return 0 because no merge happened yet
|
|
345
|
+
expect(result).toBe(0);
|
|
346
|
+
// PR should be reset to 'approved' to be retried on next cycle
|
|
347
|
+
expect(getPullRequestById(db, pr.id)?.status).toBe('approved');
|
|
348
|
+
});
|
|
349
|
+
|
|
269
350
|
it('should skip approved PRs marked for manual merge', async () => {
|
|
270
351
|
const pr = createPullRequest(db, {
|
|
271
352
|
storyId,
|
package/src/utils/auto-merge.ts
CHANGED
|
@@ -29,6 +29,7 @@ const PR_MERGE_TIMEOUT_MS = 60000;
|
|
|
29
29
|
interface GitHubPRState {
|
|
30
30
|
state: string;
|
|
31
31
|
mergeable: string;
|
|
32
|
+
mergeStateStatus: string;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -149,6 +150,8 @@ export async function autoMergeApprovedPRs(
|
|
|
149
150
|
claimed: ClaimedPR;
|
|
150
151
|
outcome:
|
|
151
152
|
| { type: 'merged' }
|
|
153
|
+
| { type: 'auto_merge_pending' }
|
|
154
|
+
| { type: 'branch_updated' }
|
|
152
155
|
| { type: 'already_closed'; prState: GitHubPRState }
|
|
153
156
|
| { type: 'conflicts' }
|
|
154
157
|
| { type: 'unknown_state' }
|
|
@@ -163,10 +166,9 @@ export async function autoMergeApprovedPRs(
|
|
|
163
166
|
try {
|
|
164
167
|
// Check PR state
|
|
165
168
|
let prState: GitHubPRState;
|
|
166
|
-
let mergeableStatus: boolean;
|
|
167
169
|
try {
|
|
168
170
|
const prViewOutput = execSync(
|
|
169
|
-
`gh pr view ${pr.github_pr_number} --json state,mergeable${repoFlag}`,
|
|
171
|
+
`gh pr view ${pr.github_pr_number} --json state,mergeable,mergeStateStatus${repoFlag}`,
|
|
170
172
|
{
|
|
171
173
|
stdio: 'pipe',
|
|
172
174
|
cwd: repoCwd,
|
|
@@ -175,7 +177,6 @@ export async function autoMergeApprovedPRs(
|
|
|
175
177
|
}
|
|
176
178
|
);
|
|
177
179
|
prState = JSON.parse(prViewOutput);
|
|
178
|
-
mergeableStatus = prState.mergeable === 'MERGEABLE';
|
|
179
180
|
} catch {
|
|
180
181
|
results.push({ claimed, outcome: { type: 'unknown_state' } });
|
|
181
182
|
continue;
|
|
@@ -186,7 +187,22 @@ export async function autoMergeApprovedPRs(
|
|
|
186
187
|
continue;
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
|
|
190
|
+
// Update stale branches that are behind the base branch
|
|
191
|
+
if (prState.mergeStateStatus === 'BEHIND') {
|
|
192
|
+
try {
|
|
193
|
+
execSync(`gh pr update-branch ${pr.github_pr_number}${repoFlag}`, {
|
|
194
|
+
stdio: 'pipe',
|
|
195
|
+
cwd: repoCwd,
|
|
196
|
+
timeout: PR_MERGE_TIMEOUT_MS,
|
|
197
|
+
});
|
|
198
|
+
results.push({ claimed, outcome: { type: 'branch_updated' } });
|
|
199
|
+
} catch {
|
|
200
|
+
results.push({ claimed, outcome: { type: 'unknown_state' } });
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (prState.mergeable !== 'MERGEABLE') {
|
|
190
206
|
results.push({ claimed, outcome: { type: 'conflicts' } });
|
|
191
207
|
continue;
|
|
192
208
|
}
|
|
@@ -198,7 +214,32 @@ export async function autoMergeApprovedPRs(
|
|
|
198
214
|
cwd: repoCwd,
|
|
199
215
|
timeout: PR_MERGE_TIMEOUT_MS,
|
|
200
216
|
});
|
|
201
|
-
|
|
217
|
+
|
|
218
|
+
// Verify actual merge state: --auto may queue the merge rather than merge immediately
|
|
219
|
+
let postMergeState: GitHubPRState;
|
|
220
|
+
try {
|
|
221
|
+
const postMergeOutput = execSync(
|
|
222
|
+
`gh pr view ${pr.github_pr_number} --json state,mergeable,mergeStateStatus${repoFlag}`,
|
|
223
|
+
{
|
|
224
|
+
stdio: 'pipe',
|
|
225
|
+
cwd: repoCwd,
|
|
226
|
+
encoding: 'utf-8',
|
|
227
|
+
timeout: PR_STATE_CHECK_TIMEOUT_MS,
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
postMergeState = JSON.parse(postMergeOutput);
|
|
231
|
+
} catch {
|
|
232
|
+
// If we can't re-check, assume it merged (command succeeded)
|
|
233
|
+
results.push({ claimed, outcome: { type: 'merged' } });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (postMergeState.state === 'MERGED') {
|
|
238
|
+
results.push({ claimed, outcome: { type: 'merged' } });
|
|
239
|
+
} else {
|
|
240
|
+
// PR is OPEN with auto-merge enabled — GitHub will merge when CI passes
|
|
241
|
+
results.push({ claimed, outcome: { type: 'auto_merge_pending' } });
|
|
242
|
+
}
|
|
202
243
|
} catch (mergeErr) {
|
|
203
244
|
results.push({
|
|
204
245
|
claimed,
|
|
@@ -340,6 +381,38 @@ export async function autoMergeApprovedPRs(
|
|
|
340
381
|
break;
|
|
341
382
|
}
|
|
342
383
|
|
|
384
|
+
case 'auto_merge_pending': {
|
|
385
|
+
createLog(phaseDb.db, {
|
|
386
|
+
agentId: 'manager',
|
|
387
|
+
storyId: pr.story_id || undefined,
|
|
388
|
+
eventType: 'PR_MERGE_SKIPPED',
|
|
389
|
+
status: 'info',
|
|
390
|
+
message: `PR #${pr.github_pr_number} is queued for auto-merge, waiting for CI checks to complete`,
|
|
391
|
+
metadata: { pr_id: pr.id },
|
|
392
|
+
});
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
case 'branch_updated': {
|
|
397
|
+
// Reset to 'approved' so the PR is retried on the next cycle once CI passes
|
|
398
|
+
await withTransaction(
|
|
399
|
+
phaseDb.db,
|
|
400
|
+
() => {
|
|
401
|
+
updatePullRequest(phaseDb.db, pr.id, { status: 'approved' });
|
|
402
|
+
createLog(phaseDb.db, {
|
|
403
|
+
agentId: 'manager',
|
|
404
|
+
storyId: pr.story_id || undefined,
|
|
405
|
+
eventType: 'PR_MERGE_SKIPPED',
|
|
406
|
+
status: 'info',
|
|
407
|
+
message: `Updated stale branch for PR #${pr.github_pr_number} (was behind base branch), will retry merge`,
|
|
408
|
+
metadata: { pr_id: pr.id },
|
|
409
|
+
});
|
|
410
|
+
},
|
|
411
|
+
() => phaseDb.save()
|
|
412
|
+
);
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
|
|
343
416
|
case 'merge_failed': {
|
|
344
417
|
await withTransaction(
|
|
345
418
|
phaseDb.db,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Licensed under the Hungry Ghost Hive License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
buildInstanceSessionName,
|
|
9
|
+
getInstanceId,
|
|
10
|
+
getInstancePrefix,
|
|
11
|
+
getManagerLockPath,
|
|
12
|
+
getManagerSessionName,
|
|
13
|
+
getTechLeadSessionName,
|
|
14
|
+
} from './instance.js';
|
|
15
|
+
|
|
16
|
+
describe('instance', () => {
|
|
17
|
+
let testHiveDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
testHiveDir = join(tmpdir(), `hive-test-instance-${Date.now()}`);
|
|
21
|
+
mkdirSync(testHiveDir, { recursive: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
rmSync(testHiveDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('getInstanceId', () => {
|
|
29
|
+
it('should return null if hive directory does not exist', () => {
|
|
30
|
+
const result = getInstanceId('/nonexistent/path/.hive');
|
|
31
|
+
expect(result).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should create and persist an instance ID', () => {
|
|
35
|
+
const id = getInstanceId(testHiveDir);
|
|
36
|
+
expect(id).toBeTruthy();
|
|
37
|
+
expect(typeof id).toBe('string');
|
|
38
|
+
expect(id!.length).toBe(6);
|
|
39
|
+
|
|
40
|
+
// Should be persisted
|
|
41
|
+
const fileContent = readFileSync(join(testHiveDir, 'instance.id'), 'utf-8').trim();
|
|
42
|
+
expect(fileContent).toBe(id);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return the same ID on subsequent calls', () => {
|
|
46
|
+
const id1 = getInstanceId(testHiveDir);
|
|
47
|
+
const id2 = getInstanceId(testHiveDir);
|
|
48
|
+
expect(id1).toBe(id2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should read existing instance ID from file', () => {
|
|
52
|
+
writeFileSync(join(testHiveDir, 'instance.id'), 'testid', 'utf-8');
|
|
53
|
+
const id = getInstanceId(testHiveDir);
|
|
54
|
+
expect(id).toBe('testid');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('getInstancePrefix', () => {
|
|
59
|
+
it('should return hive-<instanceId> when hive dir exists', () => {
|
|
60
|
+
writeFileSync(join(testHiveDir, 'instance.id'), 'abc123', 'utf-8');
|
|
61
|
+
const prefix = getInstancePrefix(testHiveDir);
|
|
62
|
+
expect(prefix).toBe('hive-abc123');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should fall back to hive when hive dir does not exist', () => {
|
|
66
|
+
const prefix = getInstancePrefix('/nonexistent/path/.hive');
|
|
67
|
+
expect(prefix).toBe('hive');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('buildInstanceSessionName', () => {
|
|
72
|
+
it('should build instance-scoped session name', () => {
|
|
73
|
+
writeFileSync(join(testHiveDir, 'instance.id'), 'abc123', 'utf-8');
|
|
74
|
+
const name = buildInstanceSessionName(testHiveDir, 'senior', 'my-team');
|
|
75
|
+
expect(name).toBe('hive-abc123-senior-my-team');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should include index when > 1', () => {
|
|
79
|
+
writeFileSync(join(testHiveDir, 'instance.id'), 'abc123', 'utf-8');
|
|
80
|
+
const name = buildInstanceSessionName(testHiveDir, 'senior', 'my-team', 3);
|
|
81
|
+
expect(name).toBe('hive-abc123-senior-my-team-3');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should omit index when 1', () => {
|
|
85
|
+
writeFileSync(join(testHiveDir, 'instance.id'), 'abc123', 'utf-8');
|
|
86
|
+
const name = buildInstanceSessionName(testHiveDir, 'senior', 'my-team', 1);
|
|
87
|
+
expect(name).toBe('hive-abc123-senior-my-team');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should fall back to old format when no hive dir', () => {
|
|
91
|
+
const name = buildInstanceSessionName('/nonexistent', 'senior', 'my-team');
|
|
92
|
+
expect(name).toBe('hive-senior-my-team');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('getTechLeadSessionName', () => {
|
|
97
|
+
it('should return instance-scoped tech lead session name', () => {
|
|
98
|
+
writeFileSync(join(testHiveDir, 'instance.id'), 'xyz789', 'utf-8');
|
|
99
|
+
const name = getTechLeadSessionName(testHiveDir);
|
|
100
|
+
expect(name).toBe('hive-xyz789-tech-lead');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should fall back to hive-tech-lead when no instance', () => {
|
|
104
|
+
const name = getTechLeadSessionName('/nonexistent');
|
|
105
|
+
expect(name).toBe('hive-tech-lead');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('getManagerSessionName', () => {
|
|
110
|
+
it('should return instance-scoped manager session name', () => {
|
|
111
|
+
writeFileSync(join(testHiveDir, 'instance.id'), 'xyz789', 'utf-8');
|
|
112
|
+
const name = getManagerSessionName(testHiveDir);
|
|
113
|
+
expect(name).toBe('hive-xyz789-manager');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('getManagerLockPath', () => {
|
|
118
|
+
it('should return instance-scoped lock path', () => {
|
|
119
|
+
writeFileSync(join(testHiveDir, 'instance.id'), 'xyz789', 'utf-8');
|
|
120
|
+
const lockPath = getManagerLockPath(testHiveDir);
|
|
121
|
+
expect(lockPath).toBe(join(testHiveDir, 'manager-xyz789.lock'));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should fall back to default lock path when no instance', () => {
|
|
125
|
+
const lockPath = getManagerLockPath('/nonexistent');
|
|
126
|
+
expect(lockPath).toBe(join('/nonexistent', 'manager.lock'));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Licensed under the Hungry Ghost Hive License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { nanoid } from 'nanoid';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
|
|
7
|
+
const INSTANCE_ID_FILE = 'instance.id';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get or create the instance ID for a hive workspace.
|
|
11
|
+
* The ID is stored in .hive/instance.id.
|
|
12
|
+
* Returns null if the hive directory does not exist.
|
|
13
|
+
* Only creates the file if the .hive directory already exists
|
|
14
|
+
* (i.e., this is a real workspace, not a test environment).
|
|
15
|
+
*/
|
|
16
|
+
export function getInstanceId(hiveDir: string): string | null {
|
|
17
|
+
if (!existsSync(hiveDir)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const instancePath = join(hiveDir, INSTANCE_ID_FILE);
|
|
22
|
+
|
|
23
|
+
if (existsSync(instancePath)) {
|
|
24
|
+
const id = readFileSync(instancePath, 'utf-8').trim();
|
|
25
|
+
if (id) return id;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Generate a short unique ID for this workspace instance
|
|
29
|
+
const id = nanoid(6);
|
|
30
|
+
try {
|
|
31
|
+
writeFileSync(instancePath, id, 'utf-8');
|
|
32
|
+
} catch {
|
|
33
|
+
// If write fails (e.g., read-only filesystem), use in-memory ID
|
|
34
|
+
}
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the instance-scoped tmux session prefix.
|
|
40
|
+
* Falls back to 'hive' if no instance ID is available.
|
|
41
|
+
*/
|
|
42
|
+
export function getInstancePrefix(hiveDir: string): string {
|
|
43
|
+
const instanceId = getInstanceId(hiveDir);
|
|
44
|
+
if (instanceId) {
|
|
45
|
+
return `hive-${instanceId}`;
|
|
46
|
+
}
|
|
47
|
+
return 'hive';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build an instance-scoped session name.
|
|
52
|
+
* Pattern: hive-<instanceId>-<agentType>[-<teamName>][-<index>]
|
|
53
|
+
* Falls back to hive-<agentType>[-<teamName>][-<index>] if no instance ID.
|
|
54
|
+
*/
|
|
55
|
+
export function buildInstanceSessionName(
|
|
56
|
+
hiveDir: string,
|
|
57
|
+
agentType: string,
|
|
58
|
+
teamName?: string,
|
|
59
|
+
index?: number
|
|
60
|
+
): string {
|
|
61
|
+
const prefix = getInstancePrefix(hiveDir);
|
|
62
|
+
let name = `${prefix}-${agentType}`;
|
|
63
|
+
if (teamName) {
|
|
64
|
+
name += `-${teamName}`;
|
|
65
|
+
}
|
|
66
|
+
if (index !== undefined && index > 1) {
|
|
67
|
+
name += `-${index}`;
|
|
68
|
+
}
|
|
69
|
+
return name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the instance-scoped tech lead session name.
|
|
74
|
+
*/
|
|
75
|
+
export function getTechLeadSessionName(hiveDir: string): string {
|
|
76
|
+
return buildInstanceSessionName(hiveDir, 'tech-lead');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build the instance-scoped manager session name.
|
|
81
|
+
*/
|
|
82
|
+
export function getManagerSessionName(hiveDir: string): string {
|
|
83
|
+
return buildInstanceSessionName(hiveDir, 'manager');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build the instance-scoped manager lock path.
|
|
88
|
+
*/
|
|
89
|
+
export function getManagerLockPath(hiveDir: string): string {
|
|
90
|
+
const instanceId = getInstanceId(hiveDir);
|
|
91
|
+
if (instanceId) {
|
|
92
|
+
return join(hiveDir, `manager-${instanceId}.lock`);
|
|
93
|
+
}
|
|
94
|
+
return join(hiveDir, 'manager.lock');
|
|
95
|
+
}
|
package/src/utils/paths.test.ts
CHANGED
|
@@ -102,6 +102,7 @@ describe('paths utility', () => {
|
|
|
102
102
|
agentsDir: join(rootDir, '.hive', 'agents'),
|
|
103
103
|
logsDir: join(rootDir, '.hive', 'logs'),
|
|
104
104
|
reposDir: join(rootDir, 'repos'),
|
|
105
|
+
storiesDir: join(rootDir, '.hive', 'stories'),
|
|
105
106
|
});
|
|
106
107
|
});
|
|
107
108
|
|
|
@@ -128,6 +129,7 @@ describe('paths utility', () => {
|
|
|
128
129
|
expect(result.configPath).toContain(result.hiveDir);
|
|
129
130
|
expect(result.agentsDir).toContain(result.hiveDir);
|
|
130
131
|
expect(result.logsDir).toContain(result.hiveDir);
|
|
132
|
+
expect(result.storiesDir).toContain(result.hiveDir);
|
|
131
133
|
expect(result.reposDir).not.toContain(result.hiveDir);
|
|
132
134
|
});
|
|
133
135
|
|
|
@@ -154,6 +156,12 @@ describe('paths utility', () => {
|
|
|
154
156
|
|
|
155
157
|
expect(result.logsDir).toContain(paths.LOGS_DIR_NAME);
|
|
156
158
|
});
|
|
159
|
+
|
|
160
|
+
it('should use STORIES_DIR_NAME constant for stories directory', () => {
|
|
161
|
+
const result = paths.getHivePaths(rootDir);
|
|
162
|
+
|
|
163
|
+
expect(result.storiesDir).toBe(join(rootDir, '.hive', paths.STORIES_DIR_NAME));
|
|
164
|
+
});
|
|
157
165
|
});
|
|
158
166
|
|
|
159
167
|
describe('isHiveWorkspace', () => {
|
package/src/utils/paths.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const HIVE_DIR_NAME = '.hive';
|
|
|
7
7
|
export const REPOS_DIR_NAME = 'repos';
|
|
8
8
|
export const AGENTS_DIR_NAME = 'agents';
|
|
9
9
|
export const LOGS_DIR_NAME = 'logs';
|
|
10
|
+
export const STORIES_DIR_NAME = 'stories';
|
|
10
11
|
|
|
11
12
|
export interface HivePaths {
|
|
12
13
|
root: string;
|
|
@@ -16,6 +17,7 @@ export interface HivePaths {
|
|
|
16
17
|
agentsDir: string;
|
|
17
18
|
logsDir: string;
|
|
18
19
|
reposDir: string;
|
|
20
|
+
storiesDir: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export function findHiveRoot(startDir: string = process.cwd()): string | null {
|
|
@@ -43,6 +45,7 @@ export function getHivePaths(rootDir: string): HivePaths {
|
|
|
43
45
|
agentsDir: join(hiveDir, AGENTS_DIR_NAME),
|
|
44
46
|
logsDir: join(hiveDir, LOGS_DIR_NAME),
|
|
45
47
|
reposDir: join(rootDir, REPOS_DIR_NAME),
|
|
48
|
+
storiesDir: join(hiveDir, STORIES_DIR_NAME),
|
|
46
49
|
};
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Licensed under the Hungry Ghost Hive License. See LICENSE.
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import type { StoryRow } from '../db/client.js';
|
|
7
|
+
import {
|
|
8
|
+
deleteStoryMarkdown,
|
|
9
|
+
generateStoryMarkdown,
|
|
10
|
+
writeStoryMarkdown,
|
|
11
|
+
} from './story-markdown.js';
|
|
12
|
+
|
|
13
|
+
const TEST_DIR = join('/tmp', `story-markdown-test-${Date.now()}`);
|
|
14
|
+
|
|
15
|
+
function makeStory(overrides: Partial<StoryRow> = {}): StoryRow {
|
|
16
|
+
return {
|
|
17
|
+
id: 'STORY-TEST01',
|
|
18
|
+
requirement_id: null,
|
|
19
|
+
team_id: null,
|
|
20
|
+
title: 'Test Story',
|
|
21
|
+
description: 'A test story description.',
|
|
22
|
+
acceptance_criteria: null,
|
|
23
|
+
complexity_score: null,
|
|
24
|
+
story_points: null,
|
|
25
|
+
status: 'draft',
|
|
26
|
+
assigned_agent_id: null,
|
|
27
|
+
branch_name: null,
|
|
28
|
+
pr_url: null,
|
|
29
|
+
jira_issue_key: null,
|
|
30
|
+
jira_issue_id: null,
|
|
31
|
+
jira_project_key: null,
|
|
32
|
+
jira_subtask_key: null,
|
|
33
|
+
jira_subtask_id: null,
|
|
34
|
+
external_issue_key: null,
|
|
35
|
+
external_issue_id: null,
|
|
36
|
+
external_project_key: null,
|
|
37
|
+
external_subtask_key: null,
|
|
38
|
+
external_subtask_id: null,
|
|
39
|
+
external_provider: null,
|
|
40
|
+
in_sprint: 0,
|
|
41
|
+
markdown_path: null,
|
|
42
|
+
created_at: '2026-01-01T00:00:00.000Z',
|
|
43
|
+
updated_at: '2026-01-01T00:00:00.000Z',
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('generateStoryMarkdown', () => {
|
|
49
|
+
it('should generate a markdown document with title and description', () => {
|
|
50
|
+
const story = makeStory();
|
|
51
|
+
const md = generateStoryMarkdown(story);
|
|
52
|
+
|
|
53
|
+
expect(md).toContain('# Test Story');
|
|
54
|
+
expect(md).toContain('**Story ID:** STORY-TEST01');
|
|
55
|
+
expect(md).toContain('**Status:** draft');
|
|
56
|
+
expect(md).toContain('A test story description.');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should include acceptance criteria as a checklist', () => {
|
|
60
|
+
const story = makeStory({
|
|
61
|
+
acceptance_criteria: JSON.stringify(['Criterion A', 'Criterion B']),
|
|
62
|
+
});
|
|
63
|
+
const md = generateStoryMarkdown(story);
|
|
64
|
+
|
|
65
|
+
expect(md).toContain('## Acceptance Criteria');
|
|
66
|
+
expect(md).toContain('- [ ] Criterion A');
|
|
67
|
+
expect(md).toContain('- [ ] Criterion B');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should include optional fields when present', () => {
|
|
71
|
+
const story = makeStory({
|
|
72
|
+
team_id: 'TEAM-1',
|
|
73
|
+
requirement_id: 'REQ-1',
|
|
74
|
+
assigned_agent_id: 'agent-123',
|
|
75
|
+
complexity_score: 5,
|
|
76
|
+
story_points: 5,
|
|
77
|
+
branch_name: 'feature/STORY-TEST01-test-story',
|
|
78
|
+
pr_url: 'https://github.com/test/repo/pull/42',
|
|
79
|
+
});
|
|
80
|
+
const md = generateStoryMarkdown(story);
|
|
81
|
+
|
|
82
|
+
expect(md).toContain('**Team:** TEAM-1');
|
|
83
|
+
expect(md).toContain('**Requirement:** REQ-1');
|
|
84
|
+
expect(md).toContain('**Assigned Agent:** agent-123');
|
|
85
|
+
expect(md).toContain('**Complexity:** 5');
|
|
86
|
+
expect(md).toContain('**Story Points:** 5');
|
|
87
|
+
expect(md).toContain('**Branch:** feature/STORY-TEST01-test-story');
|
|
88
|
+
expect(md).toContain('**PR:** https://github.com/test/repo/pull/42');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should include created and updated timestamps', () => {
|
|
92
|
+
const story = makeStory();
|
|
93
|
+
const md = generateStoryMarkdown(story);
|
|
94
|
+
|
|
95
|
+
expect(md).toContain('*Created: 2026-01-01T00:00:00.000Z*');
|
|
96
|
+
expect(md).toContain('*Updated: 2026-01-01T00:00:00.000Z*');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle malformed acceptance_criteria gracefully', () => {
|
|
100
|
+
const story = makeStory({ acceptance_criteria: 'not-json' });
|
|
101
|
+
const md = generateStoryMarkdown(story);
|
|
102
|
+
|
|
103
|
+
expect(md).toContain('not-json');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('writeStoryMarkdown', () => {
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should write a markdown file in storiesDir', () => {
|
|
117
|
+
const story = makeStory();
|
|
118
|
+
const filePath = writeStoryMarkdown(TEST_DIR, story);
|
|
119
|
+
|
|
120
|
+
expect(existsSync(filePath)).toBe(true);
|
|
121
|
+
expect(filePath).toBe(join(TEST_DIR, 'STORY-TEST01.md'));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should create the directory if it does not exist', () => {
|
|
125
|
+
const nestedDir = join(TEST_DIR, 'nested', 'stories');
|
|
126
|
+
const story = makeStory();
|
|
127
|
+
writeStoryMarkdown(nestedDir, story);
|
|
128
|
+
|
|
129
|
+
expect(existsSync(nestedDir)).toBe(true);
|
|
130
|
+
expect(existsSync(join(nestedDir, 'STORY-TEST01.md'))).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should write valid markdown content', () => {
|
|
134
|
+
const story = makeStory({ description: 'My description.' });
|
|
135
|
+
const filePath = writeStoryMarkdown(TEST_DIR, story);
|
|
136
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
137
|
+
|
|
138
|
+
expect(content).toContain('# Test Story');
|
|
139
|
+
expect(content).toContain('My description.');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should overwrite existing file on update', () => {
|
|
143
|
+
const story = makeStory({ title: 'Old Title' });
|
|
144
|
+
writeStoryMarkdown(TEST_DIR, story);
|
|
145
|
+
|
|
146
|
+
const updatedStory = makeStory({ title: 'New Title' });
|
|
147
|
+
const filePath = writeStoryMarkdown(TEST_DIR, updatedStory);
|
|
148
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
149
|
+
|
|
150
|
+
expect(content).toContain('# New Title');
|
|
151
|
+
expect(content).not.toContain('# Old Title');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('deleteStoryMarkdown', () => {
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
afterEach(() => {
|
|
161
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should delete the markdown file', () => {
|
|
165
|
+
const story = makeStory();
|
|
166
|
+
const filePath = writeStoryMarkdown(TEST_DIR, story);
|
|
167
|
+
expect(existsSync(filePath)).toBe(true);
|
|
168
|
+
|
|
169
|
+
deleteStoryMarkdown(TEST_DIR, story.id);
|
|
170
|
+
expect(existsSync(filePath)).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should not throw if file does not exist', () => {
|
|
174
|
+
expect(() => deleteStoryMarkdown(TEST_DIR, 'STORY-NONEXISTENT')).not.toThrow();
|
|
175
|
+
});
|
|
176
|
+
});
|