mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 17:10:50 -10:00
wip(0.10.2): ci-gate-1
This commit is contained in:
1 parent
3ae9013dd4
commit
30b07f278d
4 files changed
+252
-29
No files matched your search
@@ -1001,7 +1001,10 @@ jobs:
|
||||
RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }}
|
||||
- name: Windows Terminal clipboard acceptance
|
||||
if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
|
||||
timeout-minutes: 5
|
||||
# The script bounds itself: every desktop stage has a 45 s deadline and
|
||||
# reports its own failure with a desktop capture. This limit only has
|
||||
# to stay above the sum of those stages (about 10 minutes worst case).
|
||||
timeout-minutes: 12
|
||||
shell: pwsh
|
||||
run: node scripts/release/windows-terminal-clipboard.mjs target/debug/codewhale.exe "${{ runner.temp }}/windows-clipboard"
|
||||
- name: Retain Windows Terminal clipboard evidence
|
||||
|
||||
@@ -272,7 +272,10 @@ jobs:
|
||||
"${bin_dir}/${{ matrix.shim_binary }}" --version
|
||||
- name: Windows Terminal clipboard acceptance
|
||||
if: matrix.platform == 'windows-x64'
|
||||
timeout-minutes: 5
|
||||
# The script bounds itself: every desktop stage has a 45 s deadline and
|
||||
# reports its own failure with a desktop capture. This limit only has
|
||||
# to stay above the sum of those stages (about 10 minutes worst case).
|
||||
timeout-minutes: 12
|
||||
shell: pwsh
|
||||
run: node scripts/release/windows-terminal-clipboard.mjs "target/${{ matrix.target }}/dist/codewhale.exe" "${{ runner.temp }}/windows-clipboard"
|
||||
- name: Retain Windows Terminal clipboard evidence
|
||||
|
||||
@@ -27,7 +27,18 @@ const model = 'gpt-4o-mini';
|
||||
const expected = Array.from({ length: 24 }, (_, n) => `cw-clipboard-${String(n + 1).padStart(2, '0')} 漢字 🐳 line ${n + 1}`).join('\n');
|
||||
const requests = [];
|
||||
const receipt = { source: process.env.GITHUB_SHA, binary, result: 'failed', lineCount: 24, provider: 'loopback fixture', title };
|
||||
// One budget per desktop stage, shared with windows-terminal-clipboard.ps1.
|
||||
// The deadlines below are backstops for a desktop script that is stuck and
|
||||
// cannot report: each is the sum of the stages it covers plus slack, so a
|
||||
// stage that runs out always fails first, with its own name and screenshot.
|
||||
const stageSeconds = 45;
|
||||
// Terminal window, focus, composer, focus; slack for PowerShell start-up.
|
||||
const pasteBackstop = (4 * stageSeconds + 60) * 1000;
|
||||
// Submit marker seen, focus, rendered reply.
|
||||
const completedBackstop = (3 * stageSeconds + 30) * 1000;
|
||||
const script = fileURLToPath(new URL('./windows-terminal-clipboard.ps1', import.meta.url));
|
||||
let desktop;
|
||||
let desktopEnv;
|
||||
let desktopExit;
|
||||
let desktopError = '';
|
||||
const server = http.createServer(async (req, res) => {
|
||||
@@ -66,17 +77,37 @@ const server = http.createServer(async (req, res) => {
|
||||
} catch (error) { desktopError ||= String(error); res.writeHead(500); res.end(); }
|
||||
});
|
||||
|
||||
async function until(check, label, milliseconds = 60_000) {
|
||||
const desktopStopped = () => Boolean(desktop) && (desktop.exitCode !== null || desktop.signalCode !== null);
|
||||
async function until(check, label, milliseconds) {
|
||||
const deadline = Date.now() + milliseconds;
|
||||
while (Date.now() < deadline) {
|
||||
const result = await check();
|
||||
if (result) return result;
|
||||
assert.equal(desktop?.exitCode, null, `desktop stopped while waiting for ${label}: ${desktopError}`);
|
||||
if (desktopStopped()) {
|
||||
await desktopExit; // resolves on 'close', after stderr has been read in full
|
||||
throw new Error(`desktop stopped (exit ${desktop.exitCode}) while waiting for ${label}: ${desktopError.trim()}`);
|
||||
}
|
||||
await delay(100);
|
||||
}
|
||||
throw new Error(`deadline waiting for ${label}: ${desktopError}`);
|
||||
throw new Error(`backstop deadline (${milliseconds / 1000}s) waiting for ${label}; the desktop script was still running: ${desktopError.trim()}`);
|
||||
}
|
||||
const exists = async name => fs.access(path.join(evidence, name)).then(() => true, () => false);
|
||||
const readEvidence = async name => (await fs.readFile(path.join(evidence, name), 'utf8')).replace(/^\uFEFF/, '');
|
||||
// Runs before the desktop script is stopped, so a stuck desktop is captured
|
||||
// as it is. Nothing here can turn a failure into a pass.
|
||||
async function diagnose() {
|
||||
try {
|
||||
receipt.desktopStages = (await readEvidence('stages.log')).trim().split(/\r?\n/);
|
||||
receipt.desktopStage = receipt.desktopStages.at(-1);
|
||||
} catch { receipt.desktopStage = 'none reported'; }
|
||||
try { receipt.desktopFailure = JSON.parse(await readEvidence('failure.json')); } catch { /* the script did not get to report */ }
|
||||
if (desktopStopped() && await exists('failure-desktop.png')) return;
|
||||
try {
|
||||
execFileSync('powershell.exe', ['-NoProfile', '-STA', '-File', script, '-Evidence', evidence, '-Title', title, '-DiagnoseOnly', 'backstop'],
|
||||
{ env: desktopEnv, timeout: 60_000, stdio: 'ignore' });
|
||||
receipt.backstopDiagnostics = 'backstop-desktop.png, backstop-windows.json';
|
||||
} catch (error) { receipt.backstopDiagnostics = `not captured: ${error}`; }
|
||||
}
|
||||
try {
|
||||
receipt.binarySha256 = createHash('sha256').update(await fs.readFile(binary)).digest('hex');
|
||||
receipt.version = execFileSync(binary, ['--version'], { encoding: 'utf8' }).trim();
|
||||
@@ -120,16 +151,16 @@ try {
|
||||
CODEWHALE_TELEMETRY: '0', CODEWHALE_NO_UPDATE_CHECK: '1', CODEWHALE_DISABLE_MODELS_DEV_FETCH: '1', CODEWHALE_DISABLE_LOCAL_OLLAMA_PROBE: '1', NO_ANIMATIONS: '1',
|
||||
});
|
||||
receipt.terminal = { version: '1.25.2733.0', sha256: terminalHash, distribution: 'official portable x64' };
|
||||
const script = fileURLToPath(new URL('./windows-terminal-clipboard.ps1', import.meta.url));
|
||||
desktop = spawn('powershell.exe', ['-NoProfile', '-STA', '-File', script, terminal, binary, workspace, baseUrl, title, evidence], { env, windowsHide: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
desktopEnv = env;
|
||||
desktop = spawn('powershell.exe', ['-NoProfile', '-STA', '-File', script, terminal, binary, workspace, baseUrl, title, evidence, '-StageSeconds', String(stageSeconds)], { env, windowsHide: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
desktop.stdout.on('data', data => { void fs.appendFile(path.join(evidence, 'desktop.log'), data); });
|
||||
desktop.stderr.on('data', data => { desktopError += data; void fs.appendFile(path.join(evidence, 'desktop-error.log'), data); });
|
||||
desktopExit = new Promise((resolve, reject) => { desktop.once('error', reject); desktop.once('exit', resolve); });
|
||||
await until(() => exists('pasted.json'), 'actual clipboard paste');
|
||||
desktopExit = new Promise((resolve, reject) => { desktop.once('error', reject); desktop.once('close', resolve); });
|
||||
await until(() => exists('pasted.json'), 'actual clipboard paste', pasteBackstop);
|
||||
assert.equal(requests.length, 0, 'pasting must not submit any partial turn');
|
||||
receipt.requestsBeforeEnter = 0;
|
||||
await fs.writeFile(path.join(evidence, 'submit'), 'enter once');
|
||||
await until(() => exists('completed.json'), 'rendered fixture reply');
|
||||
await until(() => exists('completed.json'), 'rendered fixture reply', completedBackstop);
|
||||
await delay(1000);
|
||||
assert.equal(requests.length, 1, 'one Enter must submit exactly one turn');
|
||||
const matching = requests[0].filter(text => text.includes('cw-clipboard-01'));
|
||||
@@ -143,8 +174,9 @@ try {
|
||||
receipt.result = 'passed';
|
||||
} catch (error) {
|
||||
receipt.error = String(error); process.exitCode = 1;
|
||||
if (desktop) await diagnose();
|
||||
} finally {
|
||||
if (desktop && desktop.exitCode === null) { desktop.kill(); await desktopExit.catch(() => {}); }
|
||||
if (desktop && !desktopStopped()) { desktop.kill(); await desktopExit.catch(() => {}); }
|
||||
server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
|
||||
await fs.writeFile(path.join(evidence, 'receipt.json'), JSON.stringify(receipt, null, 2));
|
||||
console.log(JSON.stringify(receipt));
|
||||
|
||||
@@ -1,24 +1,170 @@
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$Terminal,
|
||||
[Parameter(Mandatory)][string]$Binary,
|
||||
[Parameter(Mandatory)][string]$Workspace,
|
||||
[Parameter(Mandatory)][string]$BaseUrl,
|
||||
[Parameter(Mandatory)][string]$Title,
|
||||
[Parameter(Mandatory)][string]$Evidence
|
||||
[string]$Terminal,
|
||||
[string]$Binary,
|
||||
[string]$Workspace,
|
||||
[string]$BaseUrl,
|
||||
[string]$Title,
|
||||
[Parameter(Mandatory)][string]$Evidence,
|
||||
# One budget for every desktop stage. The Node caller derives its backstop
|
||||
# deadlines from this same number, so a stage always reports before Node
|
||||
# gives up.
|
||||
[int]$StageSeconds = 45,
|
||||
# Capture the desktop and the window list under this name, then exit. The
|
||||
# Node caller uses it when this script is stuck and cannot report itself.
|
||||
[string]$DiagnoseOnly = ''
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes, System.Windows.Forms, System.Drawing
|
||||
Add-Type @'
|
||||
# Windows PowerShell 5.1 compiles this with the C# 5 compiler: no newer syntax.
|
||||
Add-Type -IgnoreWarnings @'
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
public sealed class ClipboardFixtureWindowInfo {
|
||||
public int ZOrder;
|
||||
public long Handle;
|
||||
public uint ProcessId;
|
||||
public bool Foreground;
|
||||
public bool Minimised;
|
||||
public string Bounds;
|
||||
public string ClassName;
|
||||
public string Title;
|
||||
}
|
||||
public static class ClipboardFixtureWindow {
|
||||
public delegate bool EnumWindowsCallback(IntPtr h, IntPtr state);
|
||||
[StructLayout(LayoutKind.Sequential)] public struct Rect { public int Left; public int Top; public int Right; public int Bottom; }
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int command);
|
||||
[DllImport("user32.dll")] static extern bool BringWindowToTop(IntPtr h);
|
||||
[DllImport("user32.dll")] static extern bool IsIconic(IntPtr h);
|
||||
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
|
||||
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint processId);
|
||||
[DllImport("user32.dll")] static extern bool AttachThreadInput(uint from, uint to, bool attach);
|
||||
[DllImport("user32.dll")] static extern void keybd_event(byte key, byte scan, uint flags, UIntPtr extra);
|
||||
[DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsCallback callback, IntPtr state);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder text, int capacity);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetClassName(IntPtr h, StringBuilder text, int capacity);
|
||||
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr h, out Rect rect);
|
||||
[DllImport("kernel32.dll")] static extern uint GetCurrentThreadId();
|
||||
|
||||
static bool Owns(IntPtr h) { Thread.Sleep(60); return GetForegroundWindow() == h; }
|
||||
|
||||
// Windows refuses SetForegroundWindow from a process that neither owns the
|
||||
// foreground nor sent the last input, which is the normal state of a CI
|
||||
// step. Escalate through the documented ways around that lock and name
|
||||
// the one that worked, so the evidence shows whether the lock was hit.
|
||||
// No step here sends anything to the owned Terminal: the Alt tap is only
|
||||
// issued while a different window still holds the foreground.
|
||||
public static string Activate(IntPtr h, bool allowMinimise) {
|
||||
if (GetForegroundWindow() == h) return "already";
|
||||
ShowWindow(h, IsIconic(h) ? 9 : 5);
|
||||
BringWindowToTop(h);
|
||||
SetForegroundWindow(h);
|
||||
if (Owns(h)) return "SetForegroundWindow";
|
||||
IntPtr other = GetForegroundWindow();
|
||||
uint otherProcess;
|
||||
uint otherThread = other == IntPtr.Zero ? 0 : GetWindowThreadProcessId(other, out otherProcess);
|
||||
uint thisThread = GetCurrentThreadId();
|
||||
if (otherThread != 0 && otherThread != thisThread && AttachThreadInput(thisThread, otherThread, true)) {
|
||||
try { BringWindowToTop(h); SetForegroundWindow(h); }
|
||||
finally { AttachThreadInput(thisThread, otherThread, false); }
|
||||
if (Owns(h)) return "AttachThreadInput";
|
||||
}
|
||||
if (GetForegroundWindow() != h) {
|
||||
keybd_event(0x12, 0, 0, UIntPtr.Zero);
|
||||
keybd_event(0x12, 0, 2, UIntPtr.Zero);
|
||||
SetForegroundWindow(h);
|
||||
}
|
||||
if (Owns(h)) return "Alt+SetForegroundWindow";
|
||||
if (allowMinimise) {
|
||||
ShowWindow(h, 6);
|
||||
Thread.Sleep(150);
|
||||
ShowWindow(h, 9);
|
||||
SetForegroundWindow(h);
|
||||
if (Owns(h)) return "minimise+restore";
|
||||
}
|
||||
return "denied";
|
||||
}
|
||||
|
||||
// Front-to-back visible top-level windows. Plain user32 calls only, so
|
||||
// this still answers when UI Automation is the thing that is stuck.
|
||||
public static ClipboardFixtureWindowInfo[] VisibleTopLevelWindows() {
|
||||
List<ClipboardFixtureWindowInfo> rows = new List<ClipboardFixtureWindowInfo>();
|
||||
IntPtr foreground = GetForegroundWindow();
|
||||
EnumWindows(delegate(IntPtr h, IntPtr state) {
|
||||
if (!IsWindowVisible(h)) return true;
|
||||
StringBuilder title = new StringBuilder(512);
|
||||
GetWindowText(h, title, title.Capacity);
|
||||
StringBuilder className = new StringBuilder(256);
|
||||
GetClassName(h, className, className.Capacity);
|
||||
uint processId;
|
||||
GetWindowThreadProcessId(h, out processId);
|
||||
Rect r;
|
||||
GetWindowRect(h, out r);
|
||||
ClipboardFixtureWindowInfo info = new ClipboardFixtureWindowInfo();
|
||||
info.ZOrder = rows.Count;
|
||||
info.Handle = h.ToInt64();
|
||||
info.ProcessId = processId;
|
||||
info.Foreground = h == foreground;
|
||||
info.Minimised = IsIconic(h);
|
||||
info.Bounds = r.Left + "," + r.Top + "," + r.Right + "," + r.Bottom;
|
||||
info.ClassName = className.ToString();
|
||||
info.Title = title.ToString();
|
||||
rows.Add(info);
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
return rows.ToArray();
|
||||
}
|
||||
}
|
||||
'@
|
||||
$window = $null
|
||||
$ownedPid = $null
|
||||
$ownedHandle = [IntPtr]::Zero
|
||||
$stage = 'starting'
|
||||
function Add-EvidenceLine([string]$File, [string]$Text) {
|
||||
$line = '{0:o} {1}' -f [DateTime]::UtcNow, $Text
|
||||
[System.IO.File]::AppendAllText((Join-Path $Evidence $File), $line + "`r`n")
|
||||
[Console]::Out.WriteLine("[$File] $line")
|
||||
}
|
||||
# The last line of stages.log names where a failed or killed run was; the
|
||||
# timestamps show where a slow run spent its time.
|
||||
function Enter-Stage([string]$Name) {
|
||||
$script:stage = $Name
|
||||
Add-EvidenceLine 'stages.log' $Name
|
||||
}
|
||||
function Save-WindowList([string]$Name) {
|
||||
$rows = @(foreach ($info in [ClipboardFixtureWindow]::VisibleTopLevelWindows()) {
|
||||
$path = $null
|
||||
try { $path = (Get-Process -Id $info.ProcessId -ErrorAction Stop).Path } catch { }
|
||||
[ordered]@{
|
||||
zOrder = $info.ZOrder; foreground = $info.Foreground; minimised = $info.Minimised
|
||||
title = $info.Title; className = $info.ClassName; bounds = $info.Bounds
|
||||
processId = $info.ProcessId; processPath = $path
|
||||
ownedTerminal = [bool]($ownedPid -and $info.ProcessId -eq $ownedPid)
|
||||
fixtureTitle = [bool]($Title -and $info.Title.Contains($Title))
|
||||
}
|
||||
})
|
||||
ConvertTo-Json -InputObject $rows -Depth 4 | Set-Content -Encoding UTF8 (Join-Path $Evidence "$Name-windows.json")
|
||||
}
|
||||
function Save-Desktop([string]$Name) {
|
||||
$screen = [System.Windows.Forms.SystemInformation]::VirtualScreen
|
||||
if ($screen.Width -le 0 -or $screen.Height -le 0) { throw 'No visible desktop' }
|
||||
$bitmap = [System.Drawing.Bitmap]::new([int]$screen.Width, [int]$screen.Height)
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
||||
try {
|
||||
$graphics.CopyFromScreen([int]$screen.X, [int]$screen.Y, 0, 0, $bitmap.Size)
|
||||
$bitmap.Save((Join-Path $Evidence "$Name-desktop.png"), [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
} finally { $graphics.Dispose(); $bitmap.Dispose() }
|
||||
}
|
||||
# Whole desktop plus every visible top-level window. The two parts are
|
||||
# independent so one failing cannot hide the other or the original error.
|
||||
function Save-Diagnostics([string]$Name) {
|
||||
try { Save-WindowList $Name } catch { Write-Warning "window list ${Name}: $_" }
|
||||
try { Save-Desktop $Name } catch { Write-Warning "desktop capture ${Name}: $_" }
|
||||
}
|
||||
function Read-Terminal {
|
||||
$texts = $window.FindAll([System.Windows.Automation.TreeScope]::Descendants,
|
||||
[System.Windows.Automation.PropertyCondition]::new([System.Windows.Automation.AutomationElement]::IsTextPatternAvailableProperty, $true))
|
||||
@@ -27,12 +173,12 @@ function Read-Terminal {
|
||||
}) -join "`n")
|
||||
}
|
||||
function Wait-For([scriptblock]$Check, [string]$Label) {
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(45)
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds($StageSeconds)
|
||||
while ([DateTime]::UtcNow -lt $deadline) {
|
||||
if (& $Check) { return }
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
throw "Deadline waiting for $Label"
|
||||
throw "Deadline (${StageSeconds}s) waiting for $Label"
|
||||
}
|
||||
function Capture([string]$Name) {
|
||||
(Read-Terminal) | Set-Content -Encoding UTF8 (Join-Path $Evidence "$Name.txt")
|
||||
@@ -45,21 +191,44 @@ function Capture([string]$Name) {
|
||||
$bitmap.Save((Join-Path $Evidence "$Name.png"), [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
} finally { $graphics.Dispose(); $bitmap.Dispose() }
|
||||
}
|
||||
function Focus-OwnedWindow {
|
||||
function Focus-OwnedWindow([string]$Reason) {
|
||||
Enter-Stage "focus $Reason"
|
||||
$process = Get-Process -Id $ownedPid
|
||||
if ($process.Path -ne $Terminal) { throw 'Terminal process identity changed' }
|
||||
$handle = [IntPtr]$window.Current.NativeWindowHandle
|
||||
[void][ClipboardFixtureWindow]::ShowWindow($handle, 9)
|
||||
[void][ClipboardFixtureWindow]::SetForegroundWindow($handle)
|
||||
Wait-For { [ClipboardFixtureWindow]::GetForegroundWindow() -eq $handle } 'owned Terminal focus'
|
||||
$script:ownedHandle = $handle
|
||||
# Re-assert until the owned window holds the foreground on two consecutive
|
||||
# looks. Only focus is retried: the paste and the Enter are each sent once.
|
||||
$attempt = 0
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds($StageSeconds)
|
||||
while ([DateTime]::UtcNow -lt $deadline) {
|
||||
$attempt++
|
||||
$method = [ClipboardFixtureWindow]::Activate($handle, ($attempt -ge 3))
|
||||
if ($attempt -le 5 -or $method -ne 'denied') { Add-EvidenceLine 'focus.log' "$Reason attempt=$attempt method=$method" }
|
||||
if ($method -ne 'denied') {
|
||||
Start-Sleep -Milliseconds 150
|
||||
if ([ClipboardFixtureWindow]::GetForegroundWindow() -eq $handle) { return }
|
||||
}
|
||||
Start-Sleep -Milliseconds 350
|
||||
}
|
||||
throw "Deadline (${StageSeconds}s) waiting for owned Terminal focus $Reason after $attempt activation attempts"
|
||||
}
|
||||
if ($DiagnoseOnly) {
|
||||
Save-Diagnostics $DiagnoseOnly
|
||||
exit 0
|
||||
}
|
||||
try {
|
||||
Enter-Stage 'launch'
|
||||
# All arguments are owned fixture paths; the fake key cannot reach a paid provider.
|
||||
foreach ($arg in @($Terminal, $Binary, $Workspace, $BaseUrl, $Title)) {
|
||||
if (-not $arg) { throw 'Missing fixture argument' }
|
||||
if ($arg.Contains('"')) { throw 'Unexpected quote in fixture argument' }
|
||||
}
|
||||
# What the runner desktop held before this fixture added its own window.
|
||||
Save-Diagnostics 'start'
|
||||
$arguments = "-w new new-tab --title `"$Title`" --suppressApplicationTitle --startingDirectory `"$Workspace`" `"$Binary`" --workspace `"$Workspace`" --no-project-config --fresh --provider openai --model gpt-4o-mini --api-key clipboard-fixture-only --base-url `"$BaseUrl`""
|
||||
Start-Process -FilePath $Terminal -ArgumentList $arguments | Out-Null
|
||||
Enter-Stage 'window'
|
||||
Wait-For {
|
||||
$windows = [System.Windows.Automation.AutomationElement]::RootElement.FindAll(
|
||||
[System.Windows.Automation.TreeScope]::Children, [System.Windows.Automation.Condition]::TrueCondition)
|
||||
@@ -73,30 +242,46 @@ try {
|
||||
}
|
||||
return $false
|
||||
} 'owned Windows Terminal window'
|
||||
Focus-OwnedWindow
|
||||
Focus-OwnedWindow 'before composer'
|
||||
Enter-Stage 'composer'
|
||||
Wait-For { (Read-Terminal).Contains('Type a message') } 'Codewhale composer'
|
||||
Capture 'ready'
|
||||
Enter-Stage 'clipboard'
|
||||
$payload = [System.IO.File]::ReadAllText((Join-Path $Evidence 'expected.txt'), [System.Text.Encoding]::UTF8)
|
||||
[System.Windows.Forms.Clipboard]::SetText($payload)
|
||||
if ([System.Windows.Forms.Clipboard]::GetText() -ne $payload) { throw 'Clipboard readback mismatch' }
|
||||
Focus-OwnedWindow
|
||||
Focus-OwnedWindow 'before paste'
|
||||
Enter-Stage 'paste'
|
||||
[System.Windows.Forms.SendKeys]::SendWait('^+v')
|
||||
$foregroundAfterPaste = [ClipboardFixtureWindow]::GetForegroundWindow() -eq $ownedHandle
|
||||
# Let every pasted line arrive before asking the independent HTTP observer
|
||||
# whether anything was submitted prematurely.
|
||||
Start-Sleep -Milliseconds 1800
|
||||
Capture 'pasted'
|
||||
@{ processId = $ownedPid; sessionId = (Get-Process -Id $ownedPid).SessionId; chars = $payload.Length; input = 'Windows clipboard + Ctrl+Shift+V' } |
|
||||
@{ processId = $ownedPid; sessionId = (Get-Process -Id $ownedPid).SessionId; chars = $payload.Length; input = 'Windows clipboard + Ctrl+Shift+V'; foregroundAfterPaste = $foregroundAfterPaste } |
|
||||
ConvertTo-Json | Set-Content -Encoding UTF8 (Join-Path $Evidence 'pasted.json')
|
||||
Enter-Stage 'await submit'
|
||||
Wait-For { Test-Path (Join-Path $Evidence 'submit') } 'independent no-submit assertion'
|
||||
Focus-OwnedWindow
|
||||
Focus-OwnedWindow 'before Enter'
|
||||
Enter-Stage 'enter'
|
||||
[System.Windows.Forms.SendKeys]::SendWait('{ENTER}')
|
||||
Enter-Stage 'reply'
|
||||
Wait-For { (Read-Terminal).Contains('cw-clipboard-ok') } 'actual provider reply'
|
||||
Capture 'completed'
|
||||
@{ enterCount = 1; renderedReply = 'cw-clipboard-ok' } | ConvertTo-Json |
|
||||
Set-Content -Encoding UTF8 (Join-Path $Evidence 'completed.json')
|
||||
Enter-Stage 'done'
|
||||
} catch {
|
||||
$failure = $_
|
||||
# Stage and message first, then the desktop (no UI Automation involved),
|
||||
# then the Terminal text, which needs the UI Automation that may have failed.
|
||||
try {
|
||||
@{ stage = $stage; error = "$failure" } | ConvertTo-Json |
|
||||
Set-Content -Encoding UTF8 (Join-Path $Evidence 'failure.json')
|
||||
} catch { Write-Warning "failure record: $_" }
|
||||
Save-Diagnostics 'failure'
|
||||
if ($window) { try { Capture 'failure' } catch { Write-Warning $_ } }
|
||||
throw
|
||||
throw $failure
|
||||
} finally {
|
||||
# Retire only the portable Terminal instance proven to belong to this fixture.
|
||||
if ($ownedPid) {
|
||||
|
||||
Reference in new issue
Block a user