mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-06-14 01:43:52 -07:00
Merge remote-tracking branch 'origin/feature/orchestrator-enterprise-support' into release/lts-2.0.0
Merges latest commits from PR #777 (git hooks fix + 129 new tests). Resolved merge conflicts: - dist/index.js, dist/index.js.map: rebuilt from merged source - src/model/input.ts: fixed 5 missing closing braces and return statements from merge
This commit is contained in:
+7120
-17
File diff suppressed because it is too large
Load Diff
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,606 @@
|
|||||||
|
/**
|
||||||
|
* Integration wiring tests for enterprise features in index.ts
|
||||||
|
*
|
||||||
|
* These tests verify the conditional gating logic in runMain():
|
||||||
|
* - Each enterprise feature is only invoked when its gate condition is met
|
||||||
|
* - Services are NOT called when their feature is disabled (the default)
|
||||||
|
* - The order of operations is correct (restore before build, save after build)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import { BuildParameters } from './model';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Service mocks — must be declared before importing index.ts (jest hoists them)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const mockChildWorkspaceService = {
|
||||||
|
buildConfig: jest.fn().mockReturnValue({ enabled: true, workspaceName: 'Test' }),
|
||||||
|
initializeWorkspace: jest.fn().mockReturnValue(false),
|
||||||
|
getWorkspaceSize: jest.fn().mockReturnValue('0 B'),
|
||||||
|
saveWorkspace: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSubmoduleProfileService = {
|
||||||
|
createInitPlan: jest.fn().mockResolvedValue([]),
|
||||||
|
execute: jest.fn().mockResolvedValue(''),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockLfsAgentService = {
|
||||||
|
configure: jest.fn().mockResolvedValue(''),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockLocalCacheService = {
|
||||||
|
resolveCacheRoot: jest.fn().mockReturnValue('/cache'),
|
||||||
|
generateCacheKey: jest.fn().mockReturnValue('key-1'),
|
||||||
|
restoreLfsCache: jest.fn().mockResolvedValue(true),
|
||||||
|
restoreLibraryCache: jest.fn().mockResolvedValue(true),
|
||||||
|
saveLibraryCache: jest.fn().mockResolvedValue(''),
|
||||||
|
saveLfsCache: jest.fn().mockResolvedValue(''),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGitHooksService = {
|
||||||
|
installHooks: jest.fn().mockResolvedValue(''),
|
||||||
|
configureSkipList: jest.fn().mockReturnValue({ LEFTHOOK_EXCLUDE: 'pre-commit' }),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock the dynamic import() targets — jest.mock with factory functions.
|
||||||
|
// The services are imported dynamically via `await import(...)` in index.ts,
|
||||||
|
// so we mock the module path and return the mock objects as named exports.
|
||||||
|
jest.mock('./model/orchestrator/services/cache/child-workspace-service', () => ({
|
||||||
|
ChildWorkspaceService: mockChildWorkspaceService,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./model/orchestrator/services/submodule/submodule-profile-service', () => ({
|
||||||
|
SubmoduleProfileService: mockSubmoduleProfileService,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./model/orchestrator/services/lfs/lfs-agent-service', () => ({
|
||||||
|
LfsAgentService: mockLfsAgentService,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./model/orchestrator/services/cache/local-cache-service', () => ({
|
||||||
|
LocalCacheService: mockLocalCacheService,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./model/orchestrator/services/hooks/git-hooks-service', () => ({
|
||||||
|
GitHooksService: mockGitHooksService,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock all non-enterprise dependencies to isolate the wiring logic
|
||||||
|
jest.mock('@actions/core');
|
||||||
|
jest.mock('./model', () => ({
|
||||||
|
Action: {
|
||||||
|
checkCompatibility: jest.fn(),
|
||||||
|
workspace: '/workspace',
|
||||||
|
actionFolder: '/action',
|
||||||
|
},
|
||||||
|
BuildParameters: {
|
||||||
|
create: jest.fn(),
|
||||||
|
},
|
||||||
|
Cache: {
|
||||||
|
verify: jest.fn(),
|
||||||
|
},
|
||||||
|
Orchestrator: {
|
||||||
|
run: jest.fn().mockResolvedValue(''),
|
||||||
|
},
|
||||||
|
Docker: {
|
||||||
|
run: jest.fn().mockResolvedValue(0),
|
||||||
|
},
|
||||||
|
ImageTag: jest.fn().mockImplementation(() => ({
|
||||||
|
toString: () => 'mock-image:latest',
|
||||||
|
})),
|
||||||
|
Output: {
|
||||||
|
setBuildVersion: jest.fn().mockResolvedValue(''),
|
||||||
|
setAndroidVersionCode: jest.fn().mockResolvedValue(''),
|
||||||
|
setEngineExitCode: jest.fn().mockResolvedValue(''),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./model/cli/cli', () => ({
|
||||||
|
Cli: {
|
||||||
|
InitCliMode: jest.fn().mockReturnValue(false),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./model/mac-builder', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
run: jest.fn().mockResolvedValue(0),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./model/platform-setup', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
setup: jest.fn().mockResolvedValue(''),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockedBuildParametersCreate = BuildParameters.create as jest.Mock;
|
||||||
|
|
||||||
|
interface EnterpriseBuildParametersOverrides {
|
||||||
|
providerStrategy?: string;
|
||||||
|
childWorkspacesEnabled?: boolean;
|
||||||
|
childWorkspaceName?: string;
|
||||||
|
childWorkspaceCacheRoot?: string;
|
||||||
|
childWorkspacePreserveGit?: boolean;
|
||||||
|
childWorkspaceSeparateLibrary?: boolean;
|
||||||
|
submoduleProfilePath?: string;
|
||||||
|
submoduleVariantPath?: string;
|
||||||
|
submoduleToken?: string;
|
||||||
|
gitPrivateToken?: string;
|
||||||
|
lfsTransferAgent?: string;
|
||||||
|
lfsTransferAgentArgs?: string;
|
||||||
|
lfsStoragePaths?: string;
|
||||||
|
localCacheEnabled?: boolean;
|
||||||
|
localCacheRoot?: string;
|
||||||
|
localCacheLibrary?: boolean;
|
||||||
|
localCacheLfs?: boolean;
|
||||||
|
gitHooksEnabled?: boolean;
|
||||||
|
gitHooksSkipList?: string;
|
||||||
|
gitHooksRunBeforeBuild?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMockBuildParameters(overrides: EnterpriseBuildParametersOverrides = {}) {
|
||||||
|
return {
|
||||||
|
// Required base properties
|
||||||
|
providerStrategy: 'local',
|
||||||
|
targetPlatform: 'StandaloneLinux64',
|
||||||
|
editorVersion: '2021.3.1f1',
|
||||||
|
buildVersion: '1.0.0',
|
||||||
|
androidVersionCode: '1',
|
||||||
|
projectPath: '.',
|
||||||
|
branch: 'main',
|
||||||
|
runnerTempPath: '/tmp',
|
||||||
|
|
||||||
|
// Enterprise features - all disabled by default
|
||||||
|
childWorkspacesEnabled: false,
|
||||||
|
childWorkspaceName: '',
|
||||||
|
childWorkspaceCacheRoot: '',
|
||||||
|
childWorkspacePreserveGit: true,
|
||||||
|
childWorkspaceSeparateLibrary: true,
|
||||||
|
submoduleProfilePath: '',
|
||||||
|
submoduleVariantPath: '',
|
||||||
|
submoduleToken: '',
|
||||||
|
gitPrivateToken: '',
|
||||||
|
lfsTransferAgent: '',
|
||||||
|
lfsTransferAgentArgs: '',
|
||||||
|
lfsStoragePaths: '',
|
||||||
|
localCacheEnabled: false,
|
||||||
|
localCacheRoot: '',
|
||||||
|
localCacheLibrary: true,
|
||||||
|
localCacheLfs: false,
|
||||||
|
gitHooksEnabled: false,
|
||||||
|
gitHooksSkipList: '',
|
||||||
|
gitHooksRunBeforeBuild: '',
|
||||||
|
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The entry point (runMain) is invoked by importing index.ts.
|
||||||
|
* Since it calls `runMain()` at module scope, we need to re-import it
|
||||||
|
* for each test. jest.isolateModules() handles this.
|
||||||
|
*/
|
||||||
|
async function runIndex(overrides: EnterpriseBuildParametersOverrides = {}): Promise<void> {
|
||||||
|
mockedBuildParametersCreate.mockResolvedValue(createMockBuildParameters(overrides));
|
||||||
|
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
require('./index');
|
||||||
|
|
||||||
|
// runMain() is async; give it a tick to complete
|
||||||
|
// We use setImmediate to ensure all microtasks from the dynamic imports resolve
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow all promises and microtasks to settle
|
||||||
|
setTimeout(resolve, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('index.ts enterprise feature wiring', () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
const originalEnvironment = { ...process.env };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
process.env.GITHUB_WORKSPACE = '/workspace';
|
||||||
|
|
||||||
|
// Force linux platform so Docker.run is used (not MacBuilder)
|
||||||
|
Object.defineProperty(process, 'platform', { value: 'linux' });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||||
|
process.env = { ...originalEnvironment };
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// GitHooksService gating
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('GitHooksService gating', () => {
|
||||||
|
it('should NOT call GitHooksService when gitHooksEnabled is false (default)', async () => {
|
||||||
|
await runIndex({ gitHooksEnabled: false });
|
||||||
|
|
||||||
|
expect(mockGitHooksService.installHooks).not.toHaveBeenCalled();
|
||||||
|
expect(mockGitHooksService.configureSkipList).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call installHooks when gitHooksEnabled is true', async () => {
|
||||||
|
await runIndex({ gitHooksEnabled: true });
|
||||||
|
|
||||||
|
expect(mockGitHooksService.installHooks).toHaveBeenCalledWith('/workspace');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call configureSkipList when gitHooksEnabled and gitHooksSkipList is set', async () => {
|
||||||
|
await runIndex({
|
||||||
|
gitHooksEnabled: true,
|
||||||
|
gitHooksSkipList: 'pre-commit,pre-push',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGitHooksService.configureSkipList).toHaveBeenCalledWith(['pre-commit', 'pre-push']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT call configureSkipList when gitHooksSkipList is empty', async () => {
|
||||||
|
await runIndex({
|
||||||
|
gitHooksEnabled: true,
|
||||||
|
gitHooksSkipList: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGitHooksService.installHooks).toHaveBeenCalled();
|
||||||
|
expect(mockGitHooksService.configureSkipList).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// LocalCacheService gating
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('LocalCacheService gating', () => {
|
||||||
|
it('should NOT call LocalCacheService when localCacheEnabled is false (default)', async () => {
|
||||||
|
await runIndex({ localCacheEnabled: false });
|
||||||
|
|
||||||
|
expect(mockLocalCacheService.resolveCacheRoot).not.toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.generateCacheKey).not.toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.restoreLibraryCache).not.toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.restoreLfsCache).not.toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.saveLibraryCache).not.toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.saveLfsCache).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call restore and save operations when localCacheEnabled is true', async () => {
|
||||||
|
await runIndex({
|
||||||
|
localCacheEnabled: true,
|
||||||
|
localCacheLibrary: true,
|
||||||
|
localCacheLfs: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockLocalCacheService.resolveCacheRoot).toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.generateCacheKey).toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.restoreLibraryCache).toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.restoreLfsCache).toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.saveLibraryCache).toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.saveLfsCache).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only cache Library when localCacheLibrary is true and localCacheLfs is false', async () => {
|
||||||
|
await runIndex({
|
||||||
|
localCacheEnabled: true,
|
||||||
|
localCacheLibrary: true,
|
||||||
|
localCacheLfs: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockLocalCacheService.restoreLibraryCache).toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.restoreLfsCache).not.toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.saveLibraryCache).toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.saveLfsCache).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only cache LFS when localCacheLfs is true and localCacheLibrary is false', async () => {
|
||||||
|
await runIndex({
|
||||||
|
localCacheEnabled: true,
|
||||||
|
localCacheLibrary: false,
|
||||||
|
localCacheLfs: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockLocalCacheService.restoreLibraryCache).not.toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.restoreLfsCache).toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.saveLibraryCache).not.toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.saveLfsCache).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// ChildWorkspaceService gating
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('ChildWorkspaceService gating', () => {
|
||||||
|
it('should NOT call ChildWorkspaceService when childWorkspacesEnabled is false (default)', async () => {
|
||||||
|
await runIndex({ childWorkspacesEnabled: false });
|
||||||
|
|
||||||
|
expect(mockChildWorkspaceService.buildConfig).not.toHaveBeenCalled();
|
||||||
|
expect(mockChildWorkspaceService.initializeWorkspace).not.toHaveBeenCalled();
|
||||||
|
expect(mockChildWorkspaceService.saveWorkspace).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT call ChildWorkspaceService when childWorkspacesEnabled is true but childWorkspaceName is empty', async () => {
|
||||||
|
await runIndex({
|
||||||
|
childWorkspacesEnabled: true,
|
||||||
|
childWorkspaceName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockChildWorkspaceService.buildConfig).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call buildConfig, initializeWorkspace, and saveWorkspace when enabled with a name', async () => {
|
||||||
|
mockChildWorkspaceService.buildConfig.mockReturnValue({ enabled: true, workspaceName: 'TurnOfWar' });
|
||||||
|
|
||||||
|
await runIndex({
|
||||||
|
childWorkspacesEnabled: true,
|
||||||
|
childWorkspaceName: 'TurnOfWar',
|
||||||
|
childWorkspaceCacheRoot: '/cache/workspaces',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockChildWorkspaceService.buildConfig).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
childWorkspacesEnabled: true,
|
||||||
|
childWorkspaceName: 'TurnOfWar',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(mockChildWorkspaceService.initializeWorkspace).toHaveBeenCalled();
|
||||||
|
expect(mockChildWorkspaceService.getWorkspaceSize).toHaveBeenCalled();
|
||||||
|
expect(mockChildWorkspaceService.saveWorkspace).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// SubmoduleProfileService gating
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('SubmoduleProfileService gating', () => {
|
||||||
|
it('should NOT call SubmoduleProfileService when submoduleProfilePath is empty (default)', async () => {
|
||||||
|
await runIndex({ submoduleProfilePath: '' });
|
||||||
|
|
||||||
|
expect(mockSubmoduleProfileService.createInitPlan).not.toHaveBeenCalled();
|
||||||
|
expect(mockSubmoduleProfileService.execute).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call createInitPlan and execute when submoduleProfilePath is set', async () => {
|
||||||
|
await runIndex({
|
||||||
|
submoduleProfilePath: '/path/to/profile.yml',
|
||||||
|
submoduleVariantPath: '',
|
||||||
|
submoduleToken: 'my-token',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSubmoduleProfileService.createInitPlan).toHaveBeenCalledWith('/path/to/profile.yml', '', '/workspace');
|
||||||
|
expect(mockSubmoduleProfileService.execute).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass variant path when provided', async () => {
|
||||||
|
await runIndex({
|
||||||
|
submoduleProfilePath: '/path/to/profile.yml',
|
||||||
|
submoduleVariantPath: '/path/to/variant.yml',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSubmoduleProfileService.createInitPlan).toHaveBeenCalledWith(
|
||||||
|
'/path/to/profile.yml',
|
||||||
|
'/path/to/variant.yml',
|
||||||
|
'/workspace',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use submoduleToken for auth, falling back to gitPrivateToken', async () => {
|
||||||
|
await runIndex({
|
||||||
|
submoduleProfilePath: '/path/to/profile.yml',
|
||||||
|
submoduleToken: '',
|
||||||
|
gitPrivateToken: 'fallback-token',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSubmoduleProfileService.execute).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
'/workspace',
|
||||||
|
'fallback-token',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer submoduleToken over gitPrivateToken', async () => {
|
||||||
|
await runIndex({
|
||||||
|
submoduleProfilePath: '/path/to/profile.yml',
|
||||||
|
submoduleToken: 'specific-token',
|
||||||
|
gitPrivateToken: 'fallback-token',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockSubmoduleProfileService.execute).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
'/workspace',
|
||||||
|
'specific-token',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// LfsAgentService gating
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('LfsAgentService gating', () => {
|
||||||
|
it('should NOT call LfsAgentService when lfsTransferAgent is empty (default)', async () => {
|
||||||
|
await runIndex({ lfsTransferAgent: '' });
|
||||||
|
|
||||||
|
expect(mockLfsAgentService.configure).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call configure when lfsTransferAgent is set', async () => {
|
||||||
|
await runIndex({
|
||||||
|
lfsTransferAgent: '/tools/elastic-git-storage',
|
||||||
|
lfsTransferAgentArgs: '--verbose',
|
||||||
|
lfsStoragePaths: '/path/a;/path/b',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockLfsAgentService.configure).toHaveBeenCalledWith(
|
||||||
|
'/tools/elastic-git-storage',
|
||||||
|
'--verbose',
|
||||||
|
['/path/a', '/path/b'],
|
||||||
|
'/workspace',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass empty array when lfsStoragePaths is empty', async () => {
|
||||||
|
await runIndex({
|
||||||
|
lfsTransferAgent: '/tools/agent',
|
||||||
|
lfsStoragePaths: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockLfsAgentService.configure).toHaveBeenCalledWith('/tools/agent', '', [], '/workspace');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Order of operations (restore before build, save after build)
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('order of operations', () => {
|
||||||
|
it('should execute restore operations before build and save operations after build', async () => {
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
|
||||||
|
// Track call order for each relevant operation
|
||||||
|
mockChildWorkspaceService.buildConfig.mockReturnValue({ enabled: true, workspaceName: 'Test' });
|
||||||
|
mockChildWorkspaceService.initializeWorkspace.mockImplementation(() => {
|
||||||
|
callOrder.push('child-workspace-restore');
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
mockChildWorkspaceService.getWorkspaceSize.mockImplementation(() => {
|
||||||
|
callOrder.push('child-workspace-size');
|
||||||
|
|
||||||
|
return '0 B';
|
||||||
|
});
|
||||||
|
mockSubmoduleProfileService.createInitPlan.mockImplementation(async () => {
|
||||||
|
callOrder.push('submodule-profile-plan');
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
mockSubmoduleProfileService.execute.mockImplementation(async () => {
|
||||||
|
callOrder.push('submodule-profile-execute');
|
||||||
|
});
|
||||||
|
mockLfsAgentService.configure.mockImplementation(async () => {
|
||||||
|
callOrder.push('lfs-agent-configure');
|
||||||
|
});
|
||||||
|
mockLocalCacheService.resolveCacheRoot.mockImplementation(() => {
|
||||||
|
callOrder.push('local-cache-resolve');
|
||||||
|
|
||||||
|
return '/cache';
|
||||||
|
});
|
||||||
|
mockLocalCacheService.generateCacheKey.mockImplementation(() => {
|
||||||
|
callOrder.push('local-cache-keygen');
|
||||||
|
|
||||||
|
return 'key-1';
|
||||||
|
});
|
||||||
|
mockLocalCacheService.restoreLfsCache.mockImplementation(async () => {
|
||||||
|
callOrder.push('local-cache-restore-lfs');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
mockLocalCacheService.restoreLibraryCache.mockImplementation(async () => {
|
||||||
|
callOrder.push('local-cache-restore-library');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
mockGitHooksService.installHooks.mockImplementation(async () => {
|
||||||
|
callOrder.push('git-hooks-install');
|
||||||
|
});
|
||||||
|
mockLocalCacheService.saveLibraryCache.mockImplementation(async () => {
|
||||||
|
callOrder.push('local-cache-save-library');
|
||||||
|
});
|
||||||
|
mockLocalCacheService.saveLfsCache.mockImplementation(async () => {
|
||||||
|
callOrder.push('local-cache-save-lfs');
|
||||||
|
});
|
||||||
|
mockChildWorkspaceService.saveWorkspace.mockImplementation(() => {
|
||||||
|
callOrder.push('child-workspace-save');
|
||||||
|
});
|
||||||
|
|
||||||
|
await runIndex({
|
||||||
|
childWorkspacesEnabled: true,
|
||||||
|
childWorkspaceName: 'TurnOfWar',
|
||||||
|
submoduleProfilePath: '/profile.yml',
|
||||||
|
lfsTransferAgent: '/tools/agent',
|
||||||
|
localCacheEnabled: true,
|
||||||
|
localCacheLfs: true,
|
||||||
|
localCacheLibrary: true,
|
||||||
|
gitHooksEnabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify restore operations happen before save operations.
|
||||||
|
// The expected order from index.ts is:
|
||||||
|
// 1. Child workspace restore
|
||||||
|
// 2. Submodule profile init
|
||||||
|
// 3. LFS agent configure
|
||||||
|
// 4. Local cache restore (LFS then Library)
|
||||||
|
// 5. Git hooks install
|
||||||
|
// 6. [BUILD happens here - Docker.run or MacBuilder.run]
|
||||||
|
// 7. Local cache save (Library then LFS)
|
||||||
|
// 8. Child workspace save
|
||||||
|
|
||||||
|
const restoreOps = [
|
||||||
|
'child-workspace-restore',
|
||||||
|
'submodule-profile-plan',
|
||||||
|
'submodule-profile-execute',
|
||||||
|
'lfs-agent-configure',
|
||||||
|
'local-cache-restore-lfs',
|
||||||
|
'local-cache-restore-library',
|
||||||
|
'git-hooks-install',
|
||||||
|
];
|
||||||
|
|
||||||
|
const saveOps = ['local-cache-save-library', 'local-cache-save-lfs', 'child-workspace-save'];
|
||||||
|
|
||||||
|
// All restore ops should appear before all save ops
|
||||||
|
for (const restoreOp of restoreOps) {
|
||||||
|
if (!callOrder.includes(restoreOp)) continue; // Skip if the operation wasn't called
|
||||||
|
for (const saveOp of saveOps) {
|
||||||
|
if (!callOrder.includes(saveOp)) continue;
|
||||||
|
expect(callOrder.indexOf(restoreOp)).toBeLessThan(callOrder.indexOf(saveOp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child workspace save should be last
|
||||||
|
if (callOrder.includes('child-workspace-save') && callOrder.includes('local-cache-save-lfs')) {
|
||||||
|
expect(callOrder.indexOf('local-cache-save-lfs')).toBeLessThan(callOrder.indexOf('child-workspace-save'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Non-local provider strategy
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('non-local provider strategy', () => {
|
||||||
|
it('should skip all enterprise features when providerStrategy is not local', async () => {
|
||||||
|
await runIndex({
|
||||||
|
providerStrategy: 'aws',
|
||||||
|
childWorkspacesEnabled: true,
|
||||||
|
childWorkspaceName: 'Test',
|
||||||
|
submoduleProfilePath: '/profile.yml',
|
||||||
|
lfsTransferAgent: '/tools/agent',
|
||||||
|
localCacheEnabled: true,
|
||||||
|
gitHooksEnabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// None of the enterprise services should be called because
|
||||||
|
// they are inside the `if (providerStrategy === 'local')` block
|
||||||
|
expect(mockChildWorkspaceService.buildConfig).not.toHaveBeenCalled();
|
||||||
|
expect(mockSubmoduleProfileService.createInitPlan).not.toHaveBeenCalled();
|
||||||
|
expect(mockLfsAgentService.configure).not.toHaveBeenCalled();
|
||||||
|
expect(mockLocalCacheService.resolveCacheRoot).not.toHaveBeenCalled();
|
||||||
|
expect(mockGitHooksService.installHooks).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+1
-4
@@ -192,7 +192,7 @@ async function runMain() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Git hooks
|
// Git hooks — opt-in only. When disabled (default), do not touch hooks at all.
|
||||||
if (buildParameters.gitHooksEnabled) {
|
if (buildParameters.gitHooksEnabled) {
|
||||||
const { GitHooksService } = await import('./model/orchestrator/services/hooks/git-hooks-service');
|
const { GitHooksService } = await import('./model/orchestrator/services/hooks/git-hooks-service');
|
||||||
await GitHooksService.installHooks(workspace);
|
await GitHooksService.installHooks(workspace);
|
||||||
@@ -200,9 +200,6 @@ async function runMain() {
|
|||||||
const environment = GitHooksService.configureSkipList(buildParameters.gitHooksSkipList.split(','));
|
const environment = GitHooksService.configureSkipList(buildParameters.gitHooksSkipList.split(','));
|
||||||
Object.assign(process.env, environment);
|
Object.assign(process.env, environment);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const { GitHooksService } = await import('./model/orchestrator/services/hooks/git-hooks-service');
|
|
||||||
await GitHooksService.disableHooks(workspace);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply incremental sync strategy before build
|
// Apply incremental sync strategy before build
|
||||||
|
|||||||
@@ -0,0 +1,509 @@
|
|||||||
|
/**
|
||||||
|
* Tests for enterprise input properties and their wiring into BuildParameters.
|
||||||
|
*
|
||||||
|
* Covers all 20 new input properties added for enterprise features:
|
||||||
|
* - Boolean inputs: localCacheEnabled, childWorkspacesEnabled, gitHooksEnabled,
|
||||||
|
* localCacheLibrary, localCacheLfs, childWorkspacePreserveGit, childWorkspaceSeparateLibrary
|
||||||
|
* - String inputs: submoduleProfilePath, submoduleVariantPath, submoduleToken,
|
||||||
|
* localCacheRoot, childWorkspaceName, childWorkspaceCacheRoot, lfsTransferAgent,
|
||||||
|
* lfsTransferAgentArgs, lfsStoragePaths, providerExecutable, gitHooksSkipList,
|
||||||
|
* gitHooksRunBeforeBuild
|
||||||
|
*
|
||||||
|
* Special attention to boolean inputs: GitHub Actions always passes inputs as strings,
|
||||||
|
* so 'false' must NOT evaluate as truthy (the #1 source of bugs).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as core from '@actions/core';
|
||||||
|
import Input from './input';
|
||||||
|
import Versioning from './versioning';
|
||||||
|
import BuildParameters from './build-parameters';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Setup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Part 1: Input getters — defaults and explicit values
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('Enterprise Input properties', () => {
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Boolean inputs — default and string parsing
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('localCacheEnabled', () => {
|
||||||
|
it('returns false by default', () => {
|
||||||
|
expect(Input.localCacheEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when string "true" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
|
expect(Input.localCacheEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when string "false" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
|
expect(Input.localCacheEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when empty string is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('');
|
||||||
|
expect(Input.localCacheEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('localCacheLibrary', () => {
|
||||||
|
it('returns true by default (library caching on by default when cache enabled)', () => {
|
||||||
|
expect(Input.localCacheLibrary).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when string "true" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
|
expect(Input.localCacheLibrary).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when string "false" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
|
expect(Input.localCacheLibrary).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('localCacheLfs', () => {
|
||||||
|
it('returns false by default', () => {
|
||||||
|
expect(Input.localCacheLfs).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when string "true" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
|
expect(Input.localCacheLfs).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when string "false" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
|
expect(Input.localCacheLfs).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('childWorkspacesEnabled', () => {
|
||||||
|
it('returns false by default', () => {
|
||||||
|
expect(Input.childWorkspacesEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when string "true" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
|
expect(Input.childWorkspacesEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when string "false" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
|
expect(Input.childWorkspacesEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when empty string is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('');
|
||||||
|
expect(Input.childWorkspacesEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('childWorkspacePreserveGit', () => {
|
||||||
|
it('returns true by default', () => {
|
||||||
|
expect(Input.childWorkspacePreserveGit).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when string "false" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
|
expect(Input.childWorkspacePreserveGit).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when string "true" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
|
expect(Input.childWorkspacePreserveGit).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('childWorkspaceSeparateLibrary', () => {
|
||||||
|
it('returns true by default', () => {
|
||||||
|
expect(Input.childWorkspaceSeparateLibrary).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when string "false" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
|
expect(Input.childWorkspaceSeparateLibrary).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when string "true" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
|
expect(Input.childWorkspaceSeparateLibrary).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('gitHooksEnabled', () => {
|
||||||
|
it('returns false by default', () => {
|
||||||
|
expect(Input.gitHooksEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when string "true" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
|
expect(Input.gitHooksEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when string "false" is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
|
expect(Input.gitHooksEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when empty string is passed', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('');
|
||||||
|
expect(Input.gitHooksEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// Boolean truthiness edge cases — the #1 source of bugs
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('boolean input string handling (edge cases)', () => {
|
||||||
|
// These tests verify that the === 'true' comparison is correct.
|
||||||
|
// In JavaScript, 'false' is truthy when used in a boolean context,
|
||||||
|
// but the Input class correctly uses === 'true' comparison.
|
||||||
|
|
||||||
|
const booleanInputs: Array<{
|
||||||
|
name: string;
|
||||||
|
getter: () => boolean;
|
||||||
|
defaultValue: boolean;
|
||||||
|
}> = [
|
||||||
|
{ name: 'localCacheEnabled', getter: () => Input.localCacheEnabled, defaultValue: false },
|
||||||
|
{ name: 'localCacheLfs', getter: () => Input.localCacheLfs, defaultValue: false },
|
||||||
|
{ name: 'childWorkspacesEnabled', getter: () => Input.childWorkspacesEnabled, defaultValue: false },
|
||||||
|
{ name: 'gitHooksEnabled', getter: () => Input.gitHooksEnabled, defaultValue: false },
|
||||||
|
|
||||||
|
// These default to true:
|
||||||
|
{ name: 'localCacheLibrary', getter: () => Input.localCacheLibrary, defaultValue: true },
|
||||||
|
{ name: 'childWorkspacePreserveGit', getter: () => Input.childWorkspacePreserveGit, defaultValue: true },
|
||||||
|
{ name: 'childWorkspaceSeparateLibrary', getter: () => Input.childWorkspaceSeparateLibrary, defaultValue: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(booleanInputs)('$name: "false" string does NOT evaluate as truthy', ({ getter }) => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('false');
|
||||||
|
expect(getter()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each(booleanInputs)('$name: "true" string evaluates as truthy', ({ getter }) => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('true');
|
||||||
|
expect(getter()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each(booleanInputs)('$name: "TRUE" (uppercase) does NOT evaluate as true (case sensitive)', ({ getter }) => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('TRUE');
|
||||||
|
expect(getter()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each(booleanInputs)('$name: "1" does NOT evaluate as true', ({ getter }) => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('1');
|
||||||
|
expect(getter()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each(booleanInputs)('$name: "yes" does NOT evaluate as true', ({ getter }) => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('yes');
|
||||||
|
expect(getter()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// String inputs — defaults and explicit values
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('submoduleProfilePath', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.submoduleProfilePath).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('config/submodule-profiles/tow/ec/profile.yml');
|
||||||
|
expect(Input.submoduleProfilePath).toBe('config/submodule-profiles/tow/ec/profile.yml');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('submoduleVariantPath', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.submoduleVariantPath).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('config/submodule-profiles/tow/ec/server.yml');
|
||||||
|
expect(Input.submoduleVariantPath).toBe('config/submodule-profiles/tow/ec/server.yml');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('submoduleToken', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.submoduleToken).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('ghp_abc123');
|
||||||
|
expect(Input.submoduleToken).toBe('ghp_abc123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('localCacheRoot', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.localCacheRoot).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('/d/cache/game-ci');
|
||||||
|
expect(Input.localCacheRoot).toBe('/d/cache/game-ci');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('childWorkspaceName', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.childWorkspaceName).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('TurnOfWarEndlessCrusade');
|
||||||
|
expect(Input.childWorkspaceName).toBe('TurnOfWarEndlessCrusade');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('childWorkspaceCacheRoot', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.childWorkspaceCacheRoot).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('/d/workspaces');
|
||||||
|
expect(Input.childWorkspaceCacheRoot).toBe('/d/workspaces');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lfsTransferAgent', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.lfsTransferAgent).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('/tools/elastic-git-storage');
|
||||||
|
expect(Input.lfsTransferAgent).toBe('/tools/elastic-git-storage');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lfsTransferAgentArgs', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.lfsTransferAgentArgs).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('--verbose --timeout=60');
|
||||||
|
expect(Input.lfsTransferAgentArgs).toBe('--verbose --timeout=60');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lfsStoragePaths', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.lfsStoragePaths).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('/storage/primary;/storage/secondary');
|
||||||
|
expect(Input.lfsStoragePaths).toBe('/storage/primary;/storage/secondary');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('providerExecutable', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.providerExecutable).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('/usr/local/bin/custom-provider');
|
||||||
|
expect(Input.providerExecutable).toBe('/usr/local/bin/custom-provider');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('gitHooksSkipList', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.gitHooksSkipList).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('pre-commit,pre-push');
|
||||||
|
expect(Input.gitHooksSkipList).toBe('pre-commit,pre-push');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('gitHooksRunBeforeBuild', () => {
|
||||||
|
it('returns empty string by default', () => {
|
||||||
|
expect(Input.gitHooksRunBeforeBuild).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('takes input from workflow', () => {
|
||||||
|
jest.spyOn(core, 'getInput').mockReturnValue('pre-commit');
|
||||||
|
expect(Input.gitHooksRunBeforeBuild).toBe('pre-commit');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Part 2: BuildParameters.create() maps new inputs to properties
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const testLicense =
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?><root>\n <License id="Terms">\n <MachineBindings>\n <Binding Key="1" Value="576562626572264761624c65526f7578"/>\n <Binding Key="2" Value="576562626572264761624c65526f7578"/>\n </MachineBindings>\n <MachineID Value="D7nTUnjNAmtsUMcnoyrqkgIbYdM="/>\n <SerialHash Value="2033b8ac3e6faa3742ca9f0bfae44d18f2a96b80"/>\n <Features>\n <Feature Value="33"/>\n <Feature Value="1"/>\n <Feature Value="12"/>\n <Feature Value="2"/>\n <Feature Value="24"/>\n <Feature Value="3"/>\n <Feature Value="36"/>\n <Feature Value="17"/>\n <Feature Value="19"/>\n <Feature Value="62"/>\n </Features>\n <DeveloperData Value="AQAAAEY0LUJHUlgtWEQ0RS1aQ1dWLUM1SlctR0RIQg=="/>\n <SerialMasked Value="F4-BGRX-XD4E-ZCWV-C5JW-XXXX"/>\n <StartDate Value="2021-02-08T00:00:00"/>\n <UpdateDate Value="2021-02-09T00:34:57"/>\n <InitialActivationDate Value="2021-02-08T00:34:56"/>\n <LicenseVersion Value="6.x"/>\n <ClientProvidedVersion Value="2018.4.30f1"/>\n <AlwaysOnline Value="false"/>\n <Entitlements>\n <Entitlement Ns="unity_editor" Tag="UnityPersonal" Type="EDITOR" ValidTo="9999-12-31T00:00:00"/>\n <Entitlement Ns="unity_editor" Tag="DarkSkin" Type="EDITOR_FEATURE" ValidTo="9999-12-31T00:00:00"/>\n </Entitlements>\n </License>\n<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#Terms"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>m0Db8UK+ktnOLJBtHybkfetpcKo=</DigestValue></Reference></SignedInfo><SignatureValue>o/pUbSQAukz7+ZYAWhnA0AJbIlyyCPL7bKVEM2lVqbrXt7cyey+umkCXamuOgsWPVUKBMkXtMH8L\n5etLmD0getWIhTGhzOnDCk+gtIPfL4jMo9tkEuOCROQAXCci23VFscKcrkB+3X6h4wEOtA2APhOY\nB+wvC794o8/82ffjP79aVAi57rp3Wmzx+9pe9yMwoJuljAy2sc2tIMgdQGWVmOGBpQm3JqsidyzI\nJWG2kjnc7pDXK9pwYzXoKiqUqqrut90d+kQqRyv7MSZXR50HFqD/LI69h68b7P8Bjo3bPXOhNXGR\n9YCoemH6EkfCJxp2gIjzjWW+l2Hj2EsFQi8YXw==</SignatureValue></Signature></root>';
|
||||||
|
|
||||||
|
describe('BuildParameters.create() enterprise property mapping', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(Versioning, 'determineBuildVersion').mockImplementation(async () => '1.3.37');
|
||||||
|
process.env.UNITY_LICENSE = testLicense;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps submoduleProfilePath from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'submoduleProfilePath', 'get').mockReturnValue('/path/to/profile.yml');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.submoduleProfilePath).toBe('/path/to/profile.yml');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps submoduleVariantPath from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'submoduleVariantPath', 'get').mockReturnValue('/path/to/variant.yml');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.submoduleVariantPath).toBe('/path/to/variant.yml');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps submoduleToken from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'submoduleToken', 'get').mockReturnValue('ghp_token123');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.submoduleToken).toBe('ghp_token123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps localCacheEnabled from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'localCacheEnabled', 'get').mockReturnValue(true);
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.localCacheEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps localCacheRoot from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'localCacheRoot', 'get').mockReturnValue('/d/cache');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.localCacheRoot).toBe('/d/cache');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps localCacheLibrary from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'localCacheLibrary', 'get').mockReturnValue(false);
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.localCacheLibrary).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps localCacheLfs from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'localCacheLfs', 'get').mockReturnValue(true);
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.localCacheLfs).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps childWorkspacesEnabled from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'childWorkspacesEnabled', 'get').mockReturnValue(true);
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.childWorkspacesEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps childWorkspaceName from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'childWorkspaceName', 'get').mockReturnValue('TurnOfWar');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.childWorkspaceName).toBe('TurnOfWar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps childWorkspaceCacheRoot from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'childWorkspaceCacheRoot', 'get').mockReturnValue('/cache/workspaces');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.childWorkspaceCacheRoot).toBe('/cache/workspaces');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps childWorkspacePreserveGit from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'childWorkspacePreserveGit', 'get').mockReturnValue(false);
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.childWorkspacePreserveGit).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps childWorkspaceSeparateLibrary from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'childWorkspaceSeparateLibrary', 'get').mockReturnValue(false);
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.childWorkspaceSeparateLibrary).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps lfsTransferAgent from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'lfsTransferAgent', 'get').mockReturnValue('/tools/elastic-git-storage');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.lfsTransferAgent).toBe('/tools/elastic-git-storage');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps lfsTransferAgentArgs from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'lfsTransferAgentArgs', 'get').mockReturnValue('--verbose');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.lfsTransferAgentArgs).toBe('--verbose');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps lfsStoragePaths from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'lfsStoragePaths', 'get').mockReturnValue('/path/a;/path/b');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.lfsStoragePaths).toBe('/path/a;/path/b');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps gitHooksEnabled from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'gitHooksEnabled', 'get').mockReturnValue(true);
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.gitHooksEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps gitHooksSkipList from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'gitHooksSkipList', 'get').mockReturnValue('pre-commit,pre-push');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.gitHooksSkipList).toBe('pre-commit,pre-push');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps gitHooksRunBeforeBuild from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'gitHooksRunBeforeBuild', 'get').mockReturnValue('pre-commit');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.gitHooksRunBeforeBuild).toBe('pre-commit');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps providerExecutable from Input', async () => {
|
||||||
|
jest.spyOn(Input, 'providerExecutable', 'get').mockReturnValue('/usr/local/bin/provider');
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
expect(parameters.providerExecutable).toBe('/usr/local/bin/provider');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test that all enterprise properties have correct defaults when not explicitly set
|
||||||
|
it('has correct defaults for all enterprise properties', async () => {
|
||||||
|
const parameters = await BuildParameters.create();
|
||||||
|
|
||||||
|
expect(parameters.submoduleProfilePath).toBe('');
|
||||||
|
expect(parameters.submoduleVariantPath).toBe('');
|
||||||
|
expect(parameters.submoduleToken).toBe('');
|
||||||
|
expect(parameters.localCacheEnabled).toBe(false);
|
||||||
|
expect(parameters.localCacheRoot).toBe('');
|
||||||
|
expect(parameters.localCacheLibrary).toBe(true);
|
||||||
|
expect(parameters.localCacheLfs).toBe(false);
|
||||||
|
expect(parameters.childWorkspacesEnabled).toBe(false);
|
||||||
|
expect(parameters.childWorkspaceName).toBe('');
|
||||||
|
expect(parameters.childWorkspaceCacheRoot).toBe('');
|
||||||
|
expect(parameters.childWorkspacePreserveGit).toBe(true);
|
||||||
|
expect(parameters.childWorkspaceSeparateLibrary).toBe(true);
|
||||||
|
expect(parameters.lfsTransferAgent).toBe('');
|
||||||
|
expect(parameters.lfsTransferAgentArgs).toBe('');
|
||||||
|
expect(parameters.lfsStoragePaths).toBe('');
|
||||||
|
expect(parameters.gitHooksEnabled).toBe(false);
|
||||||
|
expect(parameters.gitHooksSkipList).toBe('');
|
||||||
|
expect(parameters.gitHooksRunBeforeBuild).toBe('');
|
||||||
|
expect(parameters.providerExecutable).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -488,6 +488,8 @@ class Input {
|
|||||||
|
|
||||||
static get azureSubnetId(): string {
|
static get azureSubnetId(): string {
|
||||||
return Input.getInput('azureSubnetId') ?? '';
|
return Input.getInput('azureSubnetId') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
// ### ### ###
|
// ### ### ###
|
||||||
// Remote PowerShell provider
|
// Remote PowerShell provider
|
||||||
// ### ### ###
|
// ### ### ###
|
||||||
@@ -562,8 +564,14 @@ class Input {
|
|||||||
|
|
||||||
static get ansibleVaultPassword(): string {
|
static get ansibleVaultPassword(): string {
|
||||||
return Input.getInput('ansibleVaultPassword') ?? '';
|
return Input.getInput('ansibleVaultPassword') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
static get gitIntegrityCheck(): boolean {
|
static get gitIntegrityCheck(): boolean {
|
||||||
const input = Input.getInput('gitIntegrityCheck') ?? 'false';
|
const input = Input.getInput('gitIntegrityCheck') ?? 'false';
|
||||||
|
|
||||||
|
return input === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
static get hotRunnerEnabled(): boolean {
|
static get hotRunnerEnabled(): boolean {
|
||||||
const input = Input.getInput('hotRunnerEnabled') ?? false;
|
const input = Input.getInput('hotRunnerEnabled') ?? false;
|
||||||
|
|
||||||
@@ -572,6 +580,10 @@ class Input {
|
|||||||
|
|
||||||
static get gitAutoRecover(): boolean {
|
static get gitAutoRecover(): boolean {
|
||||||
const input = Input.getInput('gitAutoRecover') ?? 'false';
|
const input = Input.getInput('gitAutoRecover') ?? 'false';
|
||||||
|
|
||||||
|
return input === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
static get hotRunnerTransport(): 'websocket' | 'grpc' | 'named-pipe' {
|
static get hotRunnerTransport(): 'websocket' | 'grpc' | 'named-pipe' {
|
||||||
return (Input.getInput('hotRunnerTransport') ?? 'websocket') as 'websocket' | 'grpc' | 'named-pipe';
|
return (Input.getInput('hotRunnerTransport') ?? 'websocket') as 'websocket' | 'grpc' | 'named-pipe';
|
||||||
}
|
}
|
||||||
@@ -616,6 +628,8 @@ class Input {
|
|||||||
|
|
||||||
static get buildArchiveRetention(): number {
|
static get buildArchiveRetention(): number {
|
||||||
return Number.parseInt(Input.getInput('buildArchiveRetention') ?? '30', 10);
|
return Number.parseInt(Input.getInput('buildArchiveRetention') ?? '30', 10);
|
||||||
|
}
|
||||||
|
|
||||||
static get testSuitePath(): string {
|
static get testSuitePath(): string {
|
||||||
return Input.getInput('testSuitePath') ?? '';
|
return Input.getInput('testSuitePath') ?? '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ submodules:
|
|||||||
mockedFs.readFileSync.mockImplementation((filePath: any) => {
|
mockedFs.readFileSync.mockImplementation((filePath: any) => {
|
||||||
if (filePath === '/base.yml') return baseYaml;
|
if (filePath === '/base.yml') return baseYaml;
|
||||||
if (filePath === '/variant.yml') return variantYaml;
|
if (filePath === '/variant.yml') return variantYaml;
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -196,6 +197,7 @@ submodules:
|
|||||||
mockedFs.readFileSync.mockImplementation((filePath: any) => {
|
mockedFs.readFileSync.mockImplementation((filePath: any) => {
|
||||||
if (String(filePath).endsWith('profile.yml')) return profileYaml;
|
if (String(filePath).endsWith('profile.yml')) return profileYaml;
|
||||||
if (String(filePath).endsWith('.gitmodules')) return gitmodulesContent;
|
if (String(filePath).endsWith('.gitmodules')) return gitmodulesContent;
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -249,6 +251,7 @@ submodules:
|
|||||||
if (p.endsWith('profile.yml')) return profileYaml;
|
if (p.endsWith('profile.yml')) return profileYaml;
|
||||||
if (p.endsWith('variant.yml')) return variantYaml;
|
if (p.endsWith('variant.yml')) return variantYaml;
|
||||||
if (p.endsWith('.gitmodules')) return gitmodulesContent;
|
if (p.endsWith('.gitmodules')) return gitmodulesContent;
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -309,4 +312,74 @@ submodules:
|
|||||||
expect(mockedSystem.Run).not.toHaveBeenCalledWith(expect.stringContaining('git config url'));
|
expect(mockedSystem.Run).not.toHaveBeenCalledWith(expect.stringContaining('git config url'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('execute — command construction safety', () => {
|
||||||
|
it('constructs expected git commands for a standard path', async () => {
|
||||||
|
mockedSystem.Run.mockResolvedValue('');
|
||||||
|
|
||||||
|
const plan = [
|
||||||
|
{
|
||||||
|
name: 'Assets/_Game/Submodules/TurnOfWar',
|
||||||
|
path: 'Assets/_Game/Submodules/TurnOfWar',
|
||||||
|
branch: 'main',
|
||||||
|
action: 'init' as const,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
await SubmoduleProfileService.execute(plan, '/repo');
|
||||||
|
|
||||||
|
expect(mockedSystem.Run).toHaveBeenCalledWith('git submodule update --init Assets/_Game/Submodules/TurnOfWar');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes path directly into git commands (paths with spaces are not quoted)', async () => {
|
||||||
|
mockedSystem.Run.mockResolvedValue('');
|
||||||
|
|
||||||
|
// This test documents current behavior: paths are passed as-is.
|
||||||
|
// If a path contained shell metacharacters, they would be passed through.
|
||||||
|
// This is acceptable because submodule paths come from .gitmodules (trusted source),
|
||||||
|
// not from user input. Still, this test documents the behavior for awareness.
|
||||||
|
const plan = [
|
||||||
|
{ name: 'Module With Spaces', path: 'Assets/Module With Spaces', branch: 'main', action: 'init' as const },
|
||||||
|
];
|
||||||
|
|
||||||
|
await SubmoduleProfileService.execute(plan, '/repo');
|
||||||
|
|
||||||
|
// The current implementation passes the path directly — no shell quoting
|
||||||
|
expect(mockedSystem.Run).toHaveBeenCalledWith('git submodule update --init Assets/Module With Spaces');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes branch name directly into git checkout command', async () => {
|
||||||
|
mockedSystem.Run.mockResolvedValue('');
|
||||||
|
|
||||||
|
// Document that branch names are passed as-is into shell commands.
|
||||||
|
// Branch names come from the trusted profile YAML, not user input.
|
||||||
|
const plan = [{ name: 'ModuleX', path: 'Assets/ModuleX', branch: 'feature/my-branch', action: 'init' as const }];
|
||||||
|
|
||||||
|
await SubmoduleProfileService.execute(plan, '/repo');
|
||||||
|
|
||||||
|
expect(mockedSystem.Run).toHaveBeenCalledWith('git -C Assets/ModuleX checkout feature/my-branch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('constructs deinit command with error suppression for skip actions', async () => {
|
||||||
|
mockedSystem.Run.mockResolvedValue('');
|
||||||
|
|
||||||
|
const plan = [{ name: 'Unused', path: 'Assets/Unused', branch: 'empty', action: 'skip' as const }];
|
||||||
|
|
||||||
|
await SubmoduleProfileService.execute(plan, '/repo');
|
||||||
|
|
||||||
|
expect(mockedSystem.Run).toHaveBeenCalledWith('git submodule deinit -f Assets/Unused 2>/dev/null || true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('injects token into git config URL insteadOf pattern', async () => {
|
||||||
|
mockedSystem.Run.mockResolvedValue('');
|
||||||
|
|
||||||
|
// The token is embedded directly into the URL pattern.
|
||||||
|
// This is the standard Git credential approach for CI.
|
||||||
|
await SubmoduleProfileService.execute([], '/repo', 'ghp_abc123xyz');
|
||||||
|
|
||||||
|
const configCall = mockedSystem.Run.mock.calls.find((call: any[]) => String(call[0]).includes('git config url'));
|
||||||
|
expect(configCall).toBeDefined();
|
||||||
|
expect(configCall![0]).toBe('git config url."https://ghp_abc123xyz@github.com/".insteadOf "https://github.com/"');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user