Consolidate three near-identical sidecar enqueue blocks (hook, triage, quick-task) into a shared enqueueSidecar() helper that handles the push + debugLog + optional UI notification + return "continue" pattern. Update static-analysis tests to accept the helper call pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
181 lines
7.5 KiB
TypeScript
181 lines
7.5 KiB
TypeScript
/**
|
|
* sidecar-queue.test.ts — Source-level contract tests for the sidecar queue pattern (S03).
|
|
*
|
|
* Verifies the structural invariants of the sidecar queue: the SidecarItem type,
|
|
* AutoSession sidecarQueue field, enqueue patterns in postUnitPostVerification,
|
|
* and dequeue logic in autoLoop. These are source-reading tests — no runtime required.
|
|
*/
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const SESSION_TS_PATH = join(__dirname, "..", "auto", "session.ts");
|
|
const POST_UNIT_TS_PATH = join(__dirname, "..", "auto-post-unit.ts");
|
|
const AUTO_LOOP_TS_PATH = join(__dirname, "..", "auto", "loop.ts");
|
|
|
|
function getSessionTsSource(): string {
|
|
return readFileSync(SESSION_TS_PATH, "utf-8");
|
|
}
|
|
|
|
function getPostUnitTsSource(): string {
|
|
return readFileSync(POST_UNIT_TS_PATH, "utf-8");
|
|
}
|
|
|
|
function getAutoLoopTsSource(): string {
|
|
return readFileSync(AUTO_LOOP_TS_PATH, "utf-8");
|
|
}
|
|
|
|
/**
|
|
* Extract the body of postUnitPostVerification from auto-post-unit.ts source.
|
|
*/
|
|
function getPostUnitPostVerificationBody(): string {
|
|
const source = getPostUnitTsSource();
|
|
const fnIdx = source.indexOf("export async function postUnitPostVerification");
|
|
assert.ok(fnIdx > -1, "postUnitPostVerification must exist in auto-post-unit.ts");
|
|
return source.slice(fnIdx);
|
|
}
|
|
|
|
// ─── SidecarItem type contract ───────────────────────────────────────────────
|
|
|
|
test("SidecarItem type is exported from session.ts", () => {
|
|
const source = getSessionTsSource();
|
|
assert.ok(
|
|
source.includes("export interface SidecarItem"),
|
|
"session.ts must export the SidecarItem interface",
|
|
);
|
|
});
|
|
|
|
test("SidecarItem has required kind field with hook/triage/quick-task union", () => {
|
|
const source = getSessionTsSource();
|
|
const ifaceIdx = source.indexOf("export interface SidecarItem");
|
|
const ifaceBlock = source.slice(ifaceIdx, ifaceIdx + 500);
|
|
assert.ok(
|
|
ifaceBlock.includes('"hook"') && ifaceBlock.includes('"triage"') && ifaceBlock.includes('"quick-task"'),
|
|
"SidecarItem.kind must be a union of 'hook' | 'triage' | 'quick-task'",
|
|
);
|
|
});
|
|
|
|
// ─── AutoSession sidecarQueue field ──────────────────────────────────────────
|
|
|
|
test("AutoSession declares sidecarQueue field", () => {
|
|
const source = getSessionTsSource();
|
|
assert.ok(
|
|
source.includes("sidecarQueue"),
|
|
"AutoSession must declare sidecarQueue property",
|
|
);
|
|
assert.ok(
|
|
source.includes("SidecarItem[]"),
|
|
"sidecarQueue must be typed as SidecarItem[]",
|
|
);
|
|
});
|
|
|
|
test("AutoSession resets sidecarQueue in reset()", () => {
|
|
const source = getSessionTsSource();
|
|
const resetIdx = source.indexOf("reset(): void");
|
|
assert.ok(resetIdx > -1, "AutoSession must have a reset() method");
|
|
const resetBlock = source.slice(resetIdx, resetIdx + 3000);
|
|
assert.ok(
|
|
resetBlock.includes("sidecarQueue"),
|
|
"reset() must clear sidecarQueue",
|
|
);
|
|
});
|
|
|
|
// ─── postUnitPostVerification: no inline dispatch ────────────────────────────
|
|
|
|
test("postUnitPostVerification does not call pi.sendMessage", () => {
|
|
const body = getPostUnitPostVerificationBody();
|
|
assert.ok(
|
|
!body.includes("pi.sendMessage"),
|
|
"postUnitPostVerification must not call pi.sendMessage — all dispatch goes through sidecar queue",
|
|
);
|
|
});
|
|
|
|
test("postUnitPostVerification does not call newSession", () => {
|
|
const body = getPostUnitPostVerificationBody();
|
|
assert.ok(
|
|
!body.includes("s.cmdCtx.newSession") && !body.includes("cmdCtx.newSession"),
|
|
"postUnitPostVerification must not call newSession — all dispatch goes through sidecar queue",
|
|
);
|
|
});
|
|
|
|
// ─── postUnitPostVerification: sidecar enqueue for hooks ─────────────────────
|
|
|
|
test("postUnitPostVerification pushes to sidecarQueue for hooks", () => {
|
|
const source = getPostUnitTsSource();
|
|
// Find the hook section (marked by the post-unit hooks comment)
|
|
const hookSectionStart = source.indexOf("// ── Post-unit hooks");
|
|
assert.ok(hookSectionStart > -1, "auto-post-unit.ts must have a post-unit hooks section");
|
|
const triageSectionStart = source.indexOf("// ── Triage check");
|
|
assert.ok(triageSectionStart > -1, "auto-post-unit.ts must have a triage check section");
|
|
const hookSection = source.slice(hookSectionStart, triageSectionStart);
|
|
assert.ok(
|
|
hookSection.includes("enqueueSidecar(") || hookSection.includes("s.sidecarQueue.push("),
|
|
"hook section must enqueue to sidecarQueue (via enqueueSidecar or direct push)",
|
|
);
|
|
assert.ok(
|
|
hookSection.includes('"hook"'),
|
|
"hook sidecar item must reference kind 'hook'",
|
|
);
|
|
});
|
|
|
|
// ─── postUnitPostVerification: sidecar enqueue for triage ────────────────────
|
|
|
|
test("postUnitPostVerification pushes to sidecarQueue for triage", () => {
|
|
const source = getPostUnitTsSource();
|
|
const triageSectionStart = source.indexOf("// ── Triage check");
|
|
const quickTaskSectionStart = source.indexOf("// ── Quick-task dispatch");
|
|
assert.ok(triageSectionStart > -1, "auto-post-unit.ts must have a triage check section");
|
|
assert.ok(quickTaskSectionStart > -1, "auto-post-unit.ts must have a quick-task dispatch section");
|
|
const triageSection = source.slice(triageSectionStart, quickTaskSectionStart);
|
|
assert.ok(
|
|
triageSection.includes("enqueueSidecar(") || triageSection.includes("s.sidecarQueue.push("),
|
|
"triage section must enqueue to sidecarQueue (via enqueueSidecar or direct push)",
|
|
);
|
|
assert.ok(
|
|
triageSection.includes('"triage"'),
|
|
"triage sidecar item must reference kind 'triage'",
|
|
);
|
|
});
|
|
|
|
// ─── postUnitPostVerification: sidecar enqueue for quick-tasks ───────────────
|
|
|
|
test("postUnitPostVerification pushes to sidecarQueue for quick-tasks", () => {
|
|
const source = getPostUnitTsSource();
|
|
const quickTaskSectionStart = source.indexOf("// ── Quick-task dispatch");
|
|
assert.ok(quickTaskSectionStart > -1, "auto-post-unit.ts must have a quick-task dispatch section");
|
|
const quickTaskSection = source.slice(quickTaskSectionStart);
|
|
assert.ok(
|
|
quickTaskSection.includes("enqueueSidecar(") || quickTaskSection.includes("s.sidecarQueue.push("),
|
|
"quick-task section must enqueue to sidecarQueue (via enqueueSidecar or direct push)",
|
|
);
|
|
assert.ok(
|
|
quickTaskSection.includes('"quick-task"'),
|
|
"quick-task sidecar item must reference kind 'quick-task'",
|
|
);
|
|
});
|
|
|
|
// ─── autoLoop: sidecar dequeue ───────────────────────────────────────────────
|
|
|
|
test("autoLoop has sidecar-dequeue phase", () => {
|
|
const source = getAutoLoopTsSource();
|
|
assert.ok(
|
|
source.includes('"sidecar-dequeue"'),
|
|
"autoLoop must log phase: 'sidecar-dequeue' when draining the sidecar queue",
|
|
);
|
|
});
|
|
|
|
test("autoLoop does not have inline dispatch loop", () => {
|
|
const source = getAutoLoopTsSource();
|
|
assert.ok(
|
|
!source.includes('"await-inline-dispatch"'),
|
|
"autoLoop must not contain 'await-inline-dispatch' — replaced by sidecar queue",
|
|
);
|
|
assert.ok(
|
|
!source.includes("while (inlineResult"),
|
|
"autoLoop must not contain a while(inlineResult...) loop — replaced by sidecar queue drain",
|
|
);
|
|
});
|