From 59515beb7089c4f451ca42b8a0c628ea44ac6ab0 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Fri, 28 Aug 2026 00:55:45 -0700 Subject: [PATCH] fix(release): recover immutable patch validation gates (#16984) * fix(release): recover immutable patch validation gates * test(e2e): locate wrapped terminal file links * test(e2e): keep sibling file links on one terminal row --- .github/workflows/release-cut.yml | 32 +++++++- .../release-e2e-dispatch-contract.test.mjs | 11 +++ .../skill-sharing-release-workflow.test.mjs | 24 ++++++ .../skills/skill-freshness-inventory.test.ts | 6 +- .../skill-provider-runtime-roots.test.ts | 34 ++++---- .../golden-source-control-open-diff.spec.ts | 2 +- tests/e2e/golden-terminal-file-link.spec.ts | 82 ++++++++++++++----- 7 files changed, 152 insertions(+), 39 deletions(-) diff --git a/.github/workflows/release-cut.yml b/.github/workflows/release-cut.yml index c035080a203..5d81a8b7ae3 100644 --- a/.github/workflows/release-cut.yml +++ b/.github/workflows/release-cut.yml @@ -844,6 +844,16 @@ jobs: with: ref: refs/tags/${{ needs.cut.outputs.tag }} + - name: Restore golden test harness from the workflow ref + shell: bash + env: + WORKFLOW_SHA: ${{ github.workflow_sha }} + run: | + git fetch --no-tags --depth=1 origin "$WORKFLOW_SHA" + git checkout "$WORKFLOW_SHA" -- \ + tests/e2e/golden-source-control-open-diff.spec.ts \ + tests/e2e/golden-terminal-file-link.spec.ts + - name: Install native build tools if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y build-essential python3 xvfb @@ -946,6 +956,16 @@ jobs: with: ref: refs/tags/${{ needs.cut.outputs.tag }} + - name: Restore skill-sharing test harness from the workflow ref + shell: bash + env: + WORKFLOW_SHA: ${{ github.workflow_sha }} + run: | + git fetch --no-tags --depth=1 origin "$WORKFLOW_SHA" + git checkout "$WORKFLOW_SHA" -- \ + src/main/skills/skill-freshness-inventory.test.ts \ + src/main/skills/skill-provider-runtime-roots.test.ts + - uses: ./.github/actions/install-node-dependencies with: native-runtime: node @@ -978,13 +998,23 @@ jobs: steps: - name: Install Ubuntu 20.04 prerequisites - run: apt-get update && apt-get install -y build-essential ca-certificates git python3 + run: apt-get update && apt-get install -y build-essential ca-certificates git python3 unzip - name: Checkout uses: actions/checkout@v6 with: ref: refs/tags/${{ needs.cut.outputs.tag }} + - name: Restore skill-sharing test harness from the workflow ref + shell: bash + env: + WORKFLOW_SHA: ${{ github.workflow_sha }} + run: | + git fetch --no-tags --depth=1 origin "$WORKFLOW_SHA" + git checkout "$WORKFLOW_SHA" -- \ + src/main/skills/skill-freshness-inventory.test.ts \ + src/main/skills/skill-provider-runtime-roots.test.ts + - uses: ./.github/actions/install-node-dependencies with: native-runtime: node diff --git a/config/scripts/release-e2e-dispatch-contract.test.mjs b/config/scripts/release-e2e-dispatch-contract.test.mjs index 18fe76c4c44..d48c7b3fa30 100644 --- a/config/scripts/release-e2e-dispatch-contract.test.mjs +++ b/config/scripts/release-e2e-dispatch-contract.test.mjs @@ -10,6 +10,17 @@ const releaseWorkflow = parse( const e2eWorkflow = parse(readFileSync(join(projectDir, '.github/workflows/e2e.yml'), 'utf8')) describe('release E2E dispatch contract', () => { + it('validates immutable tags with the current golden test harness', () => { + const restoreStep = releaseWorkflow.jobs['terminal-rendering-golden'].steps.find( + (step) => step.name === 'Restore golden test harness from the workflow ref' + ) + + expect(restoreStep.env.WORKFLOW_SHA).toBe('${{ github.workflow_sha }}') + expect(restoreStep.run).toContain('git fetch --no-tags --depth=1 origin "$WORKFLOW_SHA"') + expect(restoreStep.run).toContain('golden-source-control-open-diff.spec.ts') + expect(restoreStep.run).toContain('golden-terminal-file-link.spec.ts') + }) + it('dispatches tag-scoped E2E only after publication', () => { const dispatchJob = releaseWorkflow.jobs['post-release-e2e'] const dispatchStep = dispatchJob.steps.find((step) => step.name === 'Dispatch tag-scoped E2E') diff --git a/config/scripts/skill-sharing-release-workflow.test.mjs b/config/scripts/skill-sharing-release-workflow.test.mjs index d2b6adcf7d4..4b963ae38f3 100644 --- a/config/scripts/skill-sharing-release-workflow.test.mjs +++ b/config/scripts/skill-sharing-release-workflow.test.mjs @@ -44,6 +44,30 @@ describe('skill-sharing release workflow', () => { expect(command).toContain('src/shared/skill-bundle-install-contract.test.ts') }) + it('installs the archive tool required by Electron on the Linux floor', () => { + const linux = workflow.jobs['skill-sharing-linux-floor-release-gate'] + const prerequisites = stepNamed(linux, 'Install Ubuntu 20.04 prerequisites') + + expect(prerequisites.run).toMatch(/apt-get install[^\n]*\bunzip\b/) + }) + + it('validates immutable tags with the current skill-sharing test harness', () => { + for (const jobName of [ + 'skill-sharing-release-gate', + 'skill-sharing-linux-floor-release-gate' + ]) { + const restore = stepNamed( + workflow.jobs[jobName], + 'Restore skill-sharing test harness from the workflow ref' + ) + + expect(restore.env.WORKFLOW_SHA).toBe('${{ github.workflow_sha }}') + expect(restore.run).toContain('git fetch --no-tags --depth=1 origin "$WORKFLOW_SHA"') + expect(restore.run).toContain('skill-freshness-inventory.test.ts') + expect(restore.run).toContain('skill-provider-runtime-roots.test.ts') + } + }) + it('archives bounded machine-readable evidence from every platform', () => { for (const jobName of [ 'skill-sharing-release-gate', diff --git a/src/main/skills/skill-freshness-inventory.test.ts b/src/main/skills/skill-freshness-inventory.test.ts index 04e778c42dc..5073827081c 100644 --- a/src/main/skills/skill-freshness-inventory.test.ts +++ b/src/main/skills/skill-freshness-inventory.test.ts @@ -157,7 +157,11 @@ async function writeSkillLockHash(homeDir: string, skillFolderHash: string): Pro } afterEach(async () => { - await Promise.all(temporaryDirectories.splice(0).map((root) => rm(root, { recursive: true }))) + await Promise.all( + temporaryDirectories + .splice(0) + .map((root) => rm(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })) + ) }) describe('read-only skill freshness inventory', () => { diff --git a/src/main/skills/skill-provider-runtime-roots.test.ts b/src/main/skills/skill-provider-runtime-roots.test.ts index f933d4b2ff0..43298e0a738 100644 --- a/src/main/skills/skill-provider-runtime-roots.test.ts +++ b/src/main/skills/skill-provider-runtime-roots.test.ts @@ -1,4 +1,4 @@ -import { join } from 'node:path' +import { join, resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { resolveDefaultHermesSkillsRoot, @@ -10,14 +10,16 @@ import { describe('skill provider runtime roots', () => { it('maps Claude and Grok config homes to their global skill roots', () => { + const claudeRoot = resolve('/srv/claude') + const grokRoot = resolve('/srv/grok') expect( resolveEnvironmentSkillProviderRoots({ - CLAUDE_CONFIG_DIR: join('/srv', 'claude'), - GROK_HOME: join('/srv', 'grok') + CLAUDE_CONFIG_DIR: claudeRoot, + GROK_HOME: grokRoot }) ).toEqual({ - claude: join('/srv', 'claude', 'skills'), - grok: join('/srv', 'grok', 'skills') + claude: join(claudeRoot, 'skills'), + grok: join(grokRoot, 'skills') }) }) @@ -27,14 +29,16 @@ describe('skill provider runtime roots', () => { GROK_HOME: '../grok' }) expect(roots).toEqual({}) - expect(withClaudeSkillProviderRoot(roots, join('/managed', 'claude'))).toEqual({ - claude: join('/managed', 'claude', 'skills') + const managedClaudeRoot = resolve('/managed/claude') + expect(withClaudeSkillProviderRoot(roots, managedClaudeRoot)).toEqual({ + claude: join(managedClaudeRoot, 'skills') }) }) it('maps a relocated HERMES_HOME to its skill root and ignores relative ones', () => { - expect(resolveEnvironmentHermesSkillsRoot({ HERMES_HOME: join('/srv', 'hermes') })).toBe( - join('/srv', 'hermes', 'skills') + const hermesRoot = resolve('/srv/hermes') + expect(resolveEnvironmentHermesSkillsRoot({ HERMES_HOME: hermesRoot })).toBe( + join(hermesRoot, 'skills') ) expect(resolveEnvironmentHermesSkillsRoot({ HERMES_HOME: '../hermes' })).toBeNull() expect(resolveEnvironmentHermesSkillsRoot({})).toBeNull() @@ -52,13 +56,14 @@ describe('skill provider runtime roots', () => { }) it('defaults the Hermes skills root under LOCALAPPDATA on Windows', () => { + const localAppData = resolve('/local') const resolved = resolveDefaultHermesSkillsRoot({ homeDir: join('/users', 'alice'), platform: 'win32', - env: { LOCALAPPDATA: join('/local') }, - directoryExists: (candidate) => candidate === join('/local', 'hermes') + env: { LOCALAPPDATA: localAppData }, + directoryExists: (candidate) => candidate === join(localAppData, 'hermes') }) - expect(resolved).toBe(join('/local', 'hermes', 'skills')) + expect(resolved).toBe(join(localAppData, 'hermes', 'skills')) }) it('keeps a pre-LOCALAPPDATA Windows dotfolder install discoverable', () => { @@ -72,13 +77,14 @@ describe('skill provider runtime roots', () => { }) it('prefers the LOCALAPPDATA tree on Windows when both layouts exist', () => { + const localAppData = resolve('/local') const resolved = resolveDefaultHermesSkillsRoot({ homeDir: join('/users', 'alice'), platform: 'win32', - env: { LOCALAPPDATA: join('/local') }, + env: { LOCALAPPDATA: localAppData }, directoryExists: () => true }) - expect(resolved).toBe(join('/local', 'hermes', 'skills')) + expect(resolved).toBe(join(localAppData, 'hermes', 'skills')) }) it('falls back to the dotfolder when Windows exposes no usable LOCALAPPDATA', () => { diff --git a/tests/e2e/golden-source-control-open-diff.spec.ts b/tests/e2e/golden-source-control-open-diff.spec.ts index 1c7f2735889..2b58b1a3934 100644 --- a/tests/e2e/golden-source-control-open-diff.spec.ts +++ b/tests/e2e/golden-source-control-open-diff.spec.ts @@ -43,7 +43,7 @@ test('@golden opens an unstaged file diff from Source Control', async ({ ).toBeVisible() await expect(orcaPage.locator('.editor-header-path').first()).toHaveAttribute( 'title', - `${realpathSync(path.join(fixture.worktreePath, GOLDEN_CHANGED_PATH))} (diff)` + `${realpathSync(path.join(fixture.worktreePath, GOLDEN_CHANGED_PATH)).replaceAll('\\', '/')} (diff)` ) const probe = orcaPage.getByRole('button', { name: /Source Control/ }) diff --git a/tests/e2e/golden-terminal-file-link.spec.ts b/tests/e2e/golden-terminal-file-link.spec.ts index 35e6b20959a..a5d940c4cb5 100644 --- a/tests/e2e/golden-terminal-file-link.spec.ts +++ b/tests/e2e/golden-terminal-file-link.spec.ts @@ -18,6 +18,11 @@ type LinkClientPoint = { x: number; y: number } const LINK_SCAN_CHAR_LIMIT = 12_000 +function canonicalFileIdentity(value: string): string { + const normalized = path.resolve(value).replaceAll('\\', '/') + return process.platform === 'win32' ? normalized.toLowerCase() : normalized +} + async function locateLink(page: Page, needle: string): Promise { return page.evaluate((needle) => { const state = window.__store?.getState() @@ -34,11 +39,19 @@ async function locateLink(page: Page, needle: string): Promise return null } const buffer = pane.terminal.buffer.active - for (let row = 0; row < pane.terminal.rows; row += 1) { - const line = buffer.getLine(buffer.viewportY + row) - const col = line?.translateToString(true).indexOf(needle) ?? -1 - if (col >= 0) { - return { col: col + Math.floor(needle.length / 2), row, tabId } + // Preserve fixed-width cells so paths split across xterm rows stay searchable. + const visibleCells = Array.from({ length: pane.terminal.rows }, (_, row) => + (buffer.getLine(buffer.viewportY + row)?.translateToString(false) ?? '').padEnd( + pane.terminal.cols + ) + ).join('') + const start = visibleCells.indexOf(needle) + if (start !== -1) { + const center = start + Math.floor(needle.length / 2) + return { + col: center % pane.terminal.cols, + row: Math.floor(center / pane.terminal.cols), + tabId } } return null @@ -162,14 +175,13 @@ test('opens a terminal file link and observes an external edit @golden', async ( await expect(explorerRow).toHaveAttribute('data-selected', 'true', { timeout: 10_000 }) await expect .poll( - () => - orcaPage.evaluate( - (expectedPath) => window.__monacoEditorE2E?.filePath === expectedPath, - filePath + async () => + canonicalFileIdentity( + (await orcaPage.evaluate(() => window.__monacoEditorE2E?.filePath)) ?? '' ), { timeout: 20_000, message: 'Monaco opened a different file identity' } ) - .toBe(true) + .toBe(canonicalFileIdentity(filePath)) writeFileSync(filePath, `${original.trimEnd()}\n\n${changedMarker}\n`) await expect @@ -196,14 +208,18 @@ test('reuses a terminal file link already open in a sibling workspace @golden', test.setTimeout(180_000) await waitForSessionReady(orcaPage) const sourceWorktreeId = await waitForActiveWorktree(orcaPage) - const sibling = await orcaPage.evaluate((sourceId) => { + const worktrees = await orcaPage.evaluate((sourceId) => { const state = window.__store?.getState() - return ( - Object.values(state?.worktreesByRepo ?? {}) - .flat() - .find((worktree) => worktree.id !== sourceId) ?? null - ) + const entries = Object.values(state?.worktreesByRepo ?? {}).flat() + return { + source: entries.find((worktree) => worktree.id === sourceId) ?? null, + sibling: entries.find((worktree) => worktree.id !== sourceId) ?? null + } }, sourceWorktreeId) + const { source, sibling } = worktrees + if (!source) { + throw new Error('source worktree fixture unavailable') + } if (!sibling) { throw new Error('sibling worktree fixture unavailable') } @@ -230,7 +246,24 @@ test('reuses a terminal file link already open in a sibling workspace @golden', await ensureTerminalVisible(orcaPage) await waitForActiveTerminalManager(orcaPage, 30_000) - const ptyId = await waitForActivePanePtyId(orcaPage) + await orcaPage.evaluate(() => { + const state = window.__store?.getState() + state?.setSidebarOpen(false) + state?.setRightSidebarOpen(false) + }) + await expect + .poll( + () => + orcaPage.evaluate(() => { + const state = window.__store?.getState() + const tabId = state?.activeTabId + const manager = tabId ? window.__paneManagers?.get(tabId) : null + return manager?.getActivePane?.()?.terminal.cols ?? 0 + }), + { message: 'terminal did not expand after closing the sidebars' } + ) + .toBeGreaterThan(120) + const ptyId = await waitForActivePanePtyId(orcaPage, 30_000) await waitForPtyShellEcho(orcaPage, ptyId, 15_000) const printedPath = process.platform === 'win32' ? filePath.replaceAll('\\', '/') : filePath const command = nodeTerminalCommand(['-e', `console.log(${JSON.stringify(printedPath)})`]) @@ -261,13 +294,18 @@ test('reuses a terminal file link already open in a sibling workspace @golden', await expect(editorHeader).toContainText('package.json', { timeout: 20_000 }) await expect .poll( - () => - orcaPage.evaluate(() => ({ - filePath: window.__monacoEditorE2E?.filePath ?? null, + async () => { + const rendered = await orcaPage.evaluate(() => ({ + filePath: window.__monacoEditorE2E?.filePath ?? '', activeWorktreeId: window.__store?.getState()?.activeWorktreeId ?? null - })), + })) + return { + filePath: canonicalFileIdentity(rendered.filePath), + activeWorktreeId: rendered.activeWorktreeId + } + }, { timeout: 20_000, message: 'sibling workspace never rendered the linked file' } ) - .toEqual({ filePath, activeWorktreeId: sibling.id }) + .toEqual({ filePath: canonicalFileIdentity(filePath), activeWorktreeId: sibling.id }) await expect(orcaPage.getByText('Loading...', { exact: true })).toHaveCount(0) })