test: remove duplicate coverage and strengthen boundary assertions (#33047)

* test: strengthen launch, subject identity, and release witnesses

* test: strengthen approval paths and prompt answer boundaries

* test: strengthen conversation names and mobile log persistence

* refactor: group background task stop contract with stop operations

* test: strengthen question, title, and release validation

* test: distinguish row text and generated dispatch titles

* test: verify mobile revival events and retry exclusivity

* test: reject incomplete child row output assertions

* test: verify copied modules and option selection contracts

* test: verify mobile host state and status retry boundaries

* test: observe connector refusals and real diagnostic delivery

* test: consolidate platform memory coverage and verify bounded work

* test: allow cached legacy failure reads within work bounds

* test: verify packaging boundaries with independent fixtures

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-24 00:13:25 -07:00
committed by GitHub
co-authored by OrcaWin
parent 156f3f4ef0
commit 70c4c55db9
53 changed files with 1033 additions and 393 deletions
@@ -76,6 +76,14 @@ function summarize() {
const reports = readdirSync(reportDir, { recursive: true })
.filter((file) => /(?:baseline|shared)-.*\.json$/.test(file))
.map((file) => JSON.parse(readFileSync(join(reportDir, file), 'utf8')))
const baselines = reports.filter((report) => report.mode === 'baseline')
assert.equal(reports.length, 8, 'Expected both measurements on all four packaging hosts')
assert.equal(baselines.length, 4, 'Expected a baseline on all four packaging hosts')
assert.equal(
new Set(reports.map((report) => `${report.mode}/${report.platform}/${report.arch}`)).size,
reports.length,
'Duplicate release JavaScript measurements'
)
const { jobs } = JSON.parse(readFileSync(join(reportDir, 'jobs.json'), 'utf8'))
const seconds = (step) => (Date.parse(step.completed_at) - Date.parse(step.started_at)) / 1000
const lines = [
@@ -84,7 +92,7 @@ function summarize() {
]
let savedMs = 0
let differences = 0
for (const baseline of reports.filter((report) => report.mode === 'baseline')) {
for (const baseline of baselines) {
const shared = reports.find(
(report) =>
report.mode === 'shared' &&
@@ -112,7 +120,6 @@ function summarize() {
)
}
}
assert.equal(reports.length, 8, 'Expected both measurements on all four packaging hosts')
const producer = jobs.find((job) =>
job.steps.some((step) => step.name === 'Build and archive release JavaScript')
)
+9
View File
@@ -135,6 +135,15 @@ describe('release policy', () => {
await expect(restoreLatestStable(github, repoRef)).resolves.toBe('v1.4.215')
expect(github.rest.repos.updateRelease).toHaveBeenCalledTimes(1)
expect(github.paginate).toHaveBeenCalledWith(github.rest.repos.listReleases, {
...repoRef,
per_page: 100
})
expect(github.rest.repos.updateRelease).toHaveBeenCalledWith({
...repoRef,
release_id: 14215,
make_latest: 'true'
})
})
it('compares versions numerically when picking Latest', async () => {
@@ -24,6 +24,9 @@ function pushRelease(mode, pushMain = 'true') {
rejected)
echo 'remote: error: GH013: Required status check is missing.' >&2
return 1 ;;
pull-request-text)
echo 'remote: Changes must be made through a pull request.' >&2
return 1 ;;
disconnected)
echo 'fatal: unable to access remote' >&2
return 128 ;;
@@ -64,11 +67,14 @@ describe('release tag push with protected main', () => {
expect(result.calls).toEqual(['push origin HEAD:refs/heads/main', 'push origin v1.4.229'])
})
it.each(['rejected', 'disconnected'])('stops without a tag for %s main push errors', (mode) => {
const result = pushRelease(mode)
expect(result.code).not.toBe(0)
expect(result.calls).toEqual(['push origin HEAD:refs/heads/main'])
})
it.each(['rejected', 'disconnected', 'pull-request-text'])(
'stops without a tag for %s main push errors',
(mode) => {
const result = pushRelease(mode)
expect(result.code).not.toBe(0)
expect(result.calls).toEqual(['push origin HEAD:refs/heads/main'])
}
)
it('only pushes the tag for an off-main source', () => {
const result = pushRelease('protected', 'false')
+18 -6
View File
@@ -1,4 +1,4 @@
import { execFileSync } from 'node:child_process'
import { runProcessSync } from '@orca/process-host'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -10,11 +10,9 @@ import {
} from './release-rc-history.mjs'
function git(cwd, args) {
return execFileSync('git', args, {
cwd,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
}).trim()
const result = runProcessSync({ program: 'git', args, cwd, timeoutMs: 10_000 })
expect(result.code, result.stderr).toBe(0)
return result.stdout.trim()
}
function withGitRepo(run) {
@@ -71,6 +69,8 @@ describe('release RC history', () => {
// this. Under-reporting rc.6 here lets an explicit 1.4.36-rc.6 cut land
// below the v1.4.36-rc.6.perf build that clients already run.
commit(repo, 'release: v1.4.36-rc.6.perf')
git(repo, ['tag', 'v1.4.36-rc.6.perf'])
git(repo, ['tag', '--delete', 'v1.4.36-rc.6.perf'])
expect(highestRcForBase('1.4.36', { cwd: repo })).toBe(6)
})
@@ -82,6 +82,8 @@ describe('release RC history', () => {
commit(repo, 'release: v1.4.36-rc.5')
git(repo, ['tag', 'v1.4.36-rc.5'])
commit(repo, 'release: v1.4.36-rc.6')
git(repo, ['tag', 'v1.4.36-rc.6'])
git(repo, ['tag', '--delete', 'v1.4.36-rc.6'])
expect(highestRcForBase('1.4.36', { cwd: repo })).toBe(6)
})
@@ -98,4 +100,14 @@ describe('release RC history', () => {
expect(highestRcForBase('1.4.36', { cwd: repo })).toBe(6)
})
})
it('counts an RC tag independently of release subjects and returns null without either record', () => {
withGitRepo((repo) => {
commit(repo, 'initial')
expect(highestRcForBase('1.4.36', { cwd: repo })).toBeNull()
git(repo, ['tag', 'v1.4.36-rc.7'])
expect(highestRcForBase('1.4.36', { cwd: repo })).toBe(7)
git(repo, ['tag', '--delete', 'v1.4.36-rc.7'])
expect(highestRcForBase('1.4.36', { cwd: repo })).toBeNull()
})
})
})
@@ -29,6 +29,7 @@ const DESKTOP_ONLY_PROOFS = {
},
'config/scripts/publish-complete-draft-releases.mjs': async () => {
const { isReleaseCutDraft } = await import('./publish-complete-draft-releases.mjs')
expect(isReleaseCutDraft(rulesRelease('v1.4.2-rc.1', { draft: true }))).toBe(true)
for (const tag of RULES_TAGS) {
expect(isReleaseCutDraft(rulesRelease(tag, { draft: true }))).toBe(false)
}
@@ -43,6 +44,10 @@ const DESKTOP_ONLY_PROOFS = {
},
'config/scripts/assert-github-release-is-draft.mjs': async () => {
const { matchingDesktopReleases } = await import('./assert-github-release-is-draft.mjs')
const desktop = { tag_name: 'v1.4.1' }
expect(
matchingDesktopReleases([...RULES_TAGS.map((tag) => rulesRelease(tag)), desktop], 'v1.4.1')
).toEqual([desktop])
expect(
matchingDesktopReleases(
RULES_TAGS.map((tag) => rulesRelease(tag)),
@@ -52,13 +57,18 @@ const DESKTOP_ONLY_PROOFS = {
},
'config/scripts/verify-release-required-assets.mjs': async () => {
const { verifyRequiredReleaseAssets } = await import('./verify-release-required-assets.mjs')
const releases = RULES_TAGS.map((tag) => rulesRelease(tag))
vi.stubGlobal(
'fetch',
vi.fn(async () => new Response(JSON.stringify(RULES_TAGS.map((tag) => rulesRelease(tag)))))
vi.fn(async () => new Response(JSON.stringify(releases)))
)
await expect(
verifyRequiredReleaseAssets({ repo: 'stablyai/orca', tag: 'v1.4.1', token: '' })
).rejects.toThrow('was not found')
releases.push({ tag_name: 'v1.4.1', assets: [] })
await expect(
verifyRequiredReleaseAssets({ repo: 'stablyai/orca', tag: 'v1.4.1', token: '' })
).rejects.toThrow('Release v1.4.1 is missing required assets.')
}
}
@@ -68,14 +78,9 @@ afterEach(() => {
describe('release tag pattern census', () => {
it.each(Object.keys(DESKTOP_ONLY_PROOFS))(
'%s imports the shared patterns or proves it admits only desktop tags',
'%s excludes agent state rules releases from desktop selection',
async (path) => {
const proof = DESKTOP_ONLY_PROOFS[path]
expect(
proof,
`${path} lists releases: import release-tag-patterns.mjs or add a proof`
).toBeDefined()
await proof()
await DESKTOP_ONLY_PROOFS[path]()
}
)
@@ -84,6 +89,8 @@ describe('release tag pattern census', () => {
expect(isAgentStateRulesTag(tag)).toBe(true)
expect(DESKTOP_STABLE_TAG.test(tag) || DESKTOP_RC_TAG.test(tag)).toBe(false)
}
expect(DESKTOP_STABLE_TAG.test('v1.4.1')).toBe(true)
expect(DESKTOP_RC_TAG.test('v1.4.2-rc.1')).toBe(true)
expect(() => agentStateRulesTag(1, 'beta')).toThrow()
})
})
@@ -13,15 +13,23 @@ const submissions = steps.filter((step) =>
step.uses?.startsWith('signpath/github-action-submit-signing-request@')
)
function signingAllowed(submission, outcome, attempt = 1, platform = 'win', uploaded = 'success') {
function signingAllowed(submission, outcome, attempt = 1, platform = 'win', uploads = {}) {
const expression = submission.if.replace(/steps\.([a-z][a-z0-9-]*)/g, 'steps["$1"]')
return runInNewContext(expression, {
matrix: { platform },
github: { run_attempt: attempt },
steps: {
'mac-signing-gate': { outcome },
'upload-unsigned-inner': { outcome: uploaded },
'upload-unsigned-windows-installer': { outcome: uploaded }
'upload-unsigned-inner': {
outcome: Object.hasOwn(uploads, 'upload-unsigned-inner')
? uploads['upload-unsigned-inner']
: 'success'
},
'upload-unsigned-windows-installer': {
outcome: Object.hasOwn(uploads, 'upload-unsigned-windows-installer')
? uploads['upload-unsigned-windows-installer']
: 'success'
}
}
})
}
@@ -65,11 +73,21 @@ describe('Windows release signing waits for macOS success', () => {
})
it('allows neither signing request without successful macOS verification and upload', () => {
const uploadBySubmission = {
'submit-inner-signing': 'upload-unsigned-inner',
'submit-signing-request': 'upload-unsigned-windows-installer'
}
for (const submission of submissions) {
const upload = uploadBySubmission[submission.id]
expect(upload).toBeDefined()
const otherUpload = Object.values(uploadBySubmission).find((id) => id !== upload)
expect(signingAllowed(submission, 'success')).toBe(true)
expect(signingAllowed(submission, 'success', 1, 'win', { [otherUpload]: 'failure' })).toBe(
true
)
for (const outcome of ['failure', 'cancelled', 'skipped', '', undefined]) {
expect(signingAllowed(submission, outcome), `${submission.id}: ${outcome}`).toBe(false)
expect(signingAllowed(submission, 'success', 1, 'win', outcome ?? null)).toBe(false)
expect(signingAllowed(submission, 'success', 1, 'win', { [upload]: outcome })).toBe(false)
}
for (const attempt of [2, 3]) {
expect(signingAllowed(submission, 'success', attempt)).toBe(false)
@@ -92,7 +110,7 @@ describe('Windows release signing waits for macOS success', () => {
})
it('runs the verifier CLI from paths requiring URL encoding, as Windows paths do', async () => {
const directory = realpathSync(mkdtempSync(join(tmpdir(), 'orca signing gate ')))
const directory = realpathSync(mkdtempSync(join(tmpdir(), 'orca signing gate # % ')))
const script = join(directory, 'mac verifier.mjs')
writeFileSync(script, readFileSync('config/scripts/run-release-mac-build-workflow.mjs'))
try {
@@ -17,40 +17,60 @@ function lintSources(sources) {
}
const violations = [
['unstyled class', 'export const X = () => <div className="max-h-64 overflow-y-auto" />'],
[
'unstyled class',
'export const X = () => <div className="max-h-64 overflow-y-auto" />',
'"max-h-64 overflow-y-auto"'
],
[
'unstyled suffix-important class',
'export const X = () => <div className="max-h-64 overflow-y-auto!" />'
'export const X = () => <div className="max-h-64 overflow-y-auto!" />',
'"max-h-64 overflow-y-auto!"'
],
[
'unknown scrollbar class',
'export const X = () => <div className="overflow-auto scrollbar-none" />'
'export const X = () => <div className="overflow-auto scrollbar-none" />',
'"overflow-auto scrollbar-none"'
],
[
'separate class composer arguments',
"export const X = () => <div className={cn('overflow-y-auto', 'scrollbar-sleek')} />"
"export const X = () => <div className={cn('overflow-y-auto', 'scrollbar-sleek')} />",
"'overflow-y-auto'"
],
[
'conditional scrollbar',
"export const X = ({ enabled }) => <div className={cn('overflow-y-auto', enabled && 'scrollbar-sleek')} />"
"export const X = ({ enabled }) => <div className={cn('overflow-y-auto', enabled && 'scrollbar-sleek')} />",
"'overflow-y-auto'"
],
[
'arbitrary class wrapper',
"export const X = () => <div className={identity('overflow-y-auto')} />"
"export const X = () => <div className={identity('overflow-y-auto')} />",
"'overflow-y-auto'"
],
[
'mismatched responsive variants',
'export const X = () => <div className="overflow-y-auto md:scrollbar-sleek" />'
'export const X = () => <div className="overflow-y-auto md:scrollbar-sleek" />',
'"overflow-y-auto md:scrollbar-sleek"'
],
[
'inline overflow',
"export const X = () => <div style={{ overflowY: 'auto' }} />",
"overflowY: 'auto'"
],
['inline overflow', "export const X = () => <div style={{ overflowY: 'auto' }} />"],
[
'logical inline style spread',
"export const X = ({ open }) => <div style={{ ...(open && { overflowY: 'auto' }) }} />"
"export const X = ({ open }) => <div style={{ ...(open && { overflowY: 'auto' }) }} />",
"overflowY: 'auto'"
],
[
'JSX spread class',
"export const X = () => <div {...{ className: 'overflow-y-auto' }} />",
"'overflow-y-auto'"
],
['JSX spread class', "export const X = () => <div {...{ className: 'overflow-y-auto' }} />"],
[
'later spread override',
'export const X = () => <div className="scrollbar-sleek" {...{ className: \'overflow-y-auto\' }} />'
'export const X = () => <div className="scrollbar-sleek" {...{ className: \'overflow-y-auto\' }} />',
"'overflow-y-auto'"
]
]
@@ -103,8 +123,16 @@ beforeAll(() => {
})
describe('renderer scrollbar style Oxlint plugin', () => {
it.each(violations)('reports %s', (_name, source) => {
expect(diagnosticsBySource.get(source)).toHaveLength(1)
it.each(violations)('reports %s', (_name, source, reportedSource) => {
const findings = diagnosticsBySource.get(source)
expect(findings).toHaveLength(1)
expect(findings[0].code).toBe('renderer-scrollbar-style(require-styled-vertical-scrollbar)')
const { offset, length } = findings[0].labels[0].span
expect(
Buffer.from(source)
.subarray(offset, offset + length)
.toString('utf8')
).toBe(reportedSource)
})
it.each(accepted)('accepts %s', (_name, source) => {
+18 -5
View File
@@ -1,7 +1,8 @@
import { execFileSync } from 'node:child_process'
import { runProcessSync } from '@orca/process-host'
import { mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { describe, expect, it } from 'vitest'
import { isDirectInvocation } from './script-entry-detection.mjs'
@@ -21,6 +22,15 @@ describe('isDirectInvocation', () => {
expect(isDirectInvocation(import.meta.url, '')).toBe(false)
})
it('compares the literal entry path when realpath cannot resolve it', () => {
const entry = join(tmpdir(), 'unresolved entry # %.mjs')
const realpath = () => {
throw new Error('unresolvable fixture path')
}
expect(isDirectInvocation(pathToFileURL(entry).href, entry, { realpath })).toBe(true)
expect(isDirectInvocation(import.meta.url, entry, { realpath })).toBe(false)
})
// Why an injected converter: a win32 path cannot be exercised through node:url's pathToFileURL
// on a posix runner, and CI is ubuntu.
const toWin32FileUrl = (windowsPath) => new URL(`file:///${windowsPath.replaceAll('\\', '/')}`)
@@ -35,9 +45,6 @@ describe('isDirectInvocation', () => {
realpath: keepAsIs
})
).toBe(true)
// The regression this guards: `file://${argv[1]}` yields file://C:\orca\... on Windows,
// so the builder exited 0 having written nothing and packaging failed downstream.
expect(`file://${scriptPath}`).not.toBe(moduleUrl)
})
})
@@ -58,7 +65,13 @@ describe('running a builder through a symlink', () => {
)
const link = join(scratch, 'entry-link.mjs')
await symlink(real, link)
expect(execFileSync(process.execPath, [link], { encoding: 'utf8' })).toBe('true')
const result = runProcessSync({
program: process.execPath,
args: [link],
timeoutMs: 10_000
})
expect(result.code, result.stderr).toBe(0)
expect(result.stdout).toBe('true')
} finally {
await rm(scratch, { recursive: true, force: true })
}
@@ -4,15 +4,20 @@ import {
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { afterAll, describe, expect, it } from 'vitest'
import { copyScriptWithLocalModules } from './script-module-dependencies.mjs'
const fixtureDir = mkdtempSync(join(tmpdir(), 'script-module-dependencies-'))
afterAll(() => {
rmSync(fixtureDir, { recursive: true, force: true })
})
function sourceTree(files) {
const sourceDir = mkdtempSync(join(fixtureDir, 'source-'))
for (const [name, contents] of Object.entries(files)) {
@@ -25,7 +30,11 @@ function copiedNames(files, entryName) {
const sourceDir = sourceTree(files)
const destinationDir = join(mkdtempSync(join(fixtureDir, 'dest-')), 'scripts')
copyScriptWithLocalModules(join(sourceDir, entryName), destinationDir)
return readdirSync(destinationDir).sort()
const names = readdirSync(destinationDir).sort()
for (const name of names) {
expect(readFileSync(join(destinationDir, name), 'utf8'), name).toBe(files[name])
}
return names
}
describe('copyScriptWithLocalModules', () => {
@@ -48,6 +48,20 @@ describe('sort comparator performance', () => {
(finding) => finding.code === 'sort-comparator-performance(no-repeated-collator)'
)
).toBe(true)
const reportedSources = findings.map(({ labels }) => {
const { offset, length } = labels[0].span
return Buffer.from(sources.repeatedCollation)
.subarray(offset, offset + length)
.toString('utf8')
})
expect(reportedSources.sort()).toEqual(
[
"a.name.localeCompare(b.name, locale, { sensitivity: 'base' })",
"new Intl.Collator('sv')",
"Intl.Collator('en', { numeric: true })",
"a['localeCompare'](b, undefined, options)"
].sort()
)
})
it('allows one collator per sort, bare comparisons, and unrelated callbacks', () => {
@@ -7,8 +7,10 @@ import {
describe('telemetry bundle constant patterns', () => {
it.each(['const', 'let', 'var'])('accepts %s declarations', (declaration) => {
expect(`${declaration} BUILD_IDENTITY = "rc"`).toMatch(BUILD_IDENTITY_RE)
expect(`${declaration} WRITE_KEY = "phc_example-key_123"`).toMatch(WRITE_KEY_RE)
expect(BUILD_IDENTITY_RE.exec(`${declaration} BUILD_IDENTITY = "rc"`)?.[1]).toBe('rc')
expect(WRITE_KEY_RE.exec(`${declaration} WRITE_KEY = "phc_example-key_123"`)?.[1]).toBe(
'phc_example-key_123'
)
})
it('rejects assignments and invalid values', () => {
@@ -20,7 +22,10 @@ describe('telemetry bundle constant patterns', () => {
it('accepts minified adjacent declarations', () => {
const bundle = 'var dde=`stable`,fde=`phc_example-key_123`,pde=(dde===`stable`)'
expect(bundle).toMatch(MINIFIED_TELEMETRY_RE)
expect(MINIFIED_TELEMETRY_RE.exec(bundle)?.slice(1, 3)).toEqual([
'stable',
'phc_example-key_123'
])
})
it('accepts the identity as a later declarator in a shared declaration', () => {
@@ -1,8 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
verifyPackagedBrowserParticipation,
PACKAGED_BROWSER_TEST_TITLES
} from './verify-packaged-browser-participation.mjs'
import { verifyPackagedBrowserParticipation } from './verify-packaged-browser-participation.mjs'
function report() {
return {
@@ -11,7 +8,10 @@ function report() {
{
suites: [
{
specs: PACKAGED_BROWSER_TEST_TITLES.map((title) => ({
specs: [
'keeps an old packaged client on the current server-hosted path',
'keeps a current client on an old packaged server-hosted path'
].map((title) => ({
title,
tests: Array.from({ length: 3 }, () => ({
expectedStatus: 'passed',
@@ -29,7 +29,7 @@ afterEach(async () => {
})
describe('verifyPackagedOrcadTemplate', () => {
it('accepts the exact six-target packaged template', async () => {
it('accepts the complete packaged template', async () => {
const fixture = await createFixture()
expect(() => verifyPackagedOrcadTemplate(fixture.root)).not.toThrow()
@@ -134,13 +134,10 @@ describe('assertPackagedServeSelectionEntry', () => {
return resourcesDir
}
it('looks where orca serve runs the entry from', () => {
expect(SERVE_SELECTION_ENTRY).toBe(`main/${ORCAD_LOCAL_SERVE_SELECTION_ENTRY}.js`)
})
it('passes an unpacked entry whose relative requires are all unpacked', () => {
const resourcesDir = packagedEntry({
[SERVE_SELECTION_ENTRY]: 'require("../chunks/a.js"); require("node:fs")',
[`main/${ORCAD_LOCAL_SERVE_SELECTION_ENTRY}.js`]:
'require("../chunks/a.js"); require("node:fs")',
'main/chunks/a.js': 'require("./b"); require("../persistence")',
'main/chunks/b.js': '',
'main/persistence/index.js': ''
@@ -76,33 +76,44 @@ describe('extractManifestAssetNames', () => {
})
describe('verifyRequiredReleaseAssets', () => {
function releaseFetch(release, manifestAsset, manifestText) {
return vi.fn(async (url) => {
const releasesUrl = 'https://api.github.com/repos/stablyai/orca/releases'
if (url === `${releasesUrl}?per_page=100`) {
return jsonResponse([release])
}
if (url === `${releasesUrl}/assets/${manifestAsset.id}`) {
return jsonResponse(manifestText)
}
if (release.assets.some((asset) => url === `${releasesUrl}/assets/${asset.id}`)) {
return jsonResponse('version: 1.4.27\n')
}
throw new Error(`Unexpected release request: ${url}`)
})
}
it('fails when a manifest-referenced asset has not been uploaded', async () => {
const tag = 'v1.4.27'
const required = getRequiredReleaseAssetNames(tag)
const assets = required.filter((name) => name !== 'Orca-1.4.27-arm64-mac.zip')
const release = releaseWithAssets(tag, assets)
const release = releaseWithAssets(tag, getRequiredReleaseAssetNames(tag))
const latestMacAsset = release.assets.find((asset) => asset.name === 'latest-mac.yml')
const fetchMock = vi
.fn()
.mockResolvedValueOnce(jsonResponse([release]))
.mockResolvedValueOnce(
jsonResponse(
[
'version: 1.4.27',
'files:',
' - url: Orca-1.4.27-arm64-mac.zip',
' sha512: test',
'path: Orca-1.4.27-arm64-mac.zip'
].join('\n')
)
vi.stubGlobal(
'fetch',
releaseFetch(
release,
latestMacAsset,
[
'version: 1.4.27',
'files:',
' - url: mac-updater-only.zip',
' sha512: test',
'path: mac-updater-only.zip'
].join('\n')
)
.mockResolvedValue(jsonResponse('version: 1.4.27\n'))
vi.stubGlobal('fetch', fetchMock)
)
await expect(
verifyRequiredReleaseAssets({ repo: 'stablyai/orca', tag, token: 'token' })
).rejects.toThrow('Missing: Orca-1.4.27-arm64-mac.zip')
expect(latestMacAsset).toBeTruthy()
).rejects.toThrow('Missing: mac-updater-only.zip')
})
it('checks assets referenced by the Linux arm64 updater manifest', async () => {
@@ -110,26 +121,22 @@ describe('verifyRequiredReleaseAssets', () => {
const required = getRequiredReleaseAssetNames(tag)
const release = releaseWithAssets(tag, required)
const arm64Manifest = release.assets.find((asset) => asset.name === 'latest-linux-arm64.yml')
const fetchMock = vi
.fn()
.mockResolvedValueOnce(jsonResponse([release]))
.mockResolvedValueOnce(jsonResponse('version: 1.4.27\n'))
.mockResolvedValueOnce(
jsonResponse(
[
'version: 1.4.27',
'files:',
' - url: orca-linux-arm64.AppImage.blockmap',
'path: orca-linux-arm64.AppImage'
].join('\n')
)
vi.stubGlobal(
'fetch',
releaseFetch(
release,
arm64Manifest,
[
'version: 1.4.27',
'files:',
' - url: orca-linux-arm64.AppImage.blockmap',
'path: orca-linux-arm64.AppImage'
].join('\n')
)
.mockResolvedValue(jsonResponse('version: 1.4.27\n'))
vi.stubGlobal('fetch', fetchMock)
)
await expect(
verifyRequiredReleaseAssets({ repo: 'stablyai/orca', tag, token: 'token' })
).rejects.toThrow('Missing: orca-linux-arm64.AppImage.blockmap')
expect(arm64Manifest).toBeTruthy()
})
})
@@ -1,45 +1,83 @@
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { Mock } from 'vitest'
import { subscribeConnectionLogBackgroundFlush } from './connection-log-background-flush'
type AppStateListener = (next: string) => void
let appStateListener: AppStateListener | null = null
const appStateListeners = new Map<string, Set<AppStateListener>>()
const appStateRemove = vi.fn()
const subscriptions = new Set<() => void>()
vi.mock('react-native', () => ({
AppState: {
addEventListener: (_event: string, listener: AppStateListener) => {
appStateListener = listener
return { remove: appStateRemove }
addEventListener: (event: string, listener: AppStateListener) => {
const listeners = appStateListeners.get(event) ?? new Set<AppStateListener>()
appStateListeners.set(event, listeners)
listeners.add(listener)
return {
remove: () => {
appStateRemove()
listeners.delete(listener)
}
}
}
}
}))
function subscribe(flush: () => void): () => void {
const unsubscribe = subscribeConnectionLogBackgroundFlush(flush)
subscriptions.add(unsubscribe)
return () => {
subscriptions.delete(unsubscribe)
unsubscribe()
}
}
function emitAppState(next: string): void {
for (const listener of appStateListeners.get('change') ?? []) {
listener(next)
}
}
describe('subscribeConnectionLogBackgroundFlush', () => {
let flush: Mock<() => void>
beforeEach(() => {
vi.clearAllMocks()
appStateListener = null
appStateListeners.clear()
flush = vi.fn<() => void>()
})
afterEach(() => {
for (const unsubscribe of subscriptions) {
unsubscribe()
}
subscriptions.clear()
expect([...appStateListeners.values()].every((listeners) => listeners.size === 0)).toBe(true)
})
it('flushes when the app goes to the background', () => {
subscribeConnectionLogBackgroundFlush(flush)
appStateListener?.('background')
subscribe(flush)
emitAppState('background')
expect(flush).toHaveBeenCalledTimes(1)
})
it('does not flush on foreground or transient inactive states', () => {
subscribeConnectionLogBackgroundFlush(flush)
appStateListener?.('active')
appStateListener?.('inactive')
subscribe(flush)
expect(appStateListeners.get('change')?.size).toBe(1)
emitAppState('active')
emitAppState('inactive')
expect(flush).not.toHaveBeenCalled()
})
it('removes the listener on unsubscribe', () => {
const unsubscribe = subscribeConnectionLogBackgroundFlush(flush)
const otherFlush = vi.fn()
const unsubscribe = subscribe(flush)
subscribe(otherFlush)
unsubscribe()
emitAppState('background')
expect(flush).not.toHaveBeenCalled()
expect(otherFlush).toHaveBeenCalledTimes(1)
expect(appStateRemove).toHaveBeenCalledTimes(1)
})
})
@@ -76,18 +76,26 @@ describe('connection log buffer', () => {
it('redacts credentials before retaining or persisting an event', async () => {
const save = vi.fn(async () => {})
const store = createConnectionLogStore(3, { load: async () => [], save })
const store = createConnectionLogStore(3, {
load: async () => [{ ...entry(0), detail: 'authorization=Bearer legacy-secret' }],
save
})
store.append('host-a', {
...entry(1),
detail: 'resumeToken=do-not-copy deviceToken:also-secret'
})
expect(store.get('host-a')[0]?.detail).toBe('resumeToken=[redacted] deviceToken:[redacted]')
await store.hydrate('host-a')
expect(store.get('host-a')[0]?.detail).toBe('resumeToken=[redacted] deviceToken:[redacted]')
expect(store.get('host-a').map((event) => event.detail)).toEqual([
'authorization=[redacted]',
'resumeToken=[redacted] deviceToken:[redacted]'
])
await vi.waitFor(() => expect(save).toHaveBeenCalled())
expect(JSON.stringify(save.mock.calls)).not.toContain('do-not-copy')
expect(JSON.stringify(save.mock.calls)).not.toContain('also-secret')
expect(JSON.stringify(save.mock.calls)).not.toContain('legacy-secret')
})
it('redacts quoted credential values when the object key is unquoted', async () => {
@@ -176,17 +184,29 @@ describe('connection log buffer', () => {
})
it('retries one transient persistence failure without requiring another append', async () => {
const save = vi.fn().mockResolvedValue(undefined)
const store = createConnectionLogStore(3, { load: async () => [], save })
vi.useFakeTimers()
try {
const save = vi.fn().mockResolvedValue(undefined)
const store = createConnectionLogStore(3, { load: async () => [], save })
await store.hydrate('host-a')
await vi.waitFor(() => expect(save).toHaveBeenCalled())
save.mockReset()
save.mockRejectedValueOnce(new Error('storage unavailable')).mockResolvedValueOnce(undefined)
store.append('host-a', entry(1))
await store.hydrate('host-a')
await vi.advanceTimersByTimeAsync(0)
expect(save).toHaveBeenCalledOnce()
save.mockReset()
save.mockRejectedValueOnce(new Error('storage unavailable')).mockResolvedValueOnce(undefined)
store.append('host-a', entry(1))
await vi.waitFor(() => expect(save).toHaveBeenCalledTimes(2))
expect(save).toHaveBeenLastCalledWith('host-a', [entry(1)])
await vi.advanceTimersByTimeAsync(0)
expect(save).toHaveBeenCalledOnce()
await vi.advanceTimersByTimeAsync(199)
expect(save).toHaveBeenCalledOnce()
await vi.advanceTimersByTimeAsync(1)
expect(save).toHaveBeenCalledTimes(2)
expect(save).toHaveBeenLastCalledWith('host-a', [entry(1)])
} finally {
vi.clearAllTimers()
vi.useRealTimers()
}
})
it('does not retry failed automatic hydration for every appended event', async () => {
@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createConnectionLogStore } from './connection-log-buffer'
import type { ConnectionLogEntry } from './types'
@@ -15,6 +15,17 @@ async function drainMicrotasks(): Promise<void> {
}
}
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
if (vi.isFakeTimers()) {
vi.clearAllTimers()
vi.useRealTimers()
}
})
describe('connection log persistence backpressure', () => {
it('writes only the latest pending snapshot when storage falls behind', async () => {
const saved: string[][] = []
@@ -44,11 +55,11 @@ describe('connection log persistence backpressure', () => {
}
expect(saved).toEqual([['0']])
release()
for (let i = 0; i < 1000; i++) {
await drainMicrotasks()
}
await drainMicrotasks()
expect(saved).toHaveLength(2)
expect(saved[1]).toEqual(store.get('a').map((item) => item.id))
const expected = Array.from({ length: 200 }, (_, index) => String(801 + index))
expect(saved[1]).toEqual(expected)
expect(store.get('a').map((item) => item.id)).toEqual(expected)
})
it('persists a different host while one host has a stalled save', async () => {
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
import { createConnectionLogStore, type ConnectionLogPersistence } from './connection-log-buffer'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createConnectionLogStore } from './connection-log-buffer'
import type { ConnectionLogPersistence } from './connection-log-buffer'
import type { ConnectionLogEntry } from './types'
const entry = (id: number): ConnectionLogEntry => ({
@@ -8,7 +9,16 @@ const entry = (id: number): ConnectionLogEntry => ({
level: 'info',
message: `event ${id}`
})
const drain = () => new Promise<void>((resolve) => setTimeout(resolve, 0))
const drain = () => vi.advanceTimersByTimeAsync(0)
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.clearAllTimers()
vi.useRealTimers()
})
describe('connection log persistence revisions', () => {
it('writes a synchronous burst once per host after hydration', async () => {
@@ -23,8 +33,14 @@ describe('connection log persistence revisions', () => {
}
await drain()
expect(save).toHaveBeenCalledTimes(2)
expect(save).toHaveBeenCalledWith('a', store.get('a'))
expect(save).toHaveBeenCalledWith('b', store.get('b'))
expect(save).toHaveBeenCalledWith(
'a',
Array.from({ length: 200 }, (_, i) => entry(800 + i))
)
expect(save).toHaveBeenCalledWith(
'b',
Array.from({ length: 200 }, (_, i) => entry(2800 + i))
)
})
it('shares one snapshot across startup appends waiting on hydration', async () => {
@@ -7,6 +7,9 @@ describe('connectionRetryAction', () => {
const reload = vi.fn()
connectionRetryAction({ hostId: 'h', needsReconnect: false, forceReconnect, reload })?.()
expect(reload).toHaveBeenCalledTimes(1)
if (forceReconnect) {
expect(forceReconnect).not.toHaveBeenCalled()
}
}
})
@@ -32,13 +35,13 @@ describe('connectionRetryAction', () => {
})
it('re-reads when there is no host to re-dial', () => {
const reload = vi.fn()
connectionRetryAction({
hostId: undefined,
needsReconnect: true,
forceReconnect: null,
reload
})?.()
expect(reload).toHaveBeenCalledTimes(1)
for (const forceReconnect of [null, vi.fn()]) {
const reload = vi.fn()
connectionRetryAction({ hostId: undefined, needsReconnect: true, forceReconnect, reload })?.()
expect(reload).toHaveBeenCalledTimes(1)
if (forceReconnect) {
expect(forceReconnect).not.toHaveBeenCalled()
}
}
})
})
@@ -1,60 +1,110 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { Mock } from 'vitest'
import { subscribeConnectionRevivalTriggers } from './connection-revival-triggers'
import { startSocketDeadlineClock } from './socket-deadline-clock'
import { recordSocketDeadlineAppState, startSocketDeadlineClock } from './socket-deadline-clock'
type RevivalNudge = Parameters<typeof subscribeConnectionRevivalTriggers>[0]
type AppStateListener = (next: string) => void
type NetworkSnapshot = { isConnected?: boolean; type?: string }
type NetworkListener = (state: NetworkSnapshot) => void
let appStateListener: AppStateListener | null = null
let networkListener: NetworkListener | null = null
const appStateListeners = new Map<string, Set<AppStateListener>>()
const networkListeners = new Set<NetworkListener>()
const subscriptions = new Set<() => void>()
let seededNetworkState: NetworkSnapshot = { isConnected: true, type: 'WIFI' }
let readNetworkState = () => Promise.resolve(seededNetworkState)
const appStateRemove = vi.fn()
const networkRemove = vi.fn()
vi.mock('react-native', () => ({
AppState: {
currentState: 'active',
addEventListener: (_event: string, listener: AppStateListener) => {
appStateListener = listener
return { remove: appStateRemove }
addEventListener: (event: string, listener: AppStateListener) => {
const listeners = appStateListeners.get(event) ?? new Set<AppStateListener>()
appStateListeners.set(event, listeners)
listeners.add(listener)
return {
remove: () => {
appStateRemove()
listeners.delete(listener)
}
}
}
}
}))
vi.mock('expo-network', () => ({
getNetworkStateAsync: () => Promise.resolve(seededNetworkState),
getNetworkStateAsync: () => readNetworkState(),
addNetworkStateListener: (listener: NetworkListener) => {
networkListener = listener
return { remove: networkRemove }
networkListeners.add(listener)
return {
remove: () => {
networkRemove()
networkListeners.delete(listener)
}
}
}
}))
// Why: the baseline seed resolves on a microtask; flush it so listener
// events in the test observe the same ordering as a real subscription.
async function subscribeAndSeed(nudge: () => void): Promise<() => void> {
const unsubscribe = subscribeConnectionRevivalTriggers(nudge)
async function subscribeAndSeed(nudge: RevivalNudge): Promise<() => void> {
const unsubscribe = subscribe(nudge)
await Promise.resolve()
return unsubscribe
}
function subscribe(nudge: RevivalNudge): () => void {
const unsubscribe = subscribeConnectionRevivalTriggers(nudge)
subscriptions.add(unsubscribe)
return () => {
subscriptions.delete(unsubscribe)
unsubscribe()
}
}
function emitAppState(next: string): void {
expect(appStateListeners.get('change')?.size).toBe(1)
for (const listener of appStateListeners.get('change') ?? []) {
listener(next)
}
}
function emitNetwork(state: NetworkSnapshot): void {
expect(networkListeners.size).toBe(1)
for (const listener of networkListeners) {
listener(state)
}
}
describe('subscribeConnectionRevivalTriggers', () => {
let nudge: ReturnType<typeof vi.fn>
let nudge: Mock<RevivalNudge>
beforeEach(() => {
vi.clearAllMocks()
appStateListener = null
networkListener = null
appStateListeners.clear()
networkListeners.clear()
readNetworkState = () => Promise.resolve(seededNetworkState)
seededNetworkState = { isConnected: true, type: 'WIFI' }
nudge = vi.fn()
nudge = vi.fn<RevivalNudge>()
})
afterEach(() => {
for (const unsubscribe of subscriptions) {
unsubscribe()
}
subscriptions.clear()
expect([...appStateListeners.values()].every((listeners) => listeners.size === 0)).toBe(true)
expect(networkListeners.size).toBe(0)
recordSocketDeadlineAppState('active')
vi.restoreAllMocks()
})
it('nudges when the app returns to the foreground, not on background', async () => {
await subscribeAndSeed(nudge)
appStateListener?.('background')
emitAppState('background')
expect(nudge).not.toHaveBeenCalled()
appStateListener?.('active')
emitAppState('active')
expect(nudge).toHaveBeenCalledTimes(1)
expect(nudge).toHaveBeenLastCalledWith('app-resume')
})
it('keeps elapsed background time when a device sleep pauses the monotonic clock', async () => {
@@ -65,12 +115,13 @@ describe('subscribeConnectionRevivalTriggers', () => {
const unsubscribe = await subscribeAndSeed(nudge)
const elapsed = startSocketDeadlineClock()
try {
appStateListener?.('background')
emitAppState('background')
wall += 3 * 60 * 60_000
monotonic += 12_000
appStateListener?.('active')
emitAppState('active')
expect(elapsed()).toBe(3 * 60 * 60_000)
expect(nudge).toHaveBeenCalledTimes(1)
expect(nudge).toHaveBeenLastCalledWith('app-resume')
} finally {
unsubscribe()
wallClock.mockRestore()
@@ -80,37 +131,51 @@ describe('subscribeConnectionRevivalTriggers', () => {
it('nudges when the network comes back online', async () => {
await subscribeAndSeed(nudge)
networkListener?.({ isConnected: false, type: 'NONE' })
emitNetwork({ isConnected: false, type: 'NONE' })
expect(nudge).not.toHaveBeenCalled()
networkListener?.({ isConnected: true, type: 'WIFI' })
emitNetwork({ isConnected: true, type: 'WIFI' })
expect(nudge).toHaveBeenCalledTimes(1)
expect(nudge).toHaveBeenLastCalledWith('network-change')
})
it('nudges when the app started offline and the first event is the recovery', async () => {
seededNetworkState = { isConnected: false, type: 'NONE' }
await subscribeAndSeed(nudge)
networkListener?.({ isConnected: true, type: 'WIFI' })
emitNetwork({ isConnected: true, type: 'WIFI' })
expect(nudge).toHaveBeenCalledTimes(1)
expect(nudge).toHaveBeenLastCalledWith('network-change')
})
it('nudges on a Wi-Fi to cellular handoff that never reports offline', async () => {
await subscribeAndSeed(nudge)
networkListener?.({ isConnected: true, type: 'CELLULAR' })
emitNetwork({ isConnected: true, type: 'CELLULAR' })
expect(nudge).toHaveBeenCalledTimes(1)
expect(nudge).toHaveBeenLastCalledWith('network-change')
})
it('stays quiet when the network state matches the seeded baseline', async () => {
await subscribeAndSeed(nudge)
networkListener?.({ isConnected: true, type: 'WIFI' })
networkListener?.({ isConnected: true, type: 'WIFI' })
emitNetwork({ isConnected: true, type: 'WIFI' })
emitNetwork({ isConnected: true, type: 'WIFI' })
expect(nudge).not.toHaveBeenCalled()
})
it('ignores a stale seed that resolves after unsubscribe', async () => {
seededNetworkState = { isConnected: false, type: 'NONE' }
const unsubscribe = subscribeConnectionRevivalTriggers(nudge)
unsubscribe()
it('keeps a newer network event when a stale seed resolves later', async () => {
let finishSeed: (state: NetworkSnapshot) => void = () => {}
const pendingSeed = new Promise<NetworkSnapshot>((resolve) => {
finishSeed = resolve
})
readNetworkState = () => pendingSeed
const unsubscribe = subscribe(nudge)
emitNetwork({ isConnected: true, type: 'CELLULAR' })
finishSeed({ isConnected: true, type: 'WIFI' })
await Promise.resolve()
emitNetwork({ isConnected: true, type: 'CELLULAR' })
expect(nudge).not.toHaveBeenCalled()
emitNetwork({ isConnected: true, type: 'WIFI' })
expect(nudge).toHaveBeenCalledTimes(1)
expect(nudge).toHaveBeenLastCalledWith('network-change')
unsubscribe()
expect(appStateRemove).toHaveBeenCalledTimes(1)
expect(networkRemove).toHaveBeenCalledTimes(1)
})
@@ -1,8 +1,9 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const asyncStorageMock = vi.hoisted(() => ({
getItem: vi.fn(),
setItem: vi.fn()
values: new Map<string, string>(),
getItem: vi.fn<(key: string) => Promise<string | null>>(),
setItem: vi.fn<(key: string, value: string) => Promise<void>>()
}))
vi.mock('@react-native-async-storage/async-storage', () => ({
@@ -14,36 +15,48 @@ import { loadHostAppVersion, recordHostAppVersion } from './host-app-version-sto
describe('host app version store', () => {
beforeEach(() => {
vi.clearAllMocks()
asyncStorageMock.getItem.mockResolvedValue(null)
asyncStorageMock.setItem.mockResolvedValue(undefined)
asyncStorageMock.values.clear()
asyncStorageMock.getItem.mockReset().mockImplementation(async (key) => {
return asyncStorageMock.values.get(key) ?? null
})
asyncStorageMock.setItem.mockReset().mockImplementation(async (key, value) => {
asyncStorageMock.values.set(key, value)
})
})
it('persists a bounded version once per host', async () => {
asyncStorageMock.getItem.mockResolvedValueOnce(null).mockResolvedValueOnce('1.4.191')
await recordHostAppVersion('host-1', ' 1.4.191 ')
await recordHostAppVersion('host-1', '1.4.191')
await recordHostAppVersion('host-2', '1.4.188')
expect(asyncStorageMock.setItem).toHaveBeenCalledOnce()
await expect(loadHostAppVersion('host-1')).resolves.toBe('1.4.191')
await expect(loadHostAppVersion('host-2')).resolves.toBe('1.4.188')
expect(asyncStorageMock.setItem).toHaveBeenCalledTimes(2)
expect(asyncStorageMock.setItem).toHaveBeenCalledWith(
'orca:host-app-version:v1:host-1',
'1.4.191'
)
expect(asyncStorageMock.setItem).toHaveBeenCalledWith(
'orca:host-app-version:v1:host-2',
'1.4.188'
)
})
it('loads a previously observed version without requiring a live host', async () => {
asyncStorageMock.getItem.mockResolvedValue('1.4.188')
asyncStorageMock.values.set('orca:host-app-version:v1:host-1', '1.4.188')
await expect(loadHostAppVersion('host-1')).resolves.toBe('1.4.188')
await expect(loadHostAppVersion('host-2')).resolves.toBeNull()
expect(asyncStorageMock.setItem).not.toHaveBeenCalled()
})
it('rejects multiline, empty, and oversized host-provided values', async () => {
expect(normalizeHostAppVersion('1.4.191\nInjected: value')).toBeNull()
expect(normalizeHostAppVersion(' ')).toBeNull()
expect(normalizeHostAppVersion('x'.repeat(65))).toBeNull()
await recordHostAppVersion('host-1', '1.4.191\nInjected: value')
for (const value of ['1.4.191\nInjected: value', ' ', 'x'.repeat(65)]) {
expect(normalizeHostAppVersion(value)).toBeNull()
await recordHostAppVersion('host-1', value)
}
expect(asyncStorageMock.getItem).not.toHaveBeenCalled()
expect(asyncStorageMock.setItem).not.toHaveBeenCalled()
})
})
@@ -1,16 +1,17 @@
import { describe, expect, it } from 'vitest'
import { selectConnectableHostProfiles, sortHostsByLastConnected } from './host-catalog-selection'
import type { HostCatalogEntry, HostProfile } from './types'
import type { HostCatalogEntry, HostProfile, StoredHostProfile } from './types'
const profile: HostProfile = {
const stored: StoredHostProfile = {
id: 'host-ready',
name: 'Ready',
endpoint: 'ws://127.0.0.1:1',
deviceToken: 'token',
publicKeyB64: 'key-ready',
lastConnected: 0
}
const profile: HostProfile = { ...stored, deviceToken: 'token' }
function unavailable(
id: string,
credentialStatus: 'temporarily-unavailable' | 'missing'
@@ -30,7 +31,7 @@ describe('selectConnectableHostProfiles', () => {
it('excludes unavailable catalog entries from connection priming', () => {
expect(
selectConnectableHostProfiles([
{ ...profile, credentialStatus: 'ready', profile },
{ ...stored, credentialStatus: 'ready', profile },
unavailable('host-locked', 'temporarily-unavailable'),
unavailable('host-missing', 'missing')
])
@@ -57,6 +57,7 @@ describe('host descriptor store', () => {
await act(async () => {
renderer = create(createElement(Row, { hostId: 'host-1' }))
})
expect(seen['host-1']?.at(-1)).toEqual({ machineName: 'Desk', platform: 'darwin' })
const renders = seen['host-1']?.length ?? 0
await act(async () => {
recordHostDescriptor('host-1', { machineName: 'Desk', platform: 'darwin' })
@@ -81,6 +82,8 @@ describe('host descriptor store', () => {
recordHostDescriptor('host-1', { machineName: 'Desk', platform: 'darwin' })
})
expect(seen['host-1']?.at(-1)).toEqual({ machineName: 'Desk', platform: 'darwin' })
expect(seen['host-2']?.at(-1)).toBeNull()
expect(seen['host-2']).toHaveLength(host2Renders)
})
})
@@ -1,10 +1,26 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { dropSharedHostListLoad, shareHostListLoad } from './host-list-load-sharing'
import {
dropSharedHostListLoad,
shareHostListLoad,
type HostListSnapshot
} from './host-list-load-sharing'
import type { HostProfile } from './types'
function snapshot(id: string): HostListSnapshot {
const stored = {
id,
name: id,
endpoint: 'ws://127.0.0.1:1',
publicKeyB64: `key-${id}`,
lastConnected: 0
}
const profile: HostProfile = { ...stored, deviceToken: 'token' }
return { catalog: [{ ...stored, credentialStatus: 'ready', profile }], profiles: [profile] }
}
function deferred() {
let resolve: (hosts: HostProfile[]) => void = () => {}
const promise = new Promise<HostProfile[]>((res) => {
let resolve: (hosts: HostListSnapshot) => void = () => {}
const promise = new Promise<HostListSnapshot>((res) => {
resolve = res
})
return { promise, resolve }
@@ -21,42 +37,60 @@ describe('shareHostListLoad', () => {
const first = shareHostListLoad(load)
const second = shareHostListLoad(load)
pass.resolve([])
const value = snapshot('host-ready')
pass.resolve(value)
expect(load).toHaveBeenCalledTimes(1)
expect(await first).toBe(await second)
expect(await first).toEqual(value)
const next = shareHostListLoad(load)
expect(load).toHaveBeenCalledTimes(2)
expect(await next).toEqual(value)
})
it('starts a fresh pass for callers that arrive after a write dropped it', async () => {
const stale = deferred()
const fresh = deferred()
const load = vi.fn().mockReturnValueOnce(stale.promise).mockReturnValueOnce(fresh.promise)
const load = vi
.fn<() => Promise<HostListSnapshot>>()
.mockReturnValueOnce(stale.promise)
.mockReturnValueOnce(fresh.promise)
const before = shareHostListLoad(load)
dropSharedHostListLoad()
const after = shareHostListLoad(load)
stale.resolve([])
fresh.resolve([])
const staleValue = snapshot('host-stale')
const freshValue = snapshot('host-fresh')
stale.resolve(staleValue)
fresh.resolve(freshValue)
expect(load).toHaveBeenCalledTimes(2)
expect(await after).not.toBe(await before)
expect(await before).toEqual(staleValue)
expect(await after).toEqual(freshValue)
})
it('keeps the replacement pass on offer when the dropped one settles late', async () => {
const stale = deferred()
const fresh = deferred()
const load = vi.fn().mockReturnValueOnce(stale.promise).mockReturnValueOnce(fresh.promise)
const load = vi
.fn<() => Promise<HostListSnapshot>>()
.mockReturnValueOnce(stale.promise)
.mockReturnValueOnce(fresh.promise)
const before = shareHostListLoad(load)
dropSharedHostListLoad()
const replacement = shareHostListLoad(load)
// The dropped pass settles after its replacement was registered.
stale.resolve([])
await before
const staleValue = snapshot('host-stale')
stale.resolve(staleValue)
expect(await before).toEqual(staleValue)
expect(shareHostListLoad(load)).toBe(replacement)
expect(load).toHaveBeenCalledTimes(2)
fresh.resolve([])
await replacement
const freshValue = snapshot('host-fresh')
fresh.resolve(freshValue)
expect(await replacement).toEqual(freshValue)
})
})
+3 -1
View File
@@ -7,13 +7,15 @@ describe('getNextHostNameFromHosts', () => {
getNextHostNameFromHosts([
{ name: 'Host 2' },
{ name: 'Custom workstation' },
{ name: 'Host 99 extra' },
{ name: 'My Host 100' },
{ name: 'Host 7' },
{ name: 'Host 3' }
])
).toBe('Host 8')
})
it('handles very large host lists without spreading number arrays', () => {
it('handles 130,000 host names', () => {
const hosts = Array.from({ length: 130_000 }, (_, index) => ({
name: `Host ${index + 1}`
}))
@@ -19,6 +19,8 @@ describe('persisted connection log store', () => {
})
afterEach(() => {
vi.restoreAllMocks()
vi.clearAllTimers()
vi.useRealTimers()
vi.resetModules()
})
@@ -40,7 +42,15 @@ describe('persisted connection log store', () => {
message: 'Relay: active relay session failed'
}
]
vi.mocked(AsyncStorage.getItem).mockResolvedValue(JSON.stringify(stored))
const key = 'orca.mobile.connection-log.v1.host-a'
const storage = new Map([[key, JSON.stringify(stored)]])
vi.mocked(AsyncStorage.getItem).mockImplementation(
async (requestedKey) => storage.get(requestedKey) ?? null
)
vi.mocked(AsyncStorage.setItem).mockImplementation(async (requestedKey, value) => {
storage.set(requestedKey, value)
})
vi.spyOn(Math, 'random').mockReturnValueOnce(0.25).mockReturnValue(0.5)
vi.resetModules()
const { connectionLogStore, recordConnectionClientSessionStart } =
await import('./persisted-connection-log-store')
@@ -48,10 +58,17 @@ describe('persisted connection log store', () => {
recordConnectionClientSessionStart('host-a')
await connectionLogStore.hydrate('host-a')
expect(connectionLogStore.get('host-a').map((entry) => entry.code)).toEqual([
const expected = connectionLogStore.get('host-a')
expect(expected.map((entry) => entry.code)).toEqual([
'client-session-started',
'relay-session-failed',
'client-session-started'
])
await vi.advanceTimersByTimeAsync(0)
expect(storage.get(key)).toBe(JSON.stringify(expected))
vi.resetModules()
const reloaded = await import('./persisted-connection-log-store')
await reloaded.connectionLogStore.hydrate('host-a')
expect(reloaded.connectionLogStore.get('host-a')).toEqual(expected)
})
})
@@ -10,16 +10,17 @@ type ProbeOutcome = RpcResponse | Error
function makeClient(outcomes: ProbeOutcome[]): {
client: UnvalidatedRpcRequestPort
calls: () => number
requests: () => Parameters<UnvalidatedRpcRequestPort['sendRequest']>[]
} {
let calls = 0
const client: UnvalidatedRpcRequestPort = {
sendRequest: () => {
const client = {
sendRequest: vi.fn<UnvalidatedRpcRequestPort['sendRequest']>(() => {
const outcome = outcomes[Math.min(calls, outcomes.length - 1)]!
calls += 1
return outcome instanceof Error ? Promise.reject(outcome) : Promise.resolve(outcome)
}
})
}
return { client, calls: () => calls }
return { client, calls: () => calls, requests: () => client.sendRequest.mock.calls }
}
const ok = (result: unknown): RpcResponse => ({
@@ -35,36 +36,50 @@ async function flushMicrotasks(): Promise<void> {
}
describe('startRuntimeStatusProbe', () => {
let cancelProbe: (() => void) | undefined
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
cancelProbe?.()
cancelProbe = undefined
vi.useRealTimers()
})
it('delivers the parsed status once and stops', async () => {
const { client, calls } = makeClient([
const { client, calls, requests } = makeClient([
ok({ machineName: 'm4airs-Air', hostPlatform: 'darwin', capabilities: ['a.v1'] })
])
const seen: (HostStatusReply | null)[] = []
const cancel = startRuntimeStatusProbe(client, (status) => seen.push(status))
cancelProbe = startRuntimeStatusProbe(client, (status) => seen.push(status))
await flushMicrotasks()
expect(seen).toHaveLength(1)
expect(seen[0]).toMatchObject({ machineName: 'm4airs-Air', hostPlatform: 'darwin' })
expect(seen[0]).toEqual({
machineName: 'm4airs-Air',
hostPlatform: 'darwin',
capabilities: ['a.v1']
})
expect(requests()).toEqual([['status.get']])
expect(calls()).toBe(1)
await vi.advanceTimersByTimeAsync(60_000)
expect(calls()).toBe(1)
cancel()
cancelProbe()
})
it('delivers null for an answer this build cannot decode, without retrying', async () => {
const { client, calls } = makeClient([ok(null)])
const { client, calls, requests } = makeClient([ok(null)])
const seen: (HostStatusReply | null)[] = []
const cancel = startRuntimeStatusProbe(client, (status) => seen.push(status))
const failed = vi.fn()
cancelProbe = startRuntimeStatusProbe(client, (status) => seen.push(status), failed)
await flushMicrotasks()
expect(seen).toEqual([null])
await vi.advanceTimersByTimeAsync(60_000)
expect(seen).toEqual([null])
expect(failed).not.toHaveBeenCalled()
expect(requests()).toEqual([['status.get']])
expect(vi.getTimerCount()).toBe(0)
expect(calls()).toBe(1)
cancel()
cancelProbe()
})
it('retries a refusal and a cutover rejection until a status lands', async () => {
@@ -74,49 +89,66 @@ describe('startRuntimeStatusProbe', () => {
error: { code: 'unavailable', message: 'no' },
_meta: { runtimeId: 'r1' }
}
const { client, calls } = makeClient([
const { client, calls, requests } = makeClient([
refusal,
new LogicalClientCutoverError(),
ok({ machineName: 'Studio' })
])
const seen: (HostStatusReply | null)[] = []
const cancel = startRuntimeStatusProbe(client, (status) => seen.push(status))
cancelProbe = startRuntimeStatusProbe(client, (status) => seen.push(status))
await flushMicrotasks()
expect(seen).toEqual([])
await vi.advanceTimersByTimeAsync(1_000)
expect(calls()).toBe(1)
await vi.advanceTimersByTimeAsync(999)
expect(calls()).toBe(1)
await vi.advanceTimersByTimeAsync(1)
await flushMicrotasks()
await vi.advanceTimersByTimeAsync(250)
expect(calls()).toBe(2)
expect(seen).toEqual([])
await vi.advanceTimersByTimeAsync(249)
expect(calls()).toBe(2)
await vi.advanceTimersByTimeAsync(1)
await flushMicrotasks()
expect(seen).toHaveLength(1)
expect(seen[0]).toMatchObject({ machineName: 'Studio' })
expect(calls()).toBe(3)
cancel()
expect(requests()).toEqual([['status.get'], ['status.get'], ['status.get']])
expect(vi.getTimerCount()).toBe(0)
cancelProbe()
})
it('reports each failed attempt once its retry is scheduled, and never an answer', async () => {
const { client } = makeClient([
it('reports failed attempts after scheduling retries and cancels pending work', async () => {
const { client, calls, requests } = makeClient([
new Error('request timed out'),
new LogicalClientCutoverError(),
ok(null)
new LogicalClientCutoverError()
])
const seen: (HostStatusReply | null)[] = []
let failed = 0
const cancel = startRuntimeStatusProbe(
const failedTimerCounts: number[] = []
cancelProbe = startRuntimeStatusProbe(
client,
(status) => seen.push(status),
() => {
failed += 1
failedTimerCounts.push(vi.getTimerCount())
}
)
await flushMicrotasks()
expect(failed).toBe(1)
expect(failedTimerCounts).toEqual([1])
expect(vi.getTimerCount()).toBe(1)
await vi.advanceTimersByTimeAsync(1_000)
await flushMicrotasks()
expect(failed).toBe(2)
await vi.advanceTimersByTimeAsync(250)
await flushMicrotasks()
expect(seen).toEqual([null])
expect(failedTimerCounts).toEqual([1, 1])
expect(vi.getTimerCount()).toBe(1)
cancelProbe()
expect(vi.getTimerCount()).toBe(0)
await vi.advanceTimersByTimeAsync(60_000)
expect(seen).toEqual([])
expect(failed).toBe(2)
cancel()
expect(failedTimerCounts).toEqual([1, 1])
expect(calls()).toBe(2)
expect(requests()).toEqual([['status.get'], ['status.get']])
})
})
-2
View File
@@ -85,10 +85,8 @@ src/test-support/rpc-recording/native-mounting-substitutes.test.ts
src/test-support/rpc-recording/operation-module-loader.test.ts
src/test-support/rpc-recording/screen-native-substitutes.test.ts
src/transport/client-context.test.ts
src/transport/connection-revival-triggers.test.ts
src/transport/host-credential-cleanup.test.ts
src/transport/host-edit-navigation.test.ts
src/transport/host-list-load-sharing.test.ts
src/transport/host-removal-lifecycle.test.ts
src/transport/host-status-gates.test.ts
src/transport/host-store.test.ts
+26 -5
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import {
beginAutomationDispatchTokenUse,
createAutomationDispatchToken,
getAutomationDispatchTokenCountForTests,
MAX_AUTOMATION_DISPATCH_TOKENS
@@ -7,10 +8,30 @@ import {
describe('automation dispatch tokens', () => {
it('bounds distinct token churn', () => {
for (let index = 0; index < MAX_AUTOMATION_DISPATCH_TOKENS + 4; index += 1) {
createAutomationDispatchToken(`automation-${index}`, `run-${index}`)
}
vi.useFakeTimers({ toFake: ['Date'] })
vi.setSystemTime(1_700_000_000_000)
try {
const tokens = Array.from({ length: MAX_AUTOMATION_DISPATCH_TOKENS + 4 }, (_, index) => {
const automationId = `automation-${index}`
const runId = `run-${index}`
return {
automationId,
runId,
token: createAutomationDispatchToken(automationId, runId)
}
})
expect(getAutomationDispatchTokenCountForTests()).toBe(MAX_AUTOMATION_DISPATCH_TOKENS)
expect(getAutomationDispatchTokenCountForTests()).toBe(MAX_AUTOMATION_DISPATCH_TOKENS)
for (const token of tokens.slice(0, 4)) {
expect(beginAutomationDispatchTokenUse({ ...token, reservationId: 'request-1' })).toBe(
false
)
}
for (const token of tokens.slice(4)) {
expect(beginAutomationDispatchTokenUse({ ...token, reservationId: 'request-1' })).toBe(true)
}
} finally {
vi.useRealTimers()
}
})
})
+22 -19
View File
@@ -568,27 +568,30 @@ describe('collectMemorySnapshot on Windows', () => {
expect(snapshot.totalMemory).toBe(52428800)
})
it('carries no commit metric on Unix, where ps has no committed-bytes column', async () => {
vi.spyOn(os, 'platform').mockReturnValue('darwin')
mockPsResponse('10 1 0 1024')
listRegisteredPtysMock.mockReturnValue([
{
ptyId: 'pty-1',
worktreeId: 'repo-1::/repo',
sessionId: 'session-1',
paneKey: 'pane-1',
pid: 10
}
])
const { collectMemorySnapshot } = await loadCollector()
it.each(['darwin', 'linux'] as const)(
'reports RSS without commit values on %s',
async (platform) => {
vi.spyOn(os, 'platform').mockReturnValue(platform)
mockPsResponse('10 1 0 1024')
listRegisteredPtysMock.mockReturnValue([
{
ptyId: 'pty-1',
worktreeId: 'repo-1::/repo',
sessionId: 'session-1',
paneKey: 'pane-1',
pid: 10
}
])
const { collectMemorySnapshot } = await loadCollector()
const snapshot = await collectMemorySnapshot(emptyStore)
const snapshot = await collectMemorySnapshot(emptyStore)
expect(snapshot.processMemoryMetric).toBe('rss')
expect(snapshot.processCommitMetric).toBeUndefined()
expect(snapshot.totalPrivateMemory).toBeUndefined()
expect(snapshot.worktrees[0].sessions[0].privateMemory).toBeUndefined()
})
expect(snapshot.processMemoryMetric).toBe('rss')
expect(snapshot.processCommitMetric).toBeUndefined()
expect(snapshot.totalPrivateMemory).toBeUndefined()
expect(snapshot.worktrees[0].sessions[0].privateMemory).toBeUndefined()
}
)
it("attributes a shared ancestor's commit to one PTY only", async () => {
vi.spyOn(os, 'platform').mockReturnValue('win32')
@@ -1,13 +0,0 @@
import { describe, expect, it } from 'vitest'
import { getProcessMemoryMetric } from './process-memory-metric'
describe('process memory metric', () => {
it('declares RSS for Unix process sweeps', () => {
expect(getProcessMemoryMetric('darwin')).toBe('rss')
expect(getProcessMemoryMetric('linux')).toBe('rss')
})
it('declares working set for Windows process sweeps', () => {
expect(getProcessMemoryMetric('win32')).toBe('working-set')
})
})
@@ -1,3 +1,7 @@
import type {
AgentChildWorkStopAcknowledgement,
AgentSessionBackgroundTaskStops
} from '../../../shared/agent-child-work-stop-targets'
// Chat Stop ends the captured child for every provider; default prompt Cancel interrupts its turn.
import type {
@@ -17,6 +21,15 @@ export type AgentSessionCancelOutcome = {
export type AgentSessionPromptCancelRoute = { kind: 'dismiss' } | { kind: 'stop' }
export type StructuredAgentSessionAdapterStop = {
/** Stop named tasks only; acknowledge each accepted stop immediately so later batch failures cannot erase it. */
stopBackgroundTasks?(input: {
sessionId: string
fence: number
taskIds: readonly string[]
acknowledge?: (acknowledgement: AgentChildWorkStopAcknowledgement) => void
}): Promise<{ cancelled: boolean; stillRunning?: true }>
/** Undefined when no live session exposes stop support. */
backgroundTaskStops?(sessionId: string): AgentSessionBackgroundTaskStops | undefined
/** Cancel only the requested turn; absent turnId also covers dispatches whose turns have not opened. */
cancelTurn(input: {
sessionId: string
@@ -7,10 +7,6 @@ import type {
} from '../../../shared/agent-session-rewind'
// The host owns journal/lease authority; adapters report delivery uncertainty without guessing.
import type {
AgentChildWorkStopAcknowledgement,
AgentSessionBackgroundTaskStops
} from '../../../shared/agent-child-work-stop-targets'
import type {
AgentJournalAnsweredTurnIdentity,
AgentJournalItemIdentity,
@@ -289,15 +285,6 @@ export type StructuredAgentSessionAdapter = StructuredAgentSessionAdapterStop &
replacesGoal: boolean
signal?: AbortSignal
}): Promise<{ ok: true } | { ok: false; rejected: string }>
/** Stop named tasks only; acknowledge each accepted stop immediately so later batch failures cannot erase it. */
stopBackgroundTasks?(input: {
sessionId: string
fence: number
taskIds: readonly string[]
acknowledge?: (acknowledgement: AgentChildWorkStopAcknowledgement) => void
}): Promise<{ cancelled: boolean; stillRunning?: true }>
/** Undefined when no live session exposes stop support. */
backgroundTaskStops?(sessionId: string): AgentSessionBackgroundTaskStops | undefined
/** Derive unanswered retained work from the live child; false without one. */
holdsDispatch?(sessionId: string): boolean
/** Captures only this input's pending answer/output in the supplied live acquisition. */
+39 -4
View File
@@ -1,10 +1,45 @@
import { describe, expect, it } from 'vitest'
import { requireSeam } from './connector'
import { eraseRpcMethods, type RpcContext } from '../core'
import { CONNECTOR_METHODS } from './connector'
const methods = eraseRpcMethods(CONNECTOR_METHODS)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: disabled handlers must refuse at this gate before reading any other runtime service.
const context = { runtime: { getExternalConversationSeam: () => null } } as RpcContext
describe('connector RPC methods', () => {
it('are refused while the host has connectors off', () => {
expect(() => requireSeam({ getExternalConversationSeam: () => null })).toThrow(
'connectors_disabled'
it.each([
[
'connector.start',
{
source: 'slack',
externalId: 'thread-1',
deliveryId: 'delivery-1',
agent: 'codex',
target: { kind: 'existing', worktree: 'workspace-1' },
prompt: 'Help with this thread'
}
],
[
'connector.send',
{ source: 'slack', externalId: 'thread-1', deliveryId: 'delivery-1', text: 'Follow up' }
],
['connector.pendingPrompt', { source: 'slack', externalId: 'thread-1' }],
[
'connector.answer',
{ source: 'slack', externalId: 'thread-1', promptId: 'prompt-1', optionId: 'option-1' }
],
['connector.cancel', { source: 'slack', externalId: 'thread-1', deliveryId: 'delivery-1' }],
['connector.watch', { forMs: 0 }]
] as const)('%s refuses requests while the host has connectors off', async (name, input) => {
const method = methods.find((candidate) => candidate.name === name)
if (!method?.params) {
throw new Error(`Missing connector method: ${name}`)
}
const params = method.params.parse(input)
await expect(Promise.resolve().then(() => method.handler(params, context))).rejects.toThrow(
new Error('connectors_disabled')
)
})
})
+19 -4
View File
@@ -6,12 +6,27 @@ import {
describe('sidecar snapshot queues', () => {
it('releases completed per-file queue entries', async () => {
await Promise.all(
Array.from({ length: 600 }, (_, index) =>
withSidecarSnapshotQueue(`snapshot-${index}`, async () => undefined)
)
const gate = Promise.withResolvers<void>()
let started = 0
const tasks = Array.from({ length: 600 }, (_, index) =>
withSidecarSnapshotQueue(`snapshot-${index}`, async () => {
started += 1
await gate.promise
return `result-${index}`
})
)
try {
await Promise.resolve()
expect(started).toBe(600)
expect(_getSidecarSnapshotPendingFileCountForTests()).toBe(600)
} finally {
gate.resolve()
}
expect(await Promise.all(tasks)).toEqual(
Array.from({ length: 600 }, (_, index) => `result-${index}`)
)
await Promise.resolve()
expect(_getSidecarSnapshotPendingFileCountForTests()).toBe(0)
})
@@ -1,8 +1,6 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('@/lib/new-workspace', () => ({
isGitLabIssueUrl: (url: string) => url.includes('gitlab.example')
}))
vi.mock('@/lib/new-workspace', () => import('./linked-work-item-provider'))
vi.mock('@/i18n/i18n', () => ({
translate: (_key: string, fallback: string) => fallback
@@ -11,7 +9,7 @@ vi.mock('@/i18n/i18n', () => ({
import { gitLabIssueNumber } from './launch-work-item-direct-messages'
describe('gitLabIssueNumber', () => {
it('preserves zero-valued issue numbers when the URL is a GitLab issue URL', () => {
it('preserves GitLab issue numbers and rejects other work items', () => {
expect(
gitLabIssueNumber({
type: 'issue',
@@ -19,5 +17,47 @@ describe('gitLabIssueNumber', () => {
url: 'https://gitlab.example/acme/project/-/issues/0'
})
).toBe(0)
expect(
gitLabIssueNumber({
type: 'issue',
number: 7,
url: 'https://gitlab.com/acme/project/-/work_items/7'
})
).toBe(7)
expect(
gitLabIssueNumber({
type: 'mr',
number: 7,
url: 'https://gitlab.example/acme/project/-/issues/7'
})
).toBeUndefined()
expect(
gitLabIssueNumber({
type: 'issue',
number: 7,
url: 'https://github.com/acme/project/issues/7'
})
).toBeUndefined()
expect(
gitLabIssueNumber({
type: 'issue',
number: 7,
url: 'https://gitlab.example/acme/project/-/merge_requests/7'
})
).toBeUndefined()
expect(
gitLabIssueNumber({
type: 'issue',
url: 'https://gitlab.example/acme/project/-/issues/7'
})
).toBeUndefined()
expect(
gitLabIssueNumber({
type: 'issue',
number: null,
url: 'https://gitlab.example/acme/project/-/issues/7'
})
).toBeUndefined()
expect(gitLabIssueNumber({ type: 'issue', number: 7 })).toBeUndefined()
})
})
@@ -7,6 +7,10 @@ import {
describe('workspace port group caches', () => {
it('returns stable empty references when no scan result exists', () => {
expect(getWorkspacePortsByWorktreeId(null).size).toBe(0)
expect(getWorkspacePortGroups(null)).toEqual([])
expect(getExternalWorkspacePorts(null)).toEqual([])
expect(getWorkspacePortsByWorktreeId(null)).toBe(getWorkspacePortsByWorktreeId(undefined))
expect(getWorkspacePortGroups(null)).toBe(getWorkspacePortGroups(undefined))
expect(getExternalWorkspacePorts(null)).toBe(getExternalWorkspacePorts(undefined))
@@ -1,19 +1,35 @@
import { describe, expect, it } from 'vitest'
import { createTestStore } from '@/store/slices/store-test-helpers'
import { createTestStore, makeTab } from '@/store/slices/store-test-helpers'
import { buildWorkspaceSessionPayload } from './workspace-session'
describe("the window's saved session", () => {
it("never carries main's resume outcomes, which are live state", () => {
const store = createTestStore()
const tab = makeTab({
id: 'tab-1',
worktreeId: 'wt-1',
ptyId: 'pty-1',
title: 'restored',
createdAt: 1
})
store.setState({
tabsByWorktree: { 'wt-1': [tab] },
terminalLayoutsByTabId: {
'tab-1': {
root: { type: 'leaf', leafId: 'leaf-1' },
activeLeafId: 'leaf-1',
expandedLeafId: null
}
},
terminalPaneResumeOutcomesByPaneKey: {
'tab-1:leaf-1': { ptyId: 'pty-1', ptyGeneration: 3, outcome: 'restored' }
}
})
const saved = JSON.stringify(buildWorkspaceSessionPayload(store.getState()))
const saved = buildWorkspaceSessionPayload(store.getState())
expect(saved).not.toContain('resumeOutcome')
expect(saved).not.toContain('restored')
expect(saved).not.toHaveProperty('terminalPaneResumeOutcomesByPaneKey')
expect(saved.tabsByWorktree).toEqual({ 'wt-1': [tab] })
expect(saved.terminalLayoutsByTabId).toEqual(store.getState().terminalLayoutsByTabId)
})
})
+8 -2
View File
@@ -149,7 +149,8 @@ describe('buildAgentChildRowModels: the display vocabulary', () => {
{ displayState: 'unverifiable', detail: { kind: 'no-update' } }
]
])('%s', (_name, views, expected) => {
expect(only(views)).toMatchObject(expected)
const row = only(views)
expect({ displayState: row.displayState, detail: row.detail }).toEqual(expected)
})
it('suppresses the tool line while monitoring, even when a stale operation arrives', () => {
@@ -356,7 +357,12 @@ describe('agentChildRowContextForParent: the reader clock', () => {
it("measures a mirrored parent on this machine's receipt clock, never the host's", () => {
const context = agentChildRowContextForParent(MIRRORED_PARENT, false)
expect(context).toMatchObject({ parentObservedAt: 1_190_000, hostClockOffsetMs: -20 * 60_000 })
expect(context).toEqual({
parentEvidenceFresh: false,
transportObservation: 'live',
parentObservedAt: 1_190_000,
hostClockOffsetMs: -20 * 60_000
})
const [legacy] = buildLegacyAgentChildRowModels(
[{ id: 'a', state: 'working', startedAt: 10 }],
context
+8 -10
View File
@@ -55,7 +55,7 @@ describe('fields the launch fingerprint covers', () => {
)
})
it('separates two launches that reserved different panes', () => {
it('separates two launches that reserved different panes or sessions', () => {
// The key is baked into the pane's env; replaying another pane's key strands the new reservation.
expect(computeAgentLaunchFingerprint({ ...BASE, paneKey: PANE_KEY })).not.toBe(
computeAgentLaunchFingerprint({ ...BASE, paneKey: OTHER_PANE_KEY })
@@ -63,6 +63,13 @@ describe('fields the launch fingerprint covers', () => {
expect(computeAgentLaunchFingerprint({ ...BASE, paneKey: PANE_KEY })).not.toBe(
computeAgentLaunchFingerprint(BASE)
)
const sessionId = '11111111-1111-4111-8111-111111111111'
expect(computeAgentLaunchFingerprint({ ...BASE, sessionId })).not.toBe(
computeAgentLaunchFingerprint({ ...BASE, sessionId: '22222222-2222-4222-8222-222222222222' })
)
expect(computeAgentLaunchFingerprint({ ...BASE, sessionId })).not.toBe(
computeAgentLaunchFingerprint(BASE)
)
})
it('separates a retry that picked another surface, or none, from the first launch', () => {
@@ -122,13 +129,4 @@ describe('compatibility with rows written before these fields existed', () => {
computeAgentLaunchFingerprint({ ...BASE, agentArgs: '--model opus', cwd: '/repo/packages/a' })
).toBe(beforePaneKey)
})
it('still matches when the caller sends only telemetry the digest excludes', () => {
const previousBuild = canonicalAgentSessionDigest({
method: 'agent.launch',
agent: BASE.agent,
target: BASE.target
})
expect(fingerprintOfWirePayload({ ...BASE, launchSource: 'quick_command' })).toBe(previousBuild)
})
})
+1 -1
View File
@@ -26,7 +26,7 @@ describe('repoIsRemote', () => {
expect(repoIsRemote({ connectionId: null, executionHostId: 'runtime:vm-1' })).toBe(false)
})
it('keeps plain local and WSL rows local', () => {
it('keeps plain local rows local', () => {
expect(repoIsRemote({ connectionId: null, executionHostId: null })).toBe(false)
expect(repoIsRemote({ connectionId: null, executionHostId: 'local' })).toBe(false)
})
+40 -27
View File
@@ -20,24 +20,23 @@ function waiting(interactivePrompt: string, toolName?: string) {
describe('readAgentPromptAnswers', () => {
it('reads an approval with the cards’ own allow and deny keys', () => {
const read = readAgentPromptAnswers(waiting(approval), 'claude', 'p1', true)
expect(read?.prompt).toEqual({
kind: 'approval',
promptId: 'p1',
tool: 'Bash',
summary: 'rm -rf build',
options: [
{ id: 'allow_once', label: 'Allow once' },
{ id: 'reject_once', label: 'Reject' }
]
})
expect(Object.fromEntries(read!.keysByOption)).toEqual({
allow_once: '1',
reject_once: '\x1b'
})
expect(
readAgentPromptAnswers(waiting(approval), 'codex', 'p1', true)?.keysByOption.get('allow_once')
).toBe('1')
for (const agent of ['claude', 'codex']) {
const read = readAgentPromptAnswers(waiting(approval), agent, 'p1', true)
expect(read?.prompt).toEqual({
kind: 'approval',
promptId: 'p1',
tool: 'Bash',
summary: 'rm -rf build',
options: [
{ id: 'allow_once', label: 'Allow once' },
{ id: 'reject_once', label: 'Reject' }
]
})
expect(Object.fromEntries(read!.keysByOption)).toEqual({
allow_once: '1',
reject_once: '\x1b'
})
}
})
it('reads a one-question single choice with each option’s selector key', () => {
@@ -58,7 +57,10 @@ describe('readAgentPromptAnswers', () => {
})
it('leaves to Orca what one keystroke cannot answer', () => {
const unsupported = { kind: 'unsupported', promptId: 'p' }
const unsupported = {
prompt: { kind: 'unsupported', promptId: 'p' },
keysByOption: new Map()
}
const cases = [
waiting(question({}, 2)),
waiting(question({ multiSelect: true })),
@@ -66,18 +68,29 @@ describe('readAgentPromptAnswers', () => {
waiting('not json'),
{ state: 'blocked' as const }
]
for (const row of cases) {
expect(readAgentPromptAnswers(row, 'claude', 'p', true)?.prompt).toEqual(unsupported)
for (const agent of ['claude', 'codex']) {
for (const row of cases) {
expect(readAgentPromptAnswers(row, agent, 'p', true)).toEqual(unsupported)
}
}
const asyncQuestion = JSON.stringify({
questions: [{ title: 'Which color?', options: ['Red', 'Blue'] }]
})
expect(
readAgentPromptAnswers(waiting(asyncQuestion, 'request_user_input_async'), 'codex', 'p', true)
).toEqual(unsupported)
})
it('leaves every prompt to Orca for an agent without hooks or known keys', () => {
expect(readAgentPromptAnswers(waiting(approval), 'claude', 'p', false)?.prompt.kind).toBe(
'unsupported'
)
expect(readAgentPromptAnswers(waiting(approval), 'aider', 'p', true)?.prompt.kind).toBe(
'unsupported'
)
const unsupported = {
prompt: { kind: 'unsupported', promptId: 'p' },
keysByOption: new Map()
}
expect(readAgentPromptAnswers(waiting(approval), 'claude', 'p', false)).toEqual(unsupported)
expect(readAgentPromptAnswers(waiting(approval), 'aider', 'p', true)).toEqual(unsupported)
expect(
readAgentPromptAnswers(waiting(question(), 'AskUserQuestion'), 'grok', 'p', true)
).toEqual(unsupported)
})
it('reads nothing while the agent is not waiting on the user', () => {
+16 -1
View File
@@ -25,10 +25,13 @@ describe('getAgentRowConversationName', () => {
})
it('keeps OpenCode semantic session titles whole', () => {
const tab = makeTab({ title: 'OC | build the release pipeline' })
const tab = makeTab({ title: 'OC | build the release pipeline', generatedTitle: 'Older title' })
expect(getAgentRowConversationName(tab, 'opencode', false)).toBe(
'OC | build the release pipeline'
)
expect(getAgentRowConversationName(tab, 'opencode', true)).toBe(
'OC | build the release pipeline'
)
})
it("shows the title owned by the row's provider session", () => {
@@ -40,6 +43,11 @@ describe('getAgentRowConversationName', () => {
expect(getAgentRowConversationName(tab, 'claude', true, undefined, 's1')).toBe(
'Fix the lease probe'
)
expect(getAgentRowConversationName(tab, 'claude', true, undefined, 's2')).toBe(
'Fix intake flow'
)
expect(getAgentRowConversationName(tab, 'codex', true, undefined, 's1')).toBe('Fix intake flow')
expect(getAgentRowConversationName(tab, 'claude', true)).toBe('Fix intake flow')
})
it('lets a manual rename and a quick command still outrank the provider title', () => {
@@ -208,6 +216,13 @@ describe('getAgentRowConversationName', () => {
})
it('rejects empty, glyph-only, and default terminal titles', () => {
expect(
getAgentRowConversationName(
makeTab({ title: 'Workbench', defaultTitle: 'Workbench' }),
'claude',
false
)
).toBeNull()
expect(getAgentRowConversationName(makeTab(), 'claude', false)).toBeNull()
expect(getAgentRowConversationName(makeTab({ title: '✳' }), 'claude', false)).toBeNull()
expect(
+18 -6
View File
@@ -72,7 +72,7 @@ Keep the raw preamble out of the sidebar.`
})
it('falls back to the raw prompt outside orchestration workers', () => {
expect(getAgentRowPrimaryText({ prompt: 'Fix checkout race' })).toBe('Fix checkout race')
expect(getAgentRowPrimaryText({ prompt: ' Fix checkout race\n ' })).toBe('Fix checkout race')
})
// Why: full preambles bury === TASK === after multi-KB CLI instructions. The
@@ -226,12 +226,10 @@ Checkout race body`,
}
})
).toBe('Fix checkout race')
})
it('ignores sticky orchestration for non-dispatch generated titles', () => {
expect(
getAgentRowGeneratedTitleText({
prompt: 'Refactor the auth middleware',
prompt:
'You are working inside Orca, a multi-agent IDE. Your task ID is: task-2 === TASK === New worker body',
orchestration: {
taskId: 'task-1',
dispatchId: 'ctx-1',
@@ -239,6 +237,20 @@ Checkout race body`,
displayName: 'Stale worker label'
}
})
).toBe('Refactor the auth middleware')
).toBe('New worker body')
})
it('ignores sticky orchestration for non-dispatch generated titles', () => {
expect(
getAgentRowGeneratedTitleText({
prompt: ' Refactor the auth middleware\n ',
orchestration: {
taskId: 'task-1',
dispatchId: 'ctx-1',
taskTitle: 'Stale task',
displayName: 'Stale worker label'
}
})
).toBe(' Refactor the auth middleware\n ')
})
})
@@ -40,6 +40,13 @@ describe('approvalBlockedPathToShow', () => {
})
it('checks only the text the card draws: a plan replaces the detail', () => {
expect(
approvalBlockedPathToShow({
blockedPath: '/repo/notes.md',
subject: { kind: 'plan', text: 'Read /repo/notes.md' },
detail: 'unused'
})
).toBeNull()
expect(
approvalBlockedPathToShow({
blockedPath: '/repo/plan.md',
@@ -72,7 +79,10 @@ describe('approvalBlockedPathToShow', () => {
).toBe('/home/me/secret.txt')
})
it('shows a path the text names only as the start of a longer name', () => {
it('distinguishes whole paths from longer names across all occurrences', () => {
expect(
approvalBlockedPathToShow({ blockedPath: '/srv/data', detail: 'ls /srv/data-old /srv/data' })
).toBeNull()
expect(
approvalBlockedPathToShow({ blockedPath: '/srv/data', detail: 'ls /srv/data-old/x' })
).toBe('/srv/data')
@@ -5,7 +5,10 @@ import {
pendingPromptsAllUnanswerableHere
} from './agent-session-approval-subject'
import type { AgentJournalItemBody } from './agent-session-journal-types'
import { AGENT_JOURNAL_APPROVAL_SUBJECT_KINDS } from './agent-session-journal-schemas'
import {
AGENT_JOURNAL_APPROVAL_SUBJECT_KINDS,
isAdmissibleAgentJournalItemBody
} from './agent-session-journal-schemas'
it('draws exactly the subject kinds the journal schema knows', () => {
expect([...DRAWN_APPROVAL_SUBJECT_KINDS].sort()).toEqual(
@@ -20,14 +23,14 @@ it("reads only a subject of a kind it does not draw as a newer build's", () => {
})
function pending(body: Record<string, unknown>): { body: AgentJournalItemBody } {
return JSON.parse(
JSON.stringify({
body: {
...body,
resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null }
}
})
)
const promptBody = {
...body,
resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null }
}
if (!isAdmissibleAgentJournalItemBody(promptBody)) {
throw new Error('Invalid pending prompt fixture')
}
return { body: promptBody }
}
const NEWER = pending({
kind: 'approval',
@@ -43,7 +46,7 @@ const PLAN = pending({
subject: { kind: 'plan', text: 'do it' },
options: [{ id: 'allow', label: 'Approve' }]
})
const QUESTION = pending({ kind: 'question', title: 'Which?', options: [] })
const QUESTION = pending({ kind: 'question', question: 'Which?', options: [] })
it('holds only while every pending prompt is an approval this build cannot answer', () => {
expect(pendingPromptsAllUnanswerableHere([])).toBe(false)
@@ -54,6 +57,8 @@ it('holds only while every pending prompt is an approval this build cannot answe
const cancelled = JSON.parse(
JSON.stringify(NEWER).replace('"state":"pending"', '"state":"cancelled"')
)
expect(pendingPromptsAllUnanswerableHere([cancelled, NEWER])).toBe(true)
expect(pendingPromptsAllUnanswerableHere([NEWER, cancelled])).toBe(true)
expect(pendingPromptsAllUnanswerableHere([cancelled, PLAN])).toBe(false)
expect(pendingPromptsAllUnanswerableHere([cancelled])).toBe(false)
})
@@ -31,7 +31,8 @@ describe('normalizeAgentSessionConversationName', () => {
})
it('bounds a pasted essay to the stored maximum', () => {
const normalized = normalizeAgentSessionConversationName('a'.repeat(1000))
const normalized = normalizeAgentSessionConversationName(`First ${'x'.repeat(994)}`)
expect(normalized).toBe(`First ${'x'.repeat(194)}`)
expect(normalized).toHaveLength(AGENT_SESSION_CONVERSATION_NAME_MAX_LENGTH)
expect(isAgentSessionConversationName(normalized)).toBe(true)
})
@@ -50,14 +51,9 @@ describe('isAgentSessionConversationName', () => {
})
describe('normalizeAgentSessionConversationName hostile text', () => {
it('strips control characters and bidi overrides', () => {
// U+202E renders what follows right-to-left, so a tab could show a label
// that reads as text the name does not contain.
expect(normalizeAgentSessionConversationName('Fix\u202Egnp.exe probe')).toBe(
'Fix gnp.exe probe'
)
it('strips controls and lone surrogates', () => {
expect(normalizeAgentSessionConversationName('Fix\u0007the probe')).toBe('Fix the probe')
expect(normalizeAgentSessionConversationName('Fix\u200Bthe probe')).toBe('Fix the probe')
expect(normalizeAgentSessionConversationName('Fix\uD800 the\uDC00 probe')).toBe('Fix the probe')
expect(normalizeAgentSessionConversationName('\u202E\u200B ')).toBeNull()
})
@@ -18,10 +18,21 @@ describe('agent-session grouped question answers', () => {
{ questionId: 'q2', optionIds: [], other: 'SSH host' }
]
it('round-trips grouped multi-select and free-text answers', () => {
it('round-trips grouped answers and rejects malformed wire payloads', () => {
expect(decodeAgentSessionQuestionAnswers(encodeAgentSessionQuestionAnswers(answers))).toEqual(
answers
)
const malformed = [
'not-grouped',
'question-group:%',
'question-group:not-json',
'question-group:{}',
`${GROUP_ANSWER_PREFIX}${JSON.stringify([answers[0], { questionId: 'q2', optionIds: [1] }])}`,
`${GROUP_ANSWER_PREFIX}${JSON.stringify([{ questionId: 'q1', optionIds: [], other: 1 }])}`
]
for (const value of malformed) {
expect(decodeAgentSessionQuestionAnswers(value)).toBeNull()
}
})
it('keeps compact answers readable by the original percent-decoding contract', () => {
@@ -48,9 +59,12 @@ describe('agent-session grouped question answers', () => {
expect(
decodeAgentSessionQuestionAnswers(encodeAgentSessionQuestionAnswers(unicodeAnswers))
).toEqual(unicodeAnswers)
expect(
decodeWithOriginalPercentDecoder(encodeAgentSessionQuestionAnswers(unicodeAnswers))
).toEqual(unicodeAnswers)
})
it("fits Claude's maximum choice group within a 512-character host response bound", () => {
it("keeps Claude's maximum choice group compact without losing selections", () => {
const maximumSelections = Array.from({ length: 4 }, (_, questionIndex) => ({
questionId: `q${questionIndex + 1}`,
optionIds: Array.from(
@@ -59,7 +73,10 @@ describe('agent-session grouped question answers', () => {
)
}))
expect(encodeAgentSessionQuestionAnswers(maximumSelections).length).toBeLessThanOrEqual(512)
const encoded = encodeAgentSessionQuestionAnswers(maximumSelections)
expect(encoded.length).toBeLessThanOrEqual(512)
expect(decodeAgentSessionQuestionAnswers(encoded)).toEqual(maximumSelections)
expect(decodeWithOriginalPercentDecoder(encoded)).toEqual(maximumSelections)
})
it('validates each grouped answer against its question shape', () => {
@@ -83,6 +100,24 @@ describe('agent-session grouped question answers', () => {
]
expect(isValidAgentSessionQuestionAnswers(questions, answers)).toBe(true)
expect(isValidAgentSessionQuestionAnswers(questions, answers.toReversed())).toBe(true)
const invalid = [
[],
[...answers, { questionId: 'q3', optionIds: [] }],
[answers[0]!, answers[0]!],
[{ ...answers[0]!, questionId: 'unknown' }, answers[1]!],
[{ ...answers[0]!, optionIds: [] }, answers[1]!],
[{ ...answers[0]!, other: 'not offered' }, answers[1]!]
]
for (const candidate of invalid) {
expect(isValidAgentSessionQuestionAnswers(questions, candidate)).toBe(false)
}
expect(
isValidAgentSessionQuestionAnswers(
[{ ...questions[0]!, multiSelect: false }, questions[1]!],
answers
)
).toBe(false)
expect(
isValidAgentSessionQuestionAnswers(questions, [
{ questionId: 'q1', optionIds: ['unknown'] },
@@ -14,18 +14,22 @@ describe('provider free-text question metadata', () => {
freeTextInput: { allowEmpty: true, multiline: true, initialValue: 'first\nsecond' }
}
it.each(['', ' ', ' first\nsecond\u2028third\u2029fourth '])(
'preserves the exact answer %j across the older wire form',
(other) => {
const answers = [{ questionId: 'editor:1', optionIds: [], other }]
const encoded = legacyAgentSessionSelectedOptionId(body, answers)
expect(encoded).not.toBeNull()
expect(legacyAgentSessionQuestionAnswers(body, encoded!)).toEqual(answers)
expect(isValidAgentSessionQuestionAnswers(agentSessionPromptQuestions(body), answers)).toBe(
true
)
}
)
it.each([
['', 'editor%3A1:'],
[' ', 'editor%3A1:%20%20'],
[
' first\nsecond\u2028third\u2029fourth ',
'editor%3A1:%20first%0Asecond%E2%80%A8third%E2%80%A9fourth%20'
]
])('preserves the exact answer %j across the older wire form', (other, expectedEncoded) => {
const answers = [{ questionId: 'editor:1', optionIds: [], other }]
const encoded = legacyAgentSessionSelectedOptionId(body, answers)
expect(encoded).toBe(expectedEncoded)
expect(legacyAgentSessionQuestionAnswers(body, encoded!)).toEqual(answers)
expect(isValidAgentSessionQuestionAnswers(agentSessionPromptQuestions(body), answers)).toBe(
true
)
})
it('requires an explicitly supplied answer even when empty is allowed', () => {
expect(
+10 -6
View File
@@ -46,15 +46,18 @@ describe('agent status subjects', () => {
makePtyRunAgentStatusSubject(scope({ wslDistro: 'Ubuntu' }), identity),
makePtyRunAgentStatusSubject(scope({ executionHostId: 'ssh:target-a' }), identity),
makePtyRunAgentStatusSubject(scope({ executionHostId: 'runtime:peer-a' }), identity),
makePtyRunAgentStatusSubject(
scope({ workspaceId: 'folder-1', workspaceKind: 'folder' }),
identity
),
makePtyRunAgentStatusSubject(scope({ workspaceId: 'workspace-2' }), identity),
makePtyRunAgentStatusSubject(scope({ workspaceKind: 'folder' }), identity),
makePtyAgentStatusSubject(scope(), identity),
makeStructuredAgentStatusSubject(scope(), identity)
]
expect(new Set(subjects.map(serializeAgentStatusSubject))).toHaveLength(subjects.length)
for (const [index, subject] of subjects.entries()) {
for (const other of subjects.slice(index + 1)) {
expect(agentStatusSubjectsEqual(subject, other)).toBe(false)
}
}
})
it('keeps the existing structured-session encoding byte-for-byte stable', () => {
@@ -69,7 +72,7 @@ describe('agent status subjects', () => {
it.each([
null,
{ kind: 'future' },
{ kind: 'future', ...scope(), paneKey: PANE_KEY },
{
kind: 'pty-run',
...scope(),
@@ -125,7 +128,8 @@ describe('agent status subjects', () => {
'',
'agent-status-subject-v1:not-json',
'agent-status-subject-v1:["pty-run","local",null,"workspace-1","git-worktree"]',
'agent-status-subject-v1:["unknown","local",null,"workspace-1","git-worktree","id"]'
'agent-status-subject-v1:["unknown","local",null,"workspace-1","git-worktree","id"]',
'agent-status-subject-v1:["pty-run","local",null,"workspace-1","git-worktree","run_11111111-1111-4111-8111-111111111111","extra"]'
])('rejects malformed serialized subject %#', (value) => {
expect(deserializeAgentStatusSubject(value)).toBeNull()
})
@@ -13,13 +13,24 @@ describe('structured agent session option codec', () => {
})
it('rejects string booleans on the UI side and non-canonical strings on the wire side', () => {
expect(encodeStructuredAgentSessionOptionValue('fastMode', 'true')).toBeNull()
expect(decodeStructuredAgentSessionOptionValue('fastMode', 'TRUE')).toBeNull()
expect(decodeStructuredAgentSessionOptionValue('fastMode', '1')).toBeNull()
for (const value of ['true', 'false']) {
expect(encodeStructuredAgentSessionOptionValue('fastMode', value)).toBeNull()
}
for (const value of ['TRUE', '1', 'False', ' true', 'false ', '', '0']) {
expect(decodeStructuredAgentSessionOptionValue('fastMode', value)).toBeNull()
}
})
it('passes existing string options through unchanged', () => {
expect(encodeStructuredAgentSessionOptionValue('model', 'gpt-example')).toBe('gpt-example')
expect(decodeStructuredAgentSessionOptionValue('effort', 'high')).toBe('high')
it('preserves string options in both directions and rejects boolean values', () => {
for (const [id, value] of [
['model', 'gpt-example'],
['effort', 'high'],
['serviceTier', 'flex']
] as const) {
expect(encodeStructuredAgentSessionOptionValue(id, value)).toBe(value)
expect(decodeStructuredAgentSessionOptionValue(id, value)).toBe(value)
expect(encodeStructuredAgentSessionOptionValue(id, true)).toBeNull()
expect(encodeStructuredAgentSessionOptionValue(id, false)).toBeNull()
}
})
})
@@ -82,7 +82,7 @@ describe('a Claude model its CLI no longer lists', () => {
}
})
it('keeps a remembered model and effort, including under a model with no listed effort', () => {
it('waits for an unlisted seed and carries a current effort without an offered menu', () => {
// The placeholder stands until the host answers with the chat's own model; create seeds it.
expect(carried(withHostList(), remembered)).toBeUndefined()
const haiku = { id: 'haiku', label: 'Haiku', isDefault: false, efforts: [] }
@@ -92,6 +92,13 @@ describe('a Claude model its CLI no longer lists', () => {
current: { model: 'haiku', effort: 'high' }
})
expect(shown(listed, undefined, {})).toEqual({ model: 'haiku', effort: 'high' })
expect(carried(listed)).toEqual({ model: 'haiku', effort: 'high' })
expect(
structuredAgentSessionShownSnapshot(listed).find((entry) => entry.id === 'effort')
).toMatchObject({
settable: false,
kind: { currentValue: 'high', choices: [{ value: 'high', label: 'high' }] }
})
})
})
@@ -102,10 +109,22 @@ describe('what a send carries', () => {
expect(carried(withHostList(fresh(), listing))).toEqual({ model: 'sonnet' })
})
it('carries a pick on its way as given', () => {
const listed = withHostList(fresh(), { ...LISTED, models: [OPUS, SONNET] })
expect(carried(listed, remembered, { model: 'sonnet' })).toMatchObject({ model: 'sonnet' })
expect(carried(withHostList(), undefined, { model: 'claude-retired' })).toMatchObject({
it('carries held picks without committing them to the chat', () => {
const listed = {
...withHostList(fresh(), { ...LISTED, models: [OPUS, SONNET] }),
pendingId: 'model'
}
const originalRecord = structuredClone(listed.record)
const view = structuredAgentSessionOptionView(listed, remembered, {
model: 'sonnet',
effort: 'high'
})
expect(structuredAgentSessionSendSelection(view)).toEqual({ model: 'sonnet', effort: 'high' })
expect(view.pendingId).toBe('model')
expect(view.record).not.toBe(listed.record)
expect(listed.record).toEqual(originalRecord)
expect(carried(listed, remembered, { model: 'sonnet' })).toEqual({ model: 'sonnet' })
expect(carried(withHostList(), undefined, { model: 'claude-retired' })).toEqual({
model: 'claude-retired'
})
})
@@ -152,6 +171,12 @@ describe("a chat's model and effort, apart from the defaults filling them in", (
const state = resumed()
expect(shown(state, undefined, {})).toEqual({ model: 'opus', effort: 'low' })
expect(carried(state)).toEqual({ model: 'opus', effort: 'low' })
expect(
structuredAgentSessionShownSnapshot(state).map(({ id, valueSource }) => ({ id, valueSource }))
).toEqual([
{ id: 'model', valueSource: 'dispatched' },
{ id: 'effort', valueSource: 'dispatched' }
])
})
it('lets a new chat pick its effort before its first message, under the default it shows', () => {