| ... | @@ -1007,6 +1007,279 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { | ... | @@ -1007,6 +1007,279 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { |
| 1007 | assert.notInclude(error.message, secret); | 1007 | assert.notInclude(error.message, secret); |
| 1008 | assert.notProperty(error, "args"); | 1008 | assert.notProperty(error, "args"); |
| 1009 | assert.notProperty(error, "stderr"); | 1009 | assert.notProperty(error, "stderr"); |
| | 1010 | assert.notProperty(error, "reason"); |
| | 1011 | }), |
| | 1012 | ); |
| | 1013 | |
| | 1014 | it.effect("names the branch conflict behind a failed worktree add", () => |
| | 1015 | Effect.gen(function* () { |
| | 1016 | const parent = yield* makeTmpDir(); |
| | 1017 | const pathService = yield* Path.Path; |
| | 1018 | const cwd = pathService.join(parent, "repo"); |
| | 1019 | const fileSystem = yield* FileSystem.FileSystem; |
| | 1020 | yield* fileSystem.makeDirectory(cwd); |
| | 1021 | const driver = yield* GitVcsDriver.GitVcsDriver; |
| | 1022 | yield* initRepoWithCommit(cwd); |
| | 1023 | const firstWorktree = pathService.join(parent, "first"); |
| | 1024 | yield* git(cwd, ["worktree", "add", firstWorktree, "-b", "shared-branch"]); |
| | 1025 | |
| | 1026 | const error = yield* driver |
| | 1027 | .execute({ |
| | 1028 | operation: "GitVcsDriver.test.worktreeAddConflict", |
| | 1029 | cwd, |
| | 1030 | args: ["worktree", "add", pathService.join(parent, "second"), "shared-branch"], |
| | 1031 | env: { LC_ALL: "C" }, |
| | 1032 | }) |
| | 1033 | .pipe(Effect.flip); |
| | 1034 | |
| | 1035 | assert.deepInclude(error, { |
| | 1036 | _tag: "GitCommandError", |
| | 1037 | operation: "GitVcsDriver.test.worktreeAddConflict", |
| | 1038 | reason: "branch_checked_out_in_worktree", |
| | 1039 | }); |
| | 1040 | assert.include(error.message, "(branch_checked_out_in_worktree)"); |
| | 1041 | assert.notInclude(error.message, firstWorktree); |
| | 1042 | }), |
| | 1043 | ); |
| | 1044 | |
| | 1045 | it.effect("names a missing repository behind a failed command", () => |
| | 1046 | Effect.gen(function* () { |
| | 1047 | const cwd = yield* makeTmpDir(); |
| | 1048 | const driver = yield* GitVcsDriver.GitVcsDriver; |
| | 1049 | |
| | 1050 | const error = yield* driver |
| | 1051 | .execute({ |
| | 1052 | operation: "GitVcsDriver.test.notARepository", |
| | 1053 | cwd, |
| | 1054 | args: ["rev-parse", "--abbrev-ref", "HEAD"], |
| | 1055 | env: { LC_ALL: "C" }, |
| | 1056 | }) |
| | 1057 | .pipe(Effect.flip); |
| | 1058 | |
| | 1059 | assert.deepInclude(error, { |
| | 1060 | _tag: "GitCommandError", |
| | 1061 | operation: "GitVcsDriver.test.notARepository", |
| | 1062 | reason: "not_a_repository", |
| | 1063 | }); |
| | 1064 | assert.include(error.message, "(not_a_repository)"); |
| | 1065 | }), |
| | 1066 | ); |
| | 1067 | |
| | 1068 | it.effect("appends the reason tag to a caller-supplied detail", () => |
| | 1069 | Effect.gen(function* () { |
| | 1070 | const parent = yield* makeTmpDir(); |
| | 1071 | const pathService = yield* Path.Path; |
| | 1072 | const cwd = pathService.join(parent, "repo"); |
| | 1073 | const fileSystem = yield* FileSystem.FileSystem; |
| | 1074 | yield* fileSystem.makeDirectory(cwd); |
| | 1075 | const driver = yield* GitVcsDriver.GitVcsDriver; |
| | 1076 | yield* initRepoWithCommit(cwd); |
| | 1077 | yield* git(cwd, [ |
| | 1078 | "worktree", |
| | 1079 | "add", |
| | 1080 | pathService.join(parent, "taken"), |
| | 1081 | "-b", |
| | 1082 | "taken-branch", |
| | 1083 | ]); |
| | 1084 | |
| | 1085 | const error = yield* driver |
| | 1086 | .createWorktree({ |
| | 1087 | cwd, |
| | 1088 | refName: "taken-branch", |
| | 1089 | path: pathService.join(parent, "second"), |
| | 1090 | }) |
| | 1091 | .pipe(Effect.flip); |
| | 1092 | |
| | 1093 | assert.equal(error.detail, "git worktree add failed"); |
| | 1094 | assert.include(error.message, "git worktree add failed (branch_checked_out_in_worktree)"); |
| | 1095 | }), |
| | 1096 | ); |
| | 1097 | |
| | 1098 | it.effect.each([ |
| | 1099 | { |
| | 1100 | label: "a refused key", |
| | 1101 | sshMessage: "git@example.invalid: Permission denied (publickey).", |
| | 1102 | expectedReason: "authentication_failed" as const, |
| | 1103 | expectedText: "(authentication_failed)", |
| | 1104 | }, |
| | 1105 | { |
| | 1106 | label: "a refused key among several methods", |
| | 1107 | sshMessage: "git@example.invalid: Permission denied (publickey,password).", |
| | 1108 | expectedReason: "authentication_failed" as const, |
| | 1109 | expectedText: "(authentication_failed)", |
| | 1110 | }, |
| | 1111 | { |
| | 1112 | label: "a refused keyboard-interactive attempt", |
| | 1113 | sshMessage: "git@example.invalid: Permission denied (keyboard-interactive).", |
| | 1114 | expectedReason: "authentication_failed" as const, |
| | 1115 | expectedText: "(authentication_failed)", |
| | 1116 | }, |
| | 1117 | { |
| | 1118 | label: "a rejected password", |
| | 1119 | sshMessage: "Permission denied, please try again.", |
| | 1120 | expectedReason: "authentication_failed" as const, |
| | 1121 | expectedText: "(authentication_failed)", |
| | 1122 | }, |
| | 1123 | { |
| | 1124 | label: "an untrusted host key", |
| | 1125 | sshMessage: "Host key verification failed.", |
| | 1126 | expectedReason: "host_key_unverified" as const, |
| | 1127 | expectedText: "(host_key_unverified)", |
| | 1128 | }, |
| | 1129 | ])( |
| | 1130 | "names $label rather than an unreachable remote", |
| | 1131 | ({ sshMessage, expectedReason, expectedText }) => |
| | 1132 | Effect.gen(function* () { |
| | 1133 | const parent = yield* makeTmpDir(); |
| | 1134 | const pathService = yield* Path.Path; |
| | 1135 | const cwd = pathService.join(parent, "repo"); |
| | 1136 | const fileSystem = yield* FileSystem.FileSystem; |
| | 1137 | const driver = yield* GitVcsDriver.GitVcsDriver; |
| | 1138 | yield* fileSystem.makeDirectory(cwd); |
| | 1139 | yield* initRepoWithCommit(cwd); |
| | 1140 | yield* git(cwd, ["remote", "add", "origin", "git@example.invalid:owner/repo.git"]); |
| | 1141 | // Stands in for ssh, which prints its refusal unprefixed and leaves |
| | 1142 | // git to add only a generic "could not read" line after it. |
| | 1143 | const fakeSsh = pathService.join(parent, "fake-ssh"); |
| | 1144 | yield* writeTextFile( |
| | 1145 | parent, |
| | 1146 | "fake-ssh", |
| | 1147 | `#!/bin/sh\necho "${sshMessage}" >&2\nexit 255\n`, |
| | 1148 | ); |
| | 1149 | yield* fileSystem.chmod(fakeSsh, 0o755); |
| | 1150 | |
| | 1151 | const error = yield* driver |
| | 1152 | .execute({ |
| | 1153 | operation: "GitVcsDriver.test.sshRefusal", |
| | 1154 | cwd, |
| | 1155 | args: ["fetch", "origin"], |
| | 1156 | env: { LC_ALL: "C", GIT_SSH_COMMAND: fakeSsh }, |
| | 1157 | }) |
| | 1158 | .pipe(Effect.flip); |
| | 1159 | |
| | 1160 | assert.deepInclude(error, { reason: expectedReason }); |
| | 1161 | assert.include(error.message, expectedText); |
| | 1162 | }), |
| | 1163 | ); |
| | 1164 | |
| | 1165 | it.effect("does not read a filesystem permission error as an ssh refusal", () => |
| | 1166 | Effect.gen(function* () { |
| | 1167 | const parent = yield* makeTmpDir(); |
| | 1168 | const pathService = yield* Path.Path; |
| | 1169 | const cwd = pathService.join(parent, "repo"); |
| | 1170 | const fileSystem = yield* FileSystem.FileSystem; |
| | 1171 | const driver = yield* GitVcsDriver.GitVcsDriver; |
| | 1172 | yield* fileSystem.makeDirectory(cwd); |
| | 1173 | yield* initRepoWithCommit(cwd); |
| | 1174 | yield* git(cwd, ["remote", "add", "origin", "git@example.invalid:owner/repo.git"]); |
| | 1175 | const fakeSsh = pathService.join(parent, "fake-ssh"); |
| | 1176 | yield* writeTextFile( |
| | 1177 | parent, |
| | 1178 | "fake-ssh", |
| | 1179 | '#!/bin/sh\necho "fatal: cannot open backup file: Permission denied (os error 13)" >&2\nexit 255\n', |
| | 1180 | ); |
| | 1181 | yield* fileSystem.chmod(fakeSsh, 0o755); |
| | 1182 | |
| | 1183 | const error = yield* driver |
| | 1184 | .execute({ |
| | 1185 | operation: "GitVcsDriver.test.osPermission", |
| | 1186 | cwd, |
| | 1187 | args: ["fetch", "origin"], |
| | 1188 | env: { LC_ALL: "C", GIT_SSH_COMMAND: fakeSsh }, |
| | 1189 | }) |
| | 1190 | .pipe(Effect.flip); |
| | 1191 | |
| | 1192 | assert.notInclude(error.message, "(authentication_failed)"); |
| | 1193 | }), |
| | 1194 | ); |
| | 1195 | |
| | 1196 | it.effect("does not read remote hook output as a git failure reason", () => |
| | 1197 | Effect.gen(function* () { |
| | 1198 | const parent = yield* makeTmpDir(); |
| | 1199 | const pathService = yield* Path.Path; |
| | 1200 | const remote = pathService.join(parent, "origin.git"); |
| | 1201 | const cwd = pathService.join(parent, "work"); |
| | 1202 | const fileSystem = yield* FileSystem.FileSystem; |
| | 1203 | const driver = yield* GitVcsDriver.GitVcsDriver; |
| | 1204 | yield* fileSystem.makeDirectory(cwd); |
| | 1205 | yield* initRepoWithCommit(cwd); |
| | 1206 | yield* git(cwd, ["init", "--bare", remote]); |
| | 1207 | yield* git(cwd, ["remote", "add", "origin", remote]); |
| | 1208 | // Git prefixes everything the server says with `remote:`, so a remote |
| | 1209 | // hook can put arbitrary text in front of the classifier. |
| | 1210 | const preReceive = pathService.join(remote, "hooks", "pre-receive"); |
| | 1211 | yield* writeTextFile( |
| | 1212 | remote, |
| | 1213 | "hooks/pre-receive", |
| | 1214 | '#!/bin/sh\necho "authentication failed" >&2\necho "Permission denied (publickey)." >&2\necho "Host key verification failed." >&2\nexit 1\n', |
| | 1215 | ); |
| | 1216 | yield* fileSystem.chmod(preReceive, 0o755); |
| | 1217 | |
| | 1218 | const error = yield* driver |
| | 1219 | .execute({ |
| | 1220 | operation: "GitVcsDriver.test.remoteHookOutput", |
| | 1221 | cwd, |
| | 1222 | args: ["push", "origin", "HEAD"], |
| | 1223 | env: { LC_ALL: "C" }, |
| | 1224 | }) |
| | 1225 | .pipe(Effect.flip); |
| | 1226 | |
| | 1227 | assert.notProperty(error, "reason"); |
| | 1228 | assert.notInclude(error.message, "(authentication_failed)"); |
| | 1229 | }), |
| | 1230 | ); |
| | 1231 | |
| | 1232 | it.effect("does not read hook output as a git failure reason", () => |
| | 1233 | Effect.gen(function* () { |
| | 1234 | const parent = yield* makeTmpDir(); |
| | 1235 | const pathService = yield* Path.Path; |
| | 1236 | const remote = pathService.join(parent, "origin.git"); |
| | 1237 | const cwd = pathService.join(parent, "work"); |
| | 1238 | const fileSystem = yield* FileSystem.FileSystem; |
| | 1239 | const driver = yield* GitVcsDriver.GitVcsDriver; |
| | 1240 | yield* fileSystem.makeDirectory(cwd); |
| | 1241 | yield* initRepoWithCommit(cwd); |
| | 1242 | yield* git(cwd, ["init", "--bare", remote]); |
| | 1243 | yield* git(cwd, ["remote", "add", "origin", remote]); |
| | 1244 | yield* writeTextFile( |
| | 1245 | cwd, |
| | 1246 | ".git/hooks/pre-push", |
| | 1247 | '#!/bin/sh\necho "authentication failed" >&2\nexit 1\n', |
| | 1248 | ); |
| | 1249 | yield* fileSystem.chmod(pathService.join(cwd, ".git/hooks/pre-push"), 0o755); |
| | 1250 | |
| | 1251 | const error = yield* driver |
| | 1252 | .execute({ |
| | 1253 | operation: "GitVcsDriver.test.hookOutput", |
| | 1254 | cwd, |
| | 1255 | args: ["push", "origin", "HEAD"], |
| | 1256 | env: { LC_ALL: "C" }, |
| | 1257 | }) |
| | 1258 | .pipe(Effect.flip); |
| | 1259 | |
| | 1260 | assert.notProperty(error, "reason"); |
| | 1261 | assert.notInclude(error.message, "(authentication_failed)"); |
| | 1262 | }), |
| | 1263 | ); |
| | 1264 | |
| | 1265 | it.effect("leaves a tag collision unclassified rather than calling it a path", () => |
| | 1266 | Effect.gen(function* () { |
| | 1267 | const cwd = yield* makeTmpDir(); |
| | 1268 | const driver = yield* GitVcsDriver.GitVcsDriver; |
| | 1269 | yield* initRepoWithCommit(cwd); |
| | 1270 | yield* git(cwd, ["tag", "v1"]); |
| | 1271 | |
| | 1272 | const error = yield* driver |
| | 1273 | .execute({ |
| | 1274 | operation: "GitVcsDriver.test.tagCollision", |
| | 1275 | cwd, |
| | 1276 | args: ["tag", "v1"], |
| | 1277 | env: { LC_ALL: "C" }, |
| | 1278 | }) |
| | 1279 | .pipe(Effect.flip); |
| | 1280 | |
| | 1281 | assert.notProperty(error, "reason"); |
| | 1282 | assert.notInclude(error.message, "(path_already_exists)"); |
| 1010 | }), | 1283 | }), |
| 1011 | ); | 1284 | ); |
| 1012 | | 1285 | |