Initial import: open-design source for helix-mind.ai distribution
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m6s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 12s
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m6s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 12s
This repository contains the open-design daemon CLI source code, built and packaged at https://helix-mind.ai/cli/open-design/latest.tgz for use by the HelixMind /design slash command. Licenses: Apache-2.0 (root) + MIT (skills/*)
This commit is contained in:
239
apps/daemon/tests/critique-transcript.test.ts
Normal file
239
apps/daemon/tests/critique-transcript.test.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, existsSync, readdirSync } from 'node:fs';
|
||||
import { rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createGunzip } from 'node:zlib';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { createInterface } from 'node:readline';
|
||||
import type { PanelEvent } from '@open-design/contracts/critique';
|
||||
import { writeTranscript, readTranscript } from '../src/critique/transcript.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeRunStarted(runId = 'r1'): PanelEvent {
|
||||
return {
|
||||
type: 'run_started',
|
||||
runId,
|
||||
protocolVersion: 1,
|
||||
cast: ['designer', 'critic', 'brand', 'a11y', 'copy'],
|
||||
maxRounds: 3,
|
||||
threshold: 8.0,
|
||||
scale: 10,
|
||||
};
|
||||
}
|
||||
|
||||
function makeShip(runId = 'r1'): PanelEvent {
|
||||
return {
|
||||
type: 'ship',
|
||||
runId,
|
||||
round: 1,
|
||||
composite: 9.0,
|
||||
status: 'shipped',
|
||||
artifactRef: { projectId: 'p1', artifactId: 'a1' },
|
||||
summary: 'done',
|
||||
};
|
||||
}
|
||||
|
||||
async function collect(iter: AsyncIterable<PanelEvent>): Promise<PanelEvent[]> {
|
||||
const out: PanelEvent[] = [];
|
||||
for await (const e of iter) out.push(e);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Setup / Teardown
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let tmpDir: string;
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), 'od-transcript-test-'));
|
||||
});
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('writeTranscript + readTranscript', () => {
|
||||
it('writes plain .ndjson for small input and events round-trip', async () => {
|
||||
const events: PanelEvent[] = [makeRunStarted(), makeShip()];
|
||||
const artifactDir = join(tmpDir, 'run1');
|
||||
const result = await writeTranscript(artifactDir, events, { gzipThresholdBytes: 1_000_000 });
|
||||
|
||||
expect(result.path).toBe('transcript.ndjson');
|
||||
expect(result.gzipped).toBe(false);
|
||||
expect(result.bytes).toBeGreaterThan(0);
|
||||
expect(existsSync(join(artifactDir, 'transcript.ndjson'))).toBe(true);
|
||||
|
||||
const roundTripped = await collect(readTranscript(artifactDir, 'transcript.ndjson'));
|
||||
expect(roundTripped).toEqual(events);
|
||||
});
|
||||
|
||||
it('writes .ndjson.gz for large input (over threshold) and events round-trip', async () => {
|
||||
// Use a very low threshold to force gzip.
|
||||
const events: PanelEvent[] = [makeRunStarted(), makeShip()];
|
||||
const artifactDir = join(tmpDir, 'run2');
|
||||
const result = await writeTranscript(artifactDir, events, { gzipThresholdBytes: 1 });
|
||||
|
||||
expect(result.path).toBe('transcript.ndjson.gz');
|
||||
expect(result.gzipped).toBe(true);
|
||||
expect(existsSync(join(artifactDir, 'transcript.ndjson.gz'))).toBe(true);
|
||||
|
||||
// Verify gzip integrity by gunzipping manually and confirming it parses.
|
||||
const lines: string[] = [];
|
||||
const rl = createInterface({
|
||||
input: createReadStream(join(artifactDir, 'transcript.ndjson.gz')).pipe(createGunzip()),
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
for await (const line of rl) {
|
||||
if (line.trim()) lines.push(line.trim());
|
||||
}
|
||||
expect(lines).toHaveLength(2);
|
||||
expect(JSON.parse(lines[0]!)).toEqual(events[0]);
|
||||
|
||||
const roundTripped = await collect(readTranscript(artifactDir, 'transcript.ndjson.gz'));
|
||||
expect(roundTripped).toEqual(events);
|
||||
});
|
||||
|
||||
it('empty events iterable writes a file with 0 bytes and round-trip yields nothing', async () => {
|
||||
const artifactDir = join(tmpDir, 'run-empty');
|
||||
const result = await writeTranscript(artifactDir, [], { gzipThresholdBytes: 1_000_000 });
|
||||
|
||||
expect(result.bytes).toBe(0);
|
||||
expect(result.gzipped).toBe(false);
|
||||
expect(existsSync(join(artifactDir, 'transcript.ndjson'))).toBe(true);
|
||||
|
||||
const roundTripped = await collect(readTranscript(artifactDir, 'transcript.ndjson'));
|
||||
expect(roundTripped).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('multibyte CJK content sizes correctly under UTF-8 byte cap', async () => {
|
||||
const cjkEvent: PanelEvent = {
|
||||
type: 'panelist_dim',
|
||||
runId: 'r1',
|
||||
round: 1,
|
||||
role: 'critic',
|
||||
dimName: 'hierarchy',
|
||||
dimScore: 7,
|
||||
dimNote: '字体层次不清晰,标题与正文对比不足',
|
||||
};
|
||||
const artifactDir = join(tmpDir, 'run-cjk');
|
||||
// Threshold below CJK content byte count to force gzip.
|
||||
const threshold = 10;
|
||||
const result = await writeTranscript(artifactDir, [cjkEvent], { gzipThresholdBytes: threshold });
|
||||
|
||||
const serialized = JSON.stringify(cjkEvent) + '\n';
|
||||
const expected = Buffer.byteLength(serialized, 'utf8');
|
||||
expect(result.bytes).toBe(expected);
|
||||
// CJK chars are multi-byte, so bytes > string length.
|
||||
expect(result.bytes).toBeGreaterThan(serialized.length);
|
||||
expect(result.gzipped).toBe(true);
|
||||
|
||||
const roundTripped = await collect(readTranscript(artifactDir, 'transcript.ndjson.gz'));
|
||||
expect(roundTripped).toEqual([cjkEvent]);
|
||||
});
|
||||
|
||||
it('temp file is cleaned up on success', async () => {
|
||||
const artifactDir = join(tmpDir, 'run-cleanup');
|
||||
await writeTranscript(artifactDir, [makeRunStarted()], { gzipThresholdBytes: 1_000_000 });
|
||||
|
||||
const files = readdirSync(artifactDir);
|
||||
const tempFiles = files.filter((f) => f.includes('.tmp.'));
|
||||
expect(tempFiles).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('temp file is cleaned up on failure and error propagates', async () => {
|
||||
const artifactDir = join(tmpDir, 'run-fail');
|
||||
await mkdirIfNeeded(artifactDir);
|
||||
|
||||
async function* failingSource(): AsyncIterable<PanelEvent> {
|
||||
yield makeRunStarted();
|
||||
throw new Error('mid-stream failure');
|
||||
}
|
||||
|
||||
await expect(
|
||||
writeTranscript(artifactDir, failingSource(), { gzipThresholdBytes: 1_000_000 }),
|
||||
).rejects.toThrow('mid-stream failure');
|
||||
|
||||
// No temp file should remain.
|
||||
const files = existsSync(artifactDir) ? readdirSync(artifactDir) : [];
|
||||
const tempFiles = files.filter((f) => f.includes('.tmp.'));
|
||||
expect(tempFiles).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('readTranscript detects .gz vs .ndjson by extension', async () => {
|
||||
const artifactDir = join(tmpDir, 'run-ext');
|
||||
const events = [makeRunStarted(), makeShip()];
|
||||
|
||||
// Write both plain and gzipped.
|
||||
await writeTranscript(artifactDir, events, { gzipThresholdBytes: 1_000_000 });
|
||||
// Write gzipped version too by using a low threshold.
|
||||
const artifactDir2 = join(tmpDir, 'run-ext2');
|
||||
await writeTranscript(artifactDir2, events, { gzipThresholdBytes: 1 });
|
||||
|
||||
const plain = await collect(readTranscript(artifactDir, 'transcript.ndjson'));
|
||||
const gz = await collect(readTranscript(artifactDir2, 'transcript.ndjson.gz'));
|
||||
|
||||
expect(plain).toEqual(events);
|
||||
expect(gz).toEqual(events);
|
||||
});
|
||||
|
||||
it('readTranscript throws on unknown extension', async () => {
|
||||
const artifactDir = join(tmpDir, 'run-badext');
|
||||
await expect(
|
||||
collect(readTranscript(artifactDir, 'transcript.json')),
|
||||
).rejects.toThrow(RangeError);
|
||||
});
|
||||
|
||||
it('writeTranscript throws RangeError on empty artifactDir', async () => {
|
||||
await expect(writeTranscript('', [])).rejects.toThrow(RangeError);
|
||||
});
|
||||
|
||||
it('writeTranscript throws RangeError on non-iterable events', async () => {
|
||||
// Pass a plain object that has neither Symbol.iterator nor Symbol.asyncIterator.
|
||||
await expect(
|
||||
writeTranscript(
|
||||
join(tmpDir, 'run-badevents'),
|
||||
{} as unknown as Iterable<PanelEvent>,
|
||||
),
|
||||
).rejects.toThrow(RangeError);
|
||||
});
|
||||
|
||||
it('gzip crash leaves no final .gz or .gz.tmp on disk (Defect 8)', async () => {
|
||||
const artifactDir = join(tmpDir, 'run-gz-crash');
|
||||
await mkdirIfNeeded(artifactDir);
|
||||
|
||||
// Source that throws mid-stream to simulate a crash during write.
|
||||
async function* failingGzipSource(): AsyncIterable<PanelEvent> {
|
||||
yield makeRunStarted();
|
||||
throw new Error('simulated gzip crash');
|
||||
}
|
||||
|
||||
// Use threshold=1 to force gzip path.
|
||||
await expect(
|
||||
writeTranscript(artifactDir, failingGzipSource(), { gzipThresholdBytes: 1 }),
|
||||
).rejects.toThrow('simulated gzip crash');
|
||||
|
||||
// The final .gz must not exist.
|
||||
expect(existsSync(join(artifactDir, 'transcript.ndjson.gz'))).toBe(false);
|
||||
|
||||
// No .gz.tmp should remain.
|
||||
const files = existsSync(artifactDir) ? readdirSync(artifactDir) : [];
|
||||
const gzTmpFiles = files.filter((f) => f.endsWith('.gz.tmp'));
|
||||
expect(gzTmpFiles).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function mkdirIfNeeded(dir: string): Promise<void> {
|
||||
const { mkdir } = await import('node:fs/promises');
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
Reference in New Issue
Block a user