Unified · Split
| @@ -424,6 +424,81 @@ it.layer(NodeServices.layer)("Antigravity installation", (it) => { · 423 unmodified lines | ||
| 424 | 424 | }), |
| 425 | 425 | ); |
| 426 | 426 | |
| 427 | + it.effect.each([ | |
| 428 | + { | |
| 429 | + name: "IPv6 diagnostic", | |
| 430 | + stderr: [ | |
| 431 | + "Check failed: AddressFamilySupported(AF_INET6, &loopback6_ok)\n", | |
| 432 | + "Failed to create an AF_INET6 socket.\n", | |
| 433 | + ], | |
| 434 | + expected: "Failed to create an AF_INET6 socket.", | |
| 435 | + }, | |
| 436 | + { | |
| 437 | + name: "bounded diagnostic tail", | |
| 438 | + stderr: [`discarded prefix ${"x".repeat(8_000)}\n`, "last startup diagnostic\n"], | |
| 439 | + expected: "last startup diagnostic", | |
| 440 | + }, | |
| 441 | + { | |
| 442 | + name: "no diagnostic", | |
| 443 | + stderr: [], | |
| 444 | + expected: "The downloaded Antigravity runtime could not start in this environment.", | |
| 445 | + }, | |
| 446 | + ])("preserves startup diagnostics when validation exits before initialize: $name", (testCase) => | |
| 447 | + Effect.gen(function* () { | |
| 448 | + const encoder = new TextEncoder(); | |
| 449 | + const spawner = ChildProcessSpawner.make( | |
| 450 | + Effect.fn("test.spawnFailingAntigravityValidator")(function* (command) { | |
| 451 | + if (command._tag !== "StandardCommand") { | |
| 452 | + return yield* Effect.die("Expected one validation process."); | |
| 453 | + } | |
| 454 | + const helper = command.args[0] === "-e"; | |
| 455 | + const exited = yield* Deferred.make<ChildProcessSpawner.ExitCode>(); | |
| 456 | + const terminate = Deferred.succeed(exited, ChildProcessSpawner.ExitCode(1)).pipe( | |
| 457 | + Effect.asVoid, | |
| 458 | + ); | |
| 459 | + yield* Effect.addFinalizer(() => terminate); | |
| 460 | + return ChildProcessSpawner.makeHandle({ | |
| 461 | + pid: ChildProcessSpawner.ProcessId(helper ? 1 : 2), | |
| 462 | + exitCode: helper | |
| 463 | + ? Effect.succeed(ChildProcessSpawner.ExitCode(0)) | |
| 464 | + : Deferred.await(exited), | |
| 465 | + isRunning: Deferred.isDone(exited).pipe(Effect.map((done) => !done)), | |
| 466 | + kill: () => terminate, | |
| 467 | + unref: Effect.succeed(Effect.void), | |
| 468 | + stdin: Sink.drain, | |
| 469 | + stdout: helper | |
| 470 | + ? Stream.empty | |
| 471 | + : Stream.fromEffect(Deferred.await(exited)).pipe(Stream.flatMap(() => Stream.empty)), | |
| 472 | + stderr: helper | |
| 473 | + ? Stream.make( | |
| 474 | + encoder.encode( | |
| 475 | + `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeJsonString(command.args.at(-1))}\n`, | |
| 476 | + ), | |
| 477 | + ) | |
| 478 | + : Stream.fromIterable(testCase.stderr.map((line) => encoder.encode(line))).pipe( | |
| 479 | + Stream.ensuring(terminate), | |
| 480 | + ), | |
| 481 | + all: Stream.empty, | |
| 482 | + getInputFd: () => Sink.drain, | |
| 483 | + getOutputFd: () => Stream.empty, | |
| 484 | + }); | |
| 485 | + }), | |
| 486 | + ); | |
| 487 | + const { installation, stagingReleased } = yield* makeHarness({ | |
| 488 | + previous: true, | |
| 489 | + useDefaultValidation: true, | |
| 490 | + }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner)); | |
| 491 | + yield* installation.start; | |
| 492 | + const state = yield* terminalState(installation); | |
| 493 | + expect(state.phase).toBe("failed"); | |
| 494 | + expect(state.message).toContain(testCase.expected); | |
| 495 | + expect(state.message?.length).toBeLessThan(4_200); | |
| 496 | + expect(state.message).not.toContain("discarded prefix"); | |
| 497 | + yield* Deferred.await(stagingReleased); | |
| 498 | + yield* expectPreviousRelease(installation); | |
| 499 | + }), | |
| 500 | + ); | |
| 501 | + | |
| 427 | 502 | it.effect("accepts an encoded Content-Length when the body is compressed", () => |
| 428 | 503 | Effect.gen(function* () { |
| 429 | 504 | // dl.google.com gzips the archive and reports the encoded size. The decoded |
| @@ -43,6 +43,7 @@ import { · 42 unmodified lines | ||
| 43 | 43 | const DRIVER = ProviderDriverKind.make("antigravity"); |
| 44 | 44 | const DOWNLOAD_TIMEOUT = "45 minutes"; |
| 45 | 45 | const VALIDATION_TIMEOUT = "90 seconds"; |
| 46 | +const VALIDATION_DIAGNOSTIC_MAX_CHARS = 4_096; | |
| 46 | 47 | const FREE_SPACE_MARGIN = 256 * 1024 * 1024; |
| 47 | 48 | const RECORD_MAX_BYTES = 8 * 1024; |
| 48 | 49 | const RELEASE_RECORD = ".install-complete.json"; |
| @@ -476,7 +477,8 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma · 427 unmodified lines | ||
| 476 | 477 | platform, |
| 477 | 478 | baseEnv: environment, |
| 478 | 479 | }); |
| 479 | − const runtime = yield* makeAntigravityAcpRuntime({ | |
| 480 | + let diagnosticTail = ""; | |
| 481 | + const initialized = yield* makeAntigravityAcpRuntime({ | |
| 480 | 482 | spawn: buildAntigravityAcpSpawnInput({ |
| 481 | 483 | installation: executable, |
| 482 | 484 | profile, |
| @@ -486,8 +488,27 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma · 3 unmodified lines | ||
| 486 | 488 | cwd: profileDirectory, |
| 487 | 489 | childProcessSpawner: spawner, |
| 488 | 490 | clientInfo: { name: "t3-code", version: "0.0.0" }, |
| 489 | − }); | |
| 490 | − const initialized = yield* runtime.initialize(); | |
| 491 | + onDiagnostic: (message) => | |
| 492 | + Effect.sync(() => { | |
| 493 | + diagnosticTail = `${diagnosticTail}${message}\n`.slice( | |
| 494 | + -VALIDATION_DIAGNOSTIC_MAX_CHARS, | |
| 495 | + ); | |
| 496 | + }), | |
| 497 | + }).pipe( | |
| 498 | + Effect.flatMap((runtime) => runtime.initialize()), | |
| 499 | + Effect.mapError((cause) => | |
| 500 | + installationError( | |
| 501 | + "verify", | |
| 502 | + [ | |
| 503 | + "The downloaded Antigravity runtime could not start in this environment.", | |
| 504 | + diagnosticTail.trim(), | |
| 505 | + ] | |
| 506 | + .filter(Boolean) | |
| 507 | + .join("\n\n"), | |
| 508 | + cause, | |
| 509 | + ), | |
| 510 | + ), | |
| 511 | + ); | |
| 491 | 512 | if ( |
| 492 | 513 | initialized.agentInfo?.name !== "antigravity-acp" || |
| 493 | 514 | initialized.agentInfo.version !== expectedVersion || |
| @@ -37,6 +37,8 @@ export interface AntigravityAcpRuntimeInput extends Omit< · 36 unmodified lines | ||
| 37 | 37 | > { |
| 38 | 38 | readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; |
| 39 | 39 | readonly onAuthorizationUrl?: (url: string) => Effect.Effect<void, EffectAcpErrors.AcpError>; |
| 40 | + /** Receives native stderr lines with authorization messages excluded. */ | |
| 41 | + readonly onDiagnostic?: (message: string) => Effect.Effect<void>; | |
| 40 | 42 | /** |
| 41 | 43 | * Advertise `fs.readTextFile` and `fs.writeTextFile`. The agent then routes |
| 42 | 44 | * workspace reads and writes through T3, which turns each edit into a |
| @@ -73,9 +75,10 @@ export const makeAntigravityAcpRuntime = Effect.fn("makeAntigravityAcpRuntime")( · 30 unmodified lines | ||
| 73 | 75 | transformStdout: makeAntigravityStdoutTransform( |
| 74 | 76 | input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}, |
| 75 | 77 | ), |
| 76 | − onStderr: makeAntigravityStderrHandler( | |
| 77 | − input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}, | |
| 78 | − ), | |
| 78 | + onStderr: makeAntigravityStderrHandler({ | |
| 79 | + ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}), | |
| 80 | + ...(input.onDiagnostic ? { onDiagnostic: input.onDiagnostic } : {}), | |
| 81 | + }), | |
| 79 | 82 | transformSessionUpdate: normalizeAntigravitySessionUpdate, |
| 80 | 83 | }).pipe( |
| 81 | 84 | Layer.provide( |
| @@ -391,6 +391,26 @@ describe("Antigravity stdout compatibility", () => { · 390 unmodified lines | ||
| 391 | 391 | }); |
| 392 | 392 | |
| 393 | 393 | describe("Antigravity stderr compatibility", () => { |
| 394 | + it.effect("keeps sign-in URLs out of collected native diagnostics", () => | |
| 395 | + Effect.gen(function* () { | |
| 396 | + const diagnostics: string[] = []; | |
| 397 | + const handleStderr = makeAntigravityStderrHandler({ | |
| 398 | + onAuthorizationUrl: () => Effect.void, | |
| 399 | + onDiagnostic: (message) => Effect.sync(() => void diagnostics.push(message)), | |
| 400 | + }); | |
| 401 | + yield* handleStderr("Failed to create an AF_"); | |
| 402 | + yield* handleStderr("INET6 socket.\r\n"); | |
| 403 | + yield* handleStderr(`${ANTIGRAVITY_AUTH_STDOUT_PREFIX}${authorizationUrl}\n`); | |
| 404 | + yield* handleStderr( | |
| 405 | + `${ANTIGRAVITY_AUTH_BROWSER_MARKER}${encodeUnknownJson(authorizationUrl)}\n`, | |
| 406 | + ); | |
| 407 | + yield* handleStderr(` ${ANTIGRAVITY_AUTH_STDOUT_PREFIX}${authorizationUrl}\n`); | |
| 408 | + yield* handleStderr(` ${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`); | |
| 409 | + yield* handleStderr(`${ANTIGRAVITY_AUTH_BROWSER_MARKER}${authorizationUrl}\n`); | |
| 410 | + expect(diagnostics).toEqual(["Failed to create an AF_INET6 socket."]); | |
| 411 | + }), | |
| 412 | + ); | |
| 413 | + | |
| 394 | 414 | it.effect("forwards fragmented native sign-in URLs from runtime 1.1.1", () => |
| 395 | 415 | Effect.gen(function* () { |
| 396 | 416 | const urls: string[] = []; |
| @@ -460,12 +460,13 @@ export function makeAntigravityStdoutTransform( · 459 unmodified lines | ||
| 460 | 460 | }); |
| 461 | 461 | } |
| 462 | 462 | |
| 463 | −/** Receives native 1.1.1 sign-in URLs and T3 browser-helper URLs without logging stderr. */ | |
| 463 | +/** Routes sign-in URLs separately from optional native diagnostic lines. */ | |
| 464 | 464 | export function makeAntigravityStderrHandler( |
| 465 | 465 | input: { |
| 466 | 466 | readonly onAuthorizationUrl?: ( |
| 467 | 467 | authorizationUrl: string, |
| 468 | 468 | ) => Effect.Effect<void, AcpErrors.AcpError>; |
| 469 | + readonly onDiagnostic?: (message: string) => Effect.Effect<void>; | |
| 469 | 470 | } = {}, |
| 470 | 471 | ) { |
| 471 | 472 | let pending = ""; |
| @@ -479,7 +480,17 @@ export function makeAntigravityStderrHandler( · 7 unmodified lines | ||
| 479 | 480 | : message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER) |
| 480 | 481 | ? decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length)) |
| 481 | 482 | : undefined; |
| 482 | − if (url === undefined) return Effect.void; | |
| 483 | + if (url === undefined) { | |
| 484 | + // Malformed or indented auth messages must not enter installation diagnostics. | |
| 485 | + const trimmed = message.trimStart(); | |
| 486 | + if ( | |
| 487 | + trimmed.startsWith(ANTIGRAVITY_AUTH_STDOUT_PREFIX) || | |
| 488 | + trimmed.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER) | |
| 489 | + ) { | |
| 490 | + return Effect.void; | |
| 491 | + } | |
| 492 | + return input.onDiagnostic?.(message) ?? Effect.void; | |
| 493 | + } | |
| 483 | 494 | return url.pipe( |
| 484 | 495 | Effect.flatMap(parseAntigravityAuthorizationUrl), |
| 485 | 496 | Effect.matchEffect({ |