Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
710fbeaaeThis file is pinned to the selected release. Download the original to open it in your design tool.
import { describe, expect, it } from 'vitest'
import { flashCommand, scriptUrl, type FlashDeps } from './flash.js'
/**
* `harness flash` owns three things — which script, how to get it, and handing the user's flags over
* untouched. Everything that decides whether a board survives the write lives in the shell script and
* is deliberately NOT duplicated here, so these assertions stop at the wrapper.
*
* Nothing in this file may touch hardware. The one thing that cannot be proven here — that a real
* board ends up running the firmware — is a manual gate, not a test.
*/
const SCRIPT = '#!/usr/bin/env bash\necho flasher\n'
function harness(over: Partial<FlashDeps> & { cache?: string | null } = {}) {
const calls: { args: string[][]; written: string[]; logs: string[]; warns: string[] } =
{ args: [], written: [], logs: [], warns: [] }
let cache = over.cache ?? null
const deps: Partial<FlashDeps> = {
fetchScript: over.fetchScript ?? (async () => SCRIPT),
readCache: over.readCache ?? (() => cache),
writeCache: over.writeCache ?? ((body) => { cache = body; calls.written.push(body) }),
run: over.run ?? (async (_p, args) => { calls.args.push(args); return 0 }),
log: (l) => calls.logs.push(l),
warn: (l) => calls.warns.push(l),
}
return { deps, calls }
}
describe('harness flash — getting the script', () => {
it('prefers the network copy and caches it', async () => {
const { deps, calls } = harness()
expect(await flashCommand([], deps)).toBe(0)
expect(calls.written).toEqual([SCRIPT])
// Says which copy it ran. A flasher that silently ran something other than the newest is exactly
// what the server's `no-store` header exists to prevent.
expect(calls.logs.join('\n')).toContain(scriptUrl())
expect(calls.warns).toEqual([])
})
/** A bench with no connection is a real situation, so the cache is a fallback — but a LOUD one. */
it('falls back to the cache when the network fails, and warns', async () => {
const { deps, calls } = harness({
fetchScript: async () => { throw new Error('getaddrinfo ENOTFOUND') },
cache: SCRIPT,
})
expect(await flashCommand([], deps)).toBe(0)
const warned = calls.warns.join('\n')
expect(warned).toContain('CACHED')
expect(warned).toContain('may be out of date')
expect(calls.args).toHaveLength(1) // it still ran
})
it('refuses, with the manual commands, when there is no network and no cache', async () => {
const { deps } = harness({
fetchScript: async () => { throw new Error('offline') },
cache: null,
})
await expect(flashCommand([], deps)).rejects.toThrow(/no cached copy/)
await expect(flashCommand([], deps)).rejects.toThrow(/curl -fsSL/)
})
/**
* A captive portal or an error page answers 200 with HTML. Writing that to the cache would poison
* the fallback for every later run, so the shebang check happens before anything is stored — this
* asserts the caller sees a failure rather than a "successful" flash of nothing.
*/
it('treats a non-script response as a failure', async () => {
const { deps, calls } = harness({
fetchScript: async () => { throw new Error('response is not a shell script (proxy or error page?)') },
cache: null,
})
await expect(flashCommand([], deps)).rejects.toThrow(/not a shell script/)
expect(calls.written).toEqual([])
})
})
describe('harness flash — handing over the flags', () => {
/**
* The wrapper does not parse the flasher's flags, so a flag it has never heard of has to reach the
* script unchanged. This is what keeps the two from drifting when the script gains an option.
*/
it('passes every argument through verbatim, including unknown ones', async () => {
const { deps, calls } = harness()
const args = ['--detect-only', '--port', '/dev/cu.usbmodem21301', '--totally-new-flag', 'x y']
await flashCommand(args, deps)
// Untouched, and still in order — a flag the script grows next month works the day it ships,
// without this file learning it exists. `--yes` leads; see the next test for why.
expect(calls.args[0]).toEqual(['--yes', ...args])
})
it('never asks the person who typed `harness flash` to confirm', async () => {
// The script's prompt is for the standalone `curl | bash` case, where the operator may not know what
// is plugged in. Reaching a board through the product's own command, at the product's own device, is
// a different situation — and the check that actually prevents damage is the script refusing to write
// to anything that is not an ESP32-S3. Passing --yes first also leaves the user's own flags able to
// override anything they collide with.
const { deps, calls } = harness()
await flashCommand([], deps)
expect(calls.args[0]).toEqual(['--yes'])
})
/** `harness flash --detect-only` has to be usable in a shell condition, so the code is not swallowed. */
it("returns the script's own exit code", async () => {
const { deps } = harness({ run: async () => 3 })
expect(await flashCommand(['--detect-only'], deps)).toBe(3)
})
it('explains itself when bash is missing rather than failing bare', async () => {
const { deps } = harness({
run: async () => { throw Object.assign(new Error('spawn bash ENOENT'), { code: 'ENOENT' }) },
})
await expect(flashCommand([], deps)).rejects.toThrow(/bash was not found/)
})
})
describe('harness flash — where it points', () => {
it('builds the URL from WEB_URL, so a local web app is used in dev', () => {
// WEB_URL already falls back to loopback when BACKEND_WS_URL is local (config/env.ts), which is
// why this command needs no environment variable of its own.
expect(scriptUrl()).toMatch(/^https?:\/\/.+\/flash-circle\.sh$/)
})
})