/* * 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', }); }); });