import * as fs from "fs"; import * as path from "path"; import test from "ava"; import * as sinon from "sinon"; import * as actionsUtil from "../actions-util"; import * as gitUtils from "../git-utils"; import { getRunnerLogger } from "../logging"; import { createTestConfig, setupTests } from "../testing-utils"; import { withTmpDir } from "../util"; import { writeBaseDatabaseOidsFile, writeOverlayChangesFile } from "."; setupTests(test); test.serial( "writeOverlayChangesFile generates correct changes file", async (t) => { await withTmpDir(async (tmpDir) => { const [dbLocation, sourceRoot, tempDir] = ["db", "src", "temp"].map((d) => path.join(tmpDir, d), ); await Promise.all( [dbLocation, sourceRoot, tempDir].map((d) => fs.promises.mkdir(d, { recursive: true }), ), ); const logger = getRunnerLogger(true); const config = createTestConfig({ dbLocation }); // Mock the getFileOidsUnderPath function to return base OIDs const baseOids = { "unchanged.js": "aaa111", "modified.js": "bbb222", "deleted.js": "ccc333", }; const getFileOidsStubForBase = sinon .stub(gitUtils, "getFileOidsUnderPath") .resolves(baseOids); // Write the base database OIDs file await writeBaseDatabaseOidsFile(config, sourceRoot); getFileOidsStubForBase.restore(); // Mock the getFileOidsUnderPath function to return overlay OIDs const currentOids = { "unchanged.js": "aaa111", "modified.js": "ddd444", // Changed OID "added.js": "eee555", // New file }; sinon.stub(gitUtils, "getFileOidsUnderPath").resolves(currentOids); // Write the overlay changes file, which uses the mocked overlay OIDs // and the base database OIDs file const diffRangeFilePath = path.join(tempDir, "pr-diff-range.json"); sinon.stub(actionsUtil, "getTemporaryDirectory").returns(tempDir); sinon .stub(actionsUtil, "getDiffRangesJsonFilePath") .returns(diffRangeFilePath); sinon.stub(gitUtils, "getGitRoot").resolves(sourceRoot); const changesFilePath = await writeOverlayChangesFile( config, sourceRoot, logger, ); const fileContent = await fs.promises.readFile(changesFilePath, "utf-8"); const parsedContent = JSON.parse(fileContent) as { changes: string[] }; t.deepEqual( parsedContent.changes.sort(), ["added.js", "deleted.js", "modified.js"], "Should identify added, deleted, and modified files", ); }); }, ); test.serial( "writeOverlayChangesFile merges additional diff files into overlay changes", async (t) => { await withTmpDir(async (tmpDir) => { const [dbLocation, sourceRoot, tempDir] = ["db", "src", "temp"].map((d) => path.join(tmpDir, d), ); await Promise.all( [dbLocation, sourceRoot, tempDir].map((d) => fs.promises.mkdir(d, { recursive: true }), ), ); const logger = getRunnerLogger(true); const config = createTestConfig({ dbLocation }); // Mock the getFileOidsUnderPath function to return base OIDs // "reverted.js" has the same OID in both base and current, simulating // a revert PR where the file content matches the overlay-base const baseOids = { "unchanged.js": "aaa111", "modified.js": "bbb222", "reverted.js": "eee555", }; const getFileOidsStubForBase = sinon .stub(gitUtils, "getFileOidsUnderPath") .resolves(baseOids); // Write the base database OIDs file await writeBaseDatabaseOidsFile(config, sourceRoot); getFileOidsStubForBase.restore(); // Mock the getFileOidsUnderPath function to return overlay OIDs // "reverted.js" has the same OID as the base -- OID comparison alone // would NOT include it, only additionalChangedFiles causes it to appear const currentOids = { "unchanged.js": "aaa111", "modified.js": "ddd444", // Changed OID "reverted.js": "eee555", // Same OID as base -- not detected by OID comparison }; sinon.stub(gitUtils, "getFileOidsUnderPath").resolves(currentOids); const diffRangeFilePath = path.join(tempDir, "pr-diff-range.json"); sinon.stub(actionsUtil, "getTemporaryDirectory").returns(tempDir); sinon .stub(actionsUtil, "getDiffRangesJsonFilePath") .returns(diffRangeFilePath); sinon.stub(gitUtils, "getGitRoot").resolves(sourceRoot); // Write a pr-diff-range.json file with diff ranges including // "reverted.js" (unchanged OIDs) and "modified.js" (already in OID changes) await fs.promises.writeFile( diffRangeFilePath, JSON.stringify([ { path: "reverted.js", startLine: 1, endLine: 10 }, { path: "modified.js", startLine: 1, endLine: 5 }, { path: "diff-only.js", startLine: 1, endLine: 3 }, ]), ); const changesFilePath = await writeOverlayChangesFile( config, sourceRoot, logger, ); const fileContent = await fs.promises.readFile(changesFilePath, "utf-8"); const parsedContent = JSON.parse(fileContent) as { changes: string[] }; t.deepEqual( parsedContent.changes.sort(), ["diff-only.js", "modified.js", "reverted.js"], "Should include OID-changed files, diff-only files, and deduplicate overlapping files", ); }); }, ); test.serial( "writeOverlayChangesFile works without additional diff files", async (t) => { await withTmpDir(async (tmpDir) => { const [dbLocation, sourceRoot, tempDir] = ["db", "src", "temp"].map((d) => path.join(tmpDir, d), ); await Promise.all( [dbLocation, sourceRoot, tempDir].map((d) => fs.promises.mkdir(d, { recursive: true }), ), ); const logger = getRunnerLogger(true); const config = createTestConfig({ dbLocation }); // Mock the getFileOidsUnderPath function to return base OIDs const baseOids = { "unchanged.js": "aaa111", "modified.js": "bbb222", }; const getFileOidsStubForBase = sinon .stub(gitUtils, "getFileOidsUnderPath") .resolves(baseOids); await writeBaseDatabaseOidsFile(config, sourceRoot); getFileOidsStubForBase.restore(); const currentOids = { "unchanged.js": "aaa111", "modified.js": "ddd444", }; sinon.stub(gitUtils, "getFileOidsUnderPath").resolves(currentOids); const diffRangeFilePath = path.join(tempDir, "pr-diff-range.json"); sinon.stub(actionsUtil, "getTemporaryDirectory").returns(tempDir); sinon .stub(actionsUtil, "getDiffRangesJsonFilePath") .returns(diffRangeFilePath); sinon.stub(gitUtils, "getGitRoot").resolves(sourceRoot); // No pr-diff-range.json file exists - should work the same as before const changesFilePath = await writeOverlayChangesFile( config, sourceRoot, logger, ); const fileContent = await fs.promises.readFile(changesFilePath, "utf-8"); const parsedContent = JSON.parse(fileContent) as { changes: string[] }; t.deepEqual( parsedContent.changes.sort(), ["modified.js"], "Should only include OID-changed files when no additional files provided", ); }); }, ); test.serial( "writeOverlayChangesFile converts diff range paths to sourceRoot-relative when sourceRoot is a subdirectory", async (t) => { await withTmpDir(async (tmpDir) => { // Simulate: repo root = tmpDir, sourceRoot = tmpDir/src const repoRoot = tmpDir; const sourceRoot = path.join(tmpDir, "src"); const [dbLocation, tempDir] = ["db", "temp"].map((d) => path.join(tmpDir, d), ); await Promise.all( [dbLocation, sourceRoot, tempDir].map((d) => fs.promises.mkdir(d, { recursive: true }), ), ); const logger = getRunnerLogger(true); const config = createTestConfig({ dbLocation }); // Base OIDs (sourceRoot-relative paths) const baseOids = { "app.js": "aaa111", "lib/util.js": "bbb222", }; const getFileOidsStubForBase = sinon .stub(gitUtils, "getFileOidsUnderPath") .resolves(baseOids); await writeBaseDatabaseOidsFile(config, sourceRoot); getFileOidsStubForBase.restore(); // Current OIDs — same as base (no OID changes) const currentOids = { "app.js": "aaa111", "lib/util.js": "bbb222", }; sinon.stub(gitUtils, "getFileOidsUnderPath").resolves(currentOids); const diffRangeFilePath = path.join(tempDir, "pr-diff-range.json"); sinon.stub(actionsUtil, "getTemporaryDirectory").returns(tempDir); sinon .stub(actionsUtil, "getDiffRangesJsonFilePath") .returns(diffRangeFilePath); // getGitRoot returns the repo root (parent of sourceRoot) sinon.stub(gitUtils, "getGitRoot").resolves(repoRoot); // Diff ranges use repo-root-relative paths (as returned by the GitHub compare API) await fs.promises.writeFile( diffRangeFilePath, JSON.stringify([ { path: "src/app.js", startLine: 1, endLine: 10 }, { path: "src/lib/util.js", startLine: 5, endLine: 8 }, { path: "other/outside.js", startLine: 1, endLine: 3 }, // not under sourceRoot ]), ); const changesFilePath = await writeOverlayChangesFile( config, sourceRoot, logger, ); const fileContent = await fs.promises.readFile(changesFilePath, "utf-8"); const parsedContent = JSON.parse(fileContent) as { changes: string[] }; t.deepEqual( parsedContent.changes.sort(), ["app.js", "lib/util.js"], "Should convert repo-root-relative paths to sourceRoot-relative and filter out files outside sourceRoot", ); }); }, );