fix: stabilize live session UI and skill publishing
Keep message navigation and progress state synchronized, and preserve sequential split-flap count updates with a bounded queue. Publish the Obelisk skill under skills/obelisk for npx skills, sharing the same staging layout between CI and local releases with regression coverage.
This commit is contained in:
@@ -24,11 +24,12 @@ test('flap state queues rapid updates without interrupting the active flap', ()
|
||||
|
||||
assert.equal(state.from, '822');
|
||||
assert.equal(state.to, '823');
|
||||
assert.equal(state.queued, '824');
|
||||
assert.deepEqual(state.queued, ['824']);
|
||||
|
||||
state = finishFlap(state);
|
||||
assert.equal(state.from, '823');
|
||||
assert.equal(state.to, '824');
|
||||
assert.deepEqual(state.queued, []);
|
||||
assert.equal(state.animating, true);
|
||||
|
||||
state = finishFlap(state);
|
||||
@@ -36,6 +37,59 @@ test('flap state queues rapid updates without interrupting the active flap', ()
|
||||
assert.equal(state.animating, false);
|
||||
});
|
||||
|
||||
test('flap queue preserves every rapid numeric step in order', () => {
|
||||
let state = createFlapState(907);
|
||||
state = requestFlap(state, 908);
|
||||
state = requestFlap(state, 909);
|
||||
state = requestFlap(state, 910);
|
||||
|
||||
assert.equal(state.to, '908');
|
||||
assert.deepEqual(state.queued, ['909', '910']);
|
||||
|
||||
state = finishFlap(state);
|
||||
assert.equal(state.from, '908');
|
||||
assert.equal(state.to, '909');
|
||||
assert.deepEqual(state.queued, ['910']);
|
||||
});
|
||||
|
||||
test('a small direct numeric jump expands into consecutive flap targets', () => {
|
||||
const state = requestFlap(createFlapState(908), 910);
|
||||
|
||||
assert.equal(state.from, '908');
|
||||
assert.equal(state.to, '909');
|
||||
assert.deepEqual(state.queued, ['910']);
|
||||
});
|
||||
|
||||
test('the flap queue stays bounded while retaining the latest target', () => {
|
||||
let state = createFlapState(0);
|
||||
for (let value = 1; value <= 10; value++) state = requestFlap(state, value);
|
||||
|
||||
assert.ok(state.queued.length <= 4);
|
||||
assert.equal(state.queued.at(-1), '10');
|
||||
|
||||
while (state.animating) state = finishFlap(state);
|
||||
assert.equal(state.settled, '10');
|
||||
});
|
||||
|
||||
test('large numeric jumps go directly to the latest value', () => {
|
||||
const state = requestFlap(createFlapState(908), 1000);
|
||||
|
||||
assert.equal(state.to, '1000');
|
||||
assert.deepEqual(state.queued, []);
|
||||
});
|
||||
|
||||
test('a reversing update discards stale forward targets', () => {
|
||||
let state = createFlapState(907);
|
||||
state = requestFlap(state, 908);
|
||||
state = requestFlap(state, 909);
|
||||
state = requestFlap(state, 907);
|
||||
|
||||
assert.deepEqual(state.queued, ['907']);
|
||||
state = finishFlap(state);
|
||||
assert.equal(state.from, '908');
|
||||
assert.equal(state.to, '907');
|
||||
});
|
||||
|
||||
test('the latest request clears an older queued value when it matches the active target', () => {
|
||||
let state = createFlapState(822);
|
||||
state = requestFlap(state, 823);
|
||||
@@ -43,7 +97,7 @@ test('the latest request clears an older queued value when it matches the active
|
||||
state = requestFlap(state, 823);
|
||||
|
||||
assert.equal(state.to, '823');
|
||||
assert.equal(state.queued, null);
|
||||
assert.deepEqual(state.queued, []);
|
||||
state = finishFlap(state);
|
||||
assert.equal(state.settled, '823');
|
||||
assert.equal(state.animating, false);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const stageScript = join(repoRoot, 'packaging', 'stage-skill-repo.sh');
|
||||
|
||||
test('skill release staging produces the npx skills repository layout', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'obelisk-skill-release-'));
|
||||
const artifact = join(root, 'artifact');
|
||||
const target = join(root, 'repo');
|
||||
try {
|
||||
mkdirSync(join(artifact, 'references'), { recursive: true });
|
||||
mkdirSync(join(artifact, 'scripts'), { recursive: true });
|
||||
mkdirSync(join(target, '.git'), { recursive: true });
|
||||
writeFileSync(join(artifact, 'SKILL.md'), '---\nname: obelisk\ndescription: test\n---\n');
|
||||
writeFileSync(join(artifact, 'package.json'), '{"type":"module"}\n');
|
||||
writeFileSync(join(artifact, 'references', 'api-reference.md'), '# API\n');
|
||||
writeFileSync(join(artifact, 'scripts', 'runtime.js'), 'export {};\n');
|
||||
writeFileSync(join(target, '.git', 'keep'), 'preserved\n');
|
||||
writeFileSync(join(target, 'stale.txt'), 'remove me\n');
|
||||
|
||||
const result = spawnSync('bash', [stageScript, target, artifact], {
|
||||
cwd: repoRoot,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
|
||||
assert.deepEqual(readdirSync(target).sort(), ['.git', 'LICENSE', 'README.md', 'skills']);
|
||||
assert.deepEqual(readdirSync(join(target, 'skills')).sort(), ['obelisk']);
|
||||
for (const relativePath of [
|
||||
'SKILL.md',
|
||||
'package.json',
|
||||
'references/api-reference.md',
|
||||
'scripts/runtime.js',
|
||||
]) {
|
||||
assert.equal(existsSync(join(target, 'skills', 'obelisk', relativePath)), true);
|
||||
}
|
||||
assert.equal(existsSync(join(target, '.git', 'keep')), true);
|
||||
assert.equal(existsSync(join(target, 'stale.txt')), false);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('CI and local publish use the same skill repository staging step', () => {
|
||||
const workflow = readFileSync(join(repoRoot, '.github', 'workflows', 'publish-skill.yml'), 'utf8');
|
||||
const localPublish = readFileSync(join(repoRoot, 'packaging', 'publish-skill.sh'), 'utf8');
|
||||
|
||||
assert.match(workflow, /packaging\/stage-skill-repo\.sh/);
|
||||
assert.match(localPublish, /packaging\/stage-skill-repo\.sh/);
|
||||
});
|
||||
@@ -186,3 +186,15 @@ test('live totals and scroll position remain isolated across interleaved updates
|
||||
assert.match(updateScrollProgress, /currentMsgIdx\.value\s*=/);
|
||||
assert.doesNotMatch(updateScrollProgress, /totalMsgs\.value\s*=/);
|
||||
});
|
||||
|
||||
test('message navigation keeps the top progress bar aligned with the current position', () => {
|
||||
const source = readFileSync(new URL('../app/src/renderer/src/views/SessionDetail.vue', import.meta.url), 'utf8');
|
||||
const setMessagePosition = functionSource(source, 'setMessagePosition');
|
||||
const updateScrollProgress = functionSource(source, 'updateScrollProgress');
|
||||
const navTo = functionSource(source, 'navTo');
|
||||
|
||||
assert.match(setMessagePosition, /currentMsgIdx\.value\s*=\s*index/);
|
||||
assert.match(setMessagePosition, /progressPct\.value\s*=/);
|
||||
assert.match(updateScrollProgress, /setMessagePosition\(bottomMsgIdx,\s*msgs\.length\)/);
|
||||
assert.match(navTo, /setMessagePosition\(idx,\s*msgs\.length\)/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user