mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 15:39:41 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17a976fa2e |
+5
-1
@@ -1031,7 +1031,11 @@ export async function ensureStatusLineExporterScript(): Promise<string> {
|
|||||||
// render, and a truncate-then-write (plus a chmod AFTER the write) opened two
|
// render, and a truncate-then-write (plus a chmod AFTER the write) opened two
|
||||||
// windows in which Claude Code could run an empty or non-executable file.
|
// windows in which Claude Code could run an empty or non-executable file.
|
||||||
// rename() swaps the complete, already-executable file in atomically.
|
// rename() swaps the complete, already-executable file in atomically.
|
||||||
const tmpPath = `${scriptPath}.${process.pid}.${Date.now()}.tmp`;
|
// ⚠️ The temp name must be unique per CALL, not per millisecond: sessions created
|
||||||
|
// concurrently (spawn_workers, a multi-tab Run) refresh this together, a shared
|
||||||
|
// name let the first rename consume the others' temp file, and their ENOENT
|
||||||
|
// dropped those sessions from tmux to the direct-PTY fallback.
|
||||||
|
const tmpPath = `${scriptPath}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
||||||
await writeFile(tmpPath, desired);
|
await writeFile(tmpPath, desired);
|
||||||
await chmod(tmpPath, 0o755);
|
await chmod(tmpPath, 0o755);
|
||||||
await rename(tmpPath, scriptPath);
|
await rename(tmpPath, scriptPath);
|
||||||
|
|||||||
+24
-64
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
|||||||
* hook definitions for desktop notifications.
|
* hook definitions for desktop notifications.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, beforeAll, beforeEach, afterAll, afterEach } from 'vitest';
|
import { describe, it, expect, beforeAll, beforeEach, afterAll, afterEach, vi } from 'vitest';
|
||||||
import {
|
import {
|
||||||
chmodSync,
|
chmodSync,
|
||||||
closeSync,
|
closeSync,
|
||||||
@@ -26,6 +26,7 @@ import { spawn } from 'node:child_process';
|
|||||||
import {
|
import {
|
||||||
applyStatusLineConfig,
|
applyStatusLineConfig,
|
||||||
ensureCodemanHooks,
|
ensureCodemanHooks,
|
||||||
|
ensureStatusLineExporterScript,
|
||||||
findEffectiveUserStatusLineCommand,
|
findEffectiveUserStatusLineCommand,
|
||||||
generateBackgroundWakeScript,
|
generateBackgroundWakeScript,
|
||||||
generateHooksConfig,
|
generateHooksConfig,
|
||||||
@@ -1418,6 +1419,26 @@ describe('resolveStatusLineCliCommand', () => {
|
|||||||
expect(siblings).toEqual([]);
|
expect(siblings).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('survives concurrent refreshes in the same millisecond (sessions created at once)', async () => {
|
||||||
|
const scriptPath = (await resolveStatusLineCliCommand(testDir, true))!;
|
||||||
|
writeFileSync(scriptPath, '#!/bin/sh\n# CODEMAN_STATUSLINE_EXPORTER_V0\necho stale\n');
|
||||||
|
// A frozen clock makes every writer agree on the timestamp, which is what two
|
||||||
|
// session creates in one millisecond do. A temp name built from pid + Date.now()
|
||||||
|
// is then shared: the first rename consumes it, every later rename fails ENOENT,
|
||||||
|
// and that session's tmux create fell back to a direct PTY.
|
||||||
|
const clock = vi.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000);
|
||||||
|
try {
|
||||||
|
const paths = await Promise.all(Array.from({ length: 8 }, () => ensureStatusLineExporterScript()));
|
||||||
|
expect(new Set(paths)).toEqual(new Set([scriptPath]));
|
||||||
|
} finally {
|
||||||
|
clock.mockRestore();
|
||||||
|
}
|
||||||
|
expect(readFileSync(scriptPath, 'utf-8')).not.toContain('echo stale');
|
||||||
|
expect(statSync(scriptPath).mode & 0o111).not.toBe(0);
|
||||||
|
const siblings = readdirSync(join(scriptPath, '..')).filter((f) => f.startsWith('statusline-exporter.sh.'));
|
||||||
|
expect(siblings).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('never overrides a real, hand-authored statusLine', async () => {
|
it('never overrides a real, hand-authored statusLine', async () => {
|
||||||
const claudeDir = join(testDir, '.claude');
|
const claudeDir = join(testDir, '.claude');
|
||||||
mkdirSync(claudeDir, { recursive: true });
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
/**
|
|
||||||
* @fileoverview Static guard for the Run menus' per-CLI logos (styles.css).
|
|
||||||
*
|
|
||||||
* Every launch surface (toolbar Run menu, phone overview picker, Custom Endpoint
|
|
||||||
* rows, model picker) renders `<span class="run-mode-dot <id>">`, and styles.css
|
|
||||||
* turns that slot into the CLI's logo through a `--run-mode-logo` data URI plus
|
|
||||||
* one of two paint groups: brand-coloured marks paint as a background image,
|
|
||||||
* monochrome marks as a mask over the row's text colour. Three ways that breaks
|
|
||||||
* silently, each pinned here:
|
|
||||||
* - a new stock CLI lands with no logo rule (its row shows a bare grey dot);
|
|
||||||
* - a logo rule exists but the id is in neither paint group (the variable is
|
|
||||||
* set and never painted, so the slot is a blank 15px gap);
|
|
||||||
* - a later `background:` shorthand on `.run-mode-dot.<id>` (e.g. a skin
|
|
||||||
* override like the ones the non-og block used to carry) resets
|
|
||||||
* background-image and wipes the logo.
|
|
||||||
*
|
|
||||||
* Port: none (pure static analysis).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { readFileSync } from 'node:fs';
|
|
||||||
import { resolve } from 'node:path';
|
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { STOCK_CLIS } from '../src/config/cli-registry/stock.js';
|
|
||||||
|
|
||||||
// Comments stripped: they sit between rules and carry commas, which would bleed
|
|
||||||
// into the selector lists split below.
|
|
||||||
const styles = readFileSync(resolve('src/web/public/styles.css'), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
||||||
|
|
||||||
/** Every id a Run menu renders as `.run-mode-dot <id>`: the stock agents, Shell, and web tabs. */
|
|
||||||
const RUN_MENU_IDS = [
|
|
||||||
...STOCK_CLIS.filter((cli) => cli.kind === 'agent').map((cli) => cli.id as string),
|
|
||||||
'shell',
|
|
||||||
'web',
|
|
||||||
];
|
|
||||||
|
|
||||||
/** The selector list of the one rule whose body contains `marker`. */
|
|
||||||
function groupIds(marker: string): string[] {
|
|
||||||
const rule = [...styles.matchAll(/([^{}]+)\{([^{}]*)\}/g)].filter((m) => m[2].includes(marker));
|
|
||||||
expect(rule, `exactly one rule paints via "${marker}"`).toHaveLength(1);
|
|
||||||
return rule[0][1]
|
|
||||||
.split(',')
|
|
||||||
.map((s) => s.trim().match(/^\.run-mode-dot\.([a-z0-9-]+)$/)?.[1])
|
|
||||||
.filter((id): id is string => Boolean(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
function logoSvg(id: string): string {
|
|
||||||
const m = styles.match(
|
|
||||||
new RegExp(`\\.run-mode-dot\\.${id} \\{ --run-mode-logo: url\\("data:image/svg\\+xml,([^"]+)"\\); \\}`)
|
|
||||||
);
|
|
||||||
expect(m, `.run-mode-dot.${id} has a --run-mode-logo rule`).not.toBeNull();
|
|
||||||
return decodeURIComponent(m![1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Run menu CLI logos', () => {
|
|
||||||
const colourIds = groupIds('background: var(--run-mode-logo)');
|
|
||||||
const maskIds = groupIds('mask: var(--run-mode-logo)');
|
|
||||||
|
|
||||||
it.each(RUN_MENU_IDS)('%s has a logo that one paint group draws', (id) => {
|
|
||||||
const svg = logoSvg(id);
|
|
||||||
expect(svg.startsWith('<svg')).toBe(true);
|
|
||||||
expect(svg).toMatch(/viewBox='[\d. ]+'/);
|
|
||||||
expect(colourIds.includes(id) !== maskIds.includes(id), `${id} is in exactly one paint group`).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('defines no logo that no surface renders, and no group member without a logo', () => {
|
|
||||||
expect([...colourIds, ...maskIds].sort()).toEqual([...RUN_MENU_IDS].sort());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('carries no script or event handler inside a data URI', () => {
|
|
||||||
for (const id of RUN_MENU_IDS) {
|
|
||||||
const svg = logoSvg(id);
|
|
||||||
expect(svg).not.toMatch(/<script|\son[a-z]+=|javascript:|href=/i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('never resets a logo slot with a background shorthand outside the colour group', () => {
|
|
||||||
const offenders = [...styles.matchAll(/([^{}]+)\{([^{}]*)\}/g)].filter(
|
|
||||||
([, selector, body]) =>
|
|
||||||
/\.run-mode-dot\.[a-z]/.test(selector) &&
|
|
||||||
/(^|[;\s])background\s*:/.test(body) &&
|
|
||||||
!body.includes('var(--run-mode-logo)')
|
|
||||||
);
|
|
||||||
expect(offenders.map((m) => m[1].trim())).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user