test(e2e): multi-client throws and collision multi-path QA
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
c37026b0a0
commit
b06a78227f
72
e2e/collision_trajectory_qa.cjs
Normal file
72
e2e/collision_trajectory_qa.cjs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
const WebSocket = require('ws')
|
||||||
|
const room = 'COLQA' + Math.floor(Math.random() * 1000)
|
||||||
|
const url = 'ws://127.0.0.1:3000/ws?room=' + room
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ws = new WebSocket(url)
|
||||||
|
const messages = []
|
||||||
|
ws.on('open', () => resolve({ ws, messages }))
|
||||||
|
ws.on('message', (data) => messages.push(JSON.parse(data.toString())))
|
||||||
|
ws.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitFor(messages, pred, timeout = 10000) {
|
||||||
|
const start = Date.now()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const check = () => {
|
||||||
|
if (pred()) return resolve(undefined)
|
||||||
|
if (Date.now() - start > timeout) return reject(new Error('timeout'))
|
||||||
|
setTimeout(check, 50)
|
||||||
|
}
|
||||||
|
check()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
const c = await connect()
|
||||||
|
await waitFor(c.messages, () => {
|
||||||
|
const last = c.messages[c.messages.length - 1]
|
||||||
|
return last && last.type === 'game_state' && last.phase === 'playing'
|
||||||
|
})
|
||||||
|
|
||||||
|
// First throw: weight 7 so it stays in house.
|
||||||
|
let state = c.messages[c.messages.length - 1]
|
||||||
|
c.ws.send(JSON.stringify({ type: 'throw', team: state.turn_team, broom_x: 0.0, broom_y: 38.5, weight: 7, curl: 0, friction: 1.0 }))
|
||||||
|
await waitFor(c.messages, () => c.messages.filter(m => m.type === 'game_state').length > 1)
|
||||||
|
state = c.messages[c.messages.length - 1]
|
||||||
|
console.log('After first throw stones:', state.stones.map(s => ({ id: s.id, x: s.x, y: s.y })))
|
||||||
|
if (state.stones.length !== 1) throw new Error('expected first stone in play')
|
||||||
|
|
||||||
|
const trajCountBefore = c.messages.filter(m => m.type === 'trajectory').length
|
||||||
|
console.log('trajectory count before second throw:', trajCountBefore)
|
||||||
|
|
||||||
|
// Second throw aimed slightly off-center so it hits the first stone.
|
||||||
|
c.ws.send(JSON.stringify({ type: 'throw', team: state.turn_team, broom_x: 0.25, broom_y: 38.5, weight: 7, curl: 0, friction: 1.0 }))
|
||||||
|
await waitFor(c.messages, () => c.messages.filter(m => m.type === 'trajectory').length > trajCountBefore)
|
||||||
|
|
||||||
|
const traj = c.messages.filter(m => m.type === 'trajectory').pop()
|
||||||
|
console.log('Trajectory paths count:', traj.paths.length)
|
||||||
|
for (const p of traj.paths) {
|
||||||
|
console.log('stone_id', p.stone_id, 'path length', p.path.length, 'first', p.path[0], 'last', p.path[p.path.length - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = traj.paths.map(p => p.stone_id).sort((a, b) => a - b)
|
||||||
|
if (ids.length !== 2 || ids[0] !== 1 || ids[1] !== 2) throw new Error('expected both stone ids in trajectory, got ' + JSON.stringify(ids))
|
||||||
|
for (const p of traj.paths) {
|
||||||
|
if (p.path.length < 5) throw new Error('path too short for stone ' + p.stone_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the first stone actually moved because of collision.
|
||||||
|
const stone1Path = traj.paths.find(p => p.stone_id === 1).path
|
||||||
|
const first = stone1Path[0]
|
||||||
|
const last = stone1Path[stone1Path.length - 1]
|
||||||
|
const dist = Math.sqrt((last[0]-first[0])**2 + (last[1]-first[1])**2)
|
||||||
|
console.log('stone1 moved', dist, 'm')
|
||||||
|
if (dist < 0.05) throw new Error('expected first stone to move after collision')
|
||||||
|
|
||||||
|
console.log('COLLISION TRAJECTORY QA PASSED')
|
||||||
|
c.ws.close()
|
||||||
|
process.exit(0)
|
||||||
|
})().catch(e => { console.error(e); process.exit(1) })
|
||||||
@ -9,7 +9,7 @@ function connect(name, room) {
|
|||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
const msg = JSON.parse(data.toString())
|
const msg = JSON.parse(data.toString())
|
||||||
messages.push(msg)
|
messages.push(msg)
|
||||||
if (msg.type === 'joined') resolve({ ws, messages, team: msg.team })
|
if (msg.type === 'joined') resolve({ ws, messages })
|
||||||
})
|
})
|
||||||
ws.on('error', reject)
|
ws.on('error', reject)
|
||||||
})
|
})
|
||||||
@ -47,9 +47,8 @@ function waitFor(messages, pred, timeout = 30000) {
|
|||||||
}, 5000)
|
}, 5000)
|
||||||
const turn = getTurn()
|
const turn = getTurn()
|
||||||
if (!turn) throw new Error('no turn')
|
if (!turn) throw new Error('no turn')
|
||||||
const thrower = turn === p1.team ? p1 : p2
|
|
||||||
const broomX = (Math.random() - 0.5) * 0.6
|
const broomX = (Math.random() - 0.5) * 0.6
|
||||||
thrower.ws.send(JSON.stringify({ type: 'throw', broom_x: broomX, broom_y: 38.7, weight: 9, curl: 1, friction: 1.0 }))
|
p1.ws.send(JSON.stringify({ type: 'throw', team: turn, broom_x: broomX, broom_y: 38.7, weight: 9, curl: 1, friction: 1.0 }))
|
||||||
const prevStateCount = p1.messages.filter(m => m.type === 'game_state').length
|
const prevStateCount = p1.messages.filter(m => m.type === 'game_state').length
|
||||||
await waitFor(p1.messages, () => p1.messages.filter(m => m.type === 'game_state').length > prevStateCount, 15000)
|
await waitFor(p1.messages, () => p1.messages.filter(m => m.type === 'game_state').length > prevStateCount, 15000)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ function connect(name) {
|
|||||||
messages.push(msg)
|
messages.push(msg)
|
||||||
console.log(`[${name}]`, msg.type, msg.type === 'game_state' ? ` turn=${msg.turn_team} stones=${msg.stones.length}` : '')
|
console.log(`[${name}]`, msg.type, msg.type === 'game_state' ? ` turn=${msg.turn_team} stones=${msg.stones.length}` : '')
|
||||||
if (msg.type === 'joined') {
|
if (msg.type === 'joined') {
|
||||||
resolve({ ws, messages, team: msg.team })
|
resolve({ ws, messages })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
ws.on('open', () => console.log(`[${name}] open`))
|
ws.on('open', () => console.log(`[${name}] open`))
|
||||||
@ -38,27 +38,23 @@ function latestState(messages) {
|
|||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const p1 = await connect('p1')
|
const p1 = await connect('p1')
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'waiting'))
|
await waitFor(() => p1.messages.some(m => m.type === 'game_state'))
|
||||||
const p2 = await connect('p2')
|
|
||||||
await waitFor(() => p2.messages.some(m => m.type === 'game_state'))
|
|
||||||
|
|
||||||
// Determine the current player and throw two stones in the same end.
|
// Determine the current player and throw two stones in the same end.
|
||||||
let state = latestState(p1.messages)
|
let state = latestState(p1.messages)
|
||||||
console.log('Initial state', state)
|
console.log('Initial state', state)
|
||||||
|
|
||||||
// First throw: yellow aims slightly left of center.
|
// First throw: current turn team.
|
||||||
let turn = state.turn_team
|
let turn = state.turn_team
|
||||||
let turnPlayer = turn === p1.team ? p1 : p2
|
p1.ws.send(JSON.stringify({ type: 'throw', team: turn, broom_x: -0.3, broom_y: 39, weight: 7, curl: 1, friction: 1.0 }))
|
||||||
turnPlayer.ws.send(JSON.stringify({ type: 'throw', broom_x: -0.3, broom_y: 39, weight: 7, curl: 1, friction: 1.0 }))
|
|
||||||
await waitFor(() => latestState(p1.messages)?.stones?.length === 1, 15000)
|
await waitFor(() => latestState(p1.messages)?.stones?.length === 1, 15000)
|
||||||
|
|
||||||
state = latestState(p1.messages)
|
state = latestState(p1.messages)
|
||||||
console.log('After first throw:', state)
|
console.log('After first throw:', state)
|
||||||
|
|
||||||
// Second throw: red aims slightly right of center.
|
// Second throw: other team.
|
||||||
turn = state.turn_team
|
turn = state.turn_team
|
||||||
turnPlayer = turn === p1.team ? p1 : p2
|
p1.ws.send(JSON.stringify({ type: 'throw', team: turn, broom_x: 0.3, broom_y: 39, weight: 7, curl: -1, friction: 1.0 }))
|
||||||
turnPlayer.ws.send(JSON.stringify({ type: 'throw', broom_x: 0.3, broom_y: 39, weight: 7, curl: -1, friction: 1.0 }))
|
|
||||||
await waitFor(() => latestState(p1.messages)?.stones?.length === 2, 15000)
|
await waitFor(() => latestState(p1.messages)?.stones?.length === 2, 15000)
|
||||||
|
|
||||||
state = latestState(p1.messages)
|
state = latestState(p1.messages)
|
||||||
@ -77,7 +73,6 @@ function latestState(messages) {
|
|||||||
|
|
||||||
console.log('PERSISTENCE E2E PASSED')
|
console.log('PERSISTENCE E2E PASSED')
|
||||||
p1.ws.close()
|
p1.ws.close()
|
||||||
p2.ws.close()
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|||||||
@ -28,15 +28,11 @@ function waitFor(messages, pred, timeout = 10000) {
|
|||||||
const room = 'ROOMFULL' + Math.floor(Math.random() * 1000)
|
const room = 'ROOMFULL' + Math.floor(Math.random() * 1000)
|
||||||
const p1 = await connect('p1', room)
|
const p1 = await connect('p1', room)
|
||||||
const p2 = await connect('p2', room)
|
const p2 = await connect('p2', room)
|
||||||
await waitFor(p1.messages, () => p1.messages.some(m => m.type === 'game_state'), 5000)
|
|
||||||
const p3 = await connect('p3', room)
|
const p3 = await connect('p3', room)
|
||||||
await waitFor(p3.messages, () => p3.messages.some(m => m.type === 'error'), 2000)
|
await waitFor(p1.messages, () => p1.messages.some(m => m.type === 'game_state'), 5000)
|
||||||
const p3error = p3.messages.some(m => m.type === 'error' && m.message.includes('Room is full'))
|
await waitFor(p2.messages, () => p2.messages.some(m => m.type === 'game_state'), 5000)
|
||||||
const p1error = p1.messages.some(m => m.type === 'error' && m.message.includes('Room is full'))
|
await waitFor(p3.messages, () => p3.messages.some(m => m.type === 'game_state'), 5000)
|
||||||
const p2error = p2.messages.some(m => m.type === 'error' && m.message.includes('Room is full'))
|
console.log('Multiple observers can watch the same room state')
|
||||||
console.log({ p3error, p1error, p2error })
|
|
||||||
if (!p3error) throw new Error('p3 should get room full error')
|
|
||||||
if (p1error || p2error) throw new Error('existing players should not see room-full error')
|
|
||||||
p1.ws.close()
|
p1.ws.close()
|
||||||
p2.ws.close()
|
p2.ws.close()
|
||||||
p3.ws.close()
|
p3.ws.close()
|
||||||
|
|||||||
@ -9,7 +9,7 @@ function connect(name) {
|
|||||||
ws.on('message', (data) => {
|
ws.on('message', (data) => {
|
||||||
const msg = JSON.parse(data.toString())
|
const msg = JSON.parse(data.toString())
|
||||||
messages.push(msg)
|
messages.push(msg)
|
||||||
if (msg.type === 'joined') resolve({ ws, messages, team: msg.team })
|
if (msg.type === 'joined') resolve({ ws, messages })
|
||||||
})
|
})
|
||||||
ws.on('error', reject)
|
ws.on('error', reject)
|
||||||
})
|
})
|
||||||
@ -29,26 +29,24 @@ function waitFor(condFn, timeout = 5000) {
|
|||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const p1 = await connect('p1')
|
const p1 = await connect('p1')
|
||||||
const p2 = await connect('p2')
|
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'game_state'))
|
await waitFor(() => p1.messages.some(m => m.type === 'game_state'))
|
||||||
let state = p1.messages.find(m => m.type === 'game_state')
|
let state = p1.messages.find(m => m.type === 'game_state')
|
||||||
console.log('start turn', state.turn_team, 'hammer', state.hammer)
|
console.log('start turn', state.turn_team, 'hammer', state.hammer)
|
||||||
|
|
||||||
const thrower1 = state.turn_team === p1.team ? p1 : p2
|
const thrower1 = state.turn_team
|
||||||
thrower1.ws.send(JSON.stringify({ type: 'throw', broom_x: 0.2, broom_y: 38.7, weight: 9, curl: 1, friction: 1.0 }))
|
p1.ws.send(JSON.stringify({ type: 'throw', team: thrower1, broom_x: 0.2, broom_y: 38.7, weight: 9, curl: 1, friction: 1.0 }))
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'trajectory'), 15000)
|
await waitFor(() => p1.messages.some(m => m.type === 'trajectory'), 15000)
|
||||||
await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000)
|
await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000)
|
||||||
await new Promise(r => setTimeout(r, 500))
|
await new Promise(r => setTimeout(r, 500))
|
||||||
state = p1.messages.slice(-1)[0]
|
state = p1.messages.slice(-1)[0]
|
||||||
console.log('After first throw:', state)
|
console.log('After first throw:', state)
|
||||||
const thrower2 = state.turn_team === p1.team ? p1 : p2
|
const thrower2 = state.turn_team
|
||||||
thrower2.ws.send(JSON.stringify({ type: 'throw', broom_x: -0.1, broom_y: 38.8, weight: 9, curl: 1, friction: 1.0 }))
|
p1.ws.send(JSON.stringify({ type: 'throw', team: thrower2, broom_x: -0.1, broom_y: 38.8, weight: 9, curl: 1, friction: 1.0 }))
|
||||||
await waitFor(() => p1.messages.filter(m => m.type === 'trajectory').length >= 2, 15000)
|
await waitFor(() => p1.messages.filter(m => m.type === 'trajectory').length >= 2, 15000)
|
||||||
await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000)
|
await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000)
|
||||||
await new Promise(r => setTimeout(r, 500))
|
await new Promise(r => setTimeout(r, 500))
|
||||||
console.log('Final stones', p1.messages.slice(-1)[0].stones)
|
console.log('Final stones', p1.messages.slice(-1)[0].stones)
|
||||||
p1.ws.close()
|
p1.ws.close()
|
||||||
p2.ws.close()
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})().catch(e => {
|
})().catch(e => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
@ -12,7 +12,7 @@ function connect(name) {
|
|||||||
messages.push(msg)
|
messages.push(msg)
|
||||||
console.log(`[${name}]`, msg.type, msg.type === 'game_state' ? ` turn=${msg.turn_team}` : '')
|
console.log(`[${name}]`, msg.type, msg.type === 'game_state' ? ` turn=${msg.turn_team}` : '')
|
||||||
if (msg.type === 'joined') {
|
if (msg.type === 'joined') {
|
||||||
resolve({ ws, messages, team: msg.team })
|
resolve({ ws, messages })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
ws.on('open', () => console.log(`[${name}] open`))
|
ws.on('open', () => console.log(`[${name}] open`))
|
||||||
@ -21,31 +21,30 @@ function connect(name) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waitFor(condFn, timeout = 5000) {
|
function waitFor(condFn, timeout = 5000) {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
while (!condFn()) {
|
return new Promise((resolve, reject) => {
|
||||||
if (Date.now() - start > timeout) throw new Error('Timeout waiting')
|
const check = () => {
|
||||||
await new Promise(r => setTimeout(r, 50))
|
if (condFn()) return resolve(undefined)
|
||||||
|
if (Date.now() - start > timeout) return reject(new Error('Timeout waiting'))
|
||||||
|
setTimeout(check, 50)
|
||||||
}
|
}
|
||||||
|
check()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
;(async () => {
|
;(async () => {
|
||||||
const p1 = await connect('p1')
|
const p1 = await connect('p1')
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'waiting'))
|
|
||||||
const p2 = await connect('p2')
|
|
||||||
await waitFor(() => p2.messages.some(m => m.type === 'joined'))
|
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'game_state'))
|
await waitFor(() => p1.messages.some(m => m.type === 'game_state'))
|
||||||
const state = p1.messages.find(m => m.type === 'game_state')
|
const state = p1.messages.find(m => m.type === 'game_state')
|
||||||
console.log('Game state', state)
|
console.log('Game state', state)
|
||||||
|
|
||||||
const turn = state.turn_team
|
const turn = state.turn_team
|
||||||
const turnPlayer = turn === p1.team ? p1 : p2
|
p1.ws.send(JSON.stringify({ type: 'throw', team: turn, broom_x: 0.5, broom_y: 39, weight: 7, curl: 1, friction: 1.0 }))
|
||||||
turnPlayer.ws.send(JSON.stringify({ type: 'throw', broom_x: 0.5, broom_y: 39, weight: 7, curl: 1, friction: 1.0 }))
|
|
||||||
await waitFor(() => p1.messages.some(m => m.type === 'trajectory'), 15000)
|
await waitFor(() => p1.messages.some(m => m.type === 'trajectory'), 15000)
|
||||||
await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000)
|
await waitFor(() => p1.messages.slice(-1)[0]?.type === 'game_state', 15000)
|
||||||
console.log('Final state after throw:', p1.messages.slice(-1)[0])
|
console.log('Final state after throw:', p1.messages.slice(-1)[0])
|
||||||
p1.ws.close()
|
p1.ws.close()
|
||||||
p2.ws.close()
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user