Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/scripts/__tests__/sync-run-contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,94 @@ const test = require('node:test');
const assert = require('node:assert/strict');

const {
buildNoChangeCanaryEvidence,
buildMarkdownSummary,
buildSyncRunReport,
summarizeResults,
} = require('../sync_run_contract');

test('buildNoChangeCanaryEvidence binds no-diff canaries to the exact plan and head', () => {
const planId = `sha256:${'a'.repeat(64)}`;
const sourceCommit = 'b'.repeat(40);
const consumerHeadSha = 'c'.repeat(40);
const result = buildNoChangeCanaryEvidence({
expectedCanaries: ['stranske/Ready', 'stranske/Travel'],
planId,
planScope: 'full',
sourceCommit,
results: [
{
repo: 'stranske/Ready',
status: 'no_changes',
plan_id: planId,
plan_scope: 'full',
scope_base_sha: '',
source_commit: sourceCommit,
consumer_head_sha: consumerHeadSha,
},
{ repo: 'stranske/Travel', status: 'created_pr', plan_id: planId },
],
});

assert.equal(result.ok, true);
assert.deepEqual(result.errors, []);
assert.deepEqual(result.evidence.results, [{
repo: 'stranske/Ready',
plan_id: planId,
plan_scope: 'full',
scope_base_sha: '',
source_commit: sourceCommit,
head_sha: consumerHeadSha,
evidence_source: 'no-change-canary',
required_check_state: 'success',
active_review_thread_count: 0,
}]);
});

test('buildNoChangeCanaryEvidence rejects stale plan and missing head claims', () => {
const result = buildNoChangeCanaryEvidence({
expectedCanaries: ['stranske/Ready'],
planId: `sha256:${'a'.repeat(64)}`,
planScope: 'source-delta',
scopeBaseSha: '1'.repeat(40),
sourceCommit: '2'.repeat(40),
results: [{
repo: 'stranske/Ready',
status: 'no_changes',
plan_id: `sha256:${'f'.repeat(64)}`,
plan_scope: 'source-delta',
scope_base_sha: '1'.repeat(40),
source_commit: '2'.repeat(40),
consumer_head_sha: '',
}],
});

assert.equal(result.ok, false);
assert.ok(result.errors.includes('no_change_canary_plan_mismatch:stranske/Ready'));
assert.ok(result.errors.includes('no_change_canary_head_invalid:stranske/Ready'));
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test('buildNoChangeCanaryEvidence rejects duplicate and immutable scope mismatches', () => {
const planId = `sha256:${'a'.repeat(64)}`;
const sourceCommit = 'b'.repeat(40);
const result = buildNoChangeCanaryEvidence({
expectedCanaries: ['stranske/Ready'],
planId,
planScope: 'source-delta',
scopeBaseSha: 'c'.repeat(40),
sourceCommit,
results: [
{ repo: 'stranske/Ready', status: 'no_changes', plan_id: planId, plan_scope: 'full', scope_base_sha: 'd'.repeat(40), source_commit: sourceCommit, consumer_head_sha: 'e'.repeat(40) },
{ repo: 'stranske/Ready', status: 'no_changes', plan_id: planId, plan_scope: 'source-delta', scope_base_sha: 'c'.repeat(40), source_commit: sourceCommit, consumer_head_sha: 'e'.repeat(40) },
],
});

assert.equal(result.ok, false);
assert.ok(result.errors.includes('no_change_canary_scope_mismatch:stranske/Ready'));
assert.ok(result.errors.includes('no_change_canary_scope_base_mismatch:stranske/Ready'));
assert.ok(result.errors.includes('duplicate_no_change_canary:stranske/Ready'));
});

test('summarizeResults counts known statuses and buckets unknown as error', () => {
assert.deepEqual(
summarizeResults([
Expand Down
250 changes: 248 additions & 2 deletions .github/scripts/__tests__/sync_pr_merge_contract.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const {
summarizeResults,
syncBranchForHash,
validateCanaryEvidence,
validateExpectedCandidateIdentity,
validateSourceDeltaEvidenceBinding,
} = require('../sync_pr_merge_contract');
const { assertRuntimeAcMergeAllowed } = require('../runtime_ac_merge_guard');
Expand Down Expand Up @@ -353,6 +354,176 @@ test('maint71 fails closed before cross-repository API calls without OWNER_PR_PA
}
});

test('maint71 accepts no-change canary evidence only while the exact base head is current', async () => {
const originalCwd = process.cwd();
const envKeys = [
'REGISTERED_REPOS_INPUT',
'CLEANUP_BRANCHES_INPUT',
'DRY_RUN_INPUT',
'AUTO_MERGE_INPUT',
'EVIDENCE_ONLY_INPUT',
'ACTIVE_SYNC_HASH_INPUT',
'EXPECTED_PLAN_ID_INPUT',
'EXPECTED_PLAN_SCOPE_INPUT',
'EXPECTED_SCOPE_BASE_SHA_INPUT',
'EXPECTED_SOURCE_COMMIT_INPUT',
'CANARY_BASELINE_EVIDENCE_JSON',
'OWNER_PR_PAT',
'CONSUMER_SYNC_CANARIES_PATH',
'TRUSTED_SYNC_ACTORS',
'SYNC_PR_MERGE_REPORT_JSON',
];
const originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'maint71-no-change-'));
const reportPath = path.join(tempDir, 'artifacts', 'merge-report.json');
const canaryConfigPath = path.join(tempDir, 'consumer-sync-canaries.json');
const repoName = 'stranske/Travel-Plan-Permission';
const planId = `sha256:${'a'.repeat(64)}`;
const sourceCommit = 'b'.repeat(40);
const headSha = 'c'.repeat(40);
fs.writeFileSync(
canaryConfigPath,
JSON.stringify({ canaries: [{ repo: repoName }] }),
);
const baseline = {
schema: 'workflows.consumer-sync-canary-evidence/v1',
version: 1,
results: [{
repo: repoName,
plan_id: planId,
plan_scope: 'full',
scope_base_sha: '',
source_commit: sourceCommit,
head_sha: headSha,
evidence_source: 'no-change-canary',
required_check_state: 'success',
active_review_thread_count: 0,
}],
};
let checkConclusion = 'success';
let requiredContexts = ['Gate / gate'];
const github = {
paginate: async (_method, params) => (
params.ref === headSha
? [{ name: 'Gate / gate', status: 'completed', conclusion: checkConclusion }]
: []
),
rest: {
pulls: { list: () => {} },
checks: { listForRef: () => {} },
repos: {
get: async () => ({ data: { default_branch: 'main' } }),
getBranchProtection: async () => ({
data: { required_status_checks: { contexts: requiredContexts, checks: [] } },
}),
getRepoRulesets: async () => ({ data: [] }),
getCombinedStatusForRef: async () => ({ data: { statuses: [] } }),
createDispatchEvent: async () => ({}),
},
git: {
getRef: async () => ({ data: { object: { sha: headSha } } }),
},
},
};
const failures = [];
const core = {
notice: () => {},
setFailed: (message) => failures.push(message),
warning: () => {},
summary: { addRaw: () => ({ write: async () => {} }) },
};

try {
process.chdir(tempDir);
process.env.REGISTERED_REPOS_INPUT = repoName;
process.env.CLEANUP_BRANCHES_INPUT = 'false';
process.env.DRY_RUN_INPUT = 'true';
process.env.AUTO_MERGE_INPUT = 'false';
process.env.EVIDENCE_ONLY_INPUT = 'true';
process.env.ACTIVE_SYNC_HASH_INPUT = 'candidate';
process.env.EXPECTED_PLAN_ID_INPUT = planId;
process.env.EXPECTED_PLAN_SCOPE_INPUT = 'full';
process.env.EXPECTED_SCOPE_BASE_SHA_INPUT = '';
process.env.EXPECTED_SOURCE_COMMIT_INPUT = sourceCommit;
process.env.CANARY_BASELINE_EVIDENCE_JSON = JSON.stringify(baseline);
process.env.OWNER_PR_PAT = 'test-owner-token';
process.env.CONSUMER_SYNC_CANARIES_PATH = canaryConfigPath;
process.env.TRUSTED_SYNC_ACTORS = 'stranske';
process.env.SYNC_PR_MERGE_REPORT_JSON = reportPath;

await run({
github,
core,
context: {
repo: { owner: 'stranske', repo: 'Workflows' },
payload: {},
runId: 3,
runNumber: 3,
workflow: 'Maint 71',
ref: 'refs/heads/main',
sha: sourceCommit,
},
});

const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
const evidence = JSON.parse(
fs.readFileSync(path.join(tempDir, 'artifacts', 'sync-canary-evidence.json'), 'utf8'),
);
assert.equal(report.summary.evidence_recovered, 1);
assert.equal(report.results[0].branch, 'sync/workflows-candidate');
assert.equal(evidence.results[0].head_sha, headSha);
assert.equal(evidence.results[0].evidence_source, 'no-change-canary');
assert.deepEqual(failures, []);

checkConclusion = 'failure';
await run({
github,
core,
context: {
repo: { owner: 'stranske', repo: 'Workflows' },
payload: {},
runId: 4,
runNumber: 4,
workflow: 'Maint 71',
ref: 'refs/heads/main',
sha: sourceCommit,
},
});
const redReport = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
assert.equal(redReport.summary.checks_failed, 1);
assert.match(failures.at(-1), /Canary evidence is incomplete or unsafe/);

checkConclusion = 'success';
requiredContexts = [];
await run({
github,
core,
context: {
repo: { owner: 'stranske', repo: 'Workflows' },
payload: {},
runId: 5,
runNumber: 5,
workflow: 'Maint 71',
ref: 'refs/heads/main',
sha: sourceCommit,
},
});
const unconfiguredReport = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
assert.equal(unconfiguredReport.summary.checks_failed, 1);
assert.equal(
unconfiguredReport.results[0].reason,
'no_change_canary_required_checks_unconfigured',
);
} finally {
process.chdir(originalCwd);
for (const [key, value] of Object.entries(originalEnv)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
fs.rmSync(tempDir, { recursive: true, force: true });
}
});

test('maint71 recovers exact-head evidence from an already-merged candidate PR', async () => {
const originalCwd = process.cwd();
const envKeys = [
Expand All @@ -362,6 +533,10 @@ test('maint71 recovers exact-head evidence from an already-merged candidate PR',
'AUTO_MERGE_INPUT',
'EVIDENCE_ONLY_INPUT',
'ACTIVE_SYNC_HASH_INPUT',
'EXPECTED_PLAN_ID_INPUT',
'EXPECTED_PLAN_SCOPE_INPUT',
'EXPECTED_SCOPE_BASE_SHA_INPUT',
'EXPECTED_SOURCE_COMMIT_INPUT',
'OWNER_PR_PAT',
'CONSUMER_SYNC_CANARIES_PATH',
'TRUSTED_SYNC_ACTORS',
Expand Down Expand Up @@ -476,6 +651,10 @@ test('maint71 recovers exact-head evidence from an already-merged candidate PR',
process.env.DRY_RUN_INPUT = 'true';
process.env.AUTO_MERGE_INPUT = 'false';
process.env.ACTIVE_SYNC_HASH_INPUT = 'candidate';
process.env.EXPECTED_PLAN_ID_INPUT = 'plan-abc';
process.env.EXPECTED_PLAN_SCOPE_INPUT = 'full';
process.env.EXPECTED_SCOPE_BASE_SHA_INPUT = '';
process.env.EXPECTED_SOURCE_COMMIT_INPUT = 'source-abc';
process.env.OWNER_PR_PAT = 'test-owner-token';
process.env.EVIDENCE_ONLY_INPUT = 'true';
process.env.CONSUMER_SYNC_CANARIES_PATH = canaryConfigPath;
Expand Down Expand Up @@ -525,20 +704,87 @@ test('normalizeSyncHash accepts raw hashes and branch names', () => {
});

test('selectLatestMergedCandidatePr recovers only the newest trusted merged candidate', () => {
const candidate = (number, mergedAt, actor = 'stranske') => ({
const candidate = (
number,
mergedAt,
actor = 'stranske',
planId = 'plan-current',
sourceCommit = 'source-current',
) => ({
...pr(number, 'sync/workflows-candidate', '2026-08-11T01:00:00Z'),
merged_at: mergedAt,
head: { ref: 'sync/workflows-candidate', sha: `head-${number}` },
user: { login: actor },
body: `<!-- sync-pr-delivery-record:v1 ${JSON.stringify({
schema: 'sync-pr-delivery-record/v1',
durable_issue_url: 'https://github.com/stranske/Workflows/issues/1836',
plan_id: planId,
generation: `candidate-${number}`,
repository: 'stranske/Ready',
desired_tree_hash: `tree-${number}`,
source_commit: sourceCommit,
lease_expires_at: '2099-08-14T00:00:00Z',
predecessor_prs: [],
successor_prs: [],
})} -->`,
});
const selected = selectLatestMergedCandidatePr([
candidate(1, '2026-08-11T02:00:00Z'),
candidate(2, '2026-08-11T03:00:00Z'),
candidate(3, '2026-08-11T04:00:00Z', 'untrusted'),
candidate(5, '2026-08-11T05:00:00Z', 'stranske', 'plan-stale', 'source-stale'),
{ ...candidate(4, null), merged_at: null },
], ['stranske']);
], ['stranske'], {
planId: 'plan-current',
sourceCommit: 'source-current',
});

assert.equal(selected.number, 2);
assert.equal(selectLatestMergedCandidatePr([
candidate(5, '2026-08-11T05:00:00Z', 'stranske', 'plan-stale', 'source-stale'),
], ['stranske'], {
planId: 'plan-current',
sourceCommit: 'source-current',
}), null);
});

test('validateExpectedCandidateIdentity binds open candidates to every immutable input', () => {
const expected = {
expectedPlanId: 'plan-current',
expectedPlanScope: 'source-delta',
expectedScopeBaseSha: 'a'.repeat(40),
expectedSourceCommit: 'b'.repeat(40),
repository: 'stranske/Ready',
};
const metadata = {
consumer_repo: 'stranske/Ready',
plan_id: 'plan-current',
plan_scope: 'source-delta',
scope_base_sha: 'a'.repeat(40),
source_sha: 'b'.repeat(40),
source_commit: 'b'.repeat(40),
};
const deliveryRecord = {
repository: 'stranske/Ready',
plan_id: 'plan-current',
source_commit: 'b'.repeat(40),
};
assert.deepEqual(validateExpectedCandidateIdentity({
metadata,
deliveryRecord,
...expected,
}), { ok: true, errors: [] });

const stale = validateExpectedCandidateIdentity({
metadata: { ...metadata, source_commit: 'c'.repeat(40) },
deliveryRecord: { ...deliveryRecord, plan_id: 'plan-stale' },
...expected,
});
assert.equal(stale.ok, false);
assert.deepEqual(stale.errors, [
'delivery_plan_id_mismatch',
'metadata_source_commit_mismatch',
]);
});

test('validateCanaryEvidence fails closed on missing, mixed, red, or reviewed canaries', () => {
Expand Down
Loading
Loading