pi-harness-runtime 1.1.403 → 1.1.405
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 +7 -0
- package/index.ts +275 -1
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/dist/auto-continue.d.ts +6 -0
- package/packages/jev-judge/dist/auto-continue.d.ts.map +1 -1
- package/packages/jev-judge/dist/auto-continue.js +13 -3
- package/packages/jev-judge/dist/auto-continue.js.map +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/jev-judge/src/auto-continue.ts +17 -3
- package/packages/jev-judge/tests/auto-continue-context.test.ts +37 -0
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/dist/adapters/line-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/line-adapter.js +6 -1
- package/packages/notification/dist/adapters/line-adapter.js.map +1 -1
- package/packages/notification/dist/notification-center.d.ts.map +1 -1
- package/packages/notification/dist/notification-center.js +32 -0
- package/packages/notification/dist/notification-center.js.map +1 -1
- package/packages/notification/dist/telegram-webhook-handler.d.ts +7 -0
- package/packages/notification/dist/telegram-webhook-handler.d.ts.map +1 -1
- package/packages/notification/dist/telegram-webhook-handler.js +7 -0
- package/packages/notification/dist/telegram-webhook-handler.js.map +1 -1
- package/packages/notification/dist/types.d.ts +1 -1
- package/packages/notification/dist/types.d.ts.map +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/adapters/line-adapter.ts +7 -1
- package/packages/notification/src/notification-center.ts +30 -0
- package/packages/notification/src/telegram-webhook-handler.ts +7 -0
- package/packages/notification/src/types.ts +1 -0
- package/packages/notification/test/notification.test.ts +31 -28
- package/packages/notification/test/tooling-proposal-payload.test.ts +212 -0
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/dist/detect.d.ts +62 -0
- package/packages/tooling-preference/dist/detect.d.ts.map +1 -0
- package/packages/tooling-preference/dist/detect.js +209 -0
- package/packages/tooling-preference/dist/detect.js.map +1 -0
- package/packages/tooling-preference/dist/index.d.ts +13 -0
- package/packages/tooling-preference/dist/index.d.ts.map +1 -0
- package/packages/tooling-preference/dist/index.js +13 -0
- package/packages/tooling-preference/dist/index.js.map +1 -0
- package/packages/tooling-preference/dist/proposal.d.ts +70 -0
- package/packages/tooling-preference/dist/proposal.d.ts.map +1 -0
- package/packages/tooling-preference/dist/proposal.js +137 -0
- package/packages/tooling-preference/dist/proposal.js.map +1 -0
- package/packages/tooling-preference/dist/registry.d.ts +50 -0
- package/packages/tooling-preference/dist/registry.d.ts.map +1 -0
- package/packages/tooling-preference/dist/registry.js +53 -0
- package/packages/tooling-preference/dist/registry.js.map +1 -0
- package/packages/tooling-preference/dist/store.d.ts +61 -0
- package/packages/tooling-preference/dist/store.d.ts.map +1 -0
- package/packages/tooling-preference/dist/store.js +129 -0
- package/packages/tooling-preference/dist/store.js.map +1 -0
- package/packages/tooling-preference/package.json +21 -0
- package/packages/tooling-preference/src/detect.ts +251 -0
- package/packages/tooling-preference/src/index.ts +44 -0
- package/packages/tooling-preference/src/proposal.ts +173 -0
- package/packages/tooling-preference/src/registry.ts +79 -0
- package/packages/tooling-preference/src/store.ts +162 -0
- package/packages/tooling-preference/tests/detect.test.ts +188 -0
- package/packages/tooling-preference/tests/proposal.test.ts +107 -0
- package/packages/tooling-preference/tests/registry.test.ts +51 -0
- package/packages/tooling-preference/tests/store.test.ts +99 -0
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
detectToolingContext,
|
|
7
|
+
evaluateBiomeProposalEligibility,
|
|
8
|
+
} from "../src/detect.ts";
|
|
9
|
+
|
|
10
|
+
function workspace(files: Record<string, string>): string {
|
|
11
|
+
const root = mkdtempSync(join(tmpdir(), "tooling-preference-"));
|
|
12
|
+
for (const [name, content] of Object.entries(files)) {
|
|
13
|
+
const path = join(root, name);
|
|
14
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
15
|
+
writeFileSync(path, content, "utf8");
|
|
16
|
+
}
|
|
17
|
+
return root;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cleanup(root: string): void {
|
|
21
|
+
rmSync(root, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("detectToolingContext", () => {
|
|
25
|
+
it("detects a Next.js + React + TypeScript workspace with ESLint and Prettier", () => {
|
|
26
|
+
const root = workspace({
|
|
27
|
+
"package.json": JSON.stringify({
|
|
28
|
+
name: "web",
|
|
29
|
+
dependencies: { next: "15.0.0", react: "19.0.0" },
|
|
30
|
+
devDependencies: { eslint: "^9.0.0", prettier: "^3.0.0", typescript: "^5.7.0" },
|
|
31
|
+
}),
|
|
32
|
+
"tsconfig.json": "{}",
|
|
33
|
+
".eslintrc.json": "{}",
|
|
34
|
+
".prettierrc": "{}",
|
|
35
|
+
"src/app/page.tsx": "export const x = 1;",
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
const context = detectToolingContext(root);
|
|
39
|
+
expect(context.hasPackageJson).toBe(true);
|
|
40
|
+
expect(context.isJavaScriptTypeScript).toBe(true);
|
|
41
|
+
expect(context.isNextJs).toBe(true);
|
|
42
|
+
expect(context.isReact).toBe(true);
|
|
43
|
+
expect(context.usesBiome).toBe(false);
|
|
44
|
+
expect(context.usesEslint).toBe(true);
|
|
45
|
+
expect(context.usesPrettier).toBe(true);
|
|
46
|
+
expect(context.frameworks).toContain("next");
|
|
47
|
+
} finally {
|
|
48
|
+
cleanup(root);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("reports a workspace that already uses Biome", () => {
|
|
53
|
+
const root = workspace({
|
|
54
|
+
"package.json": JSON.stringify({ name: "web", devDependencies: { eslint: "^9.0.0" } }),
|
|
55
|
+
"biome.json": "{}",
|
|
56
|
+
"tsconfig.json": "{}",
|
|
57
|
+
});
|
|
58
|
+
try {
|
|
59
|
+
const context = detectToolingContext(root);
|
|
60
|
+
expect(context.usesBiome).toBe(true);
|
|
61
|
+
} finally {
|
|
62
|
+
cleanup(root);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("detects Biome through the dependency alone", () => {
|
|
67
|
+
const root = workspace({
|
|
68
|
+
"package.json": JSON.stringify({
|
|
69
|
+
name: "web",
|
|
70
|
+
devDependencies: { "@biomejs/biome": "^2.4.7" },
|
|
71
|
+
}),
|
|
72
|
+
"tsconfig.json": "{}",
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
expect(detectToolingContext(root).usesBiome).toBe(true);
|
|
76
|
+
} finally {
|
|
77
|
+
cleanup(root);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("detects the package manager through the lockfile", () => {
|
|
82
|
+
const root = workspace({
|
|
83
|
+
"package.json": JSON.stringify({ name: "web" }),
|
|
84
|
+
"pnpm-lock.yaml": "",
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
expect(detectToolingContext(root).packageManager).toBe("pnpm");
|
|
88
|
+
} finally {
|
|
89
|
+
cleanup(root);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("prefers the declared package manager", () => {
|
|
94
|
+
const root = workspace({
|
|
95
|
+
"package.json": JSON.stringify({ name: "web", packageManager: "bun@1.3.0" }),
|
|
96
|
+
"package-lock.json": "{}",
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
expect(detectToolingContext(root).packageManager).toBe("bun");
|
|
100
|
+
} finally {
|
|
101
|
+
cleanup(root);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("finds a source file without a tsconfig", () => {
|
|
106
|
+
const root = workspace({
|
|
107
|
+
"package.json": JSON.stringify({ name: "web" }),
|
|
108
|
+
"src/index.ts": "export const x = 1;",
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
expect(detectToolingContext(root).isJavaScriptTypeScript).toBe(true);
|
|
112
|
+
} finally {
|
|
113
|
+
cleanup(root);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("does not walk into a dependency directory for sources", () => {
|
|
118
|
+
const root = workspace({
|
|
119
|
+
"package.json": JSON.stringify({ name: "web" }),
|
|
120
|
+
"node_modules/some-package/index.js": "module.exports = 1;",
|
|
121
|
+
});
|
|
122
|
+
try {
|
|
123
|
+
expect(detectToolingContext(root).isJavaScriptTypeScript).toBe(false);
|
|
124
|
+
} finally {
|
|
125
|
+
cleanup(root);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("evaluateBiomeProposalEligibility", () => {
|
|
131
|
+
it("offers the proposal for a JS/TS workspace with ESLint or Prettier", () => {
|
|
132
|
+
const root = workspace({
|
|
133
|
+
"package.json": JSON.stringify({ name: "web", devDependencies: { eslint: "^9.0.0" } }),
|
|
134
|
+
"tsconfig.json": "{}",
|
|
135
|
+
});
|
|
136
|
+
try {
|
|
137
|
+
const decision = evaluateBiomeProposalEligibility(detectToolingContext(root));
|
|
138
|
+
expect(decision.eligible).toBe(true);
|
|
139
|
+
} finally {
|
|
140
|
+
cleanup(root);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("never asks a workspace that already uses Biome", () => {
|
|
145
|
+
const root = workspace({
|
|
146
|
+
"package.json": JSON.stringify({
|
|
147
|
+
name: "web",
|
|
148
|
+
devDependencies: { eslint: "^9.0.0", "@biomejs/biome": "^2.4.7" },
|
|
149
|
+
}),
|
|
150
|
+
"tsconfig.json": "{}",
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
const decision = evaluateBiomeProposalEligibility(detectToolingContext(root));
|
|
154
|
+
expect(decision.eligible).toBe(false);
|
|
155
|
+
expect(decision.reason).toContain("Biome");
|
|
156
|
+
} finally {
|
|
157
|
+
cleanup(root);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("never asks a workspace without ESLint and Prettier", () => {
|
|
162
|
+
const root = workspace({
|
|
163
|
+
"package.json": JSON.stringify({ name: "web" }),
|
|
164
|
+
"tsconfig.json": "{}",
|
|
165
|
+
});
|
|
166
|
+
try {
|
|
167
|
+
const decision = evaluateBiomeProposalEligibility(detectToolingContext(root));
|
|
168
|
+
expect(decision.eligible).toBe(false);
|
|
169
|
+
expect(decision.reason).toContain("neither ESLint nor Prettier");
|
|
170
|
+
} finally {
|
|
171
|
+
cleanup(root);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("never asks a workspace that is not JavaScript/TypeScript", () => {
|
|
176
|
+
const root = workspace({
|
|
177
|
+
"package.json": JSON.stringify({ name: "docs" }),
|
|
178
|
+
"README.md": "# docs",
|
|
179
|
+
});
|
|
180
|
+
try {
|
|
181
|
+
const decision = evaluateBiomeProposalEligibility(detectToolingContext(root));
|
|
182
|
+
expect(decision.eligible).toBe(false);
|
|
183
|
+
expect(decision.reason).toContain("not a JavaScript/TypeScript project");
|
|
184
|
+
} finally {
|
|
185
|
+
cleanup(root);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildToolingButtons,
|
|
4
|
+
buildToolingImplementationPrompt,
|
|
5
|
+
buildToolingProposalMessage,
|
|
6
|
+
buildToolingRequestId,
|
|
7
|
+
parseToolingTarget,
|
|
8
|
+
TOOLING_ACTION,
|
|
9
|
+
} from "../src/proposal.ts";
|
|
10
|
+
import type { ToolingContext } from "../src/detect.ts";
|
|
11
|
+
|
|
12
|
+
const context: ToolingContext = {
|
|
13
|
+
root: "/work/web",
|
|
14
|
+
hasPackageJson: true,
|
|
15
|
+
isJavaScriptTypeScript: true,
|
|
16
|
+
isNextJs: true,
|
|
17
|
+
isReact: true,
|
|
18
|
+
usesBiome: false,
|
|
19
|
+
usesEslint: true,
|
|
20
|
+
usesPrettier: true,
|
|
21
|
+
packageManager: "pnpm",
|
|
22
|
+
frameworks: ["next"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("buildToolingRequestId", () => {
|
|
26
|
+
it("is stable for the same user and workspace", () => {
|
|
27
|
+
const first = buildToolingRequestId("user-1", "/work/web");
|
|
28
|
+
const second = buildToolingRequestId("user-1", "/work/web");
|
|
29
|
+
expect(first).toBe(second);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("differs per user and per workspace", () => {
|
|
33
|
+
expect(buildToolingRequestId("user-1", "/work/web")).not.toBe(
|
|
34
|
+
buildToolingRequestId("user-2", "/work/web"),
|
|
35
|
+
);
|
|
36
|
+
expect(buildToolingRequestId("user-1", "/work/web")).not.toBe(
|
|
37
|
+
buildToolingRequestId("user-1", "/work/other"),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("holds no underscore, so the callback parser keeps it intact", () => {
|
|
42
|
+
const requestId = buildToolingRequestId("user-1", "/work/web");
|
|
43
|
+
expect(requestId).not.toContain("_");
|
|
44
|
+
expect(requestId.startsWith("biome-")).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("buildToolingButtons", () => {
|
|
49
|
+
it("sends one action with a choice per button", () => {
|
|
50
|
+
const requestId = buildToolingRequestId("user-1", "/work/web");
|
|
51
|
+
const buttons = buildToolingButtons(requestId);
|
|
52
|
+
expect(buttons).toHaveLength(2);
|
|
53
|
+
for (const button of buttons) {
|
|
54
|
+
expect(button.action).toBe(TOOLING_ACTION);
|
|
55
|
+
expect(button.targetId).toContain(requestId);
|
|
56
|
+
}
|
|
57
|
+
expect(buttons[0].targetId.startsWith("yes-")).toBe(true);
|
|
58
|
+
expect(buttons[1].targetId.startsWith("no-")).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("keeps the action a single token", () => {
|
|
62
|
+
expect(TOOLING_ACTION).not.toContain("_");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("parseToolingTarget", () => {
|
|
67
|
+
it("reads the choice and the request ID back", () => {
|
|
68
|
+
const requestId = buildToolingRequestId("user-1", "/work/web");
|
|
69
|
+
const [yes, no] = buildToolingButtons(requestId);
|
|
70
|
+
expect(parseToolingTarget(yes.targetId)).toEqual({ choice: "yes", requestId });
|
|
71
|
+
expect(parseToolingTarget(no.targetId)).toEqual({ choice: "no", requestId });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("refuses a target that is not part of the proposal", () => {
|
|
75
|
+
expect(parseToolingTarget("q-123")).toBeNull();
|
|
76
|
+
expect(parseToolingTarget("maybe-biome-abc")).toBeNull();
|
|
77
|
+
expect(parseToolingTarget("")).toBeNull();
|
|
78
|
+
expect(parseToolingTarget(undefined)).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("survives the callback data round trip", () => {
|
|
82
|
+
const requestId = buildToolingRequestId("user-1", "/work/web");
|
|
83
|
+
const [yes] = buildToolingButtons(requestId);
|
|
84
|
+
const data = `${yes.action}_${yes.targetId}`;
|
|
85
|
+
const [action, targetId] = data.split("_");
|
|
86
|
+
expect(action).toBe(TOOLING_ACTION);
|
|
87
|
+
expect(parseToolingTarget(targetId)?.requestId).toBe(requestId);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("the wording", () => {
|
|
92
|
+
it("names the detected tools and the frameworks", () => {
|
|
93
|
+
const message = buildToolingProposalMessage(context);
|
|
94
|
+
expect(message).toContain("Next.js");
|
|
95
|
+
expect(message).toContain("ESLint");
|
|
96
|
+
expect(message).toContain("Prettier");
|
|
97
|
+
expect(message).toContain("Biome");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("asks the agent to implement Biome after a yes", () => {
|
|
101
|
+
const prompt = buildToolingImplementationPrompt(context, "/work/web");
|
|
102
|
+
expect(prompt).toContain("/work/web");
|
|
103
|
+
expect(prompt).toContain("@biomejs/biome");
|
|
104
|
+
expect(prompt).toContain("pnpm add --save-dev");
|
|
105
|
+
expect(prompt).toContain("Next.js");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { ToolingRequestRegistry } from "../src/registry.ts";
|
|
3
|
+
|
|
4
|
+
const request = {
|
|
5
|
+
requestId: "biome-abc123",
|
|
6
|
+
workspaceRoot: "/work/web",
|
|
7
|
+
userKey: "user-1",
|
|
8
|
+
frameworks: ["next"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("ToolingRequestRegistry", () => {
|
|
12
|
+
it("registers a proposal once", () => {
|
|
13
|
+
const registry = new ToolingRequestRegistry();
|
|
14
|
+
expect(registry.register(request)).toBe(true);
|
|
15
|
+
expect(registry.register(request)).toBe(false);
|
|
16
|
+
expect(registry.has(request.requestId)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("lets the first answer win", () => {
|
|
20
|
+
const registry = new ToolingRequestRegistry();
|
|
21
|
+
registry.register(request);
|
|
22
|
+
|
|
23
|
+
const first = registry.claim(request.requestId, "yes", "telegram");
|
|
24
|
+
expect(first?.resolution.preference).toBe("prefer_biome");
|
|
25
|
+
expect(first?.request.workspaceRoot).toBe("/work/web");
|
|
26
|
+
|
|
27
|
+
// The second click finds nothing - the first answer stays.
|
|
28
|
+
expect(registry.claim(request.requestId, "no", "line")).toBeNull();
|
|
29
|
+
expect(registry.resolutionOf(request.requestId)?.preference).toBe("prefer_biome");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("maps a no to the current tools", () => {
|
|
33
|
+
const registry = new ToolingRequestRegistry();
|
|
34
|
+
registry.register(request);
|
|
35
|
+
const claimed = registry.claim(request.requestId, "no", "line");
|
|
36
|
+
expect(claimed?.resolution.preference).toBe("use_project_tools");
|
|
37
|
+
expect(claimed?.resolution.channel).toBe("line");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("refuses a claim for an unknown request", () => {
|
|
41
|
+
const registry = new ToolingRequestRegistry();
|
|
42
|
+
expect(registry.claim("biome-unknown", "yes")).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("does not register a request that was already answered", () => {
|
|
46
|
+
const registry = new ToolingRequestRegistry();
|
|
47
|
+
registry.register(request);
|
|
48
|
+
registry.claim(request.requestId, "no");
|
|
49
|
+
expect(registry.register(request)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
shouldAskAgain,
|
|
7
|
+
ToolingPreferenceStore,
|
|
8
|
+
toolingPreferenceKey,
|
|
9
|
+
} from "../src/store.ts";
|
|
10
|
+
|
|
11
|
+
function storePath(): string {
|
|
12
|
+
return join(mkdtempSync(join(tmpdir(), "tooling-store-")), "tooling-preference.json");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("ToolingPreferenceStore", () => {
|
|
16
|
+
it("has no record for an unknown user and workspace", () => {
|
|
17
|
+
const path = storePath();
|
|
18
|
+
const store = new ToolingPreferenceStore(path);
|
|
19
|
+
expect(store.read("user-1", "/work/web")).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("keeps the record of a user in a workspace apart", () => {
|
|
23
|
+
const path = storePath();
|
|
24
|
+
const store = new ToolingPreferenceStore(path);
|
|
25
|
+
store.recordDecision("user-1", "/work/web", "prefer_biome", "biome-1");
|
|
26
|
+
store.recordDecision("user-2", "/work/web", "use_project_tools", "biome-2");
|
|
27
|
+
store.recordDecision("user-1", "/work/other", "use_project_tools", "biome-3");
|
|
28
|
+
|
|
29
|
+
expect(store.read("user-1", "/work/web")?.decision).toBe("prefer_biome");
|
|
30
|
+
expect(store.read("user-2", "/work/web")?.decision).toBe("use_project_tools");
|
|
31
|
+
expect(store.read("user-1", "/work/other")?.decision).toBe("use_project_tools");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("remembers that the proposal was sent", () => {
|
|
35
|
+
const path = storePath();
|
|
36
|
+
const store = new ToolingPreferenceStore(path);
|
|
37
|
+
store.markAsked("user-1", "/work/web", "biome-1");
|
|
38
|
+
const record = store.read("user-1", "/work/web");
|
|
39
|
+
expect(record?.askedAt).toBeTruthy();
|
|
40
|
+
expect(record?.requestId).toBe("biome-1");
|
|
41
|
+
expect(record?.decision).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("keeps a decision across a new store instance", () => {
|
|
45
|
+
const path = storePath();
|
|
46
|
+
new ToolingPreferenceStore(path).recordDecision(
|
|
47
|
+
"user-1",
|
|
48
|
+
"/work/web",
|
|
49
|
+
"prefer_biome",
|
|
50
|
+
"biome-1",
|
|
51
|
+
"telegram",
|
|
52
|
+
);
|
|
53
|
+
const record = new ToolingPreferenceStore(path).read("user-1", "/work/web");
|
|
54
|
+
expect(record?.decision).toBe("prefer_biome");
|
|
55
|
+
expect(record?.channel).toBe("telegram");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("survives a broken store file", () => {
|
|
59
|
+
const path = storePath();
|
|
60
|
+
const store = new ToolingPreferenceStore(path);
|
|
61
|
+
store.markAsked("user-1", "/work/web", "biome-1");
|
|
62
|
+
// A store that cannot be read must not stop the harness.
|
|
63
|
+
Bun.write(path, "not json");
|
|
64
|
+
expect(store.read("user-1", "/work/web")).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("keys a record by the user and the workspace", () => {
|
|
68
|
+
expect(toolingPreferenceKey("user-1", "/work/web")).not.toBe(
|
|
69
|
+
toolingPreferenceKey("user-2", "/work/web"),
|
|
70
|
+
);
|
|
71
|
+
expect(toolingPreferenceKey("user-1", "/work/web")).toBe(
|
|
72
|
+
toolingPreferenceKey("user-1", "/work/web"),
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("shouldAskAgain", () => {
|
|
78
|
+
const day = 24 * 60 * 60 * 1000;
|
|
79
|
+
const now = new Date("2026-10-24T12:00:00.000Z");
|
|
80
|
+
|
|
81
|
+
it("asks a workspace that was never asked", () => {
|
|
82
|
+
expect(shouldAskAgain(undefined, 30 * day, now)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("never asks a workspace with a decision", () => {
|
|
86
|
+
const record = { decision: "prefer_biome" as const, askedAt: "2026-01-01T00:00:00.000Z" };
|
|
87
|
+
expect(shouldAskAgain(record, 30 * day, now)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("does not ask again inside the cooldown", () => {
|
|
91
|
+
const record = { askedAt: new Date(now.getTime() - day).toISOString() };
|
|
92
|
+
expect(shouldAskAgain(record, 30 * day, now)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("asks again after the cooldown", () => {
|
|
96
|
+
const record = { askedAt: new Date(now.getTime() - 31 * day).toISOString() };
|
|
97
|
+
expect(shouldAskAgain(record, 30 * day, now)).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
});
|