headless.snapshot.ts 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. import { copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
  2. import { createServer } from 'node:http'
  3. import type { IncomingMessage, ServerResponse } from 'node:http'
  4. import { delimiter, dirname, join } from 'node:path'
  5. import { fileURLToPath } from 'node:url'
  6. import {
  7. normalizeSessionLog,
  8. normalizeStdout,
  9. refreshFixtureReplacements,
  10. scrubRequestHeaders,
  11. stabilizeRefreshLog,
  12. tokenizeSessionFixtureCwd,
  13. type HarvestedLog,
  14. type NormalizeContext,
  15. } from '@deepseek-ai/dsh-acp-snapshot'
  16. import { LOADER_SMOKE_TEST_TIMEOUT_MS, runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
  17. import {
  18. decompressZstdFrame,
  19. scanZstdFrames,
  20. } from '@deepseek-ai/dsh-session-persistence-jsonl/src/zstd.ts'
  21. import { describe, expect, it } from 'vitest'
  22. const snapshotsDir = join(dirname(fileURLToPath(import.meta.url)), 'snapshots')
  23. const advancedScenarioDir = join(snapshotsDir, 'advanced-toolchain')
  24. const advancedSessionFixture = join(advancedScenarioDir, 'session.jsonl')
  25. const advancedStreamExpected = join(advancedScenarioDir, 'stream-json.expected.jsonl')
  26. const advancedConfigPath = fileURLToPath(new URL('../advanced.cordis.snapshot.yml', import.meta.url))
  27. const ptyScenarioDir = join(snapshotsDir, 'pty-tools')
  28. const ptySessionFixture = join(ptyScenarioDir, 'session.jsonl')
  29. const ptyStreamExpected = join(ptyScenarioDir, 'stream-json.expected.jsonl')
  30. const ptyConfigPath = fileURLToPath(new URL('../pty.cordis.snapshot.yml', import.meta.url))
  31. const goalScenarioDir = join(snapshotsDir, 'goal-tools')
  32. const goalConfigPath = fileURLToPath(new URL('../goal.cordis.snapshot.yml', import.meta.url))
  33. const retryScenarioDir = join(snapshotsDir, 'provider-retry')
  34. const retryConfigPath = fileURLToPath(new URL('../retry.cordis.snapshot.yml', import.meta.url))
  35. const compactionScenarioDir = join(snapshotsDir, 'compaction-recovery')
  36. const compactionSessionFixture = join(compactionScenarioDir, 'session.jsonl')
  37. const compactionStreamExpected = join(compactionScenarioDir, 'stream-json.expected.jsonl')
  38. const compactionConfigPath = fileURLToPath(new URL('../compaction.cordis.snapshot.yml', import.meta.url))
  39. const credentialsScenarioDir = join(snapshotsDir, 'missing-credential')
  40. const credentialsConfigPath = fileURLToPath(new URL('../credentials.cordis.snapshot.yml', import.meta.url))
  41. // Same keyless composition as the missing-credential scenario: the endpoint is
  42. // never dialed either way, because a supplied-but-unusable key fails credential
  43. // resolution exactly where an absent one does.
  44. const invalidCredentialScenarioDir = join(snapshotsDir, 'invalid-credential')
  45. const ralphScenarioDir = join(snapshotsDir, 'ralph-loop')
  46. const ralphConfigPath = fileURLToPath(new URL('../ralph.cordis.snapshot.yml', import.meta.url))
  47. const settlementScenarioDir = join(snapshotsDir, 'subagent-settlement')
  48. const settlementConfigPath = fileURLToPath(new URL('../subagent-settlement.cordis.snapshot.yml', import.meta.url))
  49. const startupFailureConfigPath = fileURLToPath(new URL('./fixtures/startup-activation-error/cordis.yml', import.meta.url))
  50. const startupFailureExpected = join(snapshotsDir, 'startup-activation-error', 'stderr.expected.txt')
  51. const binScript = fileURLToPath(new URL('./fixtures/headless-driver.ts', import.meta.url))
  52. const dshBinScript = fileURLToPath(new URL('../../../apps/cli/src/bin.ts', import.meta.url))
  53. const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
  54. const reasoningConfigPath = fileURLToPath(new URL('./fixtures/cli.cordis.yml', import.meta.url))
  55. const deepseekDefaultsConfigPath = fileURLToPath(new URL('./fixtures/deepseek-defaults.cordis.yml', import.meta.url))
  56. const headlessOverlayPath = fileURLToPath(new URL('./fixtures/headless-profile.cordis.yml', import.meta.url))
  57. const headlessSessionExpected = join(snapshotsDir, 'headless-profile', 'session.expected.jsonl')
  58. const headlessFailureExpected = join(snapshotsDir, 'headless-profile', 'stderr.expected.txt')
  59. const cliMockLlmPluginPath = fileURLToPath(new URL('./fixtures/cli-mock-llm.ts', import.meta.url))
  60. const refreshing = process.env.DSH_SNAPSHOT === 'refresh'
  61. interface JsonObject {
  62. [key: string]: unknown
  63. }
  64. interface PersistedLog {
  65. readonly content: string
  66. readonly header: JsonObject
  67. }
  68. interface DeepSeekDefaultsServer {
  69. readonly url: string
  70. readonly requests: JsonObject[]
  71. close(): Promise<void>
  72. }
  73. /** Serve one deterministic DeepSeek-compatible response while retaining its request body. */
  74. async function deepseekDefaultsServer(): Promise<DeepSeekDefaultsServer> {
  75. const requests: JsonObject[] = []
  76. const server = createServer((request: IncomingMessage, response: ServerResponse) => {
  77. let body = ''
  78. request.setEncoding('utf8')
  79. request.on('data', (chunk: string) => { body += chunk })
  80. request.on('end', () => {
  81. requests.push(JSON.parse(body) as JsonObject)
  82. response.writeHead(200, { 'content-type': 'text/event-stream' })
  83. let keepAlives = 3
  84. const write = (): void => {
  85. if (keepAlives-- > 0) {
  86. response.write(': keep-alive\n\n')
  87. setTimeout(write, 60)
  88. return
  89. }
  90. response.end([
  91. 'data: {"choices":[{"delta":{"content":"DEFAULTS_OK"}}]}',
  92. 'data: {"choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}',
  93. 'data: [DONE]',
  94. '',
  95. ].join('\n\n'))
  96. }
  97. setTimeout(write, 60)
  98. })
  99. })
  100. await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
  101. const address = server.address()
  102. if (address === null || typeof address === 'string') throw new Error('DeepSeek defaults snapshot server has no port')
  103. return {
  104. url: `http://127.0.0.1:${address.port}`,
  105. requests,
  106. close: () => new Promise(resolve => server.close(() => { resolve() })),
  107. }
  108. }
  109. function parseJsonl(content: string): JsonObject[] {
  110. return content.split('\n')
  111. .filter(line => line.trim().length > 0)
  112. .map(line => JSON.parse(line) as JsonObject)
  113. }
  114. function contextFromLogs(contents: readonly string[]): NormalizeContext {
  115. const headers = contents.map(content => parseJsonl(content)[0])
  116. return {
  117. sessionIds: headers.flatMap(header => typeof header?.id === 'string' ? [header.id] : []),
  118. cwd: typeof headers[0]?.cwd === 'string' ? headers[0].cwd : '\0no-cwd\0',
  119. }
  120. }
  121. function normalizeHeadlessStream(rawStdout: string, cwd: string): string {
  122. const records = parseJsonl(rawStdout)
  123. if (records.length === 0) throw new Error('headless snapshot emitted no stream-json records')
  124. const final = records.at(-1)
  125. if (final?.type !== 'result') throw new Error('headless snapshot did not end with a result record')
  126. if (records.slice(0, -1).some(record => record.type !== 'session_event')) {
  127. throw new Error('headless snapshot emitted a non-event record before its result')
  128. }
  129. const sessionIds = [...new Set(records.flatMap(record => typeof record.sessionId === 'string' ? [record.sessionId] : []))]
  130. if (sessionIds.length !== 1) throw new Error(`headless snapshot streamed ${sessionIds.length} main session ids`)
  131. const context: NormalizeContext = { sessionIds, cwd }
  132. const events = records.slice(0, -1).map((record) => {
  133. if (record.event === null || typeof record.event !== 'object' || Array.isArray(record.event)) {
  134. throw new Error('headless snapshot emitted an invalid session event')
  135. }
  136. return record.event as JsonObject
  137. })
  138. const normalizedEvents = parseJsonl(scrubRequestHeaders(normalizeSessionLog(
  139. `${events.map(event => JSON.stringify(event)).join('\n')}\n`,
  140. context,
  141. )))
  142. const normalizedRecords = records.map((record, index) => index < normalizedEvents.length
  143. ? { ...record, event: normalizedEvents[index] }
  144. : record)
  145. return normalizeStdout(`${normalizedRecords.map(record => JSON.stringify(record)).join('\n')}\n`, context)
  146. }
  147. /** Zero durable goal timestamps inside both metadata records and rendered XML JSON. */
  148. function normalizeGoalTimestamps(value: unknown): unknown {
  149. if (typeof value === 'string') {
  150. return value.replace(/("(?:createdAt|updatedAt|clearedAt)":)\d+/g, '$10')
  151. }
  152. if (Array.isArray(value)) return value.map(normalizeGoalTimestamps)
  153. if (value !== null && typeof value === 'object') {
  154. return Object.fromEntries(Object.entries(value).map(([key, item]) => [
  155. key,
  156. ['createdAt', 'updatedAt', 'clearedAt'].includes(key) && typeof item === 'number'
  157. ? 0
  158. : normalizeGoalTimestamps(item),
  159. ]))
  160. }
  161. return value
  162. }
  163. /** Normalize the stream's durable goal timestamps after the shared scrubbers. */
  164. function normalizeGoalStream(rawStdout: string, cwd: string): string {
  165. return parseJsonl(normalizeHeadlessStream(rawStdout, cwd))
  166. .map(record => JSON.stringify(normalizeGoalTimestamps(record)))
  167. .join('\n') + '\n'
  168. }
  169. async function scenarioPrompt(dir: string, label: string): Promise<string> {
  170. const input = JSON.parse(await readFile(join(dir, 'input.json'), 'utf8')) as {
  171. steps?: { op?: unknown; text?: unknown }[]
  172. }
  173. const prompt = input.steps?.find(step => step.op === 'prompt')?.text
  174. if (typeof prompt !== 'string') throw new Error(`${label} input has no prompt step`)
  175. return prompt
  176. }
  177. async function readPersistedLog(file: string): Promise<string> {
  178. const content = await readFile(file)
  179. if (!file.endsWith('.zstd')) return content.toString('utf8')
  180. const scan = scanZstdFrames(content)
  181. if (scan.tornStart !== undefined) throw new Error(`persisted snapshot log has a torn Zstandard frame: ${file}`)
  182. const decoded: Buffer[] = []
  183. for (const frame of scan.frames) {
  184. decoded.push(await decompressZstdFrame(content.subarray(frame.start, frame.end)))
  185. }
  186. return Buffer.concat(decoded).toString('utf8')
  187. }
  188. async function persistedLogs(cwd: string, root: string = join(cwd, '.sessions')): Promise<PersistedLog[]> {
  189. const files = (await readdir(root, { recursive: true }))
  190. .filter(file => file.endsWith('.jsonl') || file.endsWith('.jsonl.zstd'))
  191. return Promise.all(files.map(async (file) => {
  192. const content = await readPersistedLog(join(root, file))
  193. return { content, header: parseJsonl(content)[0] ?? {} }
  194. }))
  195. }
  196. /** Install the keyless product-CLI adapter into the temporary headless profile. */
  197. async function prepareCliMockFixture(cwd: string): Promise<void> {
  198. const fixtureDir = join(cwd, '.dsh', 'profiles', 'headless', 'snapshot-fixtures')
  199. await mkdir(fixtureDir, { recursive: true })
  200. await Promise.all([
  201. copyFile(cliMockLlmPluginPath, join(fixtureDir, 'cli-mock-llm.ts')),
  202. writeFile(join(fixtureDir, 'package.json'), '{"type":"module"}\n'),
  203. ])
  204. }
  205. describe('headless stream-json snapshots', () => {
  206. it('runs one task through the product headless profile command', async () => {
  207. const task = 'Prove the product headless profile path with one real tool round trip.'
  208. const result = await runLoaderSmoke({
  209. label: 'product headless profile snapshot',
  210. tempDirPrefix: 'headless-snapshot-profile-',
  211. binScript: dshBinScript,
  212. configPath: headlessOverlayPath,
  213. binArgs: ['--profile', 'headless', '--patch', headlessOverlayPath, task],
  214. tsconfigPath,
  215. env: {
  216. DSH_PERMISSION_MODE: 'danger-full-access',
  217. DSH_TELEMETRY_DISABLED: '1',
  218. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  219. },
  220. prepare: prepareCliMockFixture,
  221. inspect: async (cwd) => {
  222. const logs = await persistedLogs(cwd, join(cwd, '.dsh', 'sessions'))
  223. expect(logs).toHaveLength(1)
  224. const actual = logs[0]
  225. if (actual === undefined) throw new Error('the headless profile did not persist its session')
  226. const context = contextFromLogs([actual.content])
  227. const session = scrubRequestHeaders(normalizeSessionLog(actual.content, context))
  228. if (refreshing) await writeFile(headlessSessionExpected, session)
  229. expect(session).toBe(await readFile(headlessSessionExpected, 'utf8'))
  230. expect(session).toContain(task)
  231. expect(session).toContain('CLI tool round trip complete: CLI_TOOL_ROUND_TRIP')
  232. },
  233. })
  234. expect(result.stdout).toBe('CLI tool round trip complete: CLI_TOOL_ROUND_TRIP\n')
  235. expect(result.stderr).toBe('')
  236. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  237. it('prints a terminal model failure through the product headless profile command', async () => {
  238. const result = await runLoaderSmoke({
  239. label: 'product headless profile model failure snapshot',
  240. tempDirPrefix: 'headless-snapshot-profile-failure-',
  241. binScript: dshBinScript,
  242. configPath: headlessOverlayPath,
  243. binArgs: ['--profile', 'headless', '--patch', headlessOverlayPath, 'Trigger the keyless model failure.'],
  244. tsconfigPath,
  245. expectedExitCode: 1,
  246. env: {
  247. DSH_CLI_MOCK_FAILURE: '1',
  248. DSH_TELEMETRY_DISABLED: '1',
  249. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  250. },
  251. prepare: prepareCliMockFixture,
  252. })
  253. expect(result.stdout).toBe('\n')
  254. await expect(result.stderr).toMatchFileSnapshot(headlessFailureExpected)
  255. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  256. it('prints the original Loader activation error through the assembled one-shot app', async () => {
  257. const result = await runLoaderSmoke({
  258. label: 'headless startup activation error snapshot',
  259. tempDirPrefix: 'headless-snapshot-startup-error-',
  260. binScript,
  261. libBinScript: binScript,
  262. configPath: startupFailureConfigPath,
  263. binArgs: [startupFailureConfigPath, 'unreachable task'],
  264. tsconfigPath,
  265. expectedExitCode: 1,
  266. })
  267. expect(result.stdout).toBe('')
  268. await expect(result.stderr).toMatchFileSnapshot(startupFailureExpected)
  269. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  270. it('retries a transient provider failure through the one-shot app', async () => {
  271. const prompt = await scenarioPrompt(retryScenarioDir, 'provider-retry')
  272. const streamExpected = join(retryScenarioDir, 'stream-json.expected.jsonl')
  273. let runCwd = ''
  274. const result = await runLoaderSmoke({
  275. label: 'provider retry headless stream-json snapshot',
  276. tempDirPrefix: 'headless-snapshot-provider-retry-',
  277. binScript,
  278. libBinScript: binScript,
  279. configPath: retryConfigPath,
  280. binArgs: [retryConfigPath, prompt],
  281. tsconfigPath,
  282. env: {
  283. DSH_SNAPSHOT: 'replay',
  284. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  285. },
  286. prepare: (cwd) => { runCwd = cwd },
  287. inspect: async (cwd) => {
  288. const logs = await persistedLogs(cwd)
  289. expect(logs).toHaveLength(1)
  290. const records = parseJsonl(logs[0]?.content ?? '')
  291. const retries = records.filter(record => record.type === 'llm/retry')
  292. expect(retries).toHaveLength(1)
  293. expect(retries[0]?.data).toMatchObject({
  294. provider: 'deepseek-official',
  295. mode: 'normal',
  296. policyKey: '["normal",1,["RATE_LIMIT"],1,1,0]',
  297. retry: 1,
  298. maxRetries: 1,
  299. delayMs: 1,
  300. failure: { message: 'snapshot transient failure', code: 'RATE_LIMIT', status: 429 },
  301. })
  302. },
  303. })
  304. expect(result.stderr).toBe('')
  305. const normalized = normalizeHeadlessStream(result.stdout, runCwd)
  306. if (refreshing) await writeFile(streamExpected, normalized)
  307. expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
  308. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  309. it('recovers from context overflow through an assembled compaction', async () => {
  310. const prompt = await scenarioPrompt(compactionScenarioDir, 'compaction-recovery')
  311. let expectedSession = await readFile(compactionSessionFixture, 'utf8')
  312. let runCwd = ''
  313. const result = await runLoaderSmoke({
  314. label: 'compaction recovery headless stream-json snapshot',
  315. tempDirPrefix: 'headless-snapshot-compaction-recovery-',
  316. binScript,
  317. libBinScript: binScript,
  318. configPath: compactionConfigPath,
  319. binArgs: [compactionConfigPath, prompt],
  320. tsconfigPath,
  321. env: {
  322. DSH_SNAPSHOT: 'replay',
  323. DSH_SNAPSHOT_FILE: compactionSessionFixture,
  324. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  325. },
  326. prepare: (cwd) => { runCwd = cwd },
  327. inspect: async (cwd) => {
  328. const logs = await persistedLogs(cwd)
  329. expect(logs).toHaveLength(1)
  330. const actual = logs[0]
  331. if (actual === undefined) throw new Error('compaction snapshot did not persist its session')
  332. const records = parseJsonl(actual.content)
  333. const types = records.map(record => record.type)
  334. expect(types.filter(type => type === 'compact/start')).toHaveLength(1)
  335. expect(types.filter(type => type === 'compact/summary')).toHaveLength(1)
  336. expect(types.filter(type => type === 'compact/end')).toHaveLength(1)
  337. const start = types.indexOf('compact/start')
  338. const summary = types.indexOf('compact/summary')
  339. const replacement = records.findIndex((record) => {
  340. if (record.type !== 'user/message') return false
  341. const surfaceOp = record.surfaceOp as JsonObject | undefined
  342. return surfaceOp?.op === 'replace'
  343. })
  344. const end = types.indexOf('compact/end')
  345. expect(start).toBeLessThan(summary)
  346. expect(summary).toBeLessThan(replacement)
  347. expect(replacement).toBeLessThan(end)
  348. const summaryRecord = records[summary]
  349. const summaryData = summaryRecord?.data as JsonObject | undefined
  350. expect(summaryData?.shadowedSeqs).toEqual(expect.arrayContaining([expect.any(Number)]))
  351. const final = [...records].reverse().find(record => record.type === 'assistant/message')
  352. expect(JSON.stringify(final)).toContain('COMPACTION RECOVERED')
  353. const actualContext = contextFromLogs([actual.content])
  354. if (refreshing) {
  355. const harvested: HarvestedLog = {
  356. id: String(actual.header.id),
  357. createdAt: Number(actual.header.createdAt),
  358. content: actual.content,
  359. }
  360. const replacements = refreshFixtureReplacements([harvested], [expectedSession])
  361. expectedSession = tokenizeSessionFixtureCwd(
  362. stabilizeRefreshLog(actual.content, expectedSession, replacements, actualContext),
  363. )
  364. await writeFile(compactionSessionFixture, expectedSession)
  365. }
  366. const expectedContext = contextFromLogs([expectedSession])
  367. expect(scrubRequestHeaders(normalizeSessionLog(actual.content, actualContext)))
  368. .toBe(scrubRequestHeaders(normalizeSessionLog(expectedSession, expectedContext)))
  369. },
  370. })
  371. expect(result.stderr).toBe('')
  372. const normalized = normalizeHeadlessStream(result.stdout, runCwd)
  373. if (refreshing) await writeFile(compactionStreamExpected, normalized)
  374. expect(normalized).toBe(await readFile(compactionStreamExpected, 'utf8'))
  375. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  376. it('logs actionable missing-credential guidance through the one-shot app', async () => {
  377. const streamExpected = join(credentialsScenarioDir, 'stream-json.expected.jsonl')
  378. let runCwd = ''
  379. const result = await runLoaderSmoke({
  380. label: 'missing-credential headless stream-json snapshot',
  381. tempDirPrefix: 'headless-snapshot-missing-credential-',
  382. binScript,
  383. libBinScript: binScript,
  384. configPath: credentialsConfigPath,
  385. binArgs: [credentialsConfigPath, 'say pong'],
  386. tsconfigPath,
  387. env: {
  388. // First-run posture: no key in the environment, none under ./.dsh.
  389. DEEPSEEK_API_KEY: '',
  390. DEEPSEEK_BASE_URL: '',
  391. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  392. },
  393. prepare: (cwd) => { runCwd = cwd },
  394. })
  395. // The failure reaches the caller through the stream, not stderr; the
  396. // recorded transcript below pins the guidance text itself, which names
  397. // both places a credential can come from and nothing else.
  398. expect(result.stderr).toBe('')
  399. const normalized = normalizeHeadlessStream(result.stdout, runCwd)
  400. if (refreshing) await writeFile(streamExpected, normalized)
  401. expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
  402. // The durable failure leads with the credential store — the path that
  403. // keeps the secret out of configuration files — then names the launching
  404. // environment, and stops there: configuration carries the reference, so
  405. // there is no literal-key escape hatch left to offer.
  406. expect(normalized).toContain(
  407. 'store DEEPSEEK_API_KEY through the credentials service (the web Models page writes it),',
  408. )
  409. expect(normalized).toContain('or export DEEPSEEK_API_KEY in the launching environment')
  410. expect(normalized).not.toContain('as a last resort')
  411. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  412. it('logs actionable invalid-credential guidance through the one-shot app', async () => {
  413. const streamExpected = join(invalidCredentialScenarioDir, 'stream-json.expected.jsonl')
  414. let runCwd = ''
  415. const result = await runLoaderSmoke({
  416. label: 'invalid-credential headless stream-json snapshot',
  417. tempDirPrefix: 'headless-snapshot-invalid-credential-',
  418. binScript,
  419. libBinScript: binScript,
  420. configPath: credentialsConfigPath,
  421. binArgs: [credentialsConfigPath, 'say pong'],
  422. tsconfigPath,
  423. env: {
  424. // A key that exists but no HTTP header can carry — the paste the
  425. // credential guard exists for: without it, `fetch` refuses to build
  426. // the header and the turn ends on a retried ByteString TypeError.
  427. DEEPSEEK_API_KEY: 'sk-\u{1F600}pasted-from-a-chat-window',
  428. DEEPSEEK_BASE_URL: '',
  429. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  430. },
  431. prepare: (cwd) => { runCwd = cwd },
  432. })
  433. expect(result.stderr).toBe('')
  434. const normalized = normalizeHeadlessStream(result.stdout, runCwd)
  435. if (refreshing) await writeFile(streamExpected, normalized)
  436. expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
  437. // The durable failure names the reference to correct and the writer that
  438. // usually owns it, and stays true in a composition that mounts no Models
  439. // page at all.
  440. expect(normalized).toContain('the API key resolved from DEEPSEEK_API_KEY contains characters')
  441. expect(normalized).toContain('the web Models page writes it')
  442. // Neither the key nor its transport-level symptom (the ByteString error)
  443. // may reach the user: the code point of one character is still the key.
  444. expect(normalized).not.toContain('pasted-from-a-chat-window')
  445. expect(normalized).not.toContain('ByteString')
  446. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  447. it('logs the model default and a dynamic next-step reasoning effort', async () => {
  448. const result = await runLoaderSmoke({
  449. label: 'reasoning effort headless stream-json snapshot',
  450. tempDirPrefix: 'headless-snapshot-reasoning-effort-',
  451. binScript,
  452. libBinScript: binScript,
  453. configPath: reasoningConfigPath,
  454. binArgs: [reasoningConfigPath, 'prove dynamic reasoning effort'],
  455. tsconfigPath,
  456. })
  457. expect(result.stderr).toBe('')
  458. const headers = parseJsonl(result.stdout)
  459. .map(record => record.event)
  460. .filter((event): event is JsonObject => (
  461. event !== null
  462. && typeof event === 'object'
  463. && !Array.isArray(event)
  464. && 'type' in event
  465. && event.type === 'request/header'
  466. ))
  467. .map((event) => {
  468. const data = event.data as JsonObject
  469. return (data.header as JsonObject).config
  470. })
  471. expect(headers).toMatchInlineSnapshot(`
  472. [
  473. {
  474. "model": "cli-mock",
  475. "provider": "cli-mock",
  476. "reasoningEffort": "high",
  477. },
  478. {
  479. "model": "cli-mock",
  480. "provider": "cli-mock",
  481. "reasoningEffort": "off",
  482. },
  483. ]
  484. `)
  485. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  486. it('keeps provider comments alive and sends DeepSeek defaults through the one-shot app', async () => {
  487. const server = await deepseekDefaultsServer()
  488. try {
  489. const result = await runLoaderSmoke({
  490. label: 'DeepSeek adapter defaults headless stream-json snapshot',
  491. tempDirPrefix: 'headless-snapshot-deepseek-defaults-',
  492. binScript,
  493. libBinScript: binScript,
  494. configPath: deepseekDefaultsConfigPath,
  495. binArgs: [
  496. deepseekDefaultsConfigPath,
  497. 'return the deterministic response',
  498. ],
  499. tsconfigPath,
  500. env: {
  501. // Configuration carries only the reference; the key rides the
  502. // launching environment, which is the whole credential plane here.
  503. DEEPSEEK_API_KEY: 'snapshot-key',
  504. DSH_SNAPSHOT_BASE_URL: server.url,
  505. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  506. },
  507. })
  508. expect(result.stderr).toBe('')
  509. expect(server.requests).toHaveLength(1)
  510. expect(server.requests[0]?.max_tokens).toBe(256_000)
  511. const header = (parseJsonl(result.stdout)
  512. .map(record => record.event)
  513. .find((event): event is JsonObject => (
  514. event !== null
  515. && typeof event === 'object'
  516. && !Array.isArray(event)
  517. && 'type' in event
  518. && event.type === 'request/header'
  519. ))?.data as JsonObject | undefined)?.header as JsonObject | undefined
  520. expect(header?.config).toMatchInlineSnapshot(`
  521. {
  522. "maxTokens": 256000,
  523. "model": "deepseek-v4-flash",
  524. "provider": "deepseek-official",
  525. "reasoningEffort": "off",
  526. }
  527. `)
  528. expect(header?.adapterDefaults).toEqual({
  529. maxTokens: true,
  530. reasoningEffort: true,
  531. })
  532. } finally {
  533. await server.close()
  534. }
  535. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  536. it('replays the advanced toolchain through the one-shot app', async () => {
  537. const prompt = await scenarioPrompt(advancedScenarioDir, 'advanced-toolchain')
  538. const fixtureFiles = [
  539. advancedSessionFixture,
  540. join(advancedScenarioDir, 'session.1.jsonl'),
  541. join(advancedScenarioDir, 'session.2.jsonl'),
  542. ]
  543. let expectedSessions = await Promise.all(fixtureFiles.map(file => readFile(file, 'utf8')))
  544. let runCwd = ''
  545. const result = await runLoaderSmoke({
  546. label: 'advanced headless stream-json snapshot',
  547. tempDirPrefix: 'headless-snapshot-advanced-',
  548. binScript,
  549. libBinScript: binScript,
  550. configPath: advancedConfigPath,
  551. binArgs: [advancedConfigPath, prompt],
  552. tsconfigPath,
  553. env: {
  554. DSH_SNAPSHOT: 'replay',
  555. DSH_SNAPSHOT_FILE: advancedSessionFixture,
  556. DSH_SNAPSHOT_CHILD_FILES: [
  557. join(advancedScenarioDir, 'session.1.jsonl'),
  558. join(advancedScenarioDir, 'session.2.jsonl'),
  559. ].join(delimiter),
  560. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  561. },
  562. prepare: (cwd) => { runCwd = cwd },
  563. inspect: async (cwd) => {
  564. const logs = await persistedLogs(cwd)
  565. expect(logs).toHaveLength(3)
  566. const parents = logs.filter(log => typeof log.header.parentSession !== 'string')
  567. expect(parents).toHaveLength(1)
  568. const parent = parents[0]
  569. if (parent === undefined) throw new Error('headless snapshot did not persist its main session')
  570. const children = logs.filter(log => typeof log.header.parentSession === 'string')
  571. .sort((left, right) => Number(left.header.createdAt) - Number(right.header.createdAt))
  572. const actualSessions = [parent, ...children]
  573. const actualContext = contextFromLogs(actualSessions.map(log => log.content))
  574. if (refreshing) {
  575. const harvested = actualSessions.map((log): HarvestedLog => ({
  576. id: String(log.header.id),
  577. createdAt: Number(log.header.createdAt),
  578. ...typeof log.header.parentSession === 'string'
  579. ? { parentSession: log.header.parentSession }
  580. : {},
  581. content: log.content,
  582. }))
  583. const replacements = refreshFixtureReplacements(harvested, expectedSessions)
  584. expectedSessions = await Promise.all(actualSessions.map(async (actual, index) => {
  585. const existing = expectedSessions[index]
  586. const file = fixtureFiles[index]
  587. if (existing === undefined || file === undefined) {
  588. throw new Error(`headless snapshot has no fixture for persisted log ${index}`)
  589. }
  590. const stable = tokenizeSessionFixtureCwd(
  591. stabilizeRefreshLog(actual.content, existing, replacements, actualContext),
  592. )
  593. await writeFile(file, stable)
  594. return stable
  595. }))
  596. }
  597. const expectedContext = contextFromLogs(expectedSessions)
  598. for (const [index, actual] of actualSessions.entries()) {
  599. const expected = expectedSessions[index]
  600. if (expected === undefined) throw new Error(`headless snapshot has no fixture for persisted log ${index}`)
  601. expect(scrubRequestHeaders(normalizeSessionLog(actual.content, actualContext)))
  602. .toBe(scrubRequestHeaders(normalizeSessionLog(expected, expectedContext)))
  603. }
  604. },
  605. })
  606. expect(result.stderr).toBe('')
  607. const normalized = normalizeHeadlessStream(result.stdout, runCwd)
  608. if (refreshing) await writeFile(advancedStreamExpected, normalized)
  609. expect(normalized).toBe(await readFile(advancedStreamExpected, 'utf8'))
  610. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  611. it('replays persisted goal tools through the one-shot app', async () => {
  612. const prompt = await scenarioPrompt(goalScenarioDir, 'goal-tools')
  613. const streamExpected = join(goalScenarioDir, 'stream-json.expected.jsonl')
  614. let runCwd = ''
  615. const result = await runLoaderSmoke({
  616. label: 'goal tools headless stream-json snapshot',
  617. tempDirPrefix: 'headless-snapshot-goal-tools-',
  618. binScript,
  619. libBinScript: binScript,
  620. configPath: goalConfigPath,
  621. binArgs: [goalConfigPath, prompt],
  622. tsconfigPath,
  623. env: {
  624. DSH_SNAPSHOT: 'replay',
  625. DSH_SNAPSHOT_FILE: join(goalScenarioDir, 'session.jsonl'),
  626. DSH_SNAPSHOT_OVERRIDE: join(goalScenarioDir, 'replay.override.json'),
  627. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  628. },
  629. prepare: (cwd) => { runCwd = cwd },
  630. inspect: async (cwd) => {
  631. const logs = await persistedLogs(cwd)
  632. expect(logs).toHaveLength(1)
  633. const records = parseJsonl(logs[0]?.content ?? '')
  634. const calls = records.filter(record => record.type === 'tool/call')
  635. .map(record => (record.data as JsonObject | undefined)?.name)
  636. expect(calls).toEqual(['update_goal', 'create_goal', 'get_goal'])
  637. const probeResult = records.find((record) => {
  638. if (record.type !== 'tool/result') return false
  639. const data = record.data as JsonObject | undefined
  640. const message = data?.message as JsonObject | undefined
  641. const source = message?.source as JsonObject | undefined
  642. return source?.callId === 'call_goal_probe'
  643. })
  644. const probeData = probeResult?.data as JsonObject | undefined
  645. const probeMessage = probeData?.message as JsonObject | undefined
  646. const probeContent = probeMessage?.content as JsonObject[] | undefined
  647. expect(probeContent?.[0]?.isError).toBe(true)
  648. expect((probeData?.error as JsonObject | undefined)?.code).toBe('GOAL_NOT_FOUND')
  649. const goalChanges = records.filter(record => record.type === 'goal/change')
  650. expect(goalChanges).toHaveLength(1)
  651. const data = goalChanges[0]?.data as JsonObject | undefined
  652. const goal = data?.goal as JsonObject | undefined
  653. expect(data?.operation).toBe('create')
  654. expect(goal).toMatchObject({
  655. objective: 'Finish the headless goal-tool snapshot proof',
  656. phase: 'active',
  657. maxGoalRounds: 7,
  658. })
  659. },
  660. })
  661. expect(result.stderr).toBe('')
  662. const normalized = normalizeGoalStream(result.stdout, runCwd)
  663. if (refreshing) await writeFile(streamExpected, normalized)
  664. expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
  665. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  666. it('replays two fresh Ralph rounds through the one-shot app', async () => {
  667. const prompt = await scenarioPrompt(ralphScenarioDir, 'ralph-loop')
  668. const streamExpected = join(ralphScenarioDir, 'stream-json.expected.jsonl')
  669. let runCwd = ''
  670. const result = await runLoaderSmoke({
  671. label: 'Ralph loop headless stream-json snapshot',
  672. tempDirPrefix: 'headless-snapshot-ralph-loop-',
  673. binScript,
  674. libBinScript: binScript,
  675. configPath: ralphConfigPath,
  676. binArgs: [ralphConfigPath, prompt],
  677. tsconfigPath,
  678. env: {
  679. DSH_SNAPSHOT: 'replay',
  680. DSH_SNAPSHOT_FILE: join(ralphScenarioDir, 'session.jsonl'),
  681. DSH_SNAPSHOT_OVERRIDE: join(ralphScenarioDir, 'replay.override.json'),
  682. DSH_SNAPSHOT_CHILD_FILES: [
  683. join(ralphScenarioDir, 'session.1.jsonl'),
  684. join(ralphScenarioDir, 'session.2.jsonl'),
  685. ].join(delimiter),
  686. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  687. },
  688. prepare: (cwd) => { runCwd = cwd },
  689. inspect: async (cwd) => {
  690. const logs = await persistedLogs(cwd)
  691. expect(logs).toHaveLength(3)
  692. const parent = logs.find(log => typeof log.header.parentSession !== 'string')
  693. if (parent === undefined) throw new Error('Ralph snapshot did not persist its parent session')
  694. const parentId = parent.header.id
  695. expect(typeof parentId).toBe('string')
  696. const children = logs.filter(log => typeof log.header.parentSession === 'string')
  697. .sort((left, right) => Number(left.header.createdAt) - Number(right.header.createdAt))
  698. expect(children).toHaveLength(2)
  699. expect(children.map(child => child.header.parentSession)).toEqual([parentId, parentId])
  700. expect(children.map(child => child.header.cwd)).toEqual([parent.header.cwd, parent.header.cwd])
  701. expect(parent.header.delegationDepth).toBe(0)
  702. expect(children.map(child => child.header.delegationDepth)).toEqual([1, 1])
  703. expect(children.map(child => child.header.seedLength)).toEqual([undefined, undefined])
  704. expect(new Set(children.map(child => child.header.id)).size).toBe(2)
  705. const parentRecords = parseJsonl(parent.content)
  706. const parentCalls = parentRecords.filter(record => record.type === 'tool/call')
  707. expect(parentCalls.map(record => (record.data as JsonObject | undefined)?.name)).toEqual(['ralph'])
  708. const parentResult = parentRecords.find(record => record.type === 'tool/result')
  709. const parentResultData = parentResult?.data as JsonObject | undefined
  710. const parentMessage = parentResultData?.message as JsonObject | undefined
  711. const parentContent = parentMessage?.content as JsonObject[] | undefined
  712. expect(parentContent?.[0]?.isError).toBe(false)
  713. expect(JSON.stringify(parentContent?.[0]?.content)).toContain('reported completion after 2 rounds')
  714. const childRecords = children.map(child => parseJsonl(child.content))
  715. const childPrompts = childRecords.map((records) => {
  716. const message = records.find(record => record.type === 'user/message')
  717. return JSON.stringify((message?.data as JsonObject | undefined)?.content)
  718. })
  719. expect(childPrompts[0]).toContain('Ralph round: 1 of 2.')
  720. expect(childPrompts[0]).toContain('(none — this is the first round)')
  721. expect(childPrompts[0]).not.toContain('ROUND_ONE_HANDOFF')
  722. expect(childPrompts[1]).toContain('Ralph round: 2 of 2.')
  723. expect(childPrompts[1]).toContain('ROUND_ONE_HANDOFF')
  724. for (const childPrompt of childPrompts) {
  725. expect(childPrompt).toContain('Prove two fresh Ralph rounds through the shipped headless app.')
  726. expect(childPrompt).not.toContain('Run a two-round fresh-agent Ralph loop')
  727. }
  728. for (const records of childRecords) {
  729. const calls = records.filter(record => record.type === 'tool/call')
  730. expect(calls.map(record => (record.data as JsonObject | undefined)?.name))
  731. .toEqual(['structured_output'])
  732. }
  733. },
  734. })
  735. expect(result.stderr).toBe('')
  736. const normalized = normalizeHeadlessStream(result.stdout, runCwd)
  737. if (refreshing) await writeFile(streamExpected, normalized)
  738. expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
  739. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  740. it('delivers a continuable child result without parent polling', async () => {
  741. const parentReplay = join(settlementScenarioDir, 'parent.replay.jsonl')
  742. const parentOverride = join(settlementScenarioDir, 'parent.override.json')
  743. const childReplay = join(settlementScenarioDir, 'child.replay.jsonl')
  744. const childExpected = join(settlementScenarioDir, 'child.expected.jsonl')
  745. const streamExpected = join(settlementScenarioDir, 'stream-json.expected.jsonl')
  746. const task = 'Start one continuable background subagent and answer from its completion notice. Do not call list_agents, send_message, task_output, or task_list.'
  747. let runCwd = ''
  748. const result = await runLoaderSmoke({
  749. label: 'continuable settlement headless stream-json snapshot',
  750. tempDirPrefix: 'headless-snapshot-subagent-settlement-',
  751. binScript,
  752. libBinScript: binScript,
  753. configPath: settlementConfigPath,
  754. binArgs: [settlementConfigPath, task],
  755. tsconfigPath,
  756. env: {
  757. // The override fully supplies the parent script; the child fixture
  758. // remains separate so replay binds it to the fresh child Session.
  759. DSH_SNAPSHOT_FILE: parentReplay,
  760. DSH_SNAPSHOT_OVERRIDE: parentOverride,
  761. DSH_SNAPSHOT_CHILD_FILES: childReplay,
  762. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  763. },
  764. prepare: (cwd) => { runCwd = cwd },
  765. inspect: async (cwd) => {
  766. const logs = await persistedLogs(cwd)
  767. expect(logs).toHaveLength(2)
  768. const parent = logs.find(log => typeof log.header.parentSession !== 'string')
  769. const child = logs.find(log => typeof log.header.parentSession === 'string')
  770. if (parent === undefined || child === undefined) throw new Error('missing persisted parent or child log')
  771. const parentRecords = parseJsonl(parent.content)
  772. const calls = parentRecords.filter(record => record.type === 'tool/call')
  773. expect(calls.map(record => (record.data as JsonObject | undefined)?.name)).toEqual(['subagent'])
  774. const callArguments = (calls[0]?.data as JsonObject | undefined)?.arguments
  775. if (typeof callArguments !== 'string') throw new Error('subagent call did not persist its arguments')
  776. expect(JSON.parse(callArguments)).not.toHaveProperty('run_in_background')
  777. const notices = parentRecords.flatMap((record) => {
  778. if (record.type !== 'agent/inbox/spliced') return []
  779. const inserted = (record.data as JsonObject | undefined)?.inserted
  780. if (!Array.isArray(inserted)) return []
  781. return (inserted as JsonObject[]).filter((message) => {
  782. const source = message.source as JsonObject | undefined
  783. return source?.kind === 'subagent-settled'
  784. })
  785. })
  786. expect(notices).toHaveLength(1)
  787. expect(JSON.stringify(notices[0])).toContain('CHILD_RESULT')
  788. const context = contextFromLogs([parent.content, child.content])
  789. const normalizedChild = scrubRequestHeaders(normalizeSessionLog(child.content, context))
  790. if (refreshing) await writeFile(childExpected, normalizedChild)
  791. expect(normalizedChild).toBe(await readFile(childExpected, 'utf8'))
  792. expect(normalizedChild).toContain('CHILD_RESULT')
  793. expect(normalizedChild).not.toContain('"name":"report"')
  794. },
  795. })
  796. expect(result.stderr).toBe('')
  797. const records = parseJsonl(result.stdout)
  798. expect(records.at(-1)).toMatchObject({
  799. type: 'result',
  800. output: 'PARENT_RECEIVED_CHILD_RESULT',
  801. })
  802. const normalized = normalizeHeadlessStream(result.stdout, runCwd)
  803. if (refreshing) await writeFile(streamExpected, normalized)
  804. expect(normalized).toBe(await readFile(streamExpected, 'utf8'))
  805. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  806. it('replays persistent PTY tools through the one-shot app', async () => {
  807. const input = JSON.parse(await readFile(join(ptyScenarioDir, 'input.json'), 'utf8')) as {
  808. steps?: { op?: unknown; text?: unknown }[]
  809. }
  810. const prompt = input.steps?.find(step => step.op === 'prompt')?.text
  811. if (typeof prompt !== 'string') throw new Error('pty-tools input has no prompt step')
  812. let expectedSession = await readFile(ptySessionFixture, 'utf8')
  813. let runCwd = ''
  814. const result = await runLoaderSmoke({
  815. label: 'headless persistent PTY snapshot',
  816. tempDirPrefix: 'headless-snapshot-pty-',
  817. binScript,
  818. libBinScript: binScript,
  819. configPath: ptyConfigPath,
  820. binArgs: [ptyConfigPath, prompt],
  821. tsconfigPath,
  822. env: {
  823. DSH_SNAPSHOT: 'replay',
  824. DSH_SNAPSHOT_FILE: ptySessionFixture,
  825. NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
  826. },
  827. prepare: (cwd) => { runCwd = cwd },
  828. inspect: async (cwd) => {
  829. const logs = await persistedLogs(cwd)
  830. expect(logs).toHaveLength(1)
  831. const actual = logs[0]
  832. if (actual === undefined) throw new Error('headless PTY snapshot did not persist its session')
  833. const actualContext = contextFromLogs([actual.content])
  834. if (refreshing) {
  835. const harvested: HarvestedLog = {
  836. id: String(actual.header.id),
  837. createdAt: Number(actual.header.createdAt),
  838. content: actual.content,
  839. }
  840. const replacements = refreshFixtureReplacements([harvested], [expectedSession])
  841. expectedSession = tokenizeSessionFixtureCwd(
  842. stabilizeRefreshLog(actual.content, expectedSession, replacements, actualContext),
  843. )
  844. await writeFile(ptySessionFixture, expectedSession)
  845. }
  846. const expectedContext = contextFromLogs([expectedSession])
  847. expect(scrubRequestHeaders(normalizeSessionLog(actual.content, actualContext)))
  848. .toBe(scrubRequestHeaders(normalizeSessionLog(expectedSession, expectedContext)))
  849. },
  850. })
  851. expect(result.stderr).toBe('')
  852. const normalized = normalizeHeadlessStream(result.stdout, runCwd)
  853. if (refreshing) await writeFile(ptyStreamExpected, normalized)
  854. expect(normalized).toBe(await readFile(ptyStreamExpected, 'utf8'))
  855. }, LOADER_SMOKE_TEST_TIMEOUT_MS)
  856. })