gentle-pi 2.5.0 → 2.6.1
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/README.md +139 -30
- package/assets/agents/gentle-ai-worker.md +4 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +21 -3
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +67 -0
- package/docs/telemetry.md +57 -1
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +143 -15
- package/extensions/codegraph-tools.ts +1 -0
- package/extensions/gentle-agents.ts +795 -50
- package/extensions/gentle-ai.ts +2033 -322
- package/extensions/gentle-shell.ts +145 -42
- package/extensions/gentle-todo.ts +47 -12
- package/extensions/quiet-tools.ts +1 -0
- package/extensions/runtime-metrics.ts +121 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +7 -10
- package/lib/agents-history.ts +9 -1
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +77 -5
- package/lib/agents-runner.ts +548 -26
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +548 -191
- package/lib/agents-widget.ts +33 -14
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +8 -6
- package/lib/native-review-cli.ts +288 -1
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +296 -46
- package/lib/review-candidate-view.ts +30 -20
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +53 -8
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +187 -5
- package/lib/review-last-event-controller.ts +7 -4
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-session-standing-permission.ts +27 -6
- package/lib/runtime-metrics-children.ts +197 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +174 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +308 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +47 -1
- package/lib/shell-card.ts +1 -4
- package/lib/shell-changes-view.ts +362 -37
- package/lib/shell-changes.ts +81 -1
- package/lib/shell-prompt.ts +11 -15
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +28 -11
- package/lib/telemetry-trigger.ts +2 -0
- package/package.json +6 -3
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +288 -1
- package/runtime/review-integration-v2.mjs +187 -5
- package/runtime/telemetry-trigger.mjs +2 -0
- package/scripts/build-runtime-modules.mjs +9 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +16 -2
- package/scripts/types-baseline.json +95 -0
- package/scripts/verify-package-files.mjs +4 -2
- package/skills/_shared/review-ledger-contract.md +17 -1
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +62 -0
- package/tests/agents-fake-child.ts +15 -1
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +45 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner.test.ts +571 -14
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-view.test.ts +476 -65
- package/tests/agents-widget.test.ts +31 -1
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +169 -3
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +2 -1
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +2 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +1454 -33
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +38 -0
- package/tests/gentle-ai.test.ts +944 -4
- package/tests/gentle-shell.test.ts +303 -12
- package/tests/gentle-todo.test.ts +54 -10
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-review-capability-contract.test.ts +23 -1
- package/tests/native-review-cli.test.ts +277 -3
- package/tests/native-review-parity.test.ts +14 -7
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/package-manifest.test.ts +232 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +1 -0
- package/tests/rdd-aware-verification-contract.test.ts +10 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +304 -6
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +14 -0
- package/tests/review-controller-native-routing.test.ts +563 -3
- package/tests/review-controller.test.ts +1 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +364 -4
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +164 -0
- package/tests/review-last-event-closure.test.ts +105 -1
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-session-standing-permission-controller.test.ts +52 -4
- package/tests/review-session-standing-permission.test.ts +30 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +207 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +196 -0
- package/tests/runtime-metrics-model.test.ts +76 -0
- package/tests/runtime-metrics-native.test.ts +275 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +187 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-card.test.ts +24 -3
- package/tests/shell-changes-view.test.ts +471 -8
- package/tests/shell-changes.test.ts +168 -0
- package/tests/shell-prompt.test.ts +28 -6
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +100 -11
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +3 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { PassThrough } from "node:stream";
|
|
4
|
+
import { test } from "node:test";
|
|
5
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
6
|
+
import { setGentleAiDevBinaryEnvironmentForTesting } from "../lib/gentle-ai-binary.ts";
|
|
7
|
+
import * as native from "../lib/runtime-metrics-native.ts";
|
|
8
|
+
import { parseAgentClass, type RuntimeMetricBucket } from "../lib/runtime-metrics.ts";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import schema from "../contracts/telemetry/runtime-aggregate-v1.schema.json" with { type: "json" };
|
|
11
|
+
import fixturePayloads from "./fixtures/runtime-metrics-native-batches.json" with { type: "json" };
|
|
12
|
+
|
|
13
|
+
function source(): RuntimeMetricBucket {
|
|
14
|
+
const token = () => ({ reported: 1, unavailable: 0, unsupported: 0, sum: 7 });
|
|
15
|
+
const duration = () => ({ measured: 0, unavailable: 1, unsupported: 0, sum: 0 });
|
|
16
|
+
return { hostAgent: "pi", agentClass: parseAgentClass("worker")!, executor: "worker", provider: "openai", modelFamily: "gpt",
|
|
17
|
+
selectedProvider: "openai", selectedModelId: "gpt-5.4", observedModelId: "private-alias", responseModelId: "gpt-5.4",
|
|
18
|
+
effort: "high", providerThinkingLevel: "low", error: "none", responses: 1,
|
|
19
|
+
tokens: { input: token(), output: token(), cacheRead: token(), cacheWrite: token(), reasoning: token(), totalTokens: token() },
|
|
20
|
+
responseHeadersMs: duration(), fullResponseMs: duration() };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("production encoder emits exact one-shot fixture with source occurrence coverage", () => {
|
|
24
|
+
const payload = native.encodeNativeRuntimeEvent([source()]);
|
|
25
|
+
assert.equal(typeof payload, "string", "production encoder must be enabled");
|
|
26
|
+
const value = JSON.parse(payload!);
|
|
27
|
+
assert.deepEqual(Object.keys(value).sort(), [...schema.required].sort());
|
|
28
|
+
assert.deepEqual(Object.keys(value.rows[0]).sort(), [...schema.$defs.row.required].sort());
|
|
29
|
+
assert.equal(value.rows[0].launches, null);
|
|
30
|
+
assert.equal(value.rows[0].responses, 1);
|
|
31
|
+
assert.equal(value.rows[0].selected_effort, "high");
|
|
32
|
+
assert.equal(value.rows[0].effective_effort, "low");
|
|
33
|
+
assert.equal(value.rows[0].model_evidence, "response");
|
|
34
|
+
assert.deepEqual(value.rows[0].model, { provider: "openai", id: "gpt-5.4" });
|
|
35
|
+
assert.ok(!payload!.includes("private"));
|
|
36
|
+
assert.ok(!payload!.includes("batch_id") && !payload!.includes("delivery_id"));
|
|
37
|
+
assert.deepEqual(fixturePayloads.batches, [payload]);
|
|
38
|
+
assert.equal(fixturePayloads.schema_sha256, createHash("sha256").update(readFileSync(new URL("../contracts/telemetry/runtime-aggregate-v1.schema.json", import.meta.url))).digest("hex"));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("child launch occurrence retains selection evidence separately from response evidence", () => {
|
|
42
|
+
const payload = native.encodeNativeRuntimeEvent([source()], [{ evidence: "launch_configuration", agentClass: parseAgentClass("worker")!,
|
|
43
|
+
selectedProvider: "openai", selectedModelId: "gpt-5.4", selectedEffort: "high", launches: 1 }]);
|
|
44
|
+
const rows = JSON.parse(payload!).rows;
|
|
45
|
+
assert.equal(rows.length, 2);
|
|
46
|
+
assert.equal(rows[0].model_evidence, "response");
|
|
47
|
+
assert.equal(rows[1].model_evidence, "selected");
|
|
48
|
+
assert.equal(rows[1].launches, 1);
|
|
49
|
+
assert.equal(rows[1].responses, null);
|
|
50
|
+
assert.equal(rows[1].selected_effort, "high");
|
|
51
|
+
assert.equal(rows[1].effective_effort, "unavailable");
|
|
52
|
+
for (const field of ["input_tokens", "output_tokens", "cache_read_tokens", "cache_creation_tokens", "reasoning_tokens", "total_tokens"])
|
|
53
|
+
assert.deepEqual(rows[1][field], { reported: 0, unavailable: 0, unsupported: 0, sum: 0 });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("encoder bounds and invalid coverage discard whole events without splitting", () => {
|
|
57
|
+
assert.equal(native.encodeNativeRuntimeEvent([]), undefined);
|
|
58
|
+
assert.equal(native.encodeNativeRuntimeEvent(Array(33).fill(source())), undefined);
|
|
59
|
+
const invalid = source(); invalid.tokens.input.reported = 0;
|
|
60
|
+
assert.equal(native.encodeNativeRuntimeEvent([invalid]), undefined);
|
|
61
|
+
invalid.tokens.input.sum = -1;
|
|
62
|
+
assert.equal(native.encodeNativeRuntimeEvent([invalid]), undefined);
|
|
63
|
+
const valid = source();
|
|
64
|
+
assert.equal(typeof native.encodeNativeRuntimeEvent([valid]), "string");
|
|
65
|
+
assert.equal(native.encodeNativeRuntimeEvent(Array(32).fill(valid)), undefined, "16 KiB bound applies even below row limit");
|
|
66
|
+
valid.fullResponseMs = { measured: 1, unavailable: 0, unsupported: 0, sum: 12.5 };
|
|
67
|
+
assert.deepEqual(JSON.parse(native.encodeNativeRuntimeEvent([valid])!).rows[0].duration,
|
|
68
|
+
{ kind: "request", measured_count: 1, sum_ms: 12.5 });
|
|
69
|
+
valid.tokens.input = { reported: 0, unavailable: 0, unsupported: 1, sum: 0 };
|
|
70
|
+
assert.deepEqual(JSON.parse(native.encodeNativeRuntimeEvent([valid])!).rows[0].input_tokens, valid.tokens.input);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("encoder filters custom identities and never promotes SDK model to response proof", () => {
|
|
74
|
+
const row = source(); row.provider = "custom"; row.responseModelId = "private-model";
|
|
75
|
+
row.agentClass = "private-agent" as any; row.effort = "private-effort" as any;
|
|
76
|
+
row.error = "authentication";
|
|
77
|
+
const encoded = JSON.parse(native.encodeNativeRuntimeEvent([row])!);
|
|
78
|
+
// "private-model" never normalizes to a public id, so it never claims
|
|
79
|
+
// response evidence; the row falls through to the still-public selection
|
|
80
|
+
// (source()'s default selectedModelId "gpt-5.4") instead of leaking it or
|
|
81
|
+
// collapsing a perfectly good selected identity to custom/custom.
|
|
82
|
+
assert.deepEqual(encoded.rows[0].model, { provider: "openai", id: "gpt-5.4" });
|
|
83
|
+
assert.equal(encoded.rows[0].model_evidence, "selected");
|
|
84
|
+
assert.equal(encoded.rows[0].agent_class, "unknown");
|
|
85
|
+
assert.equal(encoded.rows[0].selected_effort, "unavailable");
|
|
86
|
+
assert.equal(encoded.rows[0].error_category, "auth");
|
|
87
|
+
assert.ok(!JSON.stringify(encoded).includes("private"));
|
|
88
|
+
row.selectedModelId = "unknown";
|
|
89
|
+
const observedFallback = JSON.parse(native.encodeNativeRuntimeEvent([row])!);
|
|
90
|
+
// With no public selected model either, it falls through once more, but
|
|
91
|
+
// source()'s default observedModelId ("private-alias") is not public either.
|
|
92
|
+
assert.equal(observedFallback.rows[0].model_evidence, "unknown");
|
|
93
|
+
assert.deepEqual(observedFallback.rows[0].model, { provider: "unknown", id: "unknown" });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a public observed model is a fallback evidence tier, behind response and selected", () => {
|
|
97
|
+
const row = source();
|
|
98
|
+
row.responseModelId = "unknown"; row.selectedModelId = "unknown"; row.observedModelId = "glm5.3-flash"; row.provider = "nan";
|
|
99
|
+
const encoded = JSON.parse(native.encodeNativeRuntimeEvent([row])!);
|
|
100
|
+
assert.deepEqual(encoded.rows[0].model, { provider: "nan", id: "glm5.3-flash" });
|
|
101
|
+
assert.equal(encoded.rows[0].model_evidence, "selected", "the transport has no separate observed evidence category");
|
|
102
|
+
// A public response model still wins over a public observed model.
|
|
103
|
+
const withResponse = source();
|
|
104
|
+
withResponse.selectedModelId = "unknown"; withResponse.observedModelId = "glm5.3-flash"; withResponse.provider = "nan";
|
|
105
|
+
const encodedResponse = JSON.parse(native.encodeNativeRuntimeEvent([withResponse])!);
|
|
106
|
+
assert.equal(encodedResponse.rows[0].model_evidence, "response");
|
|
107
|
+
// A public selected model still wins over a public observed model.
|
|
108
|
+
const withSelected = source();
|
|
109
|
+
withSelected.responseModelId = "unknown"; withSelected.observedModelId = "glm5.3-flash"; withSelected.provider = "nan";
|
|
110
|
+
const encodedSelected = JSON.parse(native.encodeNativeRuntimeEvent([withSelected])!);
|
|
111
|
+
assert.equal(encodedSelected.rows[0].model_evidence, "selected");
|
|
112
|
+
assert.deepEqual(encodedSelected.rows[0].model, { provider: withSelected.selectedProvider, id: withSelected.selectedModelId });
|
|
113
|
+
// A non-public observed model still falls through to unknown/unknown.
|
|
114
|
+
const noEvidence = source();
|
|
115
|
+
noEvidence.responseModelId = "unknown"; noEvidence.selectedModelId = "unknown"; noEvidence.observedModelId = "private-alias";
|
|
116
|
+
const encodedUnknown = JSON.parse(native.encodeNativeRuntimeEvent([noEvidence])!);
|
|
117
|
+
assert.deepEqual(encodedUnknown.rows[0].model, { provider: "unknown", id: "unknown" });
|
|
118
|
+
assert.equal(encodedUnknown.rows[0].model_evidence, "unknown");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("every encoded model satisfies the schema's anyOf, by pattern or by sentinel", () => {
|
|
122
|
+
const idRule = schema.$defs.model.anyOf[0].properties.id as { pattern: string };
|
|
123
|
+
const providerRule = schema.$defs.model.anyOf[0].properties.provider as { pattern: string };
|
|
124
|
+
const idPattern = new RegExp(idRule.pattern);
|
|
125
|
+
const providerPattern = new RegExp(providerRule.pattern);
|
|
126
|
+
const sentinels = schema.$defs.model.anyOf.slice(1) as Array<{ properties: { provider: { const: string }; id: { const: string } } }>;
|
|
127
|
+
function matchesSchema(model: { provider: string; id: string }): boolean {
|
|
128
|
+
if (providerPattern.test(model.provider) && idPattern.test(model.id)) return true;
|
|
129
|
+
return sentinels.some(({ properties }) => properties.provider.const === model.provider && properties.id.const === model.id);
|
|
130
|
+
}
|
|
131
|
+
// Response/selected/observed rows only ever win evidence with an id that is
|
|
132
|
+
// already public, so their encoded model is either the public-pattern
|
|
133
|
+
// branch or the terminal unknown/unknown sentinel. The custom/custom and
|
|
134
|
+
// opencode/custom sentinels are reachable only through a launch row, which
|
|
135
|
+
// (already upstream-classified, always "selected" evidence) is paired
|
|
136
|
+
// through the same normalizer unconditionally.
|
|
137
|
+
const rows: RuntimeMetricBucket[] = [
|
|
138
|
+
source(),
|
|
139
|
+
{ ...source(), provider: "nan", selectedProvider: "nan", selectedModelId: "deepseek-v4-flash", responseModelId: "unknown" },
|
|
140
|
+
{ ...source(), responseModelId: "unknown", selectedModelId: "unknown", observedModelId: "unknown" },
|
|
141
|
+
];
|
|
142
|
+
const launches = [
|
|
143
|
+
{ evidence: "launch_configuration" as const, agentClass: parseAgentClass("worker")!, selectedProvider: "opencode", selectedModelId: "custom", selectedEffort: "high" as const, launches: 1 },
|
|
144
|
+
{ evidence: "launch_configuration" as const, agentClass: parseAgentClass("worker")!, selectedProvider: "custom", selectedModelId: "custom", selectedEffort: "high" as const, launches: 1 },
|
|
145
|
+
{ evidence: "launch_configuration" as const, agentClass: parseAgentClass("worker")!, selectedProvider: "unknown", selectedModelId: "unknown", selectedEffort: "high" as const, launches: 1 },
|
|
146
|
+
];
|
|
147
|
+
const payload = native.encodeNativeRuntimeEvent(rows, launches);
|
|
148
|
+
assert.ok(payload);
|
|
149
|
+
const decoded = JSON.parse(payload).rows as Array<{ model: { provider: string; id: string } }>;
|
|
150
|
+
assert.equal(decoded.length, rows.length + launches.length);
|
|
151
|
+
for (const row of decoded) assert.ok(matchesSchema(row.model), `model ${JSON.stringify(row.model)} must satisfy $defs.model.anyOf`);
|
|
152
|
+
assert.deepEqual(decoded.map(row => row.model), [
|
|
153
|
+
{ provider: "openai", id: "gpt-5.4" }, { provider: "nan", id: "deepseek-v4-flash" }, { provider: "unknown", id: "unknown" },
|
|
154
|
+
{ provider: "opencode", id: "custom" }, { provider: "custom", id: "custom" }, { provider: "unknown", id: "unknown" },
|
|
155
|
+
]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
function fixture() {
|
|
159
|
+
const child = Object.assign(new EventEmitter(), {
|
|
160
|
+
stdin: new PassThrough(), stdout: new PassThrough(), stderr: new PassThrough(),
|
|
161
|
+
kill: () => { killed++; return true; }, unref() {},
|
|
162
|
+
});
|
|
163
|
+
let killed = 0;
|
|
164
|
+
const calls: unknown[][] = [];
|
|
165
|
+
let input = "";
|
|
166
|
+
child.stdin.on("data", chunk => { input += chunk; });
|
|
167
|
+
const deps = {
|
|
168
|
+
env: {}, resolve: () => "fake-native",
|
|
169
|
+
encode: () => '{"host":"pi","rows":[]}',
|
|
170
|
+
spawn: (...args: unknown[]) => { calls.push(args); return child as any; },
|
|
171
|
+
};
|
|
172
|
+
return { deps, child, calls, input: () => input, killed: () => killed,
|
|
173
|
+
close: (decision = "stored") => {
|
|
174
|
+
child.stdout.write(JSON.stringify({ schema: native.NATIVE_SEND_ACK_SCHEMA, decision }));
|
|
175
|
+
child.emit("close", 0, null);
|
|
176
|
+
} };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
test("one native stdin send; busy drops without another invocation or policy probe", async () => {
|
|
180
|
+
const f = fixture();
|
|
181
|
+
const first = native.sendNativeRuntimeEvent([], "/fixture", f.deps);
|
|
182
|
+
assert.equal(f.calls.length, 1);
|
|
183
|
+
assert.deepEqual(f.calls[0][1], ["telemetry", "runtime", "send", "--json"]);
|
|
184
|
+
assert.equal(f.input(), '{"host":"pi","rows":[]}');
|
|
185
|
+
assert.equal(await native.sendNativeRuntimeEvent([], "/fixture", f.deps), "discarded");
|
|
186
|
+
f.close();
|
|
187
|
+
assert.equal(await first, "stored");
|
|
188
|
+
assert.equal(f.calls.length, 1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("production encoder reaches the fake one-shot child without a test encoding override", async () => {
|
|
192
|
+
const f = fixture();
|
|
193
|
+
const { encode: _encode, ...deps } = f.deps;
|
|
194
|
+
const first = native.sendNativeRuntimeEvent([source()], "/fixture", deps);
|
|
195
|
+
assert.equal(f.calls.length, 1);
|
|
196
|
+
assert.equal(f.input(), fixturePayloads.batches[0]);
|
|
197
|
+
f.close(); assert.equal(await first, "stored");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("validated dev override reaches the one-shot child through the production resolver", async () => {
|
|
201
|
+
const file = realpathSync(process.execPath);
|
|
202
|
+
setGentleAiDevBinaryEnvironmentForTesting({ env: { GENTLE_PI_GENTLE_AI_DEV_BINARY: file }, home: "/unused" });
|
|
203
|
+
try {
|
|
204
|
+
const f = fixture();
|
|
205
|
+
const { resolve: _resolve, encode: _encode, ...deps } = f.deps;
|
|
206
|
+
const pending = native.sendNativeRuntimeEvent([source()], "/fixture", deps);
|
|
207
|
+
f.close();
|
|
208
|
+
assert.equal(await pending, "stored");
|
|
209
|
+
assert.equal(f.calls.length, 1);
|
|
210
|
+
assert.equal(f.calls[0][0], file);
|
|
211
|
+
assert.deepEqual(f.calls[0][1], ["telemetry", "runtime", "send", "--json"]);
|
|
212
|
+
assert.equal(f.input(), fixturePayloads.batches[0]);
|
|
213
|
+
} finally {
|
|
214
|
+
setGentleAiDevBinaryEnvironmentForTesting(undefined);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("invalid dev overrides fail closed without spawning or falling back", async () => {
|
|
219
|
+
for (const file of ["relative-binary", realpathSync(new URL(".", import.meta.url))]) {
|
|
220
|
+
setGentleAiDevBinaryEnvironmentForTesting({ env: { GENTLE_PI_GENTLE_AI_DEV_BINARY: file }, home: "/unused" });
|
|
221
|
+
try {
|
|
222
|
+
const f = fixture();
|
|
223
|
+
const { resolve: _resolve, ...deps } = f.deps;
|
|
224
|
+
assert.equal(await native.sendNativeRuntimeEvent([], "/fixture", deps), "discarded");
|
|
225
|
+
assert.equal(f.calls.length, 0);
|
|
226
|
+
} finally {
|
|
227
|
+
setGentleAiDevBinaryEnvironmentForTesting(undefined);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("opt-out and pre-cancel cause zero encoding, resolution or launch", async () => {
|
|
233
|
+
const f = fixture();
|
|
234
|
+
f.deps.resolve = () => { throw new Error("must not resolve"); };
|
|
235
|
+
f.deps.encode = () => { throw new Error("must not encode"); };
|
|
236
|
+
assert.equal(await native.sendNativeRuntimeEvent([], "/fixture", { ...f.deps, env: { DO_NOT_TRACK: "1" } }), "disabled");
|
|
237
|
+
assert.equal(await native.sendNativeRuntimeEvent([], "/fixture", { ...f.deps, signal: AbortSignal.abort() }), "discarded");
|
|
238
|
+
assert.equal(f.calls.length, 0);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("cancel kills once and keeps slot occupied until actual close", async () => {
|
|
242
|
+
const f = fixture();
|
|
243
|
+
const abort = new AbortController();
|
|
244
|
+
const first = native.sendNativeRuntimeEvent([], "/fixture", { ...f.deps, signal: abort.signal });
|
|
245
|
+
abort.abort(); abort.abort();
|
|
246
|
+
assert.equal(f.killed(), 1);
|
|
247
|
+
assert.equal(await native.sendNativeRuntimeEvent([], "/fixture", f.deps), "discarded");
|
|
248
|
+
f.close();
|
|
249
|
+
assert.equal(await first, "discarded");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
for (const decision of ["discarded", "disabled", "stored", "duplicate", "sent", "private error"]) {
|
|
253
|
+
test(`native result ${decision} never triggers retry`, async () => {
|
|
254
|
+
const f = fixture();
|
|
255
|
+
const first = native.sendNativeRuntimeEvent([], "/fixture", f.deps);
|
|
256
|
+
f.close(decision);
|
|
257
|
+
assert.equal(await first, ["stored", "duplicate", "disabled"].includes(decision) ? decision : "discarded");
|
|
258
|
+
assert.equal(f.calls.length, 1);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
test("metrics receiver, attempt gate and send transport have no filesystem persistence surface", () => {
|
|
263
|
+
for (const path of ["../extensions/runtime-metrics.ts", "../lib/runtime-metrics-delivery.ts", "../lib/runtime-metrics-native.ts"]) {
|
|
264
|
+
const source = readFileSync(new URL(path, import.meta.url), "utf8");
|
|
265
|
+
assert.doesNotMatch(source, /node:fs|appendEntry\s*\(|writeFile|mkdir|createWriteStream/);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("spawn failure silently discards", async () => {
|
|
270
|
+
const f = fixture();
|
|
271
|
+
let calls = 0;
|
|
272
|
+
assert.equal(await native.sendNativeRuntimeEvent([], "/fixture", { ...f.deps,
|
|
273
|
+
spawn: () => { calls++; throw new Error("private error"); } }), "discarded");
|
|
274
|
+
assert.equal(calls, 1);
|
|
275
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { readRuntimeMetricsPolicy, runtimeMetricsEnvAllows } from "../lib/runtime-metrics-policy.ts";
|
|
4
|
+
|
|
5
|
+
const grant = { schema: "gentle-ai.telemetry-policy/v1", operation: "policy", enabled: true, source: "state", reason: "enabled" };
|
|
6
|
+
const result = (value: unknown) => ({ stdout: JSON.stringify(value), stderr: "", exitCode: 0,
|
|
7
|
+
signal: null, timedOut: false, outputLimitExceeded: false });
|
|
8
|
+
|
|
9
|
+
test("only strict enabled policy grants; uses bounded policy invocation exclusively", async () => {
|
|
10
|
+
let calls = 0;
|
|
11
|
+
assert.equal(await readRuntimeMetricsPolicy("/workspace", {
|
|
12
|
+
resolve: () => "/fixture/gentle-ai", exec: async request => {
|
|
13
|
+
calls++;
|
|
14
|
+
assert.deepEqual(request.arguments, ["telemetry", "policy", "--json"]);
|
|
15
|
+
assert.equal(request.file, "/fixture/gentle-ai");
|
|
16
|
+
assert.equal(request.cwd, "/workspace");
|
|
17
|
+
assert.equal(request.timeoutMs, 1000);
|
|
18
|
+
assert.equal(request.maxBufferBytes, 2048);
|
|
19
|
+
return result(grant);
|
|
20
|
+
},
|
|
21
|
+
}), true);
|
|
22
|
+
assert.equal(calls, 1);
|
|
23
|
+
for (const patch of [{ schema: "wrong" }, { operation: "status" }, { enabled: "true" },
|
|
24
|
+
{ enabled: false, reason: "disabled" }, { reason: "enrollment_pending" }, { reason: "state_unavailable" },
|
|
25
|
+
{ source: "private" }, { raw: "private" }, { reason: undefined }]) {
|
|
26
|
+
assert.equal(await readRuntimeMetricsPolicy("/workspace", {
|
|
27
|
+
resolve: () => "fixture", exec: async () => result({ ...grant, ...patch }),
|
|
28
|
+
}), false);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("missing, old, timed-out and malformed binary responses fail closed without fallback", async () => {
|
|
33
|
+
for (const patch of [{ exitCode: 1 }, { timedOut: true }, { outputLimitExceeded: true },
|
|
34
|
+
{ signal: "SIGTERM" }, { stderr: "private error" }, { stdout: "unsupported command" },
|
|
35
|
+
{ stdout: "x".repeat(2049) }, { stdout: "null" }, { stdout: "[]" }]) {
|
|
36
|
+
let calls = 0;
|
|
37
|
+
assert.equal(await readRuntimeMetricsPolicy("/workspace", { resolve: () => "fixture",
|
|
38
|
+
exec: async () => { calls++; return { ...result(grant), ...patch } as any; } }), false);
|
|
39
|
+
assert.equal(calls, 1);
|
|
40
|
+
}
|
|
41
|
+
assert.equal(await readRuntimeMetricsPolicy("/workspace", {
|
|
42
|
+
resolve: () => { throw new Error("missing"); }, exec: async () => assert.fail("must not spawn"),
|
|
43
|
+
}), false);
|
|
44
|
+
assert.equal(await readRuntimeMetricsPolicy("/workspace", {
|
|
45
|
+
resolve: () => "fixture", exec: async () => { throw new Error("ENOENT"); },
|
|
46
|
+
}), false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("outer deadline releases callers even when an exec adapter never settles", async () => {
|
|
50
|
+
let signal: AbortSignal | undefined;
|
|
51
|
+
assert.equal(await readRuntimeMetricsPolicy("/workspace", { resolve: () => "fixture",
|
|
52
|
+
exec: request => { signal = request.signal; return new Promise(() => {}); } }), false);
|
|
53
|
+
assert.equal(signal?.aborted, true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("native-compatible environment veto accepts broad truthy spellings", () => {
|
|
57
|
+
for (const key of ["DO_NOT_TRACK", "CI", "GITHUB_ACTIONS"]) {
|
|
58
|
+
for (const value of ["1", "true", "TRUE", " yes ", "on", "t", "unknown"]) assert.equal(runtimeMetricsEnvAllows({ [key]: value }), false);
|
|
59
|
+
}
|
|
60
|
+
assert.equal(runtimeMetricsEnvAllows({ GENTLE_AI_TELEMETRY: "0" }), false);
|
|
61
|
+
assert.equal(runtimeMetricsEnvAllows({ DO_NOT_TRACK: "false", CI: "0" }), true);
|
|
62
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { RuntimeMetrics, type FinalResponse } from "../lib/runtime-metrics.ts";
|
|
4
|
+
|
|
5
|
+
function response(responseId = "local-response"): FinalResponse {
|
|
6
|
+
return {
|
|
7
|
+
kind: "final_assistant_response", responseId,
|
|
8
|
+
executor: "worker", provider: "openai-codex", modelFamily: "gpt",
|
|
9
|
+
effort: "high", error: "none",
|
|
10
|
+
tokens: {
|
|
11
|
+
input: { state: "reported", value: 0 }, output: { state: "reported", value: 12 },
|
|
12
|
+
cacheRead: { state: "unavailable" }, cacheWrite: { state: "unsupported" },
|
|
13
|
+
},
|
|
14
|
+
responseHeadersMs: { state: "measured", value: 25.5 },
|
|
15
|
+
fullResponseMs: { state: "measured", value: 100 },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("selected provider is independent from response provider without relabeling", () => {
|
|
20
|
+
const metrics = new RuntimeMetrics();
|
|
21
|
+
assert.equal(recordRaw(metrics, { ...response(), selectedProvider: "openai-codex",
|
|
22
|
+
selectedModelId: "gpt-5.6-terra", provider: "anthropic" }), "recorded");
|
|
23
|
+
const [row] = metrics.snapshot();
|
|
24
|
+
assert.equal(row.selectedModelId, "gpt-5.6-terra");
|
|
25
|
+
assert.equal(row.selectedProvider, "openai-codex");
|
|
26
|
+
assert.equal(row.provider, "anthropic");
|
|
27
|
+
// The provider dimension stays independent of a specific id's privacy outcome
|
|
28
|
+
// at record time; the pair is only coupled to a schema-conformant custom/custom
|
|
29
|
+
// model object later, at native encode time (see runtime-metrics-native.test.ts).
|
|
30
|
+
assert.equal(recordRaw(metrics, { ...response("private"), selectedProvider: "openai-codex",
|
|
31
|
+
selectedModelId: "private-internal-finetune" }), "recorded");
|
|
32
|
+
assert.equal(metrics.snapshot()[1].selectedProvider, "openai-codex");
|
|
33
|
+
assert.equal(metrics.snapshot()[1].selectedModelId, "custom");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("open-weight models on arbitrary providers are reported by name, not squashed to a closed enum", () => {
|
|
37
|
+
const metrics = new RuntimeMetrics();
|
|
38
|
+
assert.equal(recordRaw(metrics, { ...response(), provider: "nan", selectedProvider: "nan",
|
|
39
|
+
selectedModelId: "deepseek-v4-flash", responseModelId: "glm5.3-flash" }), "recorded");
|
|
40
|
+
const [row] = metrics.snapshot();
|
|
41
|
+
assert.equal(row.provider, "nan");
|
|
42
|
+
assert.equal(row.selectedProvider, "nan");
|
|
43
|
+
assert.equal(row.selectedModelId, "deepseek-v4-flash");
|
|
44
|
+
assert.equal(row.responseModelId, "glm5.3-flash");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Deliberately bypass static types to exercise the runtime boundary.
|
|
48
|
+
function recordRaw(metrics: RuntimeMetrics, value: unknown) {
|
|
49
|
+
return metrics.record(value as FinalResponse);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("optional native token fields preserve absence but reject explicit malformed values", () => {
|
|
53
|
+
for (const value of [null, -1, { state: "reported", value: NaN }, { state: "reported", value: 1_000_000_001 }]) {
|
|
54
|
+
assert.equal(recordRaw(new RuntimeMetrics(), { ...response(), tokens: { ...response().tokens, reasoning: value } }), "invalid");
|
|
55
|
+
}
|
|
56
|
+
const metrics = new RuntimeMetrics();
|
|
57
|
+
assert.equal(metrics.record(response()), "recorded");
|
|
58
|
+
assert.equal(metrics.snapshot()[0].tokens.reasoning.unavailable, 1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("accounts final responses with explicit zero, missing states and separate durations", () => {
|
|
62
|
+
const metrics = new RuntimeMetrics();
|
|
63
|
+
assert.equal(metrics.record(response()), "recorded");
|
|
64
|
+
assert.equal(metrics.record(response("second")), "recorded");
|
|
65
|
+
const [row] = metrics.snapshot();
|
|
66
|
+
assert.equal(row.responses, 2);
|
|
67
|
+
assert.deepEqual(row.tokens.input, { reported: 2, unavailable: 0, unsupported: 0, sum: 0 });
|
|
68
|
+
assert.equal(row.tokens.output.sum, 24);
|
|
69
|
+
assert.equal(row.tokens.cacheRead.unavailable, 2);
|
|
70
|
+
assert.equal(row.tokens.cacheWrite.unsupported, 2);
|
|
71
|
+
assert.equal(row.responseHeadersMs.sum, 51);
|
|
72
|
+
assert.equal(row.fullResponseMs.sum, 200);
|
|
73
|
+
assert.equal(row.executor, "worker");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("deduplicates distinct local IDs without retaining them in snapshots", () => {
|
|
77
|
+
const metrics = new RuntimeMetrics();
|
|
78
|
+
assert.equal(metrics.record(response()), "recorded");
|
|
79
|
+
assert.equal(metrics.record({ ...response(), effort: "low" }), "duplicate");
|
|
80
|
+
assert.equal(metrics.record(response("second")), "recorded");
|
|
81
|
+
assert.equal(metrics.snapshot()[0].responses, 2);
|
|
82
|
+
assert.ok(!JSON.stringify(metrics.snapshot()).includes("local-response"));
|
|
83
|
+
const copy = metrics.snapshot();
|
|
84
|
+
copy[0].tokens.output.sum = 999;
|
|
85
|
+
assert.equal(metrics.snapshot()[0].tokens.output.sum, 24);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("effort selections and absence states remain distinct", () => {
|
|
89
|
+
const metrics = new RuntimeMetrics();
|
|
90
|
+
const levels = ["off", "minimal", "low", "medium", "high", "xhigh", "max",
|
|
91
|
+
"not_selected", "unsupported", "unavailable"] as const;
|
|
92
|
+
for (const effort of levels) assert.equal(metrics.record({ ...response(effort), effort }), "recorded");
|
|
93
|
+
assert.deepEqual(metrics.snapshot().map(row => row.effort), levels);
|
|
94
|
+
assert.equal(recordRaw(metrics, { ...response("bad"), effort: "ultra" }), "invalid");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("only closed dimensions survive; no prompt-based executor inference", () => {
|
|
98
|
+
const metrics = new RuntimeMetrics();
|
|
99
|
+
assert.equal(recordRaw(metrics, {
|
|
100
|
+
// Free text with spaces/case never matches the schema provider pattern,
|
|
101
|
+
// unlike a genuine open-weight provider slug (see the dedicated test above).
|
|
102
|
+
...response(), provider: "Private Provider Free Text", modelFamily: "private-model-id",
|
|
103
|
+
executor: "SDD apply executor", systemPrompt: "reviewer", errorMessage: "private failure",
|
|
104
|
+
}), "recorded");
|
|
105
|
+
const [row] = metrics.snapshot();
|
|
106
|
+
assert.equal(row.provider, "custom");
|
|
107
|
+
assert.equal(row.modelFamily, "custom");
|
|
108
|
+
assert.equal(row.executor, "unknown");
|
|
109
|
+
assert.ok(!JSON.stringify(row).includes("private"));
|
|
110
|
+
assert.equal(recordRaw(metrics, { ...response("missing"), provider: undefined, modelFamily: null }), "recorded");
|
|
111
|
+
assert.equal(metrics.snapshot()[1].provider, "unknown");
|
|
112
|
+
assert.equal(metrics.snapshot()[1].modelFamily, "unknown");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("rejects malformed numbers and ambiguous measurement states atomically", () => {
|
|
116
|
+
const metrics = new RuntimeMetrics();
|
|
117
|
+
for (const value of [-1, NaN, Infinity, "12", null, 1e20]) {
|
|
118
|
+
const base = response();
|
|
119
|
+
assert.equal(recordRaw(metrics, { ...base, tokens: { ...base.tokens, input: { state: "reported", value } } }), "invalid");
|
|
120
|
+
assert.equal(recordRaw(metrics, { ...base, fullResponseMs: { state: "measured", value } }), "invalid");
|
|
121
|
+
}
|
|
122
|
+
for (const measurement of [{ value: 0 }, { state: "reported" }, { state: "unsupported", value: 0 }]) {
|
|
123
|
+
const base = response();
|
|
124
|
+
assert.equal(recordRaw(metrics, { ...base, tokens: { ...base.tokens, input: measurement } }), "invalid");
|
|
125
|
+
}
|
|
126
|
+
assert.equal(recordRaw(metrics, { ...response(), tokens: { ...response().tokens, output: { state: "reported", value: 1.5 } } }), "invalid");
|
|
127
|
+
assert.equal(recordRaw(metrics, { ...response(), fullResponseMs: { state: "reported", value: 10 } }), "invalid");
|
|
128
|
+
assert.equal(recordRaw(metrics, { ...response(), fullResponseMs: { state: "measured", value: 10 } }), "invalid");
|
|
129
|
+
assert.deepEqual(metrics.snapshot(), []);
|
|
130
|
+
assert.equal(metrics.record(response()), "recorded", "invalid inputs do not reserve IDs");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("rejects streaming, malformed records, identifiers and free-text errors", () => {
|
|
134
|
+
const metrics = new RuntimeMetrics();
|
|
135
|
+
for (const value of [null, {}, [], { ...response(), kind: "message_update" },
|
|
136
|
+
{ ...response(), responseId: "" }, { ...response(), responseId: "x".repeat(129) },
|
|
137
|
+
{ ...response(), error: "raw error text" }, { ...response(), tokens: null }]) {
|
|
138
|
+
assert.equal(recordRaw(metrics, value), "invalid");
|
|
139
|
+
}
|
|
140
|
+
for (const error of ["none", "aborted", "rate_limit", "authentication", "network", "provider", "unknown"] as const) {
|
|
141
|
+
assert.equal(metrics.record({ ...response(error), error, responseHeadersMs: { state: "unsupported" }, fullResponseMs: { state: "unavailable" } }), "recorded");
|
|
142
|
+
}
|
|
143
|
+
assert.equal(metrics.snapshot().length, 7);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("host identity stays separate from role and rejects invented model dimensions", () => {
|
|
147
|
+
const metrics = new RuntimeMetrics();
|
|
148
|
+
assert.equal(recordRaw(metrics, { ...response("custom"), identity: { origin: "custom" } }), "recorded");
|
|
149
|
+
assert.equal(recordRaw(metrics, { ...response("forged"), identity: { hostAgent: "private-worker", modelId: "secret-model" } }), "recorded");
|
|
150
|
+
const rows = metrics.snapshot();
|
|
151
|
+
assert.deepEqual(rows.map(row => [row.hostAgent, row.executor, row.selectedModelId, row.effort]), [
|
|
152
|
+
["pi", "worker", "unknown", "high"],
|
|
153
|
+
]);
|
|
154
|
+
assert.ok(!JSON.stringify(rows).includes("secret-model"));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("hard response and numeric ceilings keep aggregate totals bounded", () => {
|
|
158
|
+
const metrics = new RuntimeMetrics();
|
|
159
|
+
for (let i = 0; i < 1024; i++) {
|
|
160
|
+
const record = response(String(i));
|
|
161
|
+
record.tokens.input = { state: "reported", value: 1_000_000_000 };
|
|
162
|
+
record.responseHeadersMs = { state: "measured", value: 0 };
|
|
163
|
+
record.fullResponseMs = { state: "measured", value: 86_400_000 };
|
|
164
|
+
assert.equal(metrics.record(record), "recorded");
|
|
165
|
+
}
|
|
166
|
+
assert.equal(metrics.record(response("overflow")), "capacity");
|
|
167
|
+
const [row] = metrics.snapshot();
|
|
168
|
+
assert.equal(row.responses, 1024);
|
|
169
|
+
assert.equal(row.tokens.input.sum, 1_024_000_000_000);
|
|
170
|
+
assert.equal(row.fullResponseMs.sum, 88_473_600_000);
|
|
171
|
+
assert.equal(row.responseHeadersMs.measured, 1024);
|
|
172
|
+
assert.equal(row.responseHeadersMs.sum, 0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("capacity rejection keeps dedupe and accounting intact without eviction", () => {
|
|
176
|
+
const metrics = new RuntimeMetrics({ maxResponses: 2, maxBuckets: 1 });
|
|
177
|
+
assert.equal(metrics.record(response("one")), "recorded");
|
|
178
|
+
assert.equal(metrics.record({ ...response("two"), effort: "low" }), "capacity");
|
|
179
|
+
assert.equal(metrics.record(response("two")), "recorded");
|
|
180
|
+
assert.equal(metrics.record(response("three")), "capacity");
|
|
181
|
+
assert.equal(metrics.record(response("one")), "duplicate");
|
|
182
|
+
assert.equal(metrics.snapshot()[0].responses, 2);
|
|
183
|
+
for (const maxResponses of [0, -1, 1.5, Infinity, 1025]) {
|
|
184
|
+
assert.throws(() => new RuntimeMetrics({ maxResponses }), RangeError);
|
|
185
|
+
}
|
|
186
|
+
assert.throws(() => new RuntimeMetrics({ maxBuckets: 65 }), RangeError);
|
|
187
|
+
});
|
|
@@ -67,7 +67,7 @@ const requiredToolsByAgent: Record<string, string[]> = {
|
|
|
67
67
|
"sdd-init.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
68
68
|
"sdd-onboard.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
69
69
|
"sdd-proposal.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
70
|
-
"sdd-research.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
70
|
+
"sdd-research.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save", "fetch_content", "web_search", "source_check", "get_search_content"],
|
|
71
71
|
"sdd-spec.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
|
|
72
72
|
"sdd-status.md": ["read", "grep", "find", "bash", "mem_search", "mem_get_observation"],
|
|
73
73
|
"sdd-sync.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
|
|
@@ -103,6 +103,15 @@ test("artifact-producing SDD agents can persist OpenSpec files while status rema
|
|
|
103
103
|
assert.ok(!statusTools.includes("write"), "sdd-status.md must remain read-only");
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
test("research instructions require executed evidence rather than blanket denial", () => {
|
|
107
|
+
const source = readFileSync(join(assetsAgentsDir, "sdd-research.md"), "utf8");
|
|
108
|
+
assert.doesNotMatch(source, /documentation=\[\]; open-web=\[\]/);
|
|
109
|
+
assert.match(source, /Actually call approved tools/);
|
|
110
|
+
assert.match(source, /claim maps to source IDs/);
|
|
111
|
+
assert.match(source, /proposal_ready: false/);
|
|
112
|
+
assert.ok(!readTools(join(assetsAgentsDir, "sdd-research.md")).includes("bash"));
|
|
113
|
+
});
|
|
114
|
+
|
|
106
115
|
test("project does not ship local SDD agent overrides", () => {
|
|
107
116
|
for (const relativeDir of [join(".pi", "agents"), join(".pi", "subagents")]) {
|
|
108
117
|
const dir = join(repoRoot, relativeDir);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
const guidancePaths = [
|
|
6
|
+
"assets/support/sdd-status-contract.md",
|
|
7
|
+
"assets/sdd-orchestrator-workflow.md",
|
|
8
|
+
"assets/agents/sdd-status.md",
|
|
9
|
+
"assets/agents/sdd-verify.md",
|
|
10
|
+
"assets/agents/sdd-archive.md",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
for (const path of guidancePaths) {
|
|
14
|
+
test(`${path}: native v2 status remains read-only and authoritative`, () => {
|
|
15
|
+
const guidance = readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
|
|
16
|
+
assert.match(guidance, /native.*(?:status|v2)|gentle-ai\.sdd-status/i);
|
|
17
|
+
assert.match(guidance, /read-only/i);
|
|
18
|
+
assert.doesNotMatch(guidance, /resolve-via-engram/i);
|
|
19
|
+
assert.doesNotMatch(guidance, /local SDD status engine|manual (?:fallback )?status|reconstruct(?:ing)? (?:native )?status/i);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("workflow preserves explicit continuation and the manual sync resolver", () => {
|
|
24
|
+
const workflow = readFileSync(new URL("../assets/sdd-orchestrator-workflow.md", import.meta.url), "utf8");
|
|
25
|
+
assert.match(workflow, /only.*sdd-continue|sdd-continue.*only/i);
|
|
26
|
+
assert.match(workflow, /manual sdd-sync/i);
|
|
27
|
+
assert.doesNotMatch(workflow, /sdd-(?:apply|verify|archive).*local|local.*sdd-(?:apply|verify|archive)/i);
|
|
28
|
+
});
|