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
94 changes: 65 additions & 29 deletions plugins/dsh-genie-board/build.test.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,77 @@
import { expect, test } from 'bun:test';
import { readFile, rm, stat } from 'node:fs/promises';
import { mkdtemp, readFile, readdir, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { runInNewContext } from 'node:vm';

test('build regenerates Host bundle and lazy DSH browser factory', async () => {
test('valid repeated builds regenerate identical Host and lazy browser bundles', async () => {
const root = import.meta.dir;
const host = join(root, 'dist/index.js');
const client = join(root, 'dist/client.js');
await rm(host, { force: true });
await rm(client, { force: true });
const process = Bun.spawn(['bun', 'run', 'build'], { cwd: root, stdout: 'pipe', stderr: 'pipe' });
const [code, error] = await Promise.all([process.exited, new Response(process.stderr).text()]);
expect(error).not.toContain('error:');
expect(code).toBe(0);
expect((await stat(host)).size).toBeGreaterThan(1000);
let registration: { id: string; factory: (require: unknown) => { apply: unknown } } | undefined;
runInNewContext(await readFile(client, 'utf8'), {
window: {
__ModuleLoader__: {
load(value: typeof registration) {
registration = value;
const output = await mkdtemp(join(tmpdir(), 'genie-repeat-host-'));
const version = JSON.parse(await readFile(join(root, '../../package.json'), 'utf8')).version;
let previous: string[] | undefined;
try {
for (let attempt = 0; attempt < 2; attempt++) {
const proc = Bun.spawn(['bun', 'run', 'build', version, output], {
cwd: root,
stdout: 'pipe',
stderr: 'pipe',
});
const [code, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
expect(code).toBe(0);
expect(stderr).not.toContain('error:');
const host = join(output, 'index.js');
const client = join(output, 'client.js');
expect((await stat(host)).size).toBeGreaterThan(1000);
expect((await import(`${host}?attempt=${attempt}`)).minimumGenieVersion).toBe(version);
const bytes = [await readFile(host, 'utf8'), await readFile(client, 'utf8')];
if (previous) expect(bytes).toEqual(previous);
previous = bytes;
let registration: { id: string; factory: (require: unknown) => { apply: unknown } } | undefined;
runInNewContext(bytes[1], {
window: {
__ModuleLoader__: {
load(value: typeof registration) {
registration = value;
},
},
},
},
},
});
expect(registration?.id).toBe('@automagik/genie-dsh-board');
// No DOM is needed until Cordis activates the factory's apply method.
expect(
typeof registration?.factory(() => {
throw new Error('Unexpected browser dependency');
}).apply,
).toBe('function');
});
expect(registration?.id).toBe('@automagik/genie-dsh-board');
// No DOM is needed until Cordis activates the factory's apply method.
expect(
typeof registration?.factory(() => {
throw new Error('Unexpected browser dependency');
}).apply,
).toBe('function');
}
} finally {
await rm(output, { recursive: true, force: true });
}
}, 20_000);

test('invalid build version exits with an error and writes no bundle', async () => {
const output = await mkdtemp(join(tmpdir(), 'genie-invalid-host-'));
try {
const proc = Bun.spawn(['bun', 'run', 'build', 'invalid version', output], {
cwd: import.meta.dir,
stdout: 'pipe',
stderr: 'pipe',
});
const [code, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
expect(code).toBe(1);
expect(stdout).toBe('');
expect(stderr).toContain('invalid build version');
expect(await readdir(output)).toEqual([]);
} finally {
await rm(output, { recursive: true, force: true });
}
}, 20_000);

test('candidate build embeds override floor without changing source metadata', async () => {
const { mkdtemp, readFile, rm } = await import('node:fs/promises');
const { tmpdir } = await import('node:os');
const root = import.meta.dir;
const output = await mkdtemp(join(tmpdir(), 'genie-candidate-host-'));
const before = await readFile(join(root, 'package.json'), 'utf8');
Expand Down
49 changes: 48 additions & 1 deletion plugins/dsh-genie-board/src/board.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,49 @@ describe('closed inputs and fixed argv', () => {
for (const text of ['', ' ', 'x\0x', 'x\nx', 'x\tx', '\ntitle', 'title\n'])
expect(requestSchema.safeParse({ ...input, text }).success).toBe(false);
});
test.each([
['C1 next line', '\u0085'],
['C1 control sequence introducer', '\u009b'],
['bidi override', '\u202e'],
['bidi isolate', '\u2066'],
['zero width space', '\u200b'],
['byte order mark', '\ufeff'],
['line separator', '\u2028'],
['paragraph separator', '\u2029'],
['supplementary format control', '\u{e0001}'],
])('rejects %s at every text boundary before any CLI call', async (_name, control) => {
const f = await fixture();
await f.list();
await f.load();
const before = f.calls.length;
for (const value of [`${control}text`, `te${control}xt`, `text${control}`]) {
for (const input of [
{ action: 'create', ...selection, title: value },
{ action: 'comment', ...selection, id: 't_abc', text: value },
{ action: 'block', ...selection, id: 't_abc', text: value },
]) {
await expect(f.service.request(input)).rejects.toThrow('Control characters are not allowed');
expect(f.calls.length).toBe(before);
}
}
});
test('ordinary Unicode text is trimmed and forwarded unchanged for all three actions', async () => {
const f = await fixture();
await f.list();
await f.load();
const value = 'café 漢字 🙂 e\u0301';
const padded = ` ${value} `;
const vectors = [
[{ action: 'create', title: padded }, ['task', 'create', '--title', value, '--board', 'b_abc']],
[{ action: 'comment', id: 't_abc', text: padded }, ['task', 'comment', '--', 't_abc', value]],
[{ action: 'block', id: 't_abc', text: padded }, ['task', 'block', 't_abc', '--reason', value]],
] as const;
for (const [input, argv] of vectors) {
const before = f.calls.length;
await f.service.request({ ...selection, ...input });
expect(f.calls.slice(before)).toEqual([[...argv], ['board', '--board', 'b_abc', '--json']]);
}
});
test('byte bounds for title/comment/reason', () => {
for (const [action, field, limit] of [
['create', 'title', 200],
Expand Down Expand Up @@ -223,8 +266,12 @@ test('semver strict ordering includes prereleases and rejects malformed versions
expect(compatible(actual, minimum)).toBe(result);
});
test('process environment is an exact allowlist', () => {
const env = hostEnvironment('host');
expect(env.GENIE_AGENT_NAME).toBe('host');
expect(env.GENIE_AGENT_KIND).toBe('dsh');
expect(env.NO_COLOR).toBe('1');
expect(
Object.keys(hostEnvironment('host')).every((key) =>
Object.keys(env).every((key) =>
['PATH', 'HOME', 'GENIE_HOME', 'NO_COLOR', 'GENIE_AGENT_NAME', 'GENIE_AGENT_KIND'].includes(key),
),
).toBe(true);
Expand Down
14 changes: 2 additions & 12 deletions plugins/dsh-genie-board/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,9 @@ export const boardsSchema = z.array(
);
const bounded = (max: number) =>
text
.refine(
(value) =>
!Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127),
'Control characters are not allowed',
)
.refine((value) => !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value), 'Control characters are not allowed')
.transform((value) => value.trim())
.refine(
(value) =>
value.length > 0 &&
Buffer.byteLength(value) <= max &&
!Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127),
'Invalid text',
);
.refine((value) => value.length > 0 && Buffer.byteLength(value) <= max, 'Invalid text');
const base = { workspaceId: text.min(1).max(200), boardRef: text.regex(/^b_[a-z0-9]+$/) };
export const requestSchema = z.discriminatedUnion('action', [
z.object({ action: z.literal('list'), workspaceId: base.workspaceId }).strict(),
Expand Down
9 changes: 8 additions & 1 deletion scripts/dsh-genie-board-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ const env = {
let server: ChildProcess | undefined;
let installed = false;
async function command(binary: string, args: string[], cwd = root): Promise<string> {
const proc = Bun.spawn([binary, ...args], { cwd, env, stdout: 'pipe', stderr: 'pipe' });
const proc = Bun.spawn([binary, ...args], {
cwd,
env,
stdout: 'pipe',
stderr: 'pipe',
timeout: 120_000,
killSignal: 'SIGKILL',
});
const [stdout, stderr, code] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
Expand Down
2 changes: 1 addition & 1 deletion skills/genie-orca-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ A reviewer is a **read-only worker dispatched by the coordinator**, never the en

Resolve the fix-loop budget `B` once per group: default 2; only an explicit higher-priority user/workspace instruction may set another positive integer. Carry `B` and the attempts already used across handoffs; do not reset the budget when switching skills. Overrides never expand scope, permit unchanged retries, or skip diagnosis or independent re-review.

Coordinator re-dispatches a **fast** worker into the same worktree with the findings quoted verbatim and "apply exactly this, nothing else". Cap `B` loops per group; the coordinator may verify a trivial delta itself instead of a second review. After the cap → human gate.
Coordinator re-dispatches a **fast** worker into the same worktree with the findings quoted verbatim and "apply exactly this, nothing else". Cap `B` loops per group. After every fix, dispatch an independent re-reviewer who is not the fixer. If the re-review is still not SHIP after `B` attempts, stop fixing and follow [Escalation Diagnosis](../fix/SKILL.md#escalation-diagnosis), then its cause-specific route or human gate as applicable. Diagnosis does not reset the budget or grant additional fix attempts.

## What the integrated gate catches that group review does not

Expand Down
2 changes: 1 addition & 1 deletion src/lib/v5/task-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,7 +1467,7 @@ export function readBoardTaskSnapshot(
const read = db.transaction((): BoardTaskAggregate[] => {
const params = filter.wish ? [boardId, filter.wish] : [boardId];
const tasks = db
.query(`SELECT * FROM tasks WHERE board_id = ?${filter.wish ? ' AND wish = ?' : ''} ORDER BY created_at`)
.query(`SELECT * FROM tasks WHERE board_id = ?${filter.wish ? ' AND wish = ?' : ''} ORDER BY created_at, rowid`)
.all(...params) as RawTask[];
if (tasks.length === 0) return [];
// One JSON binding avoids variable limits while retaining task_id index probes.
Expand Down
3 changes: 3 additions & 0 deletions src/term-commands/v5-board.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,9 @@ describe('scoped board JSON aggregate v1', () => {
db.query('UPDATE tasks SET id = ? WHERE id = ?').run('a-dependency', depA.id);
db.query('UPDATE tasks SET id = ?, created_at = 10 WHERE id = ?').run('z-card', cardB.id);
db.query('UPDATE tasks SET id = ?, created_at = 10 WHERE id = ?').run('a-card', cardA.id);
// This access path sorts equal timestamps by id unless the aggregate
// explicitly requests its insertion-order tie-break.
db.run('CREATE INDEX test_board_created_id ON tasks(board_id, created_at, id)');
addDependency(db, 'a-card', 'z-dependency');
addDependency(db, 'a-card', 'a-dependency');
// Insert same-time ids in descending order. Timeline and its comment
Expand Down
Loading