Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions services/agents-login/src/worker/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,14 @@ export class LoginSession {
this.proc = this.deps.spawner(cmd.file, cmd.args, {
cwd: this.deps.paths.home,
env,
// Wide enough that the ~350-char authorize URL is emitted on one line;
// the default 80 cols wraps it and breaks URL extraction.
cols: 400,
// Wide enough that the authorize URL is emitted on ONE line. If the URL
// wraps, the `&state=...` tail lands on the next line and the parser's
// [^\s]+ stops at the break — the captured URL then lacks `state` and
// claude.ai rejects it ("Missing state parameter"). The Claude subscription
// authorize URL is ~450 chars (client_id + redirect_uri + the full scope
// set + code_challenge + state); 400 cols was too narrow and dropped state.
// 2000 leaves ample headroom.
cols: 2000,
rows: 50,
})
this.proc.onData((chunk) => this.onData(chunk, updatedBy))
Expand Down
9 changes: 6 additions & 3 deletions services/agents-login/test/helpers/fakePty.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PtyProcess, PtySpawner } from '../../src/worker/pty.js'
import type { PtyProcess, PtySpawner, PtySpawnOptions } from '../../src/worker/pty.js'

/**
* Fake PTY whose script is a list of programmed actions: emit output, or wait
Expand Down Expand Up @@ -69,13 +69,16 @@ export class FakePty implements PtyProcess {
export function fakeSpawner(scriptFor: (file: string, args: string[]) => Action[]): {
spawner: PtySpawner
instances: FakePty[]
spawns: Array<{ file: string; args: string[]; options: PtySpawnOptions }>
} {
const instances: FakePty[] = []
const spawner: PtySpawner = (file, args) => {
const spawns: Array<{ file: string; args: string[]; options: PtySpawnOptions }> = []
const spawner: PtySpawner = (file, args, options) => {
spawns.push({ file, args, options })
const pty = new FakePty(scriptFor(file, args))
instances.push(pty)
pty.run()
return pty
}
return { spawner, instances }
return { spawner, instances, spawns }
}
16 changes: 15 additions & 1 deletion services/agents-login/test/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ describe('LoginSession state machine', () => {
): {
deps: SessionDeps
instances: ReturnType<typeof fakeSpawner>['instances']
spawns: ReturnType<typeof fakeSpawner>['spawns']
} {
const { spawner, instances } = fakeSpawner(scriptFor)
const { spawner, instances, spawns } = fakeSpawner(scriptFor)
return {
deps: {
spawner,
Expand All @@ -157,9 +158,22 @@ describe('LoginSession state machine', () => {
ttlMs: 60_000,
},
instances,
spawns,
}
}

it('spawns the Claude login PTY wide enough that the authorize URL + state never wraps', () => {
// The subscription authorize URL is ~450 chars (scopes + code_challenge +
// state). A narrow PTY wraps it and the `&state=...` tail is dropped, so
// claude.ai rejects the URL with "Missing state parameter". Guard the width.
const { deps: d, spawns } = deps(() => [{ type: 'emit', data: '' }])
const mgr = new SessionManager(d)
mgr.start('claude', 'alice')
const claudeSpawn = spawns.find((s) => s.file === 'claude')
expect(claudeSpawn).toBeDefined()
expect(claudeSpawn!.options.cols ?? 0).toBeGreaterThanOrEqual(1000)
})

it('seeds Claude subscription login settings and drives bounded onboarding Enter-through', async () => {
vi.useFakeTimers()
try {
Expand Down