Govern task context and advance execution pointer
This commit is contained in:
47
web/tests/unit/context-governance.test.mjs
Normal file
47
web/tests/unit/context-governance.test.mjs
Normal file
@@ -0,0 +1,47 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { buildTaskContext } from "../../../tools/web/task-context-lib.mjs";
|
||||
import { validateContextBundle } from "../../../tools/web/context-governance.mjs";
|
||||
|
||||
test("current context satisfies document, task-card, and pointer budgets", () => {
|
||||
const bundle = buildTaskContext();
|
||||
const result = validateContextBundle(bundle);
|
||||
assert.deepEqual(result.violations, []);
|
||||
assert.ok(result.report.totalTokens <= 3500);
|
||||
});
|
||||
|
||||
test("task cards cannot smuggle full plans or history into the default context", () => {
|
||||
const bundle = buildTaskContext();
|
||||
const taskSource = `${bundle.sources.taskSource}\nnext-task-plan.json\n`;
|
||||
const result = validateContextBundle({
|
||||
...bundle,
|
||||
sources: { ...bundle.sources, taskSource },
|
||||
});
|
||||
assert.ok(result.violations.some(({ code }) => code === "FORBIDDEN_CONTEXT_REFERENCE"));
|
||||
});
|
||||
|
||||
test("governance rejects forged selection totals and unaudited exclusions", () => {
|
||||
const bundle = buildTaskContext();
|
||||
const forged = {
|
||||
...bundle,
|
||||
context: {
|
||||
...bundle.context,
|
||||
inputPaths: ["tools/web/generated/M16-GAP-00022.py"],
|
||||
inputSelection: {
|
||||
...bundle.context.inputSelection,
|
||||
selected: [{ path: "tools/web/generated/M16-GAP-00022.py", bytes: 9000, tokens: 1 }],
|
||||
excluded: [
|
||||
{ path: "tests/files/web/generated/M16-GAP-00022-datablock-Sound.blend", reason: "FORGED_REASON" },
|
||||
{ path: "tests/golden/M16-GAP-00022/" },
|
||||
],
|
||||
totals: { files: 1, bytes: 9000, tokens: 2250 },
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = validateContextBundle(forged);
|
||||
assert.ok(result.violations.some(({ code }) => code === "EVIDENCE_BYTES_OVER_BUDGET"));
|
||||
assert.ok(result.violations.some(({ code }) => code === "CONTEXT_REMAINING_BYTES_OVER_BUDGET"));
|
||||
assert.ok(result.violations.some(({ code }) => code === "INPUT_EXCLUSION_REASON_MISSING"));
|
||||
assert.ok(result.violations.some(({ code }) => code === "INPUT_EXCLUSION_REASON_UNKNOWN"));
|
||||
assert.ok(result.violations.some(({ code }) => code === "INPUT_SELECTION_ENTRY_INVALID"));
|
||||
});
|
||||
@@ -25,3 +25,13 @@ test("M14-04F commits pen stroke once and ignores late up/cancel", () => {
|
||||
state = modal.commitPenStroke(state, 9);
|
||||
assert.deepEqual([state.kind, state.mainCommitCount, state.activePointerIds], ["NONE", 1, []]);
|
||||
});
|
||||
|
||||
test("M14-04F reduces production pointer events and rejects invalid IDs", () => {
|
||||
let state = modal.createInputModalState();
|
||||
state = modal.reduceInputModal(state, { type: "pointerdown", pointerType: "touch", pointerId: 2 });
|
||||
state = modal.reduceInputModal(state, { type: "pointerdown", pointerType: "touch", pointerId: 4 });
|
||||
assert.equal(state.navigationRevision, 1);
|
||||
state = modal.reduceInputModal(state, { type: "pointercancel", pointerType: "touch", pointerId: 2 });
|
||||
assert.deepEqual([state.kind, state.activePointerIds, state.cancelled], ["NONE", [], true]);
|
||||
assert.throws(() => modal.reduceInputModal(state, { type: "pointerdown", pointerType: "pen", pointerId: -1 }), /POINTER_ID_INVALID/);
|
||||
});
|
||||
|
||||
72
web/tests/unit/task-context.test.mjs
Normal file
72
web/tests/unit/task-context.test.mjs
Normal file
@@ -0,0 +1,72 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { buildTaskContext, contextSizeReport, CONTEXT_LIMITS, readIndexedTask, selectInputPaths } from "../../../tools/web/task-context-lib.mjs";
|
||||
|
||||
test("current task context is bounded and follows the parent pointer", () => {
|
||||
const bundle = buildTaskContext();
|
||||
const report = contextSizeReport(bundle);
|
||||
assert.match(bundle.context.task, /^M\d+-GAP-\d{5}$/);
|
||||
assert.match(bundle.context.parentTask, /^M\d+-GAP-\d{5}$/);
|
||||
assert.equal(bundle.context.parent.manifest.nextTask, bundle.context.task);
|
||||
assert.ok(bundle.context.commands.length >= 1);
|
||||
assert.equal(bundle.context.sourceDocuments.taskCard, `docs/tasks/${bundle.context.task}.md`);
|
||||
assert.equal(bundle.context.sourceDocuments.taskIndex, "tests/golden/M15-03A/task-index.json");
|
||||
assert.ok(!JSON.stringify(bundle.context).includes("next-task-plan.json"));
|
||||
assert.equal(bundle.context.scope.ownerFamily, readIndexedTask(bundle.context.task).ownerFamily);
|
||||
assert.equal(report.withinBudget, true);
|
||||
assert.equal(report.totalTokens, report.sourceTokens + report.evidenceTokens);
|
||||
assert.equal(report.evidenceTokens, bundle.context.inputSelection.totals.tokens);
|
||||
assert.ok(report.totalTokens < CONTEXT_LIMITS.contextTokens);
|
||||
});
|
||||
|
||||
test("the indexed task record remains available beside the compact card", () => {
|
||||
const bundle = buildTaskContext("M16-GAP-00011");
|
||||
assert.equal(bundle.context.sourceDocuments.taskCard, "docs/tasks/M16-GAP-00011.md");
|
||||
assert.equal(bundle.context.parent.manifest.status, "done");
|
||||
assert.equal(bundle.context.nextTask, "M16-GAP-00012");
|
||||
});
|
||||
|
||||
test("input selection excludes evidence before it can enter the read list", () => {
|
||||
const fixture = "tests/files/web/generated/M16-GAP-00022-datablock-Sound.blend";
|
||||
const generator = "tools/web/generated/M16-GAP-00022.py";
|
||||
const result = selectInputPaths([fixture, generator], {
|
||||
sourceBytes: 100,
|
||||
sourceTokens: 25,
|
||||
limits: { ...CONTEXT_LIMITS, evidenceBytes: 1024 },
|
||||
});
|
||||
assert.deepEqual(result.inputPaths, [generator]);
|
||||
assert.equal(result.inputSelection.excluded[0].reason, "EVIDENCE_BYTE_BUDGET");
|
||||
assert.equal(result.inputSelection.totals.bytes, result.inputSelection.selected[0].bytes);
|
||||
});
|
||||
|
||||
test("input selection audits remaining context and file-count exclusions", () => {
|
||||
const paths = [
|
||||
"tools/web/generated/M16-GAP-00022.py",
|
||||
"tools/web/generated/M16-GAP-00021.py",
|
||||
];
|
||||
const remaining = selectInputPaths(paths, {
|
||||
sourceBytes: 0,
|
||||
sourceTokens: CONTEXT_LIMITS.contextTokens - 1,
|
||||
limits: { ...CONTEXT_LIMITS, evidenceBytes: 8192 },
|
||||
});
|
||||
assert.deepEqual(remaining.inputPaths, []);
|
||||
assert.equal(remaining.inputSelection.excluded[0].reason, "CONTEXT_REMAINING_SPACE");
|
||||
|
||||
const count = selectInputPaths(paths, {
|
||||
limits: { ...CONTEXT_LIMITS, evidenceFiles: 1, evidenceBytes: 8192 },
|
||||
});
|
||||
assert.equal(count.inputPaths.length, 1);
|
||||
assert.equal(count.inputSelection.excluded[0].reason, "EVIDENCE_FILE_COUNT");
|
||||
});
|
||||
|
||||
test("generated task outputs are excluded before their size can affect selection", () => {
|
||||
const result = selectInputPaths(["tests/golden/M16-GAP-00022/"], {
|
||||
generatedPaths: ["tests/golden/M16-GAP-00022/"],
|
||||
});
|
||||
assert.deepEqual(result.inputPaths, []);
|
||||
assert.deepEqual(result.inputSelection.excluded, [{
|
||||
path: "tests/golden/M16-GAP-00022/",
|
||||
bytes: null,
|
||||
reason: "GENERATED_EVIDENCE_OUTPUT",
|
||||
}]);
|
||||
});
|
||||
Reference in New Issue
Block a user