| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| import assert from 'node:assert/strict'; | |
| import { describe, it } from 'node:test'; | |
| import { planLifecycle, STALE_REOPEN_MARKER, STALE_WARNING_MARKER } from './issue-pr-lifecycle.mjs'; | |
| const NOW = '2026-09-01T12:00:00.000Z'; | |
| function daysAgo(days) { | |
| return new Date(Date.parse(NOW) - days * 24 * 60 * 60 * 1000).toISOString(); | |
| } | |
| function humanComment(days, body = 'Still relevant') { | |
| return { | |
| createdAt: daysAgo(days), | |
| body, | |
| author: { __typename: 'User', login: 'contributor' }, | |
| }; | |
| } | |
| function botComment(days, body = 'Automated note') { | |
| return { | |
| createdAt: daysAgo(days), | |
| body, | |
| author: { __typename: 'Bot', login: 'github-actions[bot]' }, | |
| }; | |
| } | |
| function warning(days) { | |
| return botComment(days, STALE_WARNING_MARKER); | |
| } | |
| function issue(overrides = {}) { | |
| return { | |
| kind: 'issue', | |
| createdAt: daysAgo(60), | |
| labels: [], | |
| assigneeCount: 0, | |
| comments: [], | |
| ...overrides, | |
| }; | |
| } | |
| function pullRequest(overrides = {}) { | |
| return { | |
| kind: 'pull_request', | |
| createdAt: daysAgo(60), | |
| lastCommitAt: daysAgo(60), | |
| labels: [], | |
| comments: [], | |
| ...overrides, | |
| }; | |
| } | |
| describe('issue lifecycle', () => { | |
| it('warns at the 30-day inactivity boundary', () => { | |
| assert.equal(planLifecycle(issue({ createdAt: daysAgo(30) }), NOW).action, 'warn'); | |
| assert.equal(planLifecycle(issue({ createdAt: daysAgo(29) }), NOW).action, 'none'); | |
| }); | |
| it('uses the latest human comment as activity', () => { | |
| const plan = planLifecycle(issue({ comments: [humanComment(10)] }), NOW); | |
| assert.deepEqual(plan, { action: 'none', reason: 'recent qualifying activity' }); | |
| }); | |
| it('ignores bot comments when measuring activity', () => { | |
| const plan = planLifecycle(issue({ comments: [botComment(1)] }), NOW); | |
| assert.equal(plan.action, 'warn'); | |
| }); | |
| it('treats a lifecycle reopen marker as a fresh activity timestamp', () => { | |
| const plan = planLifecycle(issue({ comments: [botComment(2, STALE_REOPEN_MARKER)] }), NOW); | |
| assert.deepEqual(plan, { action: 'none', reason: 'recent qualifying activity' }); | |
| }); | |
| it('closes only after the full 30-day grace period', () => { | |
| const pending = issue({ labels: ['stale'], comments: [warning(29)] }); | |
| const expired = issue({ labels: ['stale'], comments: [warning(30)] }); | |
| assert.deepEqual(planLifecycle(pending, NOW), { | |
| action: 'none', | |
| reason: 'within grace period', | |
| }); | |
| assert.equal(planLifecycle(expired, NOW).action, 'close'); | |
| }); | |
| it('removes stale after a human response to the warning', () => { | |
| const plan = planLifecycle( | |
| issue({ labels: ['stale'], comments: [warning(20), humanComment(2)] }), | |
| NOW, | |
| ); | |
| assert.deepEqual(plan, { | |
| action: 'unstale', | |
| reason: 'qualifying activity after warning', | |
| }); | |
| }); | |
| it('exempts assigned issues and removes an existing stale label', () => { | |
| assert.deepEqual(planLifecycle(issue({ assigneeCount: 1 }), NOW), { | |
| action: 'none', | |
| reason: 'assigned', | |
| }); | |
| assert.deepEqual(planLifecycle(issue({ assigneeCount: 1, labels: ['stale'] }), NOW), { | |
| action: 'unstale', | |
| reason: 'assigned', | |
| }); | |
| }); | |
| }); | |
| describe('pull request lifecycle', () => { | |
| it('warns based on the latest commit instead of creation or comments', () => { | |
| const active = pullRequest({ lastCommitAt: daysAgo(5) }); | |
| const inactive = pullRequest({ | |
| lastCommitAt: daysAgo(30), | |
| comments: [humanComment(1)], | |
| }); | |
| assert.equal(planLifecycle(active, NOW).action, 'none'); | |
| assert.equal(planLifecycle(inactive, NOW).action, 'warn'); | |
| }); | |
| it('closes after seven days without a new commit', () => { | |
| const plan = planLifecycle( | |
| pullRequest({ labels: ['stale'], comments: [warning(7), humanComment(1)] }), | |
| NOW, | |
| ); | |
| assert.equal(plan.action, 'close'); | |
| }); | |
| it('removes stale after a commit newer than the warning', () => { | |
| const plan = planLifecycle( | |
| pullRequest({ labels: ['stale'], comments: [warning(7)], lastCommitAt: daysAgo(1) }), | |
| NOW, | |
| ); | |
| assert.deepEqual(plan, { | |
| action: 'unstale', | |
| reason: 'qualifying activity after warning', | |
| }); | |
| }); | |
| it('ignores a lifecycle reopen marker for PR activity timing', () => { | |
| const plan = planLifecycle( | |
| pullRequest({ comments: [botComment(2, STALE_REOPEN_MARKER)] }), | |
| NOW, | |
| ); | |
| assert.equal(plan.action, 'warn'); | |
| }); | |
| it('still finds human activity when more than 100 comments are present', () => { | |
| const comments = [ | |
| humanComment(1), | |
| ...Array.from({ length: 100 }, (_, index) => botComment(index + 2)), | |
| ]; | |
| assert.deepEqual(planLifecycle(issue({ comments }), NOW), { | |
| action: 'none', | |
| reason: 'recent qualifying activity', | |
| }); | |
| }); | |
| }); | |
| describe('state repair and exemptions', () => { | |
| it('starts a fresh grace period when stale has no workflow warning', () => { | |
| const plan = planLifecycle(issue({ labels: ['stale'] }), NOW); | |
| assert.equal(plan.action, 'warn'); | |
| assert.match(plan.message, new RegExp(STALE_WARNING_MARKER)); | |
| }); | |
| it('does not trust a human-authored warning marker', () => { | |
| const plan = planLifecycle( | |
| issue({ labels: ['stale'], comments: [humanComment(40, STALE_WARNING_MARKER)] }), | |
| NOW, | |
| ); | |
| assert.equal(plan.action, 'warn'); | |
| }); | |
| it('exempts pinned issues and pull requests', () => { | |
| assert.deepEqual(planLifecycle(issue({ labels: ['pinned', 'stale'] }), NOW), { | |
| action: 'unstale', | |
| reason: 'pinned', | |
| }); | |
| assert.deepEqual(planLifecycle(pullRequest({ labels: ['pinned'] }), NOW), { | |
| action: 'none', | |
| reason: 'pinned', | |
| }); | |
| }); | |
| }); |