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 | it.effect("accepts an encoded Content-Length when the body is compressed", () => | 502 | it.effect("accepts an encoded Content-Length when the body is compressed", () => |
| 428 | Effect.gen(function* () { | 503 | Effect.gen(function* () { |
| 429 | // dl.google.com gzips the archive and reports the encoded size. The decoded | 504 | // dl.google.com gzips the archive and reports the encoded size. The decoded |
| @@ -43,6 +43,7 @@ import { · 42 unmodified lines | |||
| 43 | const DRIVER = ProviderDriverKind.make("antigravity"); | 43 | const DRIVER = ProviderDriverKind.make("antigravity"); |
| 44 | const DOWNLOAD_TIMEOUT = "45 minutes"; | 44 | const DOWNLOAD_TIMEOUT = "45 minutes"; |
| 45 | const VALIDATION_TIMEOUT = "90 seconds"; | 45 | const VALIDATION_TIMEOUT = "90 seconds"; |
| 46 | const VALIDATION_DIAGNOSTIC_MAX_CHARS = 4_096; | ||
| 46 | const FREE_SPACE_MARGIN = 256 * 1024 * 1024; | 47 | const FREE_SPACE_MARGIN = 256 * 1024 * 1024; |
| 47 | const RECORD_MAX_BYTES = 8 * 1024; | 48 | const RECORD_MAX_BYTES = 8 * 1024; |
| 48 | const RELEASE_RECORD = ".install-complete.json"; | 49 | const RELEASE_RECORD = ".install-complete.json"; |
| @@ -476,7 +477,8 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma · 427 unmodified lines | |||
| 476 | platform, | 477 | platform, |
| 477 | baseEnv: environment, | 478 | baseEnv: environment, |
| 478 | }); | 479 | }); |
| 479 | const runtime = yield* makeAntigravityAcpRuntime({ | 480 | let diagnosticTail = ""; |
| 481 | const initialized = yield* makeAntigravityAcpRuntime({ | ||
| 480 | spawn: buildAntigravityAcpSpawnInput({ | 482 | spawn: buildAntigravityAcpSpawnInput({ |
| 481 | installation: executable, | 483 | installation: executable, |
| 482 | profile, | 484 | profile, |
| @@ -486,8 +488,27 @@ export const makeAntigravityInstallation = Effect.fn("AntigravityInstallation.ma · 3 unmodified lines | |||
| 486 | cwd: profileDirectory, | 488 | cwd: profileDirectory, |
| 487 | childProcessSpawner: spawner, | 489 | childProcessSpawner: spawner, |
| 488 | clientInfo: { name: "t3-code", version: "0.0.0" }, | 490 | clientInfo: { name: "t3-code", version: "0.0.0" }, |
| 489 | }); | 491 | onDiagnostic: (message) => |
| 490 | const initialized = yield* runtime.initialize(); | 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 | if ( | 512 | if ( |
| 492 | initialized.agentInfo?.name !== "antigravity-acp" || | 513 | initialized.agentInfo?.name !== "antigravity-acp" || |
| 493 | initialized.agentInfo.version !== expectedVersion || | 514 | initialized.agentInfo.version !== expectedVersion || |
| @@ -37,6 +37,8 @@ export interface AntigravityAcpRuntimeInput extends Omit< · 36 unmodified lines | |||
| 37 | > { | 37 | > { |
| 38 | readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; | 38 | readonly childProcessSpawner: ChildProcessSpawner.ChildProcessSpawner["Service"]; |
| 39 | readonly onAuthorizationUrl?: (url: string) => Effect.Effect<void, EffectAcpErrors.AcpError>; | 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 | * Advertise `fs.readTextFile` and `fs.writeTextFile`. The agent then routes | 43 | * Advertise `fs.readTextFile` and `fs.writeTextFile`. The agent then routes |
| 42 | * workspace reads and writes through T3, which turns each edit into a | 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 | transformStdout: makeAntigravityStdoutTransform( | 75 | transformStdout: makeAntigravityStdoutTransform( |
| 74 | input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}, | 76 | input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}, |
| 75 | ), | 77 | ), |
| 76 | onStderr: makeAntigravityStderrHandler( | 78 | onStderr: makeAntigravityStderrHandler({ |
| 77 | input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}, | 79 | ...(input.onAuthorizationUrl ? { onAuthorizationUrl: input.onAuthorizationUrl } : {}), |
| 78 | ), | 80 | ...(input.onDiagnostic ? { onDiagnostic: input.onDiagnostic } : {}), |
| 81 | }), | ||
| 79 | transformSessionUpdate: normalizeAntigravitySessionUpdate, | 82 | transformSessionUpdate: normalizeAntigravitySessionUpdate, |
| 80 | }).pipe( | 83 | }).pipe( |
| 81 | Layer.provide( | 84 | Layer.provide( |
| @@ -391,6 +391,26 @@ describe("Antigravity stdout compatibility", () => { · 390 unmodified lines | |||
| 391 | }); | 391 | }); |
| 392 | 392 | ||
| 393 | describe("Antigravity stderr compatibility", () => { | 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 | it.effect("forwards fragmented native sign-in URLs from runtime 1.1.1", () => | 414 | it.effect("forwards fragmented native sign-in URLs from runtime 1.1.1", () => |
| 395 | Effect.gen(function* () { | 415 | Effect.gen(function* () { |
| 396 | const urls: string[] = []; | 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 | export function makeAntigravityStderrHandler( | 464 | export function makeAntigravityStderrHandler( |
| 465 | input: { | 465 | input: { |
| 466 | readonly onAuthorizationUrl?: ( | 466 | readonly onAuthorizationUrl?: ( |
| 467 | authorizationUrl: string, | 467 | authorizationUrl: string, |
| 468 | ) => Effect.Effect<void, AcpErrors.AcpError>; | 468 | ) => Effect.Effect<void, AcpErrors.AcpError>; |
| 469 | readonly onDiagnostic?: (message: string) => Effect.Effect<void>; | ||
| 469 | } = {}, | 470 | } = {}, |
| 470 | ) { | 471 | ) { |
| 471 | let pending = ""; | 472 | let pending = ""; |
| @@ -479,7 +480,17 @@ export function makeAntigravityStderrHandler( · 7 unmodified lines | |||
| 479 | : message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER) | 480 | : message.startsWith(ANTIGRAVITY_AUTH_BROWSER_MARKER) |
| 480 | ? decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length)) | 481 | ? decodeBrowserHelperUrl(message.slice(ANTIGRAVITY_AUTH_BROWSER_MARKER.length)) |
| 481 | : undefined; | 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 | return url.pipe( | 494 | return url.pipe( |
| 484 | Effect.flatMap(parseAntigravityAuthorizationUrl), | 495 | Effect.flatMap(parseAntigravityAuthorizationUrl), |
| 485 | Effect.matchEffect({ | 496 | Effect.matchEffect({ |