gentle-pi 3.3.0 → 3.4.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/README.md +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +26 -2
- package/docs/readme-reference.md +32 -6
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +8 -1
- package/extensions/gentle-ai.ts +59 -18
- package/extensions/gentle-shell.ts +99 -10
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +1 -0
- package/lib/inprocess-reviewer.ts +38 -1
- package/lib/native-review-cli.ts +36 -10
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +10 -0
- package/lib/review-integration-v2.ts +4 -1
- package/lib/shell-bar.ts +13 -0
- package/lib/shell-sidebar-layout.ts +10 -4
- package/lib/shell-usage-view.ts +5 -2
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +35 -9
- package/runtime/review-integration-v2.mjs +4 -1
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/verify-package-files.mjs +2 -3
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +9 -0
- package/tests/gentle-agents.test.ts +71 -2
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +56 -4
- package/tests/gentle-shell.test.ts +355 -2
- package/tests/inprocess-reviewer.test.ts +92 -0
- package/tests/native-review-capability-contract.test.ts +16 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay.test.ts +21 -0
- package/tests/review-integration-v2.test.ts +30 -0
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +107 -2
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/shell-bar.test.ts +8 -0
- package/tests/shell-sidebar-layout.test.ts +60 -5
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/skills/release/SKILL.md +0 -137
|
@@ -706,7 +706,11 @@ function isNativeUntrackedPath(value ) {
|
|
|
706
706
|
&& value.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
707
707
|
}
|
|
708
708
|
|
|
709
|
-
function nativeUntrackedSelection(request
|
|
709
|
+
export function nativeUntrackedSelection(request
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
) {
|
|
710
714
|
const { untrackedScope, expectedUntrackedInventory, intendedUntracked } = request;
|
|
711
715
|
const declared = untrackedScope !== undefined || expectedUntrackedInventory !== undefined || intendedUntracked !== undefined;
|
|
712
716
|
if (!declared) return {};
|
|
@@ -717,16 +721,19 @@ function nativeUntrackedSelection(request )
|
|
|
717
721
|
) {
|
|
718
722
|
throw new TypeError("Native untracked selection must declare one scope, one inventory digest, and unique repository-relative paths");
|
|
719
723
|
}
|
|
720
|
-
|
|
724
|
+
// The guard above establishes the array and element types for both typed
|
|
725
|
+
// native requests and untyped facade input.
|
|
726
|
+
const paths = intendedUntracked ;
|
|
727
|
+
if (untrackedScope === NATIVE_UNTRACKED_SCOPE.EXCLUDE && (paths?.length ?? 0) > 0) {
|
|
721
728
|
throw new TypeError("Native exclude untracked selection cannot include paths");
|
|
722
729
|
}
|
|
723
|
-
if (untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT && (
|
|
730
|
+
if (untrackedScope === NATIVE_UNTRACKED_SCOPE.SELECT && (paths?.length ?? 0) === 0) {
|
|
724
731
|
throw new TypeError("Native select untracked selection requires at least one path");
|
|
725
732
|
}
|
|
726
733
|
return {
|
|
727
734
|
untrackedScope,
|
|
728
735
|
expectedUntrackedInventory,
|
|
729
|
-
intendedUntracked:
|
|
736
|
+
intendedUntracked: paths === undefined ? undefined : [...paths],
|
|
730
737
|
};
|
|
731
738
|
}
|
|
732
739
|
|
|
@@ -1019,6 +1026,13 @@ export const NATIVE_CLI_CONTRACTS = Object.freeze({
|
|
|
1019
1026
|
// and hint remain dark because neither is proven to reach the negotiated
|
|
1020
1027
|
// START path Pi consumes.
|
|
1021
1028
|
"3.4.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
1029
|
+
// v3.5.0 repeats 3.4.0: the published provider contract bundle is
|
|
1030
|
+
// byte-identical at 1.2.0, both binaries advertise capabilities/v2.6
|
|
1031
|
+
// with only build-identity differences, and no review-integration schema
|
|
1032
|
+
// changed. The v2 preflight failure identity fix does not change the
|
|
1033
|
+
// closed START/STATUS fields this row negotiates. riskEvidence and hint
|
|
1034
|
+
// remain dark; neither is proven to reach Pi's negotiated START path.
|
|
1035
|
+
"3.5.0": Object.freeze({ start: true, finalize: true, validate: true, bindSdd: true, status: true, inventory: true, reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true, mode: true, riskEvidence: false, hint: false, delivery: true }),
|
|
1022
1036
|
});
|
|
1023
1037
|
|
|
1024
1038
|
|
|
@@ -1103,8 +1117,19 @@ function decodeSelectedLenses(value , riskLevel , lensesRequired
|
|
|
1103
1117
|
function enumString(value , allowed ) { const parsed = stringValue(value); if (!allowed.includes(parsed)) throw new Error("unsupported enum"); return parsed; }
|
|
1104
1118
|
const NATIVE_DIAGNOSTIC_TEXT_LIMIT = 4_096;
|
|
1105
1119
|
|
|
1106
|
-
function sanitizeNativeDiagnosticText(value , limit = NATIVE_DIAGNOSTIC_TEXT_LIMIT) {
|
|
1107
|
-
|
|
1120
|
+
function sanitizeNativeDiagnosticText(value , limit = NATIVE_DIAGNOSTIC_TEXT_LIMIT, operation ) {
|
|
1121
|
+
// ASSESS diagnostics are projected into a public verification plan. Retain
|
|
1122
|
+
// native guidance, not local paths or environment assignment values.
|
|
1123
|
+
const input = operation === NATIVE_REVIEW_OPERATION.ASSESS
|
|
1124
|
+
? value
|
|
1125
|
+
.replace(/(?<![\w-])[a-z_][a-z0-9_]*=(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s]+)/gi, "[REDACTED ENV]")
|
|
1126
|
+
.replace(/--(?:password|token|secret|authorization|cookie|private[_-]key|access[_-]token|[a-z0-9_-]+[_-]token|api[_-]?key)[ \t]+(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s]+)/gi, "[REDACTED CREDENTIAL]")
|
|
1127
|
+
// Quoted paths have a clear boundary. For an unquoted path, the
|
|
1128
|
+
// remaining line is ambiguous (spaces may belong to the filename).
|
|
1129
|
+
// Redact that suffix rather than leak trailing path components.
|
|
1130
|
+
.replace(/"(?:[A-Za-z]:[\\/]|\/)[^"\r\n]*"|'(?:[A-Za-z]:[\\/]|\/)[^'\r\n]*'|(?:[A-Za-z]:[\\/]|\/)[^\r\n]*/g, "[REDACTED PATH]")
|
|
1131
|
+
: value;
|
|
1132
|
+
const normalized = input
|
|
1108
1133
|
.replace(/\x1b](?:[^\x07\x1b]|\x1b(?!\\))*?(?:\x07|\x1b\\)/g, "[REDACTED CONTROL]")
|
|
1109
1134
|
.replace(/\x1b[PX^_][\s\S]*?\x1b\\/g, "[REDACTED CONTROL]")
|
|
1110
1135
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "[REDACTED CONTROL]")
|
|
@@ -1138,7 +1163,7 @@ export function sanitizeForeignNativeReviewDiagnostics(value )
|
|
|
1138
1163
|
timed_out: booleanValue(raw.timed_out),
|
|
1139
1164
|
output_limit_exceeded: booleanValue(raw.output_limit_exceeded),
|
|
1140
1165
|
...(maxBufferBytes === undefined ? {} : { max_buffer_bytes: maxBufferBytes, configuration_hint: configurationHint }),
|
|
1141
|
-
...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr)) }),
|
|
1166
|
+
...(raw.stderr === undefined ? {} : { stderr: sanitizeNativeDiagnosticText(stringValue(raw.stderr), NATIVE_DIAGNOSTIC_TEXT_LIMIT, operation) }),
|
|
1142
1167
|
};
|
|
1143
1168
|
} catch { return undefined; }
|
|
1144
1169
|
}
|
|
@@ -1155,7 +1180,7 @@ function nativeProcessDiagnostics(operation , code
|
|
|
1155
1180
|
...(code === NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT && maxBufferBytes !== undefined
|
|
1156
1181
|
? { max_buffer_bytes: maxBufferBytes, configuration_hint: NATIVE_REVIEW_MAX_BUFFER_CONFIGURATION_HINT }
|
|
1157
1182
|
: {}),
|
|
1158
|
-
...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr) } : {}),
|
|
1183
|
+
...(result?.stderr.trim() ? { stderr: sanitizeNativeDiagnosticText(result.stderr, NATIVE_DIAGNOSTIC_TEXT_LIMIT, operation) } : {}),
|
|
1159
1184
|
};
|
|
1160
1185
|
}
|
|
1161
1186
|
|
|
@@ -2536,6 +2561,7 @@ export class NativeReviewCliV216 {
|
|
|
2536
2561
|
// rejects -- callers (the `gentle_review` tool's `assess` operation) fail
|
|
2537
2562
|
// closed to `high`.
|
|
2538
2563
|
async assess(request ) {
|
|
2564
|
+
const selection = nativeUntrackedSelection(request);
|
|
2539
2565
|
if (request.baseRef !== undefined && !isCanonicalProcessString(request.baseRef)) throw new TypeError("Native ASSESS baseRef must be a non-empty, trimmed, NUL-free string");
|
|
2540
2566
|
if (request.baseRef !== undefined && request.committedOnly !== true) throw new TypeError("Native ASSESS baseRef requires explicit committedOnly acknowledgement");
|
|
2541
2567
|
if (request.baseRef === undefined && request.committedOnly !== undefined) throw new TypeError("Native ASSESS committedOnly requires an explicit baseRef");
|
|
@@ -2543,7 +2569,7 @@ export class NativeReviewCliV216 {
|
|
|
2543
2569
|
const execution = await this.invoke(
|
|
2544
2570
|
NATIVE_REVIEW_OPERATION.ASSESS,
|
|
2545
2571
|
cwd,
|
|
2546
|
-
["review", "assess", "--cwd", cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), "--json"],
|
|
2572
|
+
["review", "assess", "--cwd", cwd, ...(request.baseRef === undefined ? [] : ["--base-ref", request.baseRef, "--committed-only"]), ...nativeUntrackedSelectionArguments(selection), "--json"],
|
|
2547
2573
|
false,
|
|
2548
2574
|
request.signal,
|
|
2549
2575
|
this.executablePath(NATIVE_REVIEW_OPERATION.ASSESS, false),
|
|
@@ -340,6 +340,7 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
|
|
|
340
340
|
|
|
341
341
|
|
|
342
342
|
|
|
343
|
+
|
|
343
344
|
|
|
344
345
|
|
|
345
346
|
const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "durability_limited"] ;
|
|
@@ -1151,7 +1152,8 @@ export function decodeReviewArtifactSubjectV2(value )
|
|
|
1151
1152
|
}
|
|
1152
1153
|
|
|
1153
1154
|
function decodeChangedPathEntry(value , label ) {
|
|
1154
|
-
const body = exactRecord(value, label, ["path", "status", "old_mode", "new_mode", "deleted", "type_changed", "mode_only", "intended_untracked"]);
|
|
1155
|
+
const body = exactRecord(value, label, ["path", "status", "old_mode", "new_mode", "deleted", "type_changed", "mode_only", "intended_untracked"], ["generated"]);
|
|
1156
|
+
if (body.generated !== undefined && body.generated !== true) throw new TypeError(`${label}.generated must be true when present`);
|
|
1155
1157
|
return {
|
|
1156
1158
|
path: nonempty(body.path, `${label}.path`),
|
|
1157
1159
|
status: enumeration(body.status, ["A", "D", "M", "T"] , `${label}.status`),
|
|
@@ -1161,6 +1163,7 @@ function decodeChangedPathEntry(value , label )
|
|
|
1161
1163
|
typeChanged: boolean(body.type_changed, `${label}.type_changed`),
|
|
1162
1164
|
modeOnly: boolean(body.mode_only, `${label}.mode_only`),
|
|
1163
1165
|
intendedUntracked: boolean(body.intended_untracked, `${label}.intended_untracked`),
|
|
1166
|
+
...(body.generated === undefined ? {} : { generated: true }),
|
|
1164
1167
|
};
|
|
1165
1168
|
}
|
|
1166
1169
|
|
|
@@ -36,7 +36,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
|
|
|
36
36
|
// version check below) derives from this constant instead of repeating the
|
|
37
37
|
// literal, so a pin bump cannot leave a stale copy behind. See
|
|
38
38
|
// scripts/install-gentle-ai.mjs for the incident that motivated this.
|
|
39
|
-
export const INSTALLER_VERSION = "3.
|
|
39
|
+
export const INSTALLER_VERSION = "3.5.0";
|
|
40
40
|
export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
|
|
41
41
|
export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
42
42
|
SIGNED_RELEASE_ASSET: "signed-release-asset",
|
|
@@ -45,10 +45,10 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
|
|
|
45
45
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai";
|
|
46
46
|
export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v3";
|
|
47
47
|
export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
|
|
48
|
-
// `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.
|
|
48
|
+
// `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.5.0`
|
|
49
49
|
// with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
|
|
50
|
-
// tag resolves to commit
|
|
51
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
50
|
+
// tag resolves to commit 0b335e43, the published v3.5.0 release head.
|
|
51
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:3y+Nb7CtgGB3ne3bSNxkzV6j19etkOnGDXIiuv7tMp8=";
|
|
52
52
|
export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
|
|
53
53
|
export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
|
|
54
54
|
export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
|
|
@@ -67,7 +67,7 @@ export class GentleAiInstallerError extends Error {
|
|
|
67
67
|
// Sentinel used while a re-pinned gentle-ai release is not yet published. A
|
|
68
68
|
// sentinel digest can never match a real SHA-256, so installation fails closed,
|
|
69
69
|
// and verify-package-files.mjs refuses to pack/publish while any digest below
|
|
70
|
-
// still holds it. The v3.
|
|
70
|
+
// still holds it. The v3.5.0 digests are pinned from the published release:
|
|
71
71
|
// archive sha256 values verified against the minisign-signed checksums.txt and
|
|
72
72
|
// freshly computed hashes; binary sha256 values computed from the extracted
|
|
73
73
|
// executables.
|
|
@@ -109,15 +109,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// Windows is absent from signed release archives on purpose. gentle-ai stopped
|
|
112
|
-
// distributing unsigned Windows builds in c4b764d0, so v3.
|
|
112
|
+
// distributing unsigned Windows builds in c4b764d0, so v3.5.0 publishes signed
|
|
113
113
|
// Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
|
|
114
114
|
// exact-tag Go SumDB source-build path below; restore archive rows only when
|
|
115
115
|
// upstream ships signed Windows assets.
|
|
116
116
|
export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
|
|
117
|
-
"darwin/amd64": asset("gentle-ai_3.
|
|
118
|
-
"darwin/arm64": asset("gentle-ai_3.
|
|
119
|
-
"linux/amd64": asset("gentle-ai_3.
|
|
120
|
-
"linux/arm64": asset("gentle-ai_3.
|
|
117
|
+
"darwin/amd64": asset("gentle-ai_3.5.0_darwin_amd64.tar.gz", "483b8363cc2b717d224ee7a15c834ed518854c31a1d79d84b6ea0551fd7905cf", "06e5ce2936f0b0579fa382b5cd59cf42c23b07404b162d9ff9a036dd107093a4", "gentle-ai"),
|
|
118
|
+
"darwin/arm64": asset("gentle-ai_3.5.0_darwin_arm64.tar.gz", "d7a5c233ce23e8fc2ce917ed3e6a2e643655e6e8322fddb3c522324d4cf8ae52", "ec7757fe13f6cd5c9dffa6466d75c5cd68992eac44e684d330c838282c5e2e7e", "gentle-ai"),
|
|
119
|
+
"linux/amd64": asset("gentle-ai_3.5.0_linux_amd64.tar.gz", "3ea016622981f5e4f4a80632fb9c4673149c98f4e5cecc4b979b41bda758ba4a", "7a4202f75c8b90056f0ecd8fceaaef9efe407abc6f98bb42a3ee1e5ad69eb197", "gentle-ai"),
|
|
120
|
+
"linux/arm64": asset("gentle-ai_3.5.0_linux_arm64.tar.gz", "aca1997b3ae8439c17883cde39c3ba577ae38352ad6be1d1c4b2c6bc428c4505", "4d81f356f6e221bc5730a280bae049df8315232a2470089993200577e36e97cc", "gentle-ai"),
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
// A pinned asset is either a signed archive or, for a prerelease pin only,
|
|
@@ -105,7 +105,6 @@ const requiredPaths = [
|
|
|
105
105
|
"skills/issue-creation/SKILL.md",
|
|
106
106
|
"skills/judgment-day/SKILL.md",
|
|
107
107
|
"skills/rdd-defect-workflow/SKILL.md",
|
|
108
|
-
"skills/release/SKILL.md",
|
|
109
108
|
"skills/skill-creator/SKILL.md",
|
|
110
109
|
"skills/skill-improver/SKILL.md",
|
|
111
110
|
"skills/skill-registry/SKILL.md",
|
|
@@ -339,7 +338,7 @@ async function main() {
|
|
|
339
338
|
});
|
|
340
339
|
|
|
341
340
|
if (driftedContracts.length > 0) {
|
|
342
|
-
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v3.
|
|
341
|
+
console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v3.5.0 runtime's vendored Gentle AI contract artifacts:");
|
|
343
342
|
for (const drift of driftedContracts) console.error(`- ${drift.relativePath}: expected ${drift.expected}, got ${drift.actual}`);
|
|
344
343
|
process.exit(1);
|
|
345
344
|
}
|
|
@@ -384,7 +383,7 @@ async function main() {
|
|
|
384
383
|
process.exit(1);
|
|
385
384
|
}
|
|
386
385
|
|
|
387
|
-
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v3.
|
|
386
|
+
console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v3.5.0 runtime).`);
|
|
388
387
|
}
|
|
389
388
|
|
|
390
389
|
const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { ANIMATION_SCHEMA, parseAnimationPolicyFile, resolveAnimationPolicy, writeAnimationPolicy } from "../lib/animation-policy.ts";
|
|
7
|
+
|
|
8
|
+
test("animation policy strictly accepts only the v1 schema and three modes", () => {
|
|
9
|
+
for (const policy of ["quality", "performance", "potato"]) {
|
|
10
|
+
assert.equal(parseAnimationPolicyFile(JSON.stringify({ schema: ANIMATION_SCHEMA, policy })), policy);
|
|
11
|
+
}
|
|
12
|
+
for (const raw of ["bad", "null", "[]", '{}', '{"policy":"potato"}', ...[
|
|
13
|
+
{ schema: "gentle-pi.animations/v2", policy: "potato" },
|
|
14
|
+
{ schema: ANIMATION_SCHEMA, policy: "POTATO" },
|
|
15
|
+
{ schema: ANIMATION_SCHEMA, policy: false },
|
|
16
|
+
{ schema: ANIMATION_SCHEMA, policy: "quality", extra: true },
|
|
17
|
+
].map((value) => JSON.stringify(value))]) assert.equal(parseAnimationPolicyFile(raw), undefined);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("global policy defaults to quality, persists atomically, and attributes malformed input without rewriting", () => {
|
|
21
|
+
const home = mkdtempSync(join(tmpdir(), "gp-animations-"));
|
|
22
|
+
try {
|
|
23
|
+
const options = { gentlePiConfigHome: home };
|
|
24
|
+
assert.equal(resolveAnimationPolicy(options).policy, "quality");
|
|
25
|
+
assert.equal(resolveAnimationPolicy(options).source, "default");
|
|
26
|
+
for (const policy of ["potato", "performance", "quality"] as const) {
|
|
27
|
+
const path = writeAnimationPolicy(policy, options);
|
|
28
|
+
assert.deepEqual(JSON.parse(readFileSync(path, "utf8")), { schema: ANIMATION_SCHEMA, policy });
|
|
29
|
+
assert.equal(resolveAnimationPolicy(options).policy, policy);
|
|
30
|
+
assert.equal(resolveAnimationPolicy(options).source, "global_file");
|
|
31
|
+
assert.deepEqual(readdirSync(home), ["animations.json"]);
|
|
32
|
+
}
|
|
33
|
+
const path = join(home, "animations.json");
|
|
34
|
+
writeFileSync(path, "broken");
|
|
35
|
+
const result = resolveAnimationPolicy(options);
|
|
36
|
+
assert.equal(result.policy, "quality");
|
|
37
|
+
assert.equal(result.source, "global_file");
|
|
38
|
+
assert.equal(result.malformed, true);
|
|
39
|
+
assert.equal(result.globalFile, path);
|
|
40
|
+
assert.equal(readFileSync(path, "utf8"), "broken");
|
|
41
|
+
} finally { rmSync(home, { recursive: true, force: true }); }
|
|
42
|
+
});
|