mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-15 16:52:14 +08:00
Adds the foundation layer for Puppeteer-parity features: - TabSession.loadedHtml + setTabContent/getLoadedHtml/clearLoadedHtml — enables load-html content to survive context recreation (viewport --scale) via in-memory replay. ASCII lifecycle diagram in the source explains the clear-before-navigation contract. - COMMAND_ALIASES + canonicalizeCommand() helper — single source of truth for name aliases (setcontent / set-content / setContent → load-html), consumed by server dispatch and chain prevalidation. - buildUnknownCommandError() pure function — rich error messages with Levenshtein-based "Did you mean" suggestions (distance ≤ 2, input length ≥ 4 to skip 2-letter noise) and NEW_IN_VERSION upgrade hints. - load-html registered in WRITE_COMMANDS + SCOPE_WRITE so scoped write tokens can use it. - screenshot and viewport descriptions updated for upcoming flags. - New browse/test/dx-polish.test.ts (15 tests): alias canonicalization, Levenshtein threshold + alphabetical tiebreak, short-input guard, NEW_IN_VERSION upgrade hint, alias + scope integration invariants. No consumers yet — pure additive foundation. Safe to bisect on its own.
102 lines
4.1 KiB
TypeScript
102 lines
4.1 KiB
TypeScript
import { describe, it, expect } from 'bun:test';
|
|
import {
|
|
canonicalizeCommand,
|
|
COMMAND_ALIASES,
|
|
NEW_IN_VERSION,
|
|
buildUnknownCommandError,
|
|
ALL_COMMANDS,
|
|
} from '../src/commands';
|
|
|
|
describe('canonicalizeCommand', () => {
|
|
it('resolves setcontent → load-html', () => {
|
|
expect(canonicalizeCommand('setcontent')).toBe('load-html');
|
|
});
|
|
|
|
it('resolves set-content → load-html', () => {
|
|
expect(canonicalizeCommand('set-content')).toBe('load-html');
|
|
});
|
|
|
|
it('resolves setContent → load-html (case-sensitive key)', () => {
|
|
expect(canonicalizeCommand('setContent')).toBe('load-html');
|
|
});
|
|
|
|
it('passes canonical names through unchanged', () => {
|
|
expect(canonicalizeCommand('load-html')).toBe('load-html');
|
|
expect(canonicalizeCommand('goto')).toBe('goto');
|
|
});
|
|
|
|
it('passes unknown names through unchanged (alias map is allowlist, not filter)', () => {
|
|
expect(canonicalizeCommand('totally-made-up')).toBe('totally-made-up');
|
|
});
|
|
});
|
|
|
|
describe('buildUnknownCommandError', () => {
|
|
it('names the input in every error', () => {
|
|
const msg = buildUnknownCommandError('xyz', ALL_COMMANDS);
|
|
expect(msg).toContain(`Unknown command: 'xyz'`);
|
|
});
|
|
|
|
it('suggests closest match within Levenshtein 2 when input length >= 4', () => {
|
|
const msg = buildUnknownCommandError('load-htm', ALL_COMMANDS);
|
|
expect(msg).toContain(`Did you mean 'load-html'?`);
|
|
});
|
|
|
|
it('does NOT suggest for short inputs (< 4 chars, avoids noise on js/is typos)', () => {
|
|
// 'j' is distance 1 from 'js' but only 1 char — suggestion would be noisy
|
|
const msg = buildUnknownCommandError('j', ALL_COMMANDS);
|
|
expect(msg).not.toContain('Did you mean');
|
|
});
|
|
|
|
it('uses alphabetical tiebreak for deterministic suggestions', () => {
|
|
// Synthetic command set where two commands tie on distance from input
|
|
const syntheticSet = new Set(['alpha', 'beta']);
|
|
// 'alpha' vs 'delta' = 3 edits; 'beta' vs 'delta' = 2 edits
|
|
// Let's use a case that genuinely ties.
|
|
const ties = new Set(['abcd', 'abce']); // both distance 1 from 'abcf'
|
|
const msg = buildUnknownCommandError('abcf', ties, {}, {});
|
|
// Alphabetical first: 'abcd' comes before 'abce'
|
|
expect(msg).toContain(`Did you mean 'abcd'?`);
|
|
});
|
|
|
|
it('appends upgrade hint when command appears in NEW_IN_VERSION', () => {
|
|
// Synthetic: pretend load-html isn't in the command set (agent on older build)
|
|
const noLoadHtml = new Set([...ALL_COMMANDS].filter(c => c !== 'load-html'));
|
|
const msg = buildUnknownCommandError('load-html', noLoadHtml, COMMAND_ALIASES, NEW_IN_VERSION);
|
|
expect(msg).toContain('added in browse v');
|
|
expect(msg).toContain('Upgrade:');
|
|
});
|
|
|
|
it('omits upgrade hint for unknown commands not in NEW_IN_VERSION', () => {
|
|
const msg = buildUnknownCommandError('notarealcommand', ALL_COMMANDS);
|
|
expect(msg).not.toContain('added in browse v');
|
|
});
|
|
|
|
it('NEW_IN_VERSION has load-html entry', () => {
|
|
expect(NEW_IN_VERSION['load-html']).toBeTruthy();
|
|
});
|
|
|
|
it('COMMAND_ALIASES + command set are consistent — all alias targets exist', () => {
|
|
for (const target of Object.values(COMMAND_ALIASES)) {
|
|
expect(ALL_COMMANDS.has(target)).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Alias + SCOPE_WRITE integration invariant', () => {
|
|
it('load-html is in SCOPE_WRITE (alias canonicalization happens before scope check)', async () => {
|
|
const { SCOPE_WRITE } = await import('../src/token-registry');
|
|
expect(SCOPE_WRITE.has('load-html')).toBe(true);
|
|
});
|
|
|
|
it('setcontent is NOT directly in any scope set (must canonicalize first)', async () => {
|
|
const { SCOPE_WRITE, SCOPE_READ, SCOPE_ADMIN, SCOPE_CONTROL } = await import('../src/token-registry');
|
|
// The alias itself must NOT appear in any scope set — only the canonical form.
|
|
// This proves scope enforcement relies on canonicalization at dispatch time,
|
|
// not on the alias leaking through as an acceptable command.
|
|
expect(SCOPE_WRITE.has('setcontent')).toBe(false);
|
|
expect(SCOPE_READ.has('setcontent')).toBe(false);
|
|
expect(SCOPE_ADMIN.has('setcontent')).toBe(false);
|
|
expect(SCOPE_CONTROL.has('setcontent')).toBe(false);
|
|
});
|
|
});
|