Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/rangelink-vscode-extension/.vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { defineConfig } from '@vscode/test-cli';

import { ASSISTED_TIMEOUT_MS, BASE_CONFIG } from './.vscode-test.base.mjs';

const parsed = Number(process.env.RANGELINK_MOCHA_TIMEOUT);
const MOCHA_TIMEOUT = Number.isFinite(parsed) && parsed >= 0 ? parsed : ASSISTED_TIMEOUT_MS;

export default defineConfig([
{
...BASE_CONFIG,
mocha: { timeout: ASSISTED_TIMEOUT_MS, ...BASE_CONFIG.mocha },
mocha: { timeout: MOCHA_TIMEOUT, ...BASE_CONFIG.mocha },
},
]);
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,8 @@ test_cases:
- clipboard
feature: 'Clipboard Preservation'
scenario: 'always mode: clipboard content before AI assistant paste is restored after'
expected_result: 'Dummy AI tier1 contains the exact link (relPath#L1-L3). Clipboard contains the sentinel value. assertClipboardRestored passes — preserve=always silently restored the clipboard after AI delivery.'
automated: assisted
expected_result: 'Dummy AI tier1 contains the exact padded link ` relPath#L1C1-L3C7 ` (character-precise, with smart-padding spaces). Clipboard contains the sentinel value. assertClipboardRestored passes — preserve=always silently restored the clipboard after AI delivery.'
automated: true
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- id: clipboard-preservation-005
labels:
Expand Down Expand Up @@ -437,8 +437,7 @@ test_cases:
feature: 'Clipboard Preservation'
scenario: 'AI assistant auto-paste fails (focus command throws): RangeLink stays in clipboard for manual paste'
expected_result: 'Clipboard contains the generated RangeLink (not CLIPBOARD_SENTINEL). isClipboardRestorationApplicable returns false because getUserInstruction(Failure) is defined. The "Paste manually" warning toast was shown.'
command_to_run: 'pnpm test:release:grep "clipboard-preservation-010"'
automated: assisted
automated: true

- id: clipboard-preservation-011
labels:
Expand Down Expand Up @@ -1372,7 +1371,7 @@ test_cases:
feature: 'Core Send Commands — R-L'
scenario: 'R-L sends RangeLink to bound AI assistant destination'
expected_result: 'RangeLink appears in AI chat input. Success toast confirms send.'
automated: assisted
automated: true

- id: core-send-commands-r-c-001
labels:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ const VERDICT_FAIL_COMMAND_PREFIX = 'rangelink._test.verdict.fail';
// patterns would otherwise throw `command '...' already exists`.
let verdictInvocationCounter = 0;

// Tracks the currently-active verdict state so a new invocation can clean up
// stale UI from a previous invocation that never settled (e.g., Mocha timed
// out the test before the human clicked a button).
let activeVerdictDisposables: vscode.Disposable[] | undefined;
let activeVerdictReject: ((reason: unknown) => void) | undefined;

/**
* Pauses the test until the human clicks Pass or Fail in the status bar.
*
Expand Down Expand Up @@ -113,6 +119,19 @@ export const waitForHumanVerdict = async (
nodeConsole.log('Click the PASS or FAIL button in the status bar (bottom-left) when done.');
nodeConsole.log(SECTION_LINE);

// Dispose any stale UI from a previous invocation that never settled
// (e.g., Mocha timed out the test before the human clicked a button).
if (activeVerdictDisposables !== undefined) {
for (const d of activeVerdictDisposables) {
d.dispose();
}
activeVerdictDisposables = undefined;
}
if (activeVerdictReject !== undefined) {
activeVerdictReject(new Error('Superseded by a new waitForHumanVerdict invocation'));
activeVerdictReject = undefined;
}

const invocationId = ++verdictInvocationCounter;
const passCommand = `${VERDICT_PASS_COMMAND_PREFIX}.${invocationId}`;
const failCommand = `${VERDICT_FAIL_COMMAND_PREFIX}.${invocationId}`;
Expand All @@ -126,13 +145,16 @@ export const waitForHumanVerdict = async (
(progress) => {
progress.report({ message: 'Click PASS or FAIL in the bottom-left status bar' });

return new Promise<HumanVerdict>((resolve) => {
return new Promise<HumanVerdict>((resolve, reject) => {
activeVerdictReject = reject;
const disposables: vscode.Disposable[] = [];
let settled = false;

const settleWith = (verdict: HumanVerdict): void => {
if (settled) return;
settled = true;
activeVerdictDisposables = undefined;
activeVerdictReject = undefined;
for (const d of disposables) {
d.dispose();
}
Expand All @@ -159,6 +181,8 @@ export const waitForHumanVerdict = async (
failItem.color = new vscode.ThemeColor('testing.iconFailed');
failItem.show();
disposables.push(failItem);

activeVerdictDisposables = disposables;
});
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import assert from 'node:assert';
import * as vscode from 'vscode';

import {
CMD_BIND_TO_CUSTOM_AI_BY_ID,
CMD_BIND_TO_TEXT_EDITOR_HERE,
CMD_COPY_LINK_RELATIVE,
CMD_PASTE_CURRENT_FILE_PATH_RELATIVE,
Expand All @@ -20,8 +21,6 @@ import {
openAndDismiss,
standardSuite,
TERMINAL_READY_MS,
waitForHuman,
CLIPBOARD_SENTINEL,
writeClipboardSentinel,
} from '../helpers';
import type { CapturingTerminal } from '../helpers/capturingPtyHelpers';
Expand Down Expand Up @@ -206,7 +205,7 @@ standardSuite('Clipboard Preservation — Assisted', (ss) => {
ss.log('✓ Clipboard restored to sentinel and phrase landed in destination file after R-V');
});

test('[assisted] clipboard-preservation-004: always mode — AI assistant paste restores clipboard', async () => {
test('clipboard-preservation-004: always mode — AI assistant paste restores clipboard', async () => {
const { uri: fileUri } = ss.createContentFile('cbp-004', 10, (i) => `line ${i + 1} content`);

const relPath = vscode.workspace.asRelativePath(fileUri);
Expand All @@ -218,30 +217,26 @@ standardSuite('Clipboard Preservation — Assisted', (ss) => {
editor.selection = new vscode.Selection(0, 0, 2, 6);
await ss.settle();

await vscode.commands.executeCommand(CMD_BIND_TO_CUSTOM_AI_BY_ID, {
extensionId: 'rangelink.dummy-ai-extension',
});
await ss.settle();

const logCapture = getLogCapture();
logCapture.mark('before-004');
await waitForHuman(
'clipboard-preservation-004',
`Press Cmd+R Cmd+D → bind "Dummy AI (Tier 1)", click back into the editor, press Cmd+R Cmd+L`,
[
'1. Press Cmd+R Cmd+D → select "Dummy AI (Tier 1)" from the picker',
'2. Click back into the editor (lines 1-3 are pre-selected)',
'3. Press Cmd+R Cmd+L — the link should appear in Dummy AI Tier 1 textarea',
'4. Press Cancel to continue (assertions happen automatically)',
],
);

await vscode.commands.executeCommand(CMD_COPY_LINK_RELATIVE);
await ss.settle();

assertClipboardPreservationRan(logCapture, 'before-004', 'R-L');
const dummyText = (await vscode.commands.executeCommand('dummyAi.getText')) as {
tier1: string;
tier2: string;
};
// trim() strips smart-padding spaces (pasteLink='both' adds leading/trailing space)
assert.strictEqual(
dummyText.tier1.trim(),
expectedLink,
`Expected Dummy AI tier1="${expectedLink}", got: ${JSON.stringify(dummyText.tier1)}`,
dummyText.tier1,
` ${expectedLink} `,
`Expected Dummy AI tier1=" ${expectedLink} ", got: ${JSON.stringify(dummyText.tier1)}`,
);
await assertClipboardRestored('clipboard-preservation-004: always + AI paste');
ss.log('✓ Clipboard restored to sentinel and link landed in Dummy AI after R-L');
Expand Down Expand Up @@ -338,29 +333,23 @@ standardSuite('Clipboard Preservation — Assisted', (ss) => {
ss.log('✓ Clipboard unchanged after picker dismissed (no operation performed)');
});

test('[assisted] clipboard-preservation-010: focus command failure preserves link in clipboard for manual paste', async () => {
test('clipboard-preservation-010: focus command failure preserves link in clipboard for manual paste', async () => {
const { uri: fileUri } = ss.createContentFile('cbp-010', 5, (i) => `line ${i + 1}`);

const relPath = vscode.workspace.asRelativePath(fileUri);
await ss.openEditor(fileUri);
const editor = await ss.openEditor(fileUri);
editor.selection = new vscode.Selection(0, 0, 3, 0);
await ss.settle();
await writeClipboardSentinel();

await vscode.commands.executeCommand(CMD_BIND_TO_CUSTOM_AI_BY_ID, {
extensionId: 'rangelink.dummy-ai-extension-focus-fail',
});
await ss.settle();

const logCapture = getLogCapture();
logCapture.mark('before-010');

await waitForHuman(
'clipboard-preservation-010',
`clipboard.preserve="always". Bind "Dummy AI (Focus-Fail)" via Cmd+R Cmd+D, then select lines 1-3 in the test file and press Cmd+R Cmd+L. The focus command throws — you should see a warning. Sentinel: "${CLIPBOARD_SENTINEL}".`,
[
'1. Press Cmd+R Cmd+D → select "Dummy AI (Focus-Fail)" from the picker',
`2. Click back into the test file (${relPath}) and select lines 1-3`,
'3. Press Cmd+R Cmd+L — the focus command will throw an intentional error',
'4. Observe the warning message (manual paste instruction)',
'5. Press Cancel to continue (test verifies the link stayed in the clipboard)',
],
);

await vscode.commands.executeCommand(CMD_COPY_LINK_RELATIVE);
await ss.settle();

const lines010 = logCapture.getLinesSince('before-010');
Expand All @@ -384,10 +373,11 @@ standardSuite('Clipboard Preservation — Assisted', (ss) => {
);
const generatedLink = extractGeneratedLink(lines010);
assert.ok(generatedLink, 'Expected "Generated link:" log line');
const PADDED_GENERATED_LINK = ` ${generatedLink} `;
assert.strictEqual(
clipboardContent,
generatedLink,
`Expected clipboard to equal generated link "${generatedLink}", got: ${clipboardContent}`,
PADDED_GENERATED_LINK,
`Expected clipboard to equal "${PADDED_GENERATED_LINK}", got: ${JSON.stringify(clipboardContent)}`,
);
ss.log(
'✓ Clipboard not restored after focus failure — link stays in clipboard for manual paste',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'node:path';
import * as vscode from 'vscode';

import {
CMD_BIND_TO_CUSTOM_AI_BY_ID,
CMD_BIND_TO_TEXT_EDITOR_HERE,
CMD_COPY_LINK_ONLY_RELATIVE,
CMD_COPY_LINK_RELATIVE,
Expand Down Expand Up @@ -85,7 +86,7 @@ standardSuite('Core Send Commands', (ss) => {
ss.log('✓ R-L sent exact RangeLink to bound text editor destination');
});

test('[assisted] core-send-commands-r-l-003: R-L sends RangeLink to bound AI assistant destination', async () => {
test('core-send-commands-r-l-003: R-L sends RangeLink to bound AI assistant destination', async () => {
const CSC_R_L_003_LINE_COUNT = 10;
const { uri: fileUri } = ss.createContentFile(
'csc-r-l-003',
Expand All @@ -96,31 +97,30 @@ standardSuite('Core Send Commands', (ss) => {
const relPath = vscode.workspace.asRelativePath(fileUri);
const expectedLink = `${relPath}#L1-L3`;

await ss.openEditor(fileUri);
const doc = await vscode.workspace.openTextDocument(fileUri);
const editor = await vscode.window.showTextDocument(doc);
editor.selection = new vscode.Selection(0, 0, 3, 0);
await ss.settle();

await vscode.commands.executeCommand(CMD_BIND_TO_CUSTOM_AI_BY_ID, {
extensionId: 'rangelink.dummy-ai-extension',
});
await ss.settle();

const logCapture = getLogCapture();
logCapture.mark('before-r-l-003');

await waitForHuman(
'core-send-commands-r-l-003',
`Bind Dummy AI Tier 1 via Cmd+R Cmd+D, select lines 1-3 in ${relPath}, press Cmd+R Cmd+L.`,
[
'1. Press Cmd+R Cmd+D → select "Dummy AI (Tier 1)"',
`2. Click into ${relPath}, select lines 1-3`,
'3. Press Cmd+R Cmd+L',
],
);

await vscode.commands.executeCommand(CMD_COPY_LINK_RELATIVE);
await ss.settle();

const dummyText = (await vscode.commands.executeCommand('dummyAi.getText')) as {
tier1: string;
tier2: string;
};
assert.strictEqual(
dummyText.tier1.trim(),
expectedLink,
`Expected Dummy AI tier1="${expectedLink}", got: ${JSON.stringify(dummyText.tier1)}`,
dummyText.tier1,
` ${expectedLink} `,
`Expected Dummy AI tier1=" ${expectedLink} ", got: ${JSON.stringify(dummyText.tier1)}`,
);

assert.ok(
Expand Down
Loading