diff --git a/config/path-literal-baseline.txt b/config/path-literal-baseline.txt index 8967ec06c30..23eda0f2583 100644 --- a/config/path-literal-baseline.txt +++ b/config/path-literal-baseline.txt @@ -4,6 +4,11 @@ .github/workflows/agent-state-rules-publish.yml src/main/runtime/readiness-census .github/workflows/zcode-credential-contract.yml src/shared/windows-security-descriptor config/scripts/check-layout-writer-ratchets.mjs src/renderer/src/runtime/remote-workspace-frozen/ +config/scripts/pr-code-change-scope.mjs src/shared/agent-session-mutation-envelope +config/scripts/pr-code-change-scope.mjs src/shared/agent-session-provider-handle +config/scripts/pr-code-change-scope.mjs src/shared/agent-session-record +config/scripts/pr-code-change-scope.mjs src/shared/agent-session-resume-marker +config/scripts/pr-code-change-scope.mjs src/shared/agent-session-turn-record config/scripts/pr-layout-oracle-route.mjs src/main/runtime/old-session-tabs-stream/ config/scripts/pr-layout-oracle-route.mjs src/renderer/src/runtime/remote-workspace-frozen/ config/scripts/run-ssh-staged-upload-reliability.mjs src/main/ssh/ssh-relay-install-namespace.test.ts @@ -11,11 +16,3 @@ config/tsconfig.node.json src/renderer/src/components/native-chat/native-chat-co config/tsconfig.node.json src/renderer/src/components/native-chat/native-chat-resolution-receipt.ts config/tsconfig.node.json src/renderer/src/components/native-chat/structured-agent-question-projection.ts tests/e2e/capture-opencode-tui-repro.mjs src/cli/cmd/tui/ui/spinner -tests/e2e/cross-version-wire/released-chat-writers.ts src/shared/agent-session-failure-words.ts -tests/e2e/cross-version-wire/released-chat-writers.ts src/shared/agent-session-failure.ts -config/scripts/pr-code-change-scope.mjs src/shared/agent-session-mutation-envelope -config/scripts/pr-code-change-scope.mjs src/shared/agent-session-provider-handle -config/scripts/pr-code-change-scope.mjs src/shared/agent-session-record -config/scripts/pr-code-change-scope.mjs src/shared/agent-session-resume-marker -config/scripts/pr-code-change-scope.mjs src/shared/agent-session-turn-record -tests/e2e/cross-version-wire/released-chat-writers.ts src/main/claude/claude-structured-journal-translation.ts diff --git a/tests/e2e/cross-version-wire/agent-session-outcome-chat-downgrade.unit.test.ts b/tests/e2e/cross-version-wire/agent-session-outcome-chat-downgrade.unit.test.ts index 695f60ebc81..e530943ce92 100644 --- a/tests/e2e/cross-version-wire/agent-session-outcome-chat-downgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/agent-session-outcome-chat-downgrade.unit.test.ts @@ -26,17 +26,18 @@ import { importReleaseCheckoutModule, materializeReleaseCheckout, releaseModuleExport, - resolveBaselineReleaseRef, - resolveRecentStableReleaseRefs + resolveBaselineReleaseRef } from './release-checkout' // A chat this build has written, new or an existing one whose input rows an older build wrote, is -// refused by each of the two newest releases (the downgrade window) as a newer Orca's, which leaves +// refused by each of the two last releases before outcome storage as a newer Orca's, which leaves // every byte as it was; a build from before that reason opens it read-only, as it does any newer // row, and changes no saved row (it only adds a table of its own). The existing chat's input rows // stay byte for byte throughout, and this build reads the whole chat again afterwards. const SUITE_TIMEOUT_MS = 180_000 // A main build sharing this database and schema; the releases in the window are probed as well. +// v1.4.246 shipped outcome storage and reads these chats itself, so the refusing window ends before it. +const REFUSING_RELEASE_REFS = ['v1.4.245', 'v1.4.244'] as const const WRITABLE_BASELINE_REF = '3727100cc9dbcea6201f8a3e506676a3c4b53b18' const JOURNAL = 'src/main/native-chat/agent-session-journal' @@ -142,8 +143,8 @@ function savedContent(): Map { describe('a chat this build wrote, opened by a shipped build', () => { it.each([ - ['new', 'the newest release'], - ['existing', 'the newest release'], + ['new', 'the last release before outcome storage'], + ['existing', 'the last release before outcome storage'], ['new', 'the release before it'], ['existing', 'the release before it'], ['new', 'the writable baseline'], @@ -157,10 +158,10 @@ describe('a chat this build wrote, opened by a shipped build', () => { expect(before['agent-session-journal.db']).toBeDefined() const inWindow = build !== 'the writable baseline' const checkout = await materializeReleaseCheckout( - build === 'the newest release' - ? resolveBaselineReleaseRef() + build === 'the last release before outcome storage' + ? REFUSING_RELEASE_REFS[0] : inWindow - ? resolveRecentStableReleaseRefs(2)[1]! + ? REFUSING_RELEASE_REFS[1] : WRITABLE_BASELINE_REF ) const support = await importReleaseCheckoutModule( diff --git a/tests/e2e/cross-version-wire/release-checkout.ts b/tests/e2e/cross-version-wire/release-checkout.ts index 759c68d96e9..293b00640cf 100644 --- a/tests/e2e/cross-version-wire/release-checkout.ts +++ b/tests/e2e/cross-version-wire/release-checkout.ts @@ -11,6 +11,7 @@ import { publishReleaseCheckout, type MaterializeReleaseCheckoutTestHooks } from './release-checkout-publication.ts' +import { releaseModuleLocation } from './release-module-locations.ts' export type { CheckoutLockOptions, CheckoutLifecycleContext, @@ -121,13 +122,16 @@ function checkoutModulePath(root: string, rootRelativePath: string): string { } // Pass raw absolute forward-slash paths: CI vite-node treats file:// URLs as root-relative ids (run 33049571360). +// A module main moved loads from wherever this release defines it. export function importReleaseCheckoutModule( checkout: ReleaseCheckout, rootRelativePath: string, importModule: (specifier: string) => Promise> = (specifier) => import(/* @vite-ignore */ specifier) as Promise> ): Promise> { - return importModule(checkoutModulePath(checkout.root, rootRelativePath)) + return importModule( + checkoutModulePath(checkout.root, releaseModuleLocation(checkout.root, rootRelativePath)) + ) } /** An export a release module must define; callers name the shape they use, so a changed one fails the test. */ diff --git a/tests/e2e/cross-version-wire/release-module-locations.ts b/tests/e2e/cross-version-wire/release-module-locations.ts new file mode 100644 index 00000000000..7aa5e5342b8 --- /dev/null +++ b/tests/e2e/cross-version-wire/release-module-locations.ts @@ -0,0 +1,53 @@ +import { existsSync, readdirSync } from 'node:fs' +import { join, posix } from 'node:path' + +export type ReleaseTree = { + exists: (rootRelativePath: string) => boolean + /** Root-relative forward-slash paths of every file under `directory`. */ + list: (directory: string) => readonly string[] +} + +const listings = new Map() + +function checkoutTree(root: string): ReleaseTree { + return { + exists: (path) => existsSync(join(root, path)), + list: (directory) => { + const key = join(root, directory) + let files = listings.get(key) + if (!files) { + files = existsSync(key) + ? readdirSync(key, { recursive: true, encoding: 'utf8' }).map((entry) => + posix.join(directory, entry.split('\\').join('/')) + ) + : [] + listings.set(key, files) + } + return files + } + } +} + +/** + * Where the release under `root` defines the module a suite names. Suites name main's path; a + * release cut before a folder move ships the module elsewhere under the same source root, so the + * one file there with its name is that module. Missing or ambiguous names pass through and fail + * at import, naming the path the suite asked for. + */ +export function releaseModuleLocation( + root: string, + rootRelativePath: string, + tree: ReleaseTree = checkoutTree(root) +): string { + const named = rootRelativePath + .replace(/^[/\\]+/, '') + .split('\\') + .join('/') + const sourceRoot = named.split('/').slice(0, 2).join('/') + if (tree.exists(named) || !sourceRoot.startsWith('src/')) { + return named + } + const name = posix.basename(named) + const moved = tree.list(sourceRoot).filter((path) => posix.basename(path) === name) + return moved.length === 1 ? moved[0]! : named +} diff --git a/tests/e2e/cross-version-wire/release-module-locations.unit.test.ts b/tests/e2e/cross-version-wire/release-module-locations.unit.test.ts new file mode 100644 index 00000000000..f0ec9d7f818 --- /dev/null +++ b/tests/e2e/cross-version-wire/release-module-locations.unit.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest' +import { releaseModuleLocation, type ReleaseTree } from './release-module-locations' + +const CURRENT = 'src/shared/agent-session/lifecycle/agent-session-record.ts' +const SHIPPED = 'src/shared/agent-session-record.ts' +const release = (...files: string[]): ReleaseTree => ({ + exists: (path) => files.includes(path), + list: (directory) => files.filter((file) => file.startsWith(`${directory}/`)) +}) + +describe('release module locations', () => { + it('loads a moved module from wherever the release defines it, whichever path a suite names', () => { + for (const named of [CURRENT, SHIPPED, `/${SHIPPED}`]) { + expect(releaseModuleLocation('/release', named, release(SHIPPED))).toBe(SHIPPED) + expect(releaseModuleLocation('/release', named, release(CURRENT))).toBe(CURRENT) + } + }) + + it('stays inside the named source root and passes ambiguous or absent names through', () => { + expect( + releaseModuleLocation('/release', CURRENT, release('src/main/agent-session-record.ts')) + ).toBe(CURRENT) + expect( + releaseModuleLocation( + '/release', + CURRENT, + release(SHIPPED, 'src/shared/old/agent-session-record.ts') + ) + ).toBe(CURRENT) + expect(releaseModuleLocation('/release', CURRENT, release())).toBe(CURRENT) + }) +}) diff --git a/tests/e2e/cross-version-wire/released-chat-upgrade.unit.test.ts b/tests/e2e/cross-version-wire/released-chat-upgrade.unit.test.ts index bbd849f843f..adf4ccbe465 100644 --- a/tests/e2e/cross-version-wire/released-chat-upgrade.unit.test.ts +++ b/tests/e2e/cross-version-wire/released-chat-upgrade.unit.test.ts @@ -16,7 +16,7 @@ import type { AgentJournalSnapshot } from '../../../src/shared/agent-session/jou import { parseJournalRow } from '../../../src/main/native-chat/agent-session-journal/journal-row-schema' import type { JournalStoredRow } from '../../../src/main/native-chat/agent-session-journal/journal-row-table' import { JournalWriteProjection } from '../../../src/main/native-chat/agent-session-journal/journal-write-projection' -import { resolveRecentStableReleaseRefs } from './release-checkout' +import { resolveBaselineReleaseRef } from './release-checkout' import { editReleasedCard, rewindReleasedChat, @@ -30,16 +30,18 @@ import { type Agent } from './released-chat-writers' -// Each release in the downgrade window, and the oldest release whose chat database this build still +// The two releases before outcome storage, and the oldest release whose chat database this build still // reads, writes a Claude and a Codex chat through its own writers. This build admits every row as // written, opens the chat as that release had it, then edits a queued card (its first write, over // a key the old rows hold), sends, and rewinds, which saves the kept chat again in this build's // form. Every commit restores to the live chat. What this build does after the open is checked on // its own result, never against a release, so main stays free to change it. const SUITE_TIMEOUT_MS = 180_000 +// v1.4.246 shipped outcome storage, so its chats hold no rows to upgrade; the writers pin the two +// releases before it, and the newest release stays the reader this build must match. const RELEASES = { - 'the newest release': () => resolveRecentStableReleaseRefs(2)[0]!, - 'the release before it': () => resolveRecentStableReleaseRefs(2)[1]!, + 'the last release before outcome storage': () => 'v1.4.245', + 'the release before it': () => 'v1.4.244', // The first release with one chat database per host (schema 3, the oldest this build opens); // earlier releases kept a file per chat, which nothing reads any more. It never changes. 'the oldest release with this chat database': () => 'v1.4.219' @@ -145,8 +147,8 @@ async function carryOn( describe('a chat a shipped release wrote, carried on by this build', () => { it.each([ - ['the newest release', 'claude'], - ['the newest release', 'codex'], + ['the last release before outcome storage', 'claude'], + ['the last release before outcome storage', 'codex'], ['the release before it', 'claude'], ['the release before it', 'codex'], ['the oldest release with this chat database', 'claude'], @@ -197,7 +199,7 @@ describe('a chat a shipped release wrote, carried on by this build', () => { await closeTestJournalHostDatabase(directory) } // This build opens the chat as the newest release does. - const newest = await releasedJournalOpener(RELEASES['the newest release']()) + const newest = await releasedJournalOpener(resolveBaselineReleaseRef()) expect(releaseWritten(ours.opened)).toMatchObject( releaseWritten(await openedBy(newest, agent, shipped)) ) diff --git a/tests/e2e/cross-version-wire/released-chat-writers.ts b/tests/e2e/cross-version-wire/released-chat-writers.ts index 8a3fe900370..f9f7a2a9077 100644 --- a/tests/e2e/cross-version-wire/released-chat-writers.ts +++ b/tests/e2e/cross-version-wire/released-chat-writers.ts @@ -51,17 +51,11 @@ async function released(ref: string) { const load = async (path: string, name: string): Promise => releaseModuleExport(await importReleaseCheckoutModule(checkout, path), name) const fact = await load( - existsSync(join(checkout.root, 'src/shared/agent-session/failures/agent-session-failure.ts')) - ? 'src/shared/agent-session/failures/agent-session-failure.ts' - : 'src/shared/agent-session-failure.ts', + 'src/shared/agent-session/failures/agent-session-failure.ts', 'agentSessionFailureFact' ) const words = await load( - existsSync( - join(checkout.root, 'src/shared/agent-session/failures/agent-session-failure-words.ts') - ) - ? 'src/shared/agent-session/failures/agent-session-failure-words.ts' - : 'src/shared/agent-session-failure-words.ts', + 'src/shared/agent-session/failures/agent-session-failure-words.ts', 'agentSessionFailureWords' ) return { @@ -71,14 +65,7 @@ async function released(ref: string) { 'createTrackedJournalOpener' ), claude: await load( - existsSync( - join( - checkout.root, - 'src/main/claude/structured/journal/claude-structured-journal-translation.ts' - ) - ) - ? 'src/main/claude/structured/journal/claude-structured-journal-translation.ts' - : 'src/main/claude/claude-structured-journal-translation.ts', + 'src/main/claude/structured/journal/claude-structured-journal-translation.ts', 'createClaudeJournalTranslator' ), codex: await load(