| author | |
| committer | |
| log | a56b0cd7178dfef299aa49b26773a80a34abe205 |
| tree | 3fbef95adeaebc644a38fa388ecba66432676bc6 |
| parent | 4116db9807cb37b49d3db35c7a45301f5e851b01 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Simone <185146821+Lucenx9@users.noreply.github.com>4 files changed, 72 insertions(+), 1 deletions(-)
apps/server/src/pullRequest/AzureDevOpsPullRequestCli.test.ts+40| ... | @@ -97,6 +97,46 @@ layer("AzureDevOpsPullRequestCli.layer", (it) => { | ... | @@ -97,6 +97,46 @@ layer("AzureDevOpsPullRequestCli.layer", (it) => { |
| 97 | }), | 97 | }), |
| 98 | ); | 98 | ); |
| 99 | 99 | ||
| 100 | it.effect("reads an Azure pull request page larger than the VCS default output limit", () => | ||
| 101 | Effect.gen(function* () { | ||
| 102 | const rows = pullRequestRows(100, 1).map((row) => ({ | ||
| 103 | ...row, | ||
| 104 | description: "x".repeat(10_000), | ||
| 105 | })); | ||
| 106 | // @effect-diagnostics-next-line preferSchemaOverJson:off | ||
| 107 | const response = JSON.stringify(rows); | ||
| 108 | expect(Buffer.byteLength(response)).toBeGreaterThan(1_000_000); | ||
| 109 | |||
| 110 | mockedExecute.mockImplementationOnce((input) => { | ||
| 111 | const maxOutputBytes = | ||
| 112 | "maxOutputBytes" in input && typeof input.maxOutputBytes === "number" | ||
| 113 | ? input.maxOutputBytes | ||
| 114 | : 1_000_000; | ||
| 115 | return Effect.succeed( | ||
| 116 | maxOutputBytes >= Buffer.byteLength(response) | ||
| 117 | ? output(response) | ||
| 118 | : { | ||
| 119 | ...output(response.slice(0, maxOutputBytes)), | ||
| 120 | stdoutTruncated: true, | ||
| 121 | }, | ||
| 122 | ); | ||
| 123 | }); | ||
| 124 | const cli = yield* AzureDevOpsPullRequestCli.AzureDevOpsPullRequestCli; | ||
| 125 | |||
| 126 | const batch = yield* cli.listPullRequests({ | ||
| 127 | cwd: "/w", | ||
| 128 | repository: "web", | ||
| 129 | state: "merged", | ||
| 130 | involvement: "all", | ||
| 131 | viewer: "bilal@acme.dev", | ||
| 132 | limit: 99, | ||
| 133 | }); | ||
| 134 | |||
| 135 | assert.strictEqual(batch.items.length, 99); | ||
| 136 | assert.isTrue(batch.truncated); | ||
| 137 | }), | ||
| 138 | ); | ||
| 139 | |||
| 100 | it.effect("reads the page unnarrowed when asked to search, having nothing to search with", () => | 140 | it.effect("reads the page unnarrowed when asked to search, having nothing to search with", () => |
| 101 | Effect.gen(function* () { | 141 | Effect.gen(function* () { |
| 102 | mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(3, 1)))); | 142 | mockedExecute.mockReturnValueOnce(Effect.succeed(output(pullRequests(3, 1)))); |
apps/server/src/pullRequest/AzureDevOpsPullRequestCli.ts+8-1| ... | @@ -111,6 +111,7 @@ export type AzureDevOpsPullRequestCliError = | ... | @@ -111,6 +111,7 @@ export type AzureDevOpsPullRequestCliError = |
| 111 | 111 | ||
| 112 | /** The version every REST call below is pinned to, so a new default cannot reshape a response. */ | 112 | /** The version every REST call below is pinned to, so a new default cannot reshape a response. */ |
| 113 | const REST_API_VERSION = "7.1"; | 113 | const REST_API_VERSION = "7.1"; |
| 114 | const PULL_REQUEST_LIST_MAX_OUTPUT_BYTES = 16 * 1024 * 1024; | ||
| 114 | 115 | ||
| 115 | export class AzureDevOpsPullRequestCli extends Context.Service< | 116 | export class AzureDevOpsPullRequestCli extends Context.Service< |
| 116 | AzureDevOpsPullRequestCli, | 117 | AzureDevOpsPullRequestCli, |
| ... | @@ -258,10 +259,15 @@ export const make = Effect.gen(function* () { | ... | @@ -258,10 +259,15 @@ export const make = Effect.gen(function* () { |
| 258 | // how to read all of them. | 259 | // how to read all of them. |
| 259 | const detectArgs = ["--detect", "true"] as const; | 260 | const detectArgs = ["--detect", "true"] as const; |
| 260 | 261 | ||
| 261 | const executeJson = (input: { readonly cwd: string; readonly args: ReadonlyArray<string> }) => | 262 | const executeJson = (input: { |
| 263 | readonly cwd: string; | ||
| 264 | readonly args: ReadonlyArray<string>; | ||
| 265 | readonly maxOutputBytes?: number; | ||
| 266 | }) => | ||
| 262 | azure.execute({ | 267 | azure.execute({ |
| 263 | cwd: input.cwd, | 268 | cwd: input.cwd, |
| 264 | args: [...input.args, "--only-show-errors", "--output", "json"], | 269 | args: [...input.args, "--only-show-errors", "--output", "json"], |
| 270 | ...(input.maxOutputBytes === undefined ? {} : { maxOutputBytes: input.maxOutputBytes }), | ||
| 265 | }); | 271 | }); |
| 266 | 272 | ||
| 267 | /** | 273 | /** |
| ... | @@ -290,6 +296,7 @@ export const make = Effect.gen(function* () { | ... | @@ -290,6 +296,7 @@ export const make = Effect.gen(function* () { |
| 290 | const top = remaining + 1; | 296 | const top = remaining + 1; |
| 291 | return executeJson({ | 297 | return executeJson({ |
| 292 | cwd: input.cwd, | 298 | cwd: input.cwd, |
| 299 | maxOutputBytes: PULL_REQUEST_LIST_MAX_OUTPUT_BYTES, | ||
| 293 | args: [ | 300 | args: [ |
| 294 | "repos", | 301 | "repos", |
| 295 | "pr", | 302 | "pr", |
apps/server/src/sourceControl/AzureDevOpsCli.test.ts+22| ... | @@ -332,6 +332,28 @@ describe("AzureDevOpsCli.layer", () => { | ... | @@ -332,6 +332,28 @@ describe("AzureDevOpsCli.layer", () => { |
| 332 | }).pipe(Effect.provide(layer)), | 332 | }).pipe(Effect.provide(layer)), |
| 333 | ); | 333 | ); |
| 334 | 334 | ||
| 335 | it.effect("forwards explicit output limits to the process boundary", () => | ||
| 336 | Effect.gen(function* () { | ||
| 337 | mockRun.mockReturnValueOnce(Effect.succeed(processOutput(""))); | ||
| 338 | |||
| 339 | const az = yield* AzureDevOpsCli.AzureDevOpsCli; | ||
| 340 | yield* az.execute({ | ||
| 341 | cwd: "/repo", | ||
| 342 | args: ["repos", "pr", "list"], | ||
| 343 | maxOutputBytes: 16 * 1024 * 1024, | ||
| 344 | }); | ||
| 345 | |||
| 346 | expect(mockRun).toHaveBeenCalledWith({ | ||
| 347 | operation: "AzureDevOpsCli.execute", | ||
| 348 | command: "az", | ||
| 349 | args: ["repos", "pr", "list"], | ||
| 350 | cwd: "/repo", | ||
| 351 | timeoutMs: 30_000, | ||
| 352 | maxOutputBytes: 16 * 1024 * 1024, | ||
| 353 | }); | ||
| 354 | }).pipe(Effect.provide(layer)), | ||
| 355 | ); | ||
| 356 | |||
| 335 | it.effect("preserves VCS causes without copying upstream details into messages", () => | 357 | it.effect("preserves VCS causes without copying upstream details into messages", () => |
| 336 | Effect.gen(function* () { | 358 | Effect.gen(function* () { |
| 337 | const cause = new VcsProcessExitError({ | 359 | const cause = new VcsProcessExitError({ |
apps/server/src/sourceControl/AzureDevOpsCli.ts+2| ... | @@ -218,6 +218,7 @@ export class AzureDevOpsCli extends Context.Service< | ... | @@ -218,6 +218,7 @@ export class AzureDevOpsCli extends Context.Service< |
| 218 | readonly cwd: string; | 218 | readonly cwd: string; |
| 219 | readonly args: ReadonlyArray<string>; | 219 | readonly args: ReadonlyArray<string>; |
| 220 | readonly timeoutMs?: number; | 220 | readonly timeoutMs?: number; |
| 221 | readonly maxOutputBytes?: number; | ||
| 221 | }) => Effect.Effect<VcsProcess.VcsProcessOutput, AzureDevOpsCliError>; | 222 | }) => Effect.Effect<VcsProcess.VcsProcessOutput, AzureDevOpsCliError>; |
| 222 | 223 | ||
| 223 | readonly listPullRequests: (input: { | 224 | readonly listPullRequests: (input: { |
| ... | @@ -362,6 +363,7 @@ export const make = Effect.gen(function* () { | ... | @@ -362,6 +363,7 @@ export const make = Effect.gen(function* () { |
| 362 | args: input.args, | 363 | args: input.args, |
| 363 | cwd: input.cwd, | 364 | cwd: input.cwd, |
| 364 | timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS, | 365 | timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS, |
| 366 | ...(input.maxOutputBytes === undefined ? {} : { maxOutputBytes: input.maxOutputBytes }), | ||
| 365 | }) | 367 | }) |
| 366 | .pipe( | 368 | .pipe( |
| 367 | Effect.mapError((error) => | 369 | Effect.mapError((error) => |