@cad0p/pi-steering 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/LICENSE +29 -0
- package/README.md +920 -0
- package/dist/__test-helpers__.d.ts +95 -0
- package/dist/__test-helpers__.d.ts.map +1 -0
- package/dist/__test-helpers__.js +174 -0
- package/dist/__test-helpers__.js.map +1 -0
- package/dist/bin/pi-steering.d.ts +9 -0
- package/dist/bin/pi-steering.d.ts.map +1 -0
- package/dist/bin/pi-steering.js +506 -0
- package/dist/bin/pi-steering.js.map +1 -0
- package/dist/compat.d.ts +50 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +180 -0
- package/dist/compat.js.map +1 -0
- package/dist/defaults.d.ts +61 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +106 -0
- package/dist/defaults.js.map +1 -0
- package/dist/define-config.d.ts +235 -0
- package/dist/define-config.d.ts.map +1 -0
- package/dist/define-config.js +118 -0
- package/dist/define-config.js.map +1 -0
- package/dist/define-predicate.d.ts +49 -0
- package/dist/define-predicate.d.ts.map +1 -0
- package/dist/define-predicate.js +35 -0
- package/dist/define-predicate.js.map +1 -0
- package/dist/evaluator-internals/context.d.ts +186 -0
- package/dist/evaluator-internals/context.d.ts.map +1 -0
- package/dist/evaluator-internals/context.js +219 -0
- package/dist/evaluator-internals/context.js.map +1 -0
- package/dist/evaluator-internals/override.d.ts +19 -0
- package/dist/evaluator-internals/override.d.ts.map +1 -0
- package/dist/evaluator-internals/override.js +79 -0
- package/dist/evaluator-internals/override.js.map +1 -0
- package/dist/evaluator-internals/predicates.d.ts +152 -0
- package/dist/evaluator-internals/predicates.d.ts.map +1 -0
- package/dist/evaluator-internals/predicates.js +849 -0
- package/dist/evaluator-internals/predicates.js.map +1 -0
- package/dist/evaluator-internals/speculative-synthesis.d.ts +102 -0
- package/dist/evaluator-internals/speculative-synthesis.d.ts.map +1 -0
- package/dist/evaluator-internals/speculative-synthesis.js +101 -0
- package/dist/evaluator-internals/speculative-synthesis.js.map +1 -0
- package/dist/evaluator.d.ts +101 -0
- package/dist/evaluator.d.ts.map +1 -0
- package/dist/evaluator.js +714 -0
- package/dist/evaluator.js.map +1 -0
- package/dist/helpers/walker-unknown-cwd-reason.d.ts +68 -0
- package/dist/helpers/walker-unknown-cwd-reason.d.ts.map +1 -0
- package/dist/helpers/walker-unknown-cwd-reason.js +73 -0
- package/dist/helpers/walker-unknown-cwd-reason.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/drop-unused-observers.d.ts +43 -0
- package/dist/internal/drop-unused-observers.d.ts.map +1 -0
- package/dist/internal/drop-unused-observers.js +50 -0
- package/dist/internal/drop-unused-observers.js.map +1 -0
- package/dist/internal/finalize-plugin-state.d.ts +22 -0
- package/dist/internal/finalize-plugin-state.d.ts.map +1 -0
- package/dist/internal/finalize-plugin-state.js +14 -0
- package/dist/internal/finalize-plugin-state.js.map +1 -0
- package/dist/internal/merge-observers.d.ts +30 -0
- package/dist/internal/merge-observers.d.ts.map +1 -0
- package/dist/internal/merge-observers.js +27 -0
- package/dist/internal/merge-observers.js.map +1 -0
- package/dist/internal/pattern-utils.d.ts +33 -0
- package/dist/internal/pattern-utils.d.ts.map +1 -0
- package/dist/internal/pattern-utils.js +20 -0
- package/dist/internal/pattern-utils.js.map +1 -0
- package/dist/internal/ref-text.d.ts +11 -0
- package/dist/internal/ref-text.d.ts.map +1 -0
- package/dist/internal/ref-text.js +15 -0
- package/dist/internal/ref-text.js.map +1 -0
- package/dist/internal/session-runtime.d.ts +44 -0
- package/dist/internal/session-runtime.d.ts.map +1 -0
- package/dist/internal/session-runtime.js +143 -0
- package/dist/internal/session-runtime.js.map +1 -0
- package/dist/internal/watch-matcher.d.ts +56 -0
- package/dist/internal/watch-matcher.d.ts.map +1 -0
- package/dist/internal/watch-matcher.js +179 -0
- package/dist/internal/watch-matcher.js.map +1 -0
- package/dist/loader.d.ts +97 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +527 -0
- package/dist/loader.js.map +1 -0
- package/dist/observer-dispatcher.d.ts +69 -0
- package/dist/observer-dispatcher.d.ts.map +1 -0
- package/dist/observer-dispatcher.js +184 -0
- package/dist/observer-dispatcher.js.map +1 -0
- package/dist/plugin-merger.d.ts +175 -0
- package/dist/plugin-merger.d.ts.map +1 -0
- package/dist/plugin-merger.js +450 -0
- package/dist/plugin-merger.js.map +1 -0
- package/dist/plugins/git/branch-tracker.d.ts +85 -0
- package/dist/plugins/git/branch-tracker.d.ts.map +1 -0
- package/dist/plugins/git/branch-tracker.js +181 -0
- package/dist/plugins/git/branch-tracker.js.map +1 -0
- package/dist/plugins/git/cwd-extensions.d.ts +10 -0
- package/dist/plugins/git/cwd-extensions.d.ts.map +1 -0
- package/dist/plugins/git/cwd-extensions.js +155 -0
- package/dist/plugins/git/cwd-extensions.js.map +1 -0
- package/dist/plugins/git/git-ops.d.ts +89 -0
- package/dist/plugins/git/git-ops.d.ts.map +1 -0
- package/dist/plugins/git/git-ops.js +107 -0
- package/dist/plugins/git/git-ops.js.map +1 -0
- package/dist/plugins/git/index.d.ts +225 -0
- package/dist/plugins/git/index.d.ts.map +1 -0
- package/dist/plugins/git/index.js +52 -0
- package/dist/plugins/git/index.js.map +1 -0
- package/dist/plugins/git/predicates.d.ts +312 -0
- package/dist/plugins/git/predicates.d.ts.map +1 -0
- package/dist/plugins/git/predicates.js +580 -0
- package/dist/plugins/git/predicates.js.map +1 -0
- package/dist/plugins/git/rules.d.ts +209 -0
- package/dist/plugins/git/rules.d.ts.map +1 -0
- package/dist/plugins/git/rules.js +267 -0
- package/dist/plugins/git/rules.js.map +1 -0
- package/dist/schema.d.ts +1799 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +4 -0
- package/dist/schema.js.map +1 -0
- package/dist/testing/index.d.ts +621 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +747 -0
- package/dist/testing/index.js.map +1 -0
- package/examples/README.md +83 -0
- package/examples/combined-git-discipline/README.md +52 -0
- package/examples/combined-git-discipline/node_modules/.bin/pi-steering +43 -0
- package/examples/combined-git-discipline/package.json +18 -0
- package/examples/combined-git-discipline/steering.json +27 -0
- package/examples/combined-git-discipline/steering.test.ts +42 -0
- package/examples/combined-git-discipline/steering.ts +64 -0
- package/examples/combined-git-discipline/tsconfig.json +7 -0
- package/examples/draft-prs-only/README.md +42 -0
- package/examples/draft-prs-only/node_modules/.bin/pi-steering +43 -0
- package/examples/draft-prs-only/package.json +18 -0
- package/examples/draft-prs-only/steering.json +12 -0
- package/examples/draft-prs-only/steering.test.ts +48 -0
- package/examples/draft-prs-only/steering.ts +36 -0
- package/examples/draft-prs-only/tsconfig.json +7 -0
- package/examples/dynamic-reason-runtime-cwd/README.md +148 -0
- package/examples/dynamic-reason-runtime-cwd/node_modules/.bin/pi-steering +43 -0
- package/examples/dynamic-reason-runtime-cwd/package.json +18 -0
- package/examples/dynamic-reason-runtime-cwd/steering.test.ts +130 -0
- package/examples/dynamic-reason-runtime-cwd/steering.ts +82 -0
- package/examples/dynamic-reason-runtime-cwd/tsconfig.json +7 -0
- package/examples/force-push-strict/README.md +75 -0
- package/examples/force-push-strict/node_modules/.bin/pi-steering +43 -0
- package/examples/force-push-strict/package.json +18 -0
- package/examples/force-push-strict/steering.json +12 -0
- package/examples/force-push-strict/steering.test.ts +51 -0
- package/examples/force-push-strict/steering.ts +54 -0
- package/examples/force-push-strict/tsconfig.json +7 -0
- package/examples/no-amend/README.md +52 -0
- package/examples/no-amend/node_modules/.bin/pi-steering +43 -0
- package/examples/no-amend/package.json +18 -0
- package/examples/no-amend/steering.cwd-scoped.json +12 -0
- package/examples/no-amend/steering.json +11 -0
- package/examples/no-amend/steering.test.ts +49 -0
- package/examples/no-amend/steering.ts +38 -0
- package/examples/no-amend/tsconfig.json +7 -0
- package/examples/work-item-plugin/README.md +103 -0
- package/examples/work-item-plugin/node_modules/.bin/pi-steering +43 -0
- package/examples/work-item-plugin/package.json +19 -0
- package/examples/work-item-plugin/src/index.test.ts +168 -0
- package/examples/work-item-plugin/src/index.ts +122 -0
- package/examples/work-item-plugin/src/observers/npm-test-tracker.test.ts +95 -0
- package/examples/work-item-plugin/src/observers/npm-test-tracker.ts +88 -0
- package/examples/work-item-plugin/src/observers/retest-required-tracker.test.ts +81 -0
- package/examples/work-item-plugin/src/observers/retest-required-tracker.ts +81 -0
- package/examples/work-item-plugin/src/predicates/work-item-format.test.ts +157 -0
- package/examples/work-item-plugin/src/predicates/work-item-format.ts +99 -0
- package/examples/work-item-plugin/src/rules/commit-description-check.test.ts +107 -0
- package/examples/work-item-plugin/src/rules/commit-description-check.ts +83 -0
- package/examples/work-item-plugin/src/rules/commit-requires-work-item.test.ts +79 -0
- package/examples/work-item-plugin/src/rules/commit-requires-work-item.ts +56 -0
- package/examples/work-item-plugin/src/rules/push-requires-tests.test.ts +245 -0
- package/examples/work-item-plugin/src/rules/push-requires-tests.ts +62 -0
- package/examples/work-item-plugin/tsconfig.json +8 -0
- package/package.json +88 -0
- package/skills/steering-authoring/SKILL.md +102 -0
- package/src/INVARIANTS.md +116 -0
- package/src/__test-helpers__.ts +243 -0
- package/src/bin/pi-steering.test.ts +920 -0
- package/src/bin/pi-steering.ts +584 -0
- package/src/compat.test.ts +431 -0
- package/src/compat.ts +282 -0
- package/src/defaults.test.ts +585 -0
- package/src/defaults.ts +148 -0
- package/src/define-config.test.ts +888 -0
- package/src/define-config.ts +375 -0
- package/src/define-predicate.test.ts +70 -0
- package/src/define-predicate.ts +57 -0
- package/src/evaluator-internals/context.ts +290 -0
- package/src/evaluator-internals/override.test.ts +99 -0
- package/src/evaluator-internals/override.ts +83 -0
- package/src/evaluator-internals/predicates.ts +1037 -0
- package/src/evaluator-internals/speculative-synthesis.test.ts +411 -0
- package/src/evaluator-internals/speculative-synthesis.ts +199 -0
- package/src/evaluator.test.ts +6296 -0
- package/src/evaluator.ts +1106 -0
- package/src/factory-time-load.test.ts +630 -0
- package/src/helpers/walker-unknown-cwd-reason.test.ts +73 -0
- package/src/helpers/walker-unknown-cwd-reason.ts +83 -0
- package/src/index.test.ts +836 -0
- package/src/index.ts +229 -0
- package/src/internal/drop-unused-observers.test.ts +141 -0
- package/src/internal/drop-unused-observers.ts +81 -0
- package/src/internal/finalize-plugin-state.ts +43 -0
- package/src/internal/merge-observers.ts +51 -0
- package/src/internal/pattern-utils.test.ts +52 -0
- package/src/internal/pattern-utils.ts +38 -0
- package/src/internal/ref-text.test.ts +54 -0
- package/src/internal/ref-text.ts +17 -0
- package/src/internal/session-runtime.test.ts +629 -0
- package/src/internal/session-runtime.ts +201 -0
- package/src/internal/watch-matcher.test.ts +326 -0
- package/src/internal/watch-matcher.ts +200 -0
- package/src/jsdoc-propagation.test.ts +277 -0
- package/src/loader.test.ts +1098 -0
- package/src/loader.ts +581 -0
- package/src/not-block-onunknown.test.ts +554 -0
- package/src/observer-dispatcher.test.ts +939 -0
- package/src/observer-dispatcher.ts +290 -0
- package/src/per-predicate-typing.test.ts +361 -0
- package/src/plugin-merger.test.ts +790 -0
- package/src/plugin-merger.ts +598 -0
- package/src/plugins/git/README.md +485 -0
- package/src/plugins/git/branch-tracker.test.ts +281 -0
- package/src/plugins/git/branch-tracker.ts +187 -0
- package/src/plugins/git/cwd-extensions.test.ts +230 -0
- package/src/plugins/git/cwd-extensions.ts +160 -0
- package/src/plugins/git/git-ops.test.ts +397 -0
- package/src/plugins/git/git-ops.ts +173 -0
- package/src/plugins/git/index.ts +253 -0
- package/src/plugins/git/integration.test.ts +705 -0
- package/src/plugins/git/predicates.test.ts +1401 -0
- package/src/plugins/git/predicates.ts +693 -0
- package/src/plugins/git/rules.test.ts +1001 -0
- package/src/plugins/git/rules.ts +307 -0
- package/src/schema.test.ts +259 -0
- package/src/schema.ts +1963 -0
- package/src/testing/index.test.ts +1696 -0
- package/src/testing/index.ts +1472 -0
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Factory-time load + cwd-mismatch integration tests.
|
|
6
|
+
*
|
|
7
|
+
* Direct unit-level tests for `buildSessionRuntime` live in
|
|
8
|
+
* `internal/session-runtime.test.ts`; the bridge-glue tests
|
|
9
|
+
* (lifecycle wiring, default-rules, agent_loop threading) live in
|
|
10
|
+
* `index.test.ts`. This file covers 15 integration scenarios where
|
|
11
|
+
* the bridge factory's eager-load path through `buildSessionRuntime`
|
|
12
|
+
* exercises the strict-mode throw rule end-to-end:
|
|
13
|
+
*
|
|
14
|
+
* - tracker-name-collision error throws even with
|
|
15
|
+
* `failOnWarnings: false`,
|
|
16
|
+
* - per-layer import failure escalates,
|
|
17
|
+
* - predicate / observer / rule plugin-merger collisions
|
|
18
|
+
* escalate (proves merger warnings are now plumbed),
|
|
19
|
+
* - `failOnWarnings: false` falls through to `console.warn` for
|
|
20
|
+
* warning-class only,
|
|
21
|
+
* - `disabledPlugins` resolves a would-be plugin-name-collision
|
|
22
|
+
* before the collision check runs,
|
|
23
|
+
* - `failOnWarnings` undefined coerces to true (default),
|
|
24
|
+
* - cwd-mismatch fires the `session_start` console.warn while
|
|
25
|
+
* leaving the evaluator + dispatcher non-null,
|
|
26
|
+
* - aggregated render snapshot pins the multi-line format.
|
|
27
|
+
*
|
|
28
|
+
* The bridge factory is async; tests `chdir` into a fresh scratch
|
|
29
|
+
* `$HOME` so the loader walk-up reads the per-test config.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import assert from "node:assert/strict";
|
|
33
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
36
|
+
import type {
|
|
37
|
+
ExtensionAPI,
|
|
38
|
+
ToolCallEvent,
|
|
39
|
+
ToolCallEventResult,
|
|
40
|
+
} from "@earendil-works/pi-coding-agent";
|
|
41
|
+
import {
|
|
42
|
+
makeCtx,
|
|
43
|
+
useScratchHome,
|
|
44
|
+
writeSteeringSingleFileConfig,
|
|
45
|
+
} from "./__test-helpers__.ts";
|
|
46
|
+
import register from "./index.ts";
|
|
47
|
+
|
|
48
|
+
/* -------------------------------------------------------------------------- */
|
|
49
|
+
/* Mock ExtensionAPI */
|
|
50
|
+
/* -------------------------------------------------------------------------- */
|
|
51
|
+
|
|
52
|
+
type EventName = "agent_start" | "session_start" | "tool_call" | "tool_result";
|
|
53
|
+
|
|
54
|
+
interface MockPi {
|
|
55
|
+
api: unknown;
|
|
56
|
+
handlers: Partial<
|
|
57
|
+
Record<EventName, (event: unknown, ctx: unknown) => unknown>
|
|
58
|
+
>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function makeMockPi(): MockPi {
|
|
62
|
+
const handlers: MockPi["handlers"] = {};
|
|
63
|
+
const api = {
|
|
64
|
+
on(event: EventName, handler: (e: unknown, ctx: unknown) => unknown) {
|
|
65
|
+
handlers[event] = handler;
|
|
66
|
+
},
|
|
67
|
+
appendEntry() {},
|
|
68
|
+
async exec() {
|
|
69
|
+
return { stdout: "", stderr: "", code: 0, killed: false };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
return { api, handlers };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* -------------------------------------------------------------------------- */
|
|
76
|
+
/* Scratch home + chdir fixture */
|
|
77
|
+
/* -------------------------------------------------------------------------- */
|
|
78
|
+
|
|
79
|
+
let tmpHome: string;
|
|
80
|
+
function useFactoryTimeScratchHome(): void {
|
|
81
|
+
useScratchHome("pi-steering-factory-time-", (t) => {
|
|
82
|
+
tmpHome = t;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* -------------------------------------------------------------------------- */
|
|
87
|
+
/* console.warn capture */
|
|
88
|
+
/* -------------------------------------------------------------------------- */
|
|
89
|
+
|
|
90
|
+
let capturedWarns: string[];
|
|
91
|
+
let priorWarn: typeof console.warn;
|
|
92
|
+
function captureWarns(): void {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
capturedWarns = [];
|
|
95
|
+
priorWarn = console.warn;
|
|
96
|
+
console.warn = (msg: unknown) => {
|
|
97
|
+
capturedWarns.push(String(msg));
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
console.warn = priorWarn;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* -------------------------------------------------------------------------- */
|
|
106
|
+
/* register()-throws assertion helper */
|
|
107
|
+
/* -------------------------------------------------------------------------- */
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Asserts that `register()` rejects with an Error whose `message`
|
|
111
|
+
* matches every regex in `matchers`. Optional `extraChecks` runs
|
|
112
|
+
* additional assertions against the rejected error (e.g. count
|
|
113
|
+
* matches, structured fields).
|
|
114
|
+
*
|
|
115
|
+
* Folds the per-site `const mock = makeMockPi(); await
|
|
116
|
+
* assert.rejects(...)` boilerplate.
|
|
117
|
+
*/
|
|
118
|
+
async function expectRegisterThrow(
|
|
119
|
+
matchers: RegExp[],
|
|
120
|
+
extraChecks?: (err: Error) => void,
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const mock = makeMockPi();
|
|
123
|
+
await assert.rejects(
|
|
124
|
+
() => register(mock.api as ExtensionAPI),
|
|
125
|
+
(err: Error) => {
|
|
126
|
+
for (const m of matchers) {
|
|
127
|
+
assert.match(err.message, m);
|
|
128
|
+
}
|
|
129
|
+
extraChecks?.(err);
|
|
130
|
+
return true;
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* -------------------------------------------------------------------------- */
|
|
136
|
+
/* Factory throws on each diagnostic kind */
|
|
137
|
+
/* -------------------------------------------------------------------------- */
|
|
138
|
+
|
|
139
|
+
describe("register(): factory throws on diagnostics", () => {
|
|
140
|
+
useFactoryTimeScratchHome();
|
|
141
|
+
captureWarns();
|
|
142
|
+
|
|
143
|
+
it("throws on tracker-name-collision (error-class, failOnWarnings default)", async () => {
|
|
144
|
+
writeSteeringSingleFileConfig(
|
|
145
|
+
tmpHome,
|
|
146
|
+
`const t = { initial: "?", unknown: "unknown", modifiers: {}, subshellSemantics: "isolated" };
|
|
147
|
+
export default {
|
|
148
|
+
disableDefaults: true,
|
|
149
|
+
plugins: [
|
|
150
|
+
{ name: "pa", trackers: { branch: t } },
|
|
151
|
+
{ name: "pb", trackers: { branch: t } },
|
|
152
|
+
],
|
|
153
|
+
};`,
|
|
154
|
+
);
|
|
155
|
+
await expectRegisterThrow(
|
|
156
|
+
[/\[error\]/, /tracker name collision/, /"branch"/],
|
|
157
|
+
(err) => {
|
|
158
|
+
// Integration mirror of O2 single-emission lock; see
|
|
159
|
+
// internal/session-runtime.test.ts.
|
|
160
|
+
const collisionMatches = err.message.match(/tracker name collision/g);
|
|
161
|
+
assert.equal(
|
|
162
|
+
collisionMatches?.length,
|
|
163
|
+
1,
|
|
164
|
+
"tracker-name-collision must appear exactly once",
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("throws on reserved-tracker-name (error-class)", async () => {
|
|
171
|
+
writeSteeringSingleFileConfig(
|
|
172
|
+
tmpHome,
|
|
173
|
+
`const t = { initial: "?", unknown: "unknown", modifiers: {}, subshellSemantics: "isolated" };
|
|
174
|
+
export default {
|
|
175
|
+
disableDefaults: true,
|
|
176
|
+
plugins: [
|
|
177
|
+
{ name: "pa", trackers: { events: t } },
|
|
178
|
+
],
|
|
179
|
+
};`,
|
|
180
|
+
);
|
|
181
|
+
await expectRegisterThrow([
|
|
182
|
+
/\[error\]/,
|
|
183
|
+
/tracker name "events" is reserved/,
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("throws on reserved-predicate-key (error-class)", async () => {
|
|
188
|
+
writeSteeringSingleFileConfig(
|
|
189
|
+
tmpHome,
|
|
190
|
+
`export default {
|
|
191
|
+
disableDefaults: true,
|
|
192
|
+
plugins: [
|
|
193
|
+
{
|
|
194
|
+
name: "pa",
|
|
195
|
+
predicates: {
|
|
196
|
+
onUnknown: () => () => true,
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};`,
|
|
201
|
+
);
|
|
202
|
+
await expectRegisterThrow([
|
|
203
|
+
/\[error\]/,
|
|
204
|
+
/reserved predicate key "onUnknown"/,
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("throws on tracker-name-collision EVEN WITH failOnWarnings: false", async () => {
|
|
209
|
+
// Errors override the opt-out; the engine cannot operate
|
|
210
|
+
// safely with two plugins claiming the same state dimension.
|
|
211
|
+
writeSteeringSingleFileConfig(
|
|
212
|
+
tmpHome,
|
|
213
|
+
`const t = { initial: "?", unknown: "unknown", modifiers: {}, subshellSemantics: "isolated" };
|
|
214
|
+
export default {
|
|
215
|
+
disableDefaults: true,
|
|
216
|
+
failOnWarnings: false,
|
|
217
|
+
plugins: [
|
|
218
|
+
{ name: "pa", trackers: { branch: t } },
|
|
219
|
+
{ name: "pb", trackers: { branch: t } },
|
|
220
|
+
],
|
|
221
|
+
};`,
|
|
222
|
+
);
|
|
223
|
+
await expectRegisterThrow([/\[error\]/, /tracker name collision/]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("throws on plugin-name-collision (warning-class, failOnWarnings default)", async () => {
|
|
227
|
+
// Two layers ship the same plugin name. The collision is
|
|
228
|
+
// warning-class but escalates under the default
|
|
229
|
+
// `failOnWarnings: true`.
|
|
230
|
+
mkdirSync(join(tmpHome, "inner"), { recursive: true });
|
|
231
|
+
writeSteeringSingleFileConfig(
|
|
232
|
+
tmpHome,
|
|
233
|
+
`export default {
|
|
234
|
+
disableDefaults: true,
|
|
235
|
+
plugins: [{ name: "shared" }],
|
|
236
|
+
};`,
|
|
237
|
+
);
|
|
238
|
+
writeSteeringSingleFileConfig(
|
|
239
|
+
join(tmpHome, "inner"),
|
|
240
|
+
`export default {
|
|
241
|
+
plugins: [{ name: "shared" }],
|
|
242
|
+
};`,
|
|
243
|
+
);
|
|
244
|
+
process.chdir(join(tmpHome, "inner"));
|
|
245
|
+
await expectRegisterThrow([/\[warning\]/, /plugin "shared"/]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("throws on per-layer import failure", async () => {
|
|
249
|
+
mkdirSync(join(tmpHome, ".pi"), { recursive: true });
|
|
250
|
+
writeFileSync(
|
|
251
|
+
join(tmpHome, ".pi", "steering.ts"),
|
|
252
|
+
"export default this is not valid typescript;\n",
|
|
253
|
+
"utf8",
|
|
254
|
+
);
|
|
255
|
+
// Narrow on the actual parse-failure detail so a future loader
|
|
256
|
+
// change that swallows the underlying error message (and leaves
|
|
257
|
+
// only "failed to import") trips this assertion. Node's TS
|
|
258
|
+
// stripper emits `Expected '...', got '...'` for syntax errors;
|
|
259
|
+
// older jiti-style strippers emit `SyntaxError` or `Unexpected`.
|
|
260
|
+
await expectRegisterThrow([
|
|
261
|
+
/\[warning\]/,
|
|
262
|
+
/failed to import/i,
|
|
263
|
+
/Expected|SyntaxError|Unexpected/i,
|
|
264
|
+
]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("throws on predicate-collision (proves resolvePlugins warnings are plumbed)", async () => {
|
|
268
|
+
writeSteeringSingleFileConfig(
|
|
269
|
+
tmpHome,
|
|
270
|
+
`export default {
|
|
271
|
+
disableDefaults: true,
|
|
272
|
+
plugins: [
|
|
273
|
+
{
|
|
274
|
+
name: "pa",
|
|
275
|
+
predicates: { sharedKey: () => () => true },
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "pb",
|
|
279
|
+
predicates: { sharedKey: () => () => true },
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
};`,
|
|
283
|
+
);
|
|
284
|
+
await expectRegisterThrow([
|
|
285
|
+
/\[warning\]/,
|
|
286
|
+
/duplicate predicate "when\.sharedKey"/,
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("throws on observer-collision", async () => {
|
|
291
|
+
writeSteeringSingleFileConfig(
|
|
292
|
+
tmpHome,
|
|
293
|
+
`export default {
|
|
294
|
+
disableDefaults: true,
|
|
295
|
+
plugins: [
|
|
296
|
+
{
|
|
297
|
+
name: "pa",
|
|
298
|
+
observers: [{ name: "obs-x", onResult: () => {} }],
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "pb",
|
|
302
|
+
observers: [{ name: "obs-x", onResult: () => {} }],
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};`,
|
|
306
|
+
);
|
|
307
|
+
await expectRegisterThrow([/\[warning\]/, /duplicate observer "obs-x"/]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("throws on rule-collision", async () => {
|
|
311
|
+
writeSteeringSingleFileConfig(
|
|
312
|
+
tmpHome,
|
|
313
|
+
`export default {
|
|
314
|
+
disableDefaults: true,
|
|
315
|
+
plugins: [
|
|
316
|
+
{
|
|
317
|
+
name: "pa",
|
|
318
|
+
rules: [{
|
|
319
|
+
name: "dup",
|
|
320
|
+
tool: "bash",
|
|
321
|
+
field: "command",
|
|
322
|
+
pattern: /^never-a$/,
|
|
323
|
+
reason: "from pa",
|
|
324
|
+
}],
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "pb",
|
|
328
|
+
rules: [{
|
|
329
|
+
name: "dup",
|
|
330
|
+
tool: "bash",
|
|
331
|
+
field: "command",
|
|
332
|
+
pattern: /^never-b$/,
|
|
333
|
+
reason: "from pb",
|
|
334
|
+
}],
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
};`,
|
|
338
|
+
);
|
|
339
|
+
await expectRegisterThrow([/\[warning\]/, /duplicate rule "dup"/]);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
/* -------------------------------------------------------------------------- */
|
|
344
|
+
/* Factory does NOT throw */
|
|
345
|
+
/* -------------------------------------------------------------------------- */
|
|
346
|
+
|
|
347
|
+
describe("register(): factory does NOT throw", () => {
|
|
348
|
+
useFactoryTimeScratchHome();
|
|
349
|
+
captureWarns();
|
|
350
|
+
|
|
351
|
+
it("warning-class with failOnWarnings: false falls through to console.warn", async () => {
|
|
352
|
+
writeSteeringSingleFileConfig(
|
|
353
|
+
tmpHome,
|
|
354
|
+
`export default {
|
|
355
|
+
disableDefaults: true,
|
|
356
|
+
failOnWarnings: false,
|
|
357
|
+
plugins: [
|
|
358
|
+
{
|
|
359
|
+
name: "pa",
|
|
360
|
+
observers: [{ name: "obs-x", onResult: () => {} }],
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "pb",
|
|
364
|
+
observers: [{ name: "obs-x", onResult: () => {} }],
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
};`,
|
|
368
|
+
);
|
|
369
|
+
const mock = makeMockPi();
|
|
370
|
+
// Should resolve without throwing.
|
|
371
|
+
await register(mock.api as ExtensionAPI);
|
|
372
|
+
// At least one captured warn matches the unified single-line
|
|
373
|
+
// shape: `[pi-steering] [warning] <message>`. The aggregated-throw
|
|
374
|
+
// format (with `<count> config issue` header) only renders for
|
|
375
|
+
// thrown errors; this opt-out path goes through console.warn.
|
|
376
|
+
const observerWarn = capturedWarns.find((m) =>
|
|
377
|
+
/\[pi-steering\].*observer.*obs-x/i.test(m),
|
|
378
|
+
);
|
|
379
|
+
assert.ok(
|
|
380
|
+
observerWarn !== undefined,
|
|
381
|
+
`expected observer-collision warn on console.warn; got: ${JSON.stringify(capturedWarns)}`,
|
|
382
|
+
);
|
|
383
|
+
assert.match(observerWarn, /\[warning\]/);
|
|
384
|
+
assert.doesNotMatch(observerWarn, /config issue/);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("disabledPlugins resolves plugin-name-collision before the check runs", async () => {
|
|
388
|
+
// The outer layer's `disabledPlugins: ["shared"]` removes
|
|
389
|
+
// the would-be collision before cross-layer detection,
|
|
390
|
+
// matching the disable-then-detect ordering. No throw, no
|
|
391
|
+
// warn.
|
|
392
|
+
mkdirSync(join(tmpHome, "inner"), { recursive: true });
|
|
393
|
+
writeSteeringSingleFileConfig(
|
|
394
|
+
tmpHome,
|
|
395
|
+
`export default {
|
|
396
|
+
disableDefaults: true,
|
|
397
|
+
disabledPlugins: ["shared"],
|
|
398
|
+
plugins: [{ name: "shared" }],
|
|
399
|
+
};`,
|
|
400
|
+
);
|
|
401
|
+
writeSteeringSingleFileConfig(
|
|
402
|
+
join(tmpHome, "inner"),
|
|
403
|
+
`export default {
|
|
404
|
+
plugins: [{ name: "shared" }],
|
|
405
|
+
};`,
|
|
406
|
+
);
|
|
407
|
+
process.chdir(join(tmpHome, "inner"));
|
|
408
|
+
const mock = makeMockPi();
|
|
409
|
+
await register(mock.api as ExtensionAPI);
|
|
410
|
+
const collisionWarn = capturedWarns.find((m) => /plugin "shared"/i.test(m));
|
|
411
|
+
assert.equal(
|
|
412
|
+
collisionWarn,
|
|
413
|
+
undefined,
|
|
414
|
+
`expected NO plugin-collision warn; got: ${JSON.stringify(capturedWarns)}`,
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("failOnWarnings undefined coerces to true (default-on)", async () => {
|
|
419
|
+
// No `failOnWarnings` field at all; the runtime treats the
|
|
420
|
+
// absence as `true` and escalates the warning.
|
|
421
|
+
writeSteeringSingleFileConfig(
|
|
422
|
+
tmpHome,
|
|
423
|
+
`export default {
|
|
424
|
+
disableDefaults: true,
|
|
425
|
+
plugins: [
|
|
426
|
+
{
|
|
427
|
+
name: "pa",
|
|
428
|
+
observers: [{ name: "obs-x", onResult: () => {} }],
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
name: "pb",
|
|
432
|
+
observers: [{ name: "obs-x", onResult: () => {} }],
|
|
433
|
+
},
|
|
434
|
+
],
|
|
435
|
+
};`,
|
|
436
|
+
);
|
|
437
|
+
await expectRegisterThrow([/\[warning\]/]);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
/* -------------------------------------------------------------------------- */
|
|
442
|
+
/* Cwd-mismatch session_start console.warn */
|
|
443
|
+
/* -------------------------------------------------------------------------- */
|
|
444
|
+
|
|
445
|
+
describe("register(): cwd-mismatch session_start warn", () => {
|
|
446
|
+
useFactoryTimeScratchHome();
|
|
447
|
+
captureWarns();
|
|
448
|
+
|
|
449
|
+
it("emits console.warn when ctx.cwd !== launchCwd; engine continues evaluating", async () => {
|
|
450
|
+
// A user-defined rule lives in the launch-cwd config (tmpHome) but
|
|
451
|
+
// not in foreignCwd's walk-up; if it still fires after the
|
|
452
|
+
// cwd-mismatch warn, launch-cwd config remained in force.
|
|
453
|
+
writeSteeringSingleFileConfig(
|
|
454
|
+
tmpHome,
|
|
455
|
+
`export default {
|
|
456
|
+
rules: [
|
|
457
|
+
{
|
|
458
|
+
name: "block-launch-cwd-only-rule",
|
|
459
|
+
tool: "bash",
|
|
460
|
+
field: "command",
|
|
461
|
+
pattern: /^echo LAUNCH_CWD_PROBE$/,
|
|
462
|
+
reason: "launch-cwd-only rule used by the cwd-mismatch test",
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
};`,
|
|
466
|
+
);
|
|
467
|
+
const mock = makeMockPi();
|
|
468
|
+
await register(mock.api as ExtensionAPI);
|
|
469
|
+
|
|
470
|
+
// Handlers must be wired (evaluator + dispatcher non-null).
|
|
471
|
+
assert.ok(mock.handlers.session_start);
|
|
472
|
+
assert.ok(mock.handlers.tool_call);
|
|
473
|
+
assert.ok(mock.handlers.tool_result);
|
|
474
|
+
|
|
475
|
+
// Fire session_start with a foreign cwd.
|
|
476
|
+
const foreignCwd = "/tmp/some/other/project";
|
|
477
|
+
await mock.handlers.session_start(
|
|
478
|
+
{ type: "session_start", reason: "startup" },
|
|
479
|
+
makeCtx(foreignCwd),
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
const cwdMismatchWarn = capturedWarns.find((m) =>
|
|
483
|
+
/\[pi-steering\] session cwd \(/.test(m),
|
|
484
|
+
);
|
|
485
|
+
assert.ok(
|
|
486
|
+
cwdMismatchWarn !== undefined,
|
|
487
|
+
`expected cwd-mismatch warn; got: ${JSON.stringify(capturedWarns)}`,
|
|
488
|
+
);
|
|
489
|
+
assert.match(cwdMismatchWarn, /differs from launch cwd/);
|
|
490
|
+
assert.ok(
|
|
491
|
+
cwdMismatchWarn.includes(foreignCwd),
|
|
492
|
+
`expected warn to contain foreignCwd; got: ${cwdMismatchWarn}`,
|
|
493
|
+
);
|
|
494
|
+
assert.ok(
|
|
495
|
+
cwdMismatchWarn.includes(tmpHome),
|
|
496
|
+
`expected warn to contain tmpHome; got: ${cwdMismatchWarn}`,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
// Engine continues evaluating: the launch-cwd-only rule still
|
|
500
|
+
// fires under the foreign-cwd ctx. Match on the rule name
|
|
501
|
+
// (carried in `reason` as `[steering:<rule-name>@<source>]`) so a
|
|
502
|
+
// future default-rule shadow matching the same pattern wouldn't
|
|
503
|
+
// silently satisfy `block === true`.
|
|
504
|
+
const blockedEvent: ToolCallEvent = {
|
|
505
|
+
type: "tool_call",
|
|
506
|
+
toolName: "bash",
|
|
507
|
+
toolCallId: "call-1",
|
|
508
|
+
input: { command: "echo LAUNCH_CWD_PROBE" },
|
|
509
|
+
};
|
|
510
|
+
const blocked = (await mock.handlers.tool_call(
|
|
511
|
+
blockedEvent,
|
|
512
|
+
makeCtx(foreignCwd),
|
|
513
|
+
)) as ToolCallEventResult | undefined;
|
|
514
|
+
assert.equal(blocked?.block, true);
|
|
515
|
+
assert.match(
|
|
516
|
+
blocked?.reason ?? "",
|
|
517
|
+
/block-launch-cwd-only-rule/,
|
|
518
|
+
"expected the launch-cwd-only rule to fire — not a default-rule shadow",
|
|
519
|
+
);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("does NOT emit cwd-mismatch warn when ctx.cwd === launchCwd", async () => {
|
|
523
|
+
writeSteeringSingleFileConfig(tmpHome, "export default {};");
|
|
524
|
+
const mock = makeMockPi();
|
|
525
|
+
await register(mock.api as ExtensionAPI);
|
|
526
|
+
|
|
527
|
+
await mock.handlers.session_start!(
|
|
528
|
+
{ type: "session_start", reason: "startup" },
|
|
529
|
+
makeCtx(tmpHome),
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const cwdMismatchWarn = capturedWarns.find((m) =>
|
|
533
|
+
/\[pi-steering\] session cwd \(/.test(m),
|
|
534
|
+
);
|
|
535
|
+
assert.equal(
|
|
536
|
+
cwdMismatchWarn,
|
|
537
|
+
undefined,
|
|
538
|
+
`expected NO cwd-mismatch warn for matching cwds; got: ${JSON.stringify(capturedWarns)}`,
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
/* -------------------------------------------------------------------------- */
|
|
544
|
+
/* Aggregated render snapshot — 1 error + 2 warnings */
|
|
545
|
+
/* -------------------------------------------------------------------------- */
|
|
546
|
+
|
|
547
|
+
describe("register(): aggregated render snapshot", () => {
|
|
548
|
+
useFactoryTimeScratchHome();
|
|
549
|
+
captureWarns();
|
|
550
|
+
|
|
551
|
+
it("renders 1 error + 2 warnings with errors-first ordering and the multi-line shape", async () => {
|
|
552
|
+
// Layout matches the 05-aggregated-render-mixed e2e
|
|
553
|
+
// fixture: plugin-a registers reserved tracker `events`
|
|
554
|
+
// (error), and both plugins ship `obs-x` (warning) + `dup`
|
|
555
|
+
// (warning).
|
|
556
|
+
writeSteeringSingleFileConfig(
|
|
557
|
+
tmpHome,
|
|
558
|
+
`const t = { initial: "?", unknown: "unknown", modifiers: {}, subshellSemantics: "isolated" };
|
|
559
|
+
export default {
|
|
560
|
+
disableDefaults: true,
|
|
561
|
+
plugins: [
|
|
562
|
+
{
|
|
563
|
+
name: "plugin-a",
|
|
564
|
+
trackers: { events: t },
|
|
565
|
+
observers: [{ name: "obs-x", onResult: () => {} }],
|
|
566
|
+
rules: [{
|
|
567
|
+
name: "dup",
|
|
568
|
+
tool: "bash",
|
|
569
|
+
field: "command",
|
|
570
|
+
pattern: /^never-a$/,
|
|
571
|
+
reason: "from plugin-a",
|
|
572
|
+
}],
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "plugin-b",
|
|
576
|
+
observers: [{ name: "obs-x", onResult: () => {} }],
|
|
577
|
+
rules: [{
|
|
578
|
+
name: "dup",
|
|
579
|
+
tool: "bash",
|
|
580
|
+
field: "command",
|
|
581
|
+
pattern: /^never-b$/,
|
|
582
|
+
reason: "from plugin-b",
|
|
583
|
+
}],
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
};`,
|
|
587
|
+
);
|
|
588
|
+
const mock = makeMockPi();
|
|
589
|
+
const err = await register(mock.api as ExtensionAPI).then(
|
|
590
|
+
() => null,
|
|
591
|
+
(e: Error) => e,
|
|
592
|
+
);
|
|
593
|
+
assert.ok(err, "expected register to throw");
|
|
594
|
+
|
|
595
|
+
// Header: plural form ("issues"), no leading prefix (pi adds
|
|
596
|
+
// "Failed to load extension: " automatically downstream).
|
|
597
|
+
assert.match(err.message, /^3 config issues:\n/);
|
|
598
|
+
|
|
599
|
+
// Bullet shape: two-space indent, dash, space, severity in
|
|
600
|
+
// brackets, no padding for column alignment.
|
|
601
|
+
const lines = err.message.split("\n");
|
|
602
|
+
assert.equal(
|
|
603
|
+
lines.length,
|
|
604
|
+
4,
|
|
605
|
+
`expected header + 3 bullets; got: ${err.message}`,
|
|
606
|
+
);
|
|
607
|
+
// Errors-first ordering: line 1 is the [error] bullet. Tighten
|
|
608
|
+
// to enforce that no path prefix slips between the severity
|
|
609
|
+
// tag and the message text — a future change adding a path
|
|
610
|
+
// prefix to `reserved-tracker-name` would surface here.
|
|
611
|
+
assert.match(
|
|
612
|
+
lines[1]!,
|
|
613
|
+
/^ {2}- \[error\] tracker name "events" is reserved/,
|
|
614
|
+
);
|
|
615
|
+
assert.match(lines[2]!, /^ {2}- \[warning\] /);
|
|
616
|
+
assert.match(lines[3]!, /^ {2}- \[warning\] /);
|
|
617
|
+
|
|
618
|
+
// Warnings preserve declaration order from `resolvePlugins`
|
|
619
|
+
// (observer pass before rule pass).
|
|
620
|
+
assert.match(lines[2]!, /duplicate observer "obs-x"/);
|
|
621
|
+
assert.match(lines[3]!, /duplicate rule "dup"/);
|
|
622
|
+
|
|
623
|
+
// No padding (severity tag flush against the next token).
|
|
624
|
+
assert.doesNotMatch(err.message, /\[error\] {2,}/);
|
|
625
|
+
assert.doesNotMatch(err.message, /\[warning\] {2,}/);
|
|
626
|
+
|
|
627
|
+
// No footer (the message ends at the last bullet).
|
|
628
|
+
assert.equal(lines[lines.length - 1]!.startsWith(" - "), true);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for `walkerUnknownCwdReason`.
|
|
6
|
+
*
|
|
7
|
+
* Pin the helper's contract via semantic substring assertions, NOT
|
|
8
|
+
* exact-string equality or length checks. Wording-tweak refactors
|
|
9
|
+
* (clarifying the "dynamic cwd" phrasing, swapping em-dash for colon)
|
|
10
|
+
* shouldn't fail these tests; the contract is "the message tells the
|
|
11
|
+
* agent what failed, where the shell is, and how to retry".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { describe, it } from "node:test";
|
|
16
|
+
import { mockContext } from "../testing/index.ts";
|
|
17
|
+
import { walkerUnknownCwdReason } from "./walker-unknown-cwd-reason.ts";
|
|
18
|
+
|
|
19
|
+
describe("walkerUnknownCwdReason", () => {
|
|
20
|
+
it("includes the `verifying` arg verbatim", () => {
|
|
21
|
+
const ctx = mockContext({ cwd: "/tmp/test-fixture" });
|
|
22
|
+
const out = walkerUnknownCwdReason(ctx, "working tree status");
|
|
23
|
+
assert.ok(
|
|
24
|
+
out.includes("working tree status"),
|
|
25
|
+
`expected message to include the verifying arg, got: ${out}`,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("includes the value of ctx.cwd", () => {
|
|
30
|
+
const ctx = mockContext({ cwd: "/tmp/test-fixture" });
|
|
31
|
+
const out = walkerUnknownCwdReason(ctx, "branch");
|
|
32
|
+
assert.ok(
|
|
33
|
+
out.includes("/tmp/test-fixture"),
|
|
34
|
+
`expected message to include ctx.cwd value, got: ${out}`,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("contains the literal substring 'current directory:'", () => {
|
|
39
|
+
// Pin the cue label so agents (and downstream consumers
|
|
40
|
+
// composing extra retry guidance) can rely on a stable anchor
|
|
41
|
+
// when scanning the reason text.
|
|
42
|
+
const ctx = mockContext({ cwd: "/anywhere" });
|
|
43
|
+
const out = walkerUnknownCwdReason(ctx, "upstream");
|
|
44
|
+
assert.ok(
|
|
45
|
+
out.includes("current directory:"),
|
|
46
|
+
`expected 'current directory:' anchor, got: ${out}`,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("ends with 'Retry with a literal path.'", () => {
|
|
51
|
+
// Sentence-terminating: the helper is composable via string
|
|
52
|
+
// concatenation (call sites append domain-specific guidance),
|
|
53
|
+
// so the base message must end cleanly.
|
|
54
|
+
const ctx = mockContext({ cwd: "/x" });
|
|
55
|
+
const out = walkerUnknownCwdReason(ctx, "remote");
|
|
56
|
+
assert.ok(
|
|
57
|
+
out.endsWith("Retry with a literal path."),
|
|
58
|
+
`expected trailing 'Retry with a literal path.', got: ${out}`,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("is a single line (no embedded newlines)", () => {
|
|
63
|
+
// Pi surfaces ReasonFn output to the agent as a single
|
|
64
|
+
// blocked-rule reason; embedded newlines would look ragged in
|
|
65
|
+
// the agent's transcript. Pin single-line.
|
|
66
|
+
const ctx = mockContext({ cwd: "/repo" });
|
|
67
|
+
const out = walkerUnknownCwdReason(ctx, "commit count");
|
|
68
|
+
assert.ok(
|
|
69
|
+
!out.includes("\n"),
|
|
70
|
+
`expected single-line reason, got: ${JSON.stringify(out)}`,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
});
|