Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
203 lines
6.3 KiB
JavaScript
203 lines
6.3 KiB
JavaScript
/*
|
|
* 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',
|
|
});
|
|
});
|
|
});
|