Skip to content

Commit 01f5e20

Browse files
Resolve profiles from URI in Jobs FileSystemProvider (#4285)
* resolve profiles from URI in Jobs FileSystemProvider Signed-off-by: Andrew Twydell <andrew.twydell@ibm.com> * chore: prettier + prepublish Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> --------- Signed-off-by: Andrew Twydell <andrew.twydell@ibm.com> Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Co-authored-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com> Signed-off-by: Fernando Rijo Cedeno <37381190+zFernand0@users.noreply.github.com>
1 parent 2a8c455 commit 01f5e20

6 files changed

Lines changed: 430 additions & 151 deletions

File tree

packages/zowe-explorer/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ All notable changes to the "vscode-extension-for-zowe" extension will be documen
1616
- Fixed an issue where navigating to an invalid or inaccessible path under one USS profile caused file and folder nodes under all profiles to disappear or show outdated results. [#4288](https://github.com/zowe/zowe-explorer-vscode/issues/4288)
1717
- Fixed an issue in USS where the search path in the profile node description or filter tooltip became out of sync and did not match the actual listed path after a failed search or a search timeout. [#4287](https://github.com/zowe/zowe-explorer-vscode/issues/4287)
1818
- Renamed references to "MVS Command" to "Console Command" for clarity. [#4300](https://github.com/zowe/zowe-explorer-vscode/issues/4300)
19+
- Fixed an issue where the Jobs FileSystemProvider could not resolve profiles from URIs when opened programmatically, causing errors when extensions tried to open job spool files directly. The Jobs FileSystemProvider now extracts the profile name from the URI and loads it on-demand, matching the behavior of Datasets and USS FileSystemProviders. [#4284](https://github.com/zowe/zowe-explorer-vscode/issues/4284)
1920

2021
## `3.5.0`
2122

packages/zowe-explorer/__tests__/__unit__/trees/job/JobFSProvider.unit.test.ts

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,36 @@ describe("fetchSpoolAtUri", () => {
696696
jesApiMock.mockRestore();
697697
lookupAsFileMock.mockRestore();
698698
});
699+
700+
it("resolves profile from URI when metadata is missing", async () => {
701+
const spoolEntryWithoutMetadata = { ...testEntries.spool, metadata: undefined };
702+
const lookupAsFileMock = vi.spyOn(JobFSProvider.instance as any, "_lookupAsFile").mockReturnValueOnce(spoolEntryWithoutMetadata);
703+
const getInfoFromUriMock = vi.spyOn(JobFSProvider.instance as any, "_getInfoFromUri").mockReturnValue({
704+
profile: testProfile,
705+
path: "/TESTJOB(JOB1234) - ACTIVE/JES2.JESMSGLG.2",
706+
});
707+
708+
const mockJesApi = {
709+
downloadSingleSpool: vi.fn((opts) => {
710+
opts.stream.write("test data");
711+
}),
712+
};
713+
714+
const jesApiMock = vi.spyOn(ZoweExplorerApiRegister, "getInstance").mockReturnValue({
715+
getJesApi: vi.fn().mockReturnValue(mockJesApi),
716+
registeredJesApiTypes: vi.fn().mockReturnValue(["zosmf"]),
717+
} as any);
718+
719+
const entry = await JobFSProvider.instance.fetchSpoolAtUri(testUris.spool);
720+
721+
expect(getInfoFromUriMock).toHaveBeenCalledWith(testUris.spool);
722+
expect(entry.metadata).toBeDefined();
723+
expect(entry.metadata.profile).toBe(testProfile);
724+
725+
lookupAsFileMock.mockRestore();
726+
getInfoFromUriMock.mockRestore();
727+
jesApiMock.mockRestore();
728+
});
699729
});
700730

701731
describe("readFile", () => {
@@ -718,6 +748,295 @@ describe("readFile", () => {
718748
lookupAsFileMock.mockRestore();
719749
fetchSpoolAtUriMock.mockRestore();
720750
});
751+
752+
it("creates entries from URI when spool entry doesn't exist", async () => {
753+
const spoolEntry = { ...testEntries.spool };
754+
const fileNotFoundError = vscode.FileSystemError.FileNotFound(testUris.spool);
755+
const lookupAsFileMock = vi
756+
.spyOn(JobFSProvider.instance as any, "_lookupAsFile")
757+
.mockImplementationOnce(() => {
758+
throw fileNotFoundError;
759+
})
760+
.mockReturnValueOnce(spoolEntry);
761+
const createEntriesFromUriMock = vi.spyOn(JobFSProvider.instance as any, "_createEntriesFromUri").mockResolvedValueOnce(undefined);
762+
const fetchSpoolAtUriMock = vi.spyOn(JobFSProvider.instance, "fetchSpoolAtUri").mockResolvedValueOnce(spoolEntry);
763+
764+
expect(await JobFSProvider.instance.readFile(testUris.spool)).toBe(spoolEntry.data);
765+
expect(createEntriesFromUriMock).toHaveBeenCalledWith(testUris.spool);
766+
expect(spoolEntry.wasAccessed).toBe(true);
767+
768+
lookupAsFileMock.mockRestore();
769+
createEntriesFromUriMock.mockRestore();
770+
fetchSpoolAtUriMock.mockRestore();
771+
});
772+
773+
it("throws error if entry creation fails", async () => {
774+
const fileNotFoundError = vscode.FileSystemError.FileNotFound(testUris.spool);
775+
const lookupAsFileMock = vi.spyOn(JobFSProvider.instance as any, "_lookupAsFile").mockImplementation(() => {
776+
throw fileNotFoundError;
777+
});
778+
const createEntriesFromUriMock = vi
779+
.spyOn(JobFSProvider.instance as any, "_createEntriesFromUri")
780+
.mockRejectedValueOnce(new Error("Failed to create entries"));
781+
782+
await expect(JobFSProvider.instance.readFile(testUris.spool)).rejects.toThrow("Failed to create entries");
783+
784+
lookupAsFileMock.mockRestore();
785+
createEntriesFromUriMock.mockRestore();
786+
});
787+
788+
it("throws non-FileNotFound errors immediately without creating entries", async () => {
789+
const customError = new Error("Custom error that is not FileNotFound");
790+
const lookupAsFileMock = vi.spyOn(JobFSProvider.instance as any, "_lookupAsFile").mockImplementation(() => {
791+
throw customError;
792+
});
793+
const createEntriesFromUriMock = vi.spyOn(JobFSProvider.instance as any, "_createEntriesFromUri");
794+
795+
await expect(JobFSProvider.instance.readFile(testUris.spool)).rejects.toThrow("Custom error that is not FileNotFound");
796+
expect(createEntriesFromUriMock).not.toHaveBeenCalled();
797+
798+
lookupAsFileMock.mockRestore();
799+
createEntriesFromUriMock.mockRestore();
800+
});
801+
});
802+
803+
describe("_createEntriesFromUri", () => {
804+
const mockJob = createIJobObject();
805+
const mockSpoolFile = createIJobFile();
806+
// buildUniqueSpoolName will create: TESTJOB.JOB1234.STEP.STDOUT.101
807+
const testJobUri = Uri.from({ scheme: ZoweScheme.Jobs, path: "/sestest/JOB1234/TESTJOB.JOB1234.STEP.STDOUT.101" });
808+
809+
beforeEach(() => {
810+
vi.clearAllMocks();
811+
});
812+
813+
it("throws FileNotFound error when URI has insufficient path parts", async () => {
814+
const invalidUri = Uri.from({ scheme: ZoweScheme.Jobs, path: "/sestest/JOB1234" });
815+
const getInfoFromUriMock = vi.spyOn(JobFSProvider.instance as any, "_getInfoFromUri").mockReturnValue({
816+
profile: testProfile,
817+
path: "/JOB1234",
818+
});
819+
820+
await expect((JobFSProvider.instance as any)._createEntriesFromUri(invalidUri)).rejects.toThrow();
821+
822+
getInfoFromUriMock.mockRestore();
823+
});
824+
825+
it("creates profile directory when it doesn't exist", async () => {
826+
const getInfoFromUriMock = vi.spyOn(JobFSProvider.instance as any, "_getInfoFromUri").mockReturnValue({
827+
profile: testProfile,
828+
path: "/JOB1234/TESTJOB.JOB1234.STEP.STDOUT.101",
829+
});
830+
const existsMock = vi.spyOn(JobFSProvider.instance, "exists").mockReturnValue(false);
831+
const createDirectoryMock = vi.spyOn(JobFSProvider.instance, "createDirectory").mockImplementation(() => undefined);
832+
const jobEntryWithSpools = {
833+
...testEntries.job,
834+
entries: new Map(),
835+
};
836+
const lookupAsDirMock = vi.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValue(jobEntryWithSpools);
837+
838+
const mockJesApi = {
839+
getJobsByParameters: vi.fn().mockResolvedValue([mockJob]),
840+
getSpoolFiles: vi.fn().mockResolvedValue([mockSpoolFile]),
841+
};
842+
const jesApiMock = vi.spyOn(ZoweExplorerApiRegister, "getInstance").mockReturnValue({
843+
getJesApi: vi.fn().mockReturnValue(mockJesApi),
844+
registeredJesApiTypes: vi.fn().mockReturnValue(["zosmf"]),
845+
} as any);
846+
const ensureAuthNotCancelledMock = vi.spyOn(AuthUtils, "ensureAuthNotCancelled").mockResolvedValue(undefined);
847+
const waitForUnlockMock = vi.spyOn(AuthHandler, "waitForUnlock").mockResolvedValue(undefined);
848+
849+
// Mock writeFile to add the spool entry to the job's entries map
850+
const writeFileMock = vi.spyOn(JobFSProvider.instance, "writeFile").mockImplementation((uri, content, options: any) => {
851+
if (options?.name) {
852+
jobEntryWithSpools.entries.set(options.name, testEntries.spool);
853+
}
854+
});
855+
856+
await (JobFSProvider.instance as any)._createEntriesFromUri(testJobUri);
857+
858+
expect(createDirectoryMock).toHaveBeenCalledWith(expect.objectContaining({ path: "/sestest" }), expect.objectContaining({ isFilter: true }));
859+
860+
getInfoFromUriMock.mockRestore();
861+
existsMock.mockRestore();
862+
createDirectoryMock.mockRestore();
863+
lookupAsDirMock.mockRestore();
864+
jesApiMock.mockRestore();
865+
ensureAuthNotCancelledMock.mockRestore();
866+
waitForUnlockMock.mockRestore();
867+
writeFileMock.mockRestore();
868+
});
869+
870+
it("creates job directory and fetches job information when job doesn't exist", async () => {
871+
const getInfoFromUriMock = vi.spyOn(JobFSProvider.instance as any, "_getInfoFromUri").mockReturnValue({
872+
profile: testProfile,
873+
path: "/JOB1234/TESTJOB.JOB1234.STEP.STDOUT.101",
874+
});
875+
const existsMock = vi
876+
.spyOn(JobFSProvider.instance, "exists")
877+
.mockReturnValueOnce(true) // profile exists
878+
.mockReturnValueOnce(false); // job doesn't exist
879+
const createDirectoryMock = vi.spyOn(JobFSProvider.instance, "createDirectory").mockImplementation(() => undefined);
880+
const jobEntryWithSpools = {
881+
...testEntries.job,
882+
entries: new Map(),
883+
};
884+
const lookupAsDirMock = vi.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValue(jobEntryWithSpools);
885+
886+
const mockJesApi = {
887+
getJobsByParameters: vi.fn().mockResolvedValue([mockJob]),
888+
getSpoolFiles: vi.fn().mockResolvedValue([mockSpoolFile]),
889+
};
890+
const jesApiMock = vi.spyOn(ZoweExplorerApiRegister, "getInstance").mockReturnValue({
891+
getJesApi: vi.fn().mockReturnValue(mockJesApi),
892+
registeredJesApiTypes: vi.fn().mockReturnValue(["zosmf"]),
893+
} as any);
894+
const ensureAuthNotCancelledMock = vi.spyOn(AuthUtils, "ensureAuthNotCancelled").mockResolvedValue(undefined);
895+
const waitForUnlockMock = vi.spyOn(AuthHandler, "waitForUnlock").mockResolvedValue(undefined);
896+
897+
// Mock writeFile to add the spool entry to the job's entries map
898+
const writeFileMock = vi.spyOn(JobFSProvider.instance, "writeFile").mockImplementation((uri, content, options: any) => {
899+
if (options?.name) {
900+
jobEntryWithSpools.entries.set(options.name, testEntries.spool);
901+
}
902+
});
903+
904+
await (JobFSProvider.instance as any)._createEntriesFromUri(testJobUri);
905+
906+
expect(mockJesApi.getJobsByParameters).toHaveBeenCalledWith({ jobid: "JOB1234" });
907+
expect(createDirectoryMock).toHaveBeenCalledWith(
908+
expect.objectContaining({ path: "/sestest/JOB1234" }),
909+
expect.objectContaining({ job: mockJob })
910+
);
911+
expect(ensureAuthNotCancelledMock).toHaveBeenCalledWith(testProfile);
912+
expect(waitForUnlockMock).toHaveBeenCalledWith(testProfile);
913+
914+
getInfoFromUriMock.mockRestore();
915+
existsMock.mockRestore();
916+
createDirectoryMock.mockRestore();
917+
lookupAsDirMock.mockRestore();
918+
jesApiMock.mockRestore();
919+
ensureAuthNotCancelledMock.mockRestore();
920+
waitForUnlockMock.mockRestore();
921+
writeFileMock.mockRestore();
922+
});
923+
924+
it("throws FileNotFound error when job is not found on mainframe", async () => {
925+
const getInfoFromUriMock = vi.spyOn(JobFSProvider.instance as any, "_getInfoFromUri").mockReturnValue({
926+
profile: testProfile,
927+
path: "/JOB1234/TESTJOB.JOB1234.STEP.STDOUT.101",
928+
});
929+
const existsMock = vi
930+
.spyOn(JobFSProvider.instance, "exists")
931+
.mockReturnValueOnce(true) // profile exists
932+
.mockReturnValueOnce(false); // job doesn't exist
933+
934+
const mockJesApi = {
935+
getJobsByParameters: vi.fn().mockResolvedValue([]), // No jobs found
936+
};
937+
const jesApiMock = vi.spyOn(ZoweExplorerApiRegister, "getInstance").mockReturnValue({
938+
getJesApi: vi.fn().mockReturnValue(mockJesApi),
939+
registeredJesApiTypes: vi.fn().mockReturnValue(["zosmf"]),
940+
} as any);
941+
const ensureAuthNotCancelledMock = vi.spyOn(AuthUtils, "ensureAuthNotCancelled").mockResolvedValue(undefined);
942+
const waitForUnlockMock = vi.spyOn(AuthHandler, "waitForUnlock").mockResolvedValue(undefined);
943+
944+
await expect((JobFSProvider.instance as any)._createEntriesFromUri(testJobUri)).rejects.toThrow();
945+
946+
getInfoFromUriMock.mockRestore();
947+
existsMock.mockRestore();
948+
jesApiMock.mockRestore();
949+
ensureAuthNotCancelledMock.mockRestore();
950+
waitForUnlockMock.mockRestore();
951+
});
952+
953+
it("fetches spool files when job entry has no entries", async () => {
954+
const getInfoFromUriMock = vi.spyOn(JobFSProvider.instance as any, "_getInfoFromUri").mockReturnValue({
955+
profile: testProfile,
956+
path: "/JOB1234/TESTJOB.JOB1234.STEP.STDOUT.101",
957+
});
958+
const existsMock = vi.spyOn(JobFSProvider.instance, "exists").mockReturnValue(true);
959+
const jobEntryWithNoSpools = {
960+
...testEntries.job,
961+
entries: new Map(),
962+
};
963+
const lookupAsDirMock = vi.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValue(jobEntryWithNoSpools);
964+
965+
const mockJesApi = {
966+
getSpoolFiles: vi.fn().mockResolvedValue([mockSpoolFile]),
967+
};
968+
const jesApiMock = vi.spyOn(ZoweExplorerApiRegister, "getInstance").mockReturnValue({
969+
getJesApi: vi.fn().mockReturnValue(mockJesApi),
970+
registeredJesApiTypes: vi.fn().mockReturnValue(["zosmf"]),
971+
} as any);
972+
973+
// Mock writeFile to add the spool entry to the job's entries map with the correct name
974+
const writeFileMock = vi.spyOn(JobFSProvider.instance, "writeFile").mockImplementation((uri, content, options: any) => {
975+
if (options?.name) {
976+
jobEntryWithNoSpools.entries.set(options.name, testEntries.spool);
977+
}
978+
});
979+
980+
await (JobFSProvider.instance as any)._createEntriesFromUri(testJobUri);
981+
982+
expect(mockJesApi.getSpoolFiles).toHaveBeenCalledWith(mockJob.jobname, mockJob.jobid);
983+
expect(writeFileMock).toHaveBeenCalled();
984+
985+
getInfoFromUriMock.mockRestore();
986+
existsMock.mockRestore();
987+
lookupAsDirMock.mockRestore();
988+
jesApiMock.mockRestore();
989+
writeFileMock.mockRestore();
990+
});
991+
992+
it("skips fetching spool files when job entry already has entries", async () => {
993+
const getInfoFromUriMock = vi.spyOn(JobFSProvider.instance as any, "_getInfoFromUri").mockReturnValue({
994+
profile: testProfile,
995+
path: "/JOB1234/TESTJOB.JOB1234.STEP.STDOUT.101",
996+
});
997+
const existsMock = vi.spyOn(JobFSProvider.instance, "exists").mockReturnValue(true);
998+
const jobEntryWithSpools = {
999+
...testEntries.job,
1000+
entries: new Map([["TESTJOB.JOB1234.STEP.STDOUT.101", testEntries.spool]]),
1001+
};
1002+
const lookupAsDirMock = vi.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValue(jobEntryWithSpools);
1003+
1004+
const mockJesApi = {
1005+
getSpoolFiles: vi.fn().mockResolvedValue([mockSpoolFile]),
1006+
};
1007+
const jesApiMock = vi.spyOn(ZoweExplorerApiRegister, "getInstance").mockReturnValue({
1008+
getJesApi: vi.fn().mockReturnValue(mockJesApi),
1009+
registeredJesApiTypes: vi.fn().mockReturnValue(["zosmf"]),
1010+
} as any);
1011+
1012+
await (JobFSProvider.instance as any)._createEntriesFromUri(testJobUri);
1013+
1014+
expect(mockJesApi.getSpoolFiles).not.toHaveBeenCalled();
1015+
1016+
getInfoFromUriMock.mockRestore();
1017+
existsMock.mockRestore();
1018+
lookupAsDirMock.mockRestore();
1019+
jesApiMock.mockRestore();
1020+
});
1021+
1022+
it("throws FileNotFound error when spool file is not found in job entries", async () => {
1023+
const getInfoFromUriMock = vi.spyOn(JobFSProvider.instance as any, "_getInfoFromUri").mockReturnValue({
1024+
profile: testProfile,
1025+
path: "/JOB1234/NONEXISTENT.SPOOL.FILE",
1026+
});
1027+
const existsMock = vi.spyOn(JobFSProvider.instance, "exists").mockReturnValue(true);
1028+
const jobEntryWithDifferentSpool = {
1029+
...testEntries.job,
1030+
entries: new Map([["DIFFERENT.SPOOL.NAME", testEntries.spool]]),
1031+
};
1032+
const lookupAsDirMock = vi.spyOn(JobFSProvider.instance as any, "_lookupAsDirectory").mockReturnValue(jobEntryWithDifferentSpool);
1033+
1034+
await expect((JobFSProvider.instance as any)._createEntriesFromUri(testJobUri)).rejects.toThrow();
1035+
1036+
getInfoFromUriMock.mockRestore();
1037+
existsMock.mockRestore();
1038+
lookupAsDirMock.mockRestore();
1039+
});
7211040
});
7221041

7231042
describe("writeFile", () => {

0 commit comments

Comments
 (0)