@opengsd/gsd-pi 1.10.0 → 1.11.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 +10 -9
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto-model-selection.js +1 -0
- package/dist/resources/extensions/gsd/auto-start.js +1 -1
- package/dist/resources/extensions/gsd/auto-verification.js +25 -12
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +4 -3
- package/dist/resources/extensions/gsd/db/engine.js +8 -2
- package/dist/resources/extensions/gsd/db-canonical-foundation-schema.js +120 -0
- package/dist/resources/extensions/gsd/db-migration-steps.js +4 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +12 -3
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +16 -4
- package/dist/resources/extensions/gsd/model-cost-table.js +6 -3
- package/dist/resources/extensions/gsd/model-router.js +10 -5
- package/dist/resources/extensions/gsd/planning-subagent-registry.js +12 -4
- package/dist/resources/extensions/gsd/preferences-models.js +50 -10
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +41 -6
- package/dist/resources/extensions/gsd/preferences.js +7 -0
- package/dist/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
- package/dist/resources/extensions/gsd/tools/plan-slice.js +5 -1
- package/dist/resources/extensions/gsd/verification-evidence.js +5 -3
- package/dist/resources/extensions/ollama/ollama-chat-provider.js +2 -2
- package/dist/resources/extensions/subagent/index.js +1 -1
- package/dist/startup-model-validation.d.ts +1 -1
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
- package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +7 -6
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/dist/rpc.d.ts +1 -1
- package/packages/contracts/dist/rpc.d.ts.map +1 -1
- package/packages/contracts/dist/rpc.js +1 -1
- package/packages/contracts/dist/rpc.js.map +1 -1
- package/packages/contracts/dist/rpc.test.js +1 -1
- package/packages/contracts/dist/rpc.test.js.map +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/cli/args.js +2 -2
- package/packages/gsd-agent-modes/dist/cli/args.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js +1 -0
- package/packages/gsd-agent-modes/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/README.md +12 -2
- package/packages/gsd-cloud/dist/background-cli.test.js +157 -80
- package/packages/gsd-cloud/dist/background-cli.test.js.map +1 -1
- package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cli.js +36 -12
- package/packages/gsd-cloud/dist/cli.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts +6 -1
- package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.js +106 -24
- package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
- package/packages/gsd-cloud/dist/cloud-runtime.test.js +224 -59
- package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-artifacts.d.ts +3 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.js +36 -0
- package/packages/gsd-cloud/dist/runtime-artifacts.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts +5 -0
- package/packages/gsd-cloud/dist/runtime-process.d.ts.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.js +429 -61
- package/packages/gsd-cloud/dist/runtime-process.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-process.test.js +493 -47
- package/packages/gsd-cloud/dist/runtime-process.test.js.map +1 -1
- package/packages/gsd-cloud/dist/runtime-telemetry.d.ts +117 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.js +291 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.js.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts +2 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.d.ts.map +1 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.js +434 -0
- package/packages/gsd-cloud/dist/runtime-telemetry.test.js.map +1 -0
- package/packages/gsd-cloud/package.json +2 -2
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +2 -2
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +113 -2
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +84 -9
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/models.js +18 -6
- package/packages/pi-ai/dist/models.js.map +1 -1
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +3 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/google.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +9 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -3
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.d.ts +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/theme/theme.js +1 -0
- package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/dist/theme/theme.d.ts +1 -1
- package/pkg/dist/theme/theme.d.ts.map +1 -1
- package/pkg/dist/theme/theme.js +1 -0
- package/pkg/dist/theme/theme.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-model-selection.ts +1 -0
- package/src/resources/extensions/gsd/auto-start.ts +1 -1
- package/src/resources/extensions/gsd/auto-verification.ts +46 -12
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +4 -3
- package/src/resources/extensions/gsd/db/engine.ts +9 -1
- package/src/resources/extensions/gsd/db-canonical-foundation-schema.ts +129 -0
- package/src/resources/extensions/gsd/db-migration-steps.ts +5 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +12 -3
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +15 -4
- package/src/resources/extensions/gsd/model-cost-table.ts +14 -3
- package/src/resources/extensions/gsd/model-router.ts +10 -5
- package/src/resources/extensions/gsd/planning-subagent-registry.ts +18 -4
- package/src/resources/extensions/gsd/preferences-models.ts +54 -10
- package/src/resources/extensions/gsd/preferences-types.ts +10 -2
- package/src/resources/extensions/gsd/preferences-validation.ts +49 -6
- package/src/resources/extensions/gsd/preferences.ts +13 -0
- package/src/resources/extensions/gsd/templates/PREFERENCES.md +3 -0
- package/src/resources/extensions/gsd/tests/db-canonical-foundation.test.ts +459 -0
- package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +22 -7
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/plan-slice.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +38 -1
- package/src/resources/extensions/gsd/tests/preferences.test.ts +71 -4
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-faults.test.ts +238 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-fixture.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-fixture.ts +141 -0
- package/src/resources/extensions/gsd/tests/workflow-authority-projection-conflict.test.ts +177 -0
- package/src/resources/extensions/gsd/tests/workflow-fault-harness.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/workflow-fault-harness.ts +72 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +40 -0
- package/src/resources/extensions/gsd/tools/plan-slice.ts +5 -1
- package/src/resources/extensions/gsd/verification-evidence.ts +5 -2
- package/src/resources/extensions/ollama/ollama-chat-provider.ts +2 -2
- package/src/resources/extensions/ollama/tests/ollama-chat-provider-think.test.ts +4 -0
- package/src/resources/extensions/subagent/index.ts +1 -1
- /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{EAhnhYMt5UVOiG9-iPDjc → 8ub6oiEp2YnlOHQU20LJr}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Executable contract for the additive v31 canonical database foundation.
|
|
3
|
+
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { copyFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { afterEach, test } from "node:test";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
SCHEMA_VERSION,
|
|
15
|
+
_getAdapter,
|
|
16
|
+
_setMigrationFaultForTest,
|
|
17
|
+
closeDatabase,
|
|
18
|
+
openDatabase,
|
|
19
|
+
} from "../gsd-db.ts";
|
|
20
|
+
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
const tempDirs = new Set<string>();
|
|
23
|
+
|
|
24
|
+
interface RawDb {
|
|
25
|
+
exec(sql: string): void;
|
|
26
|
+
prepare(sql: string): {
|
|
27
|
+
get(...args: unknown[]): Record<string, unknown> | undefined;
|
|
28
|
+
all(...args: unknown[]): Array<Record<string, unknown>>;
|
|
29
|
+
};
|
|
30
|
+
close(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function openRawDatabase(path: string): RawDb {
|
|
34
|
+
const sqlite = require("node:sqlite") as { DatabaseSync: new (path: string) => RawDb };
|
|
35
|
+
return new sqlite.DatabaseSync(path);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createDatabasePath(): string {
|
|
39
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-canonical-foundation-"));
|
|
40
|
+
tempDirs.add(dir);
|
|
41
|
+
return join(dir, "gsd.db");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tableExists(db: RawDb, table: string): boolean {
|
|
45
|
+
return Boolean(
|
|
46
|
+
db.prepare("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = ?").get(table),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function maxSchemaVersion(db: RawDb): number {
|
|
51
|
+
return Number(db.prepare("SELECT MAX(version) AS version FROM schema_version").get()?.version);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function rewindToV30(dbPath: string): void {
|
|
55
|
+
assert.equal(openDatabase(dbPath), true);
|
|
56
|
+
closeDatabase();
|
|
57
|
+
|
|
58
|
+
const raw = openRawDatabase(dbPath);
|
|
59
|
+
try {
|
|
60
|
+
raw.exec(`
|
|
61
|
+
DROP TABLE IF EXISTS workflow_outbox;
|
|
62
|
+
DROP TABLE IF EXISTS workflow_domain_events;
|
|
63
|
+
DROP TABLE IF EXISTS workflow_operations;
|
|
64
|
+
DROP TABLE IF EXISTS project_authority;
|
|
65
|
+
DELETE FROM schema_version;
|
|
66
|
+
INSERT INTO schema_version (version, applied_at) VALUES (30, '2026-07-12T00:00:00.000Z');
|
|
67
|
+
INSERT OR IGNORE INTO milestones (id, title, status, created_at)
|
|
68
|
+
VALUES ('M-LEGACY', 'Preserved legacy milestone', 'active', '2026-07-12T00:00:00.000Z');
|
|
69
|
+
INSERT OR IGNORE INTO audit_events
|
|
70
|
+
(event_id, trace_id, category, type, ts, payload_json)
|
|
71
|
+
VALUES
|
|
72
|
+
('audit-legacy', 'trace-legacy', 'workflow', 'legacy', '2026-07-12T00:00:00.000Z', '{"kept":true}');
|
|
73
|
+
`);
|
|
74
|
+
} finally {
|
|
75
|
+
raw.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function inspectFromFreshProcess(dbPath: string): Record<string, unknown> {
|
|
80
|
+
const moduleHref = pathToFileURL(
|
|
81
|
+
join(process.cwd(), "src/resources/extensions/gsd/gsd-db.ts"),
|
|
82
|
+
).href;
|
|
83
|
+
const script = [
|
|
84
|
+
`import { openDatabase, closeDatabase, _getAdapter } from ${JSON.stringify(moduleHref)};`,
|
|
85
|
+
"const path = process.argv[1];",
|
|
86
|
+
"if (!openDatabase(path)) throw new Error('database open failed');",
|
|
87
|
+
"const db = _getAdapter();",
|
|
88
|
+
"const version = db.prepare('SELECT MAX(version) AS value FROM schema_version').get().value;",
|
|
89
|
+
"const authority = db.prepare('SELECT project_id, revision, authority_epoch FROM project_authority').get();",
|
|
90
|
+
"const legacy = db.prepare(\"SELECT title FROM milestones WHERE id = 'M-LEGACY'\").get();",
|
|
91
|
+
"console.log(JSON.stringify({ version, authority, legacy }));",
|
|
92
|
+
"closeDatabase();",
|
|
93
|
+
].join("\n");
|
|
94
|
+
const env = { ...process.env };
|
|
95
|
+
delete env.NODE_TEST_CONTEXT;
|
|
96
|
+
const child = spawnSync(
|
|
97
|
+
process.execPath,
|
|
98
|
+
[
|
|
99
|
+
"--import",
|
|
100
|
+
"./src/resources/extensions/gsd/tests/resolve-ts.mjs",
|
|
101
|
+
"--experimental-strip-types",
|
|
102
|
+
"--input-type=module",
|
|
103
|
+
"-e",
|
|
104
|
+
script,
|
|
105
|
+
dbPath,
|
|
106
|
+
],
|
|
107
|
+
{ cwd: process.cwd(), encoding: "utf8", env, timeout: 30_000 },
|
|
108
|
+
);
|
|
109
|
+
assert.equal(child.status, 0, child.stderr || child.stdout);
|
|
110
|
+
return JSON.parse(child.stdout) as Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
afterEach(() => {
|
|
114
|
+
_setMigrationFaultForTest(false);
|
|
115
|
+
closeDatabase();
|
|
116
|
+
for (const dir of tempDirs) rmSync(dir, { recursive: true, force: true });
|
|
117
|
+
tempDirs.clear();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("fresh database creates the v31 authority root and linked operation journal", () => {
|
|
121
|
+
assert.equal(SCHEMA_VERSION, 31);
|
|
122
|
+
const dbPath = createDatabasePath();
|
|
123
|
+
assert.equal(openDatabase(dbPath), true);
|
|
124
|
+
const db = _getAdapter();
|
|
125
|
+
assert.ok(db);
|
|
126
|
+
|
|
127
|
+
const authority = db.prepare(`
|
|
128
|
+
SELECT singleton, project_id, project_root_realpath, revision, authority_epoch
|
|
129
|
+
FROM project_authority
|
|
130
|
+
`).get();
|
|
131
|
+
assert.deepEqual(
|
|
132
|
+
{
|
|
133
|
+
singleton: authority?.singleton,
|
|
134
|
+
projectIdLength: String(authority?.project_id ?? "").length,
|
|
135
|
+
root: authority?.project_root_realpath,
|
|
136
|
+
revision: authority?.revision,
|
|
137
|
+
epoch: authority?.authority_epoch,
|
|
138
|
+
},
|
|
139
|
+
{ singleton: 1, projectIdLength: 32, root: "", revision: 0, epoch: 0 },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
assert.throws(
|
|
143
|
+
() => db.exec("INSERT INTO project_authority (singleton, project_id) VALUES (2, 'second')"),
|
|
144
|
+
/CHECK constraint failed/i,
|
|
145
|
+
);
|
|
146
|
+
assert.throws(
|
|
147
|
+
() => db.exec("UPDATE project_authority SET revision = -1 WHERE singleton = 1"),
|
|
148
|
+
/CHECK constraint failed/i,
|
|
149
|
+
);
|
|
150
|
+
assert.throws(
|
|
151
|
+
() => db.exec("UPDATE project_authority SET authority_epoch = -1 WHERE singleton = 1"),
|
|
152
|
+
/CHECK constraint failed/i,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
db.exec(`
|
|
156
|
+
INSERT INTO workflow_operations (
|
|
157
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
158
|
+
expected_revision, resulting_revision,
|
|
159
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
160
|
+
actor_type, actor_id, source_transport, request_hash, created_at
|
|
161
|
+
)
|
|
162
|
+
SELECT
|
|
163
|
+
'op-1', project_id, 'test.operation', 'idem-1',
|
|
164
|
+
0, 1, 0, 0, 'agent', 'test-agent', 'test', 'request-1', '2026-07-12T00:00:01.000Z'
|
|
165
|
+
FROM project_authority WHERE singleton = 1;
|
|
166
|
+
|
|
167
|
+
INSERT INTO workflow_domain_events (
|
|
168
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
169
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
170
|
+
)
|
|
171
|
+
SELECT
|
|
172
|
+
'event-1', 'op-1', 0, project_id, 1,
|
|
173
|
+
0, 'test.started', 'project', project_id, '{"step":1}', '2026-07-12T00:00:02.000Z'
|
|
174
|
+
FROM project_authority WHERE singleton = 1;
|
|
175
|
+
|
|
176
|
+
INSERT INTO workflow_domain_events (
|
|
177
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
178
|
+
authority_epoch, event_type, entity_type, entity_id, caused_by_event_id,
|
|
179
|
+
payload_json, created_at
|
|
180
|
+
)
|
|
181
|
+
SELECT
|
|
182
|
+
'event-2', 'op-1', 1, project_id, 1,
|
|
183
|
+
0, 'test.finished', 'project', project_id, 'event-1',
|
|
184
|
+
'{"step":2}', '2026-07-12T00:00:03.000Z'
|
|
185
|
+
FROM project_authority WHERE singleton = 1;
|
|
186
|
+
|
|
187
|
+
INSERT INTO workflow_outbox (event_id, destination, available_at)
|
|
188
|
+
VALUES ('event-2', 'projection', '2026-07-12T00:00:04.000Z');
|
|
189
|
+
`);
|
|
190
|
+
|
|
191
|
+
const chain = db.prepare(`
|
|
192
|
+
SELECT
|
|
193
|
+
p.revision AS authority_revision,
|
|
194
|
+
o.expected_revision,
|
|
195
|
+
o.resulting_revision,
|
|
196
|
+
COUNT(DISTINCT e.event_id) AS event_count,
|
|
197
|
+
COUNT(DISTINCT x.outbox_id) AS outbox_count
|
|
198
|
+
FROM project_authority p
|
|
199
|
+
JOIN workflow_operations o ON o.project_id = p.project_id
|
|
200
|
+
JOIN workflow_domain_events e ON e.operation_id = o.operation_id
|
|
201
|
+
LEFT JOIN workflow_outbox x ON x.event_id = e.event_id
|
|
202
|
+
GROUP BY p.revision, o.expected_revision, o.resulting_revision
|
|
203
|
+
`).get();
|
|
204
|
+
assert.deepEqual(chain, {
|
|
205
|
+
authority_revision: 0,
|
|
206
|
+
expected_revision: 0,
|
|
207
|
+
resulting_revision: 1,
|
|
208
|
+
event_count: 2,
|
|
209
|
+
outbox_count: 1,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
assert.throws(
|
|
213
|
+
() => db.exec(`
|
|
214
|
+
INSERT INTO workflow_operations (
|
|
215
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
216
|
+
expected_revision, resulting_revision,
|
|
217
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
218
|
+
actor_type, source_transport, request_hash, created_at
|
|
219
|
+
)
|
|
220
|
+
SELECT 'op-duplicate', project_id, 'test.operation', 'idem-1', 1, 2, 0, 0,
|
|
221
|
+
'agent', 'test', 'request-2', '2026-07-12T00:00:05.000Z'
|
|
222
|
+
FROM project_authority WHERE singleton = 1
|
|
223
|
+
`),
|
|
224
|
+
/UNIQUE constraint failed/i,
|
|
225
|
+
);
|
|
226
|
+
assert.throws(
|
|
227
|
+
() => db.exec(`
|
|
228
|
+
INSERT INTO workflow_operations (
|
|
229
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
230
|
+
expected_revision, resulting_revision,
|
|
231
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
232
|
+
actor_type, source_transport, request_hash, created_at
|
|
233
|
+
)
|
|
234
|
+
SELECT 'op-same-revision', project_id, 'test.operation', 'idem-2', 0, 1, 0, 0,
|
|
235
|
+
'agent', 'test', 'request-2', '2026-07-12T00:00:05.000Z'
|
|
236
|
+
FROM project_authority WHERE singleton = 1
|
|
237
|
+
`),
|
|
238
|
+
/UNIQUE constraint failed/i,
|
|
239
|
+
);
|
|
240
|
+
assert.throws(
|
|
241
|
+
() => db.exec(`
|
|
242
|
+
INSERT INTO workflow_domain_events (
|
|
243
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
244
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
245
|
+
)
|
|
246
|
+
SELECT 'event-duplicate-index', 'op-1', 1, project_id, 1, 0,
|
|
247
|
+
'test.duplicate', 'project', project_id, '{}', '2026-07-12T00:00:06.000Z'
|
|
248
|
+
FROM project_authority WHERE singleton = 1
|
|
249
|
+
`),
|
|
250
|
+
/UNIQUE constraint failed/i,
|
|
251
|
+
);
|
|
252
|
+
assert.throws(
|
|
253
|
+
() => db.exec(`
|
|
254
|
+
INSERT INTO workflow_domain_events (
|
|
255
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
256
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
257
|
+
)
|
|
258
|
+
SELECT 'event-wrong-revision', 'op-1', 2, project_id, 2, 0,
|
|
259
|
+
'test.wrong-revision', 'project', project_id, '{}', '2026-07-12T00:00:06.000Z'
|
|
260
|
+
FROM project_authority WHERE singleton = 1
|
|
261
|
+
`),
|
|
262
|
+
/FOREIGN KEY constraint failed/i,
|
|
263
|
+
);
|
|
264
|
+
assert.throws(
|
|
265
|
+
() => db.exec(`
|
|
266
|
+
INSERT INTO workflow_outbox (event_id, destination, available_at)
|
|
267
|
+
VALUES ('missing-event', 'projection', '2026-07-12T00:00:07.000Z')
|
|
268
|
+
`),
|
|
269
|
+
/FOREIGN KEY constraint failed/i,
|
|
270
|
+
);
|
|
271
|
+
assert.throws(
|
|
272
|
+
() => db.exec(`
|
|
273
|
+
INSERT INTO workflow_outbox (event_id, destination, available_at)
|
|
274
|
+
VALUES ('event-2', 'projection', '2026-07-12T00:00:08.000Z')
|
|
275
|
+
`),
|
|
276
|
+
/UNIQUE constraint failed/i,
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("operations record the resulting authority epoch used by their events", () => {
|
|
281
|
+
const dbPath = createDatabasePath();
|
|
282
|
+
assert.equal(openDatabase(dbPath), true);
|
|
283
|
+
const db = _getAdapter();
|
|
284
|
+
assert.ok(db);
|
|
285
|
+
|
|
286
|
+
db.exec(`
|
|
287
|
+
INSERT INTO workflow_operations (
|
|
288
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
289
|
+
expected_revision, resulting_revision,
|
|
290
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
291
|
+
actor_type, source_transport, request_hash, created_at
|
|
292
|
+
)
|
|
293
|
+
SELECT 'op-cutover', project_id, 'authority.cutover', 'idem-cutover',
|
|
294
|
+
0, 1, 0, 1, 'agent', 'test', 'request-cutover', '2026-07-12T00:00:01.000Z'
|
|
295
|
+
FROM project_authority WHERE singleton = 1;
|
|
296
|
+
|
|
297
|
+
INSERT INTO workflow_domain_events (
|
|
298
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
299
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
300
|
+
)
|
|
301
|
+
SELECT 'event-cutover', 'op-cutover', 0, project_id, 1, 1,
|
|
302
|
+
'authority.cutover', 'project', project_id, '{}', '2026-07-12T00:00:02.000Z'
|
|
303
|
+
FROM project_authority WHERE singleton = 1;
|
|
304
|
+
|
|
305
|
+
INSERT INTO workflow_operations (
|
|
306
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
307
|
+
expected_revision, resulting_revision,
|
|
308
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
309
|
+
actor_type, source_transport, request_hash, created_at
|
|
310
|
+
)
|
|
311
|
+
SELECT 'op-unchanged-epoch', project_id, 'test.operation', 'idem-unchanged-epoch',
|
|
312
|
+
1, 2, 1, 1, 'agent', 'test', 'request-unchanged', '2026-07-12T00:00:03.000Z'
|
|
313
|
+
FROM project_authority WHERE singleton = 1;
|
|
314
|
+
`);
|
|
315
|
+
|
|
316
|
+
assert.throws(
|
|
317
|
+
() => db.exec(`
|
|
318
|
+
INSERT INTO workflow_domain_events (
|
|
319
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
320
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
321
|
+
)
|
|
322
|
+
SELECT 'event-stale-epoch', 'op-cutover', 1, project_id, 1, 0,
|
|
323
|
+
'authority.stale', 'project', project_id, '{}', '2026-07-12T00:00:04.000Z'
|
|
324
|
+
FROM project_authority WHERE singleton = 1
|
|
325
|
+
`),
|
|
326
|
+
/FOREIGN KEY constraint failed/i,
|
|
327
|
+
);
|
|
328
|
+
assert.throws(
|
|
329
|
+
() => db.exec(`
|
|
330
|
+
INSERT INTO workflow_operations (
|
|
331
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
332
|
+
expected_revision, resulting_revision,
|
|
333
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
334
|
+
actor_type, source_transport, request_hash, created_at
|
|
335
|
+
)
|
|
336
|
+
SELECT 'op-epoch-jump', project_id, 'authority.cutover', 'idem-epoch-jump',
|
|
337
|
+
2, 3, 1, 3, 'agent', 'test', 'request-jump', '2026-07-12T00:00:05.000Z'
|
|
338
|
+
FROM project_authority WHERE singleton = 1
|
|
339
|
+
`),
|
|
340
|
+
/CHECK constraint failed/i,
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("domain events reject updates and deletes", () => {
|
|
345
|
+
const dbPath = createDatabasePath();
|
|
346
|
+
assert.equal(openDatabase(dbPath), true);
|
|
347
|
+
const db = _getAdapter();
|
|
348
|
+
assert.ok(db);
|
|
349
|
+
|
|
350
|
+
db.exec(`
|
|
351
|
+
INSERT INTO workflow_operations (
|
|
352
|
+
operation_id, project_id, operation_type, idempotency_key,
|
|
353
|
+
expected_revision, resulting_revision,
|
|
354
|
+
expected_authority_epoch, resulting_authority_epoch,
|
|
355
|
+
actor_type, source_transport, request_hash, created_at
|
|
356
|
+
)
|
|
357
|
+
SELECT 'op-immutable', project_id, 'test.operation', 'idem-immutable',
|
|
358
|
+
0, 1, 0, 0, 'agent', 'test', 'request-immutable', '2026-07-12T00:00:01.000Z'
|
|
359
|
+
FROM project_authority WHERE singleton = 1;
|
|
360
|
+
|
|
361
|
+
INSERT INTO workflow_domain_events (
|
|
362
|
+
event_id, operation_id, event_index, project_id, project_revision,
|
|
363
|
+
authority_epoch, event_type, entity_type, entity_id, payload_json, created_at
|
|
364
|
+
)
|
|
365
|
+
SELECT 'event-immutable', 'op-immutable', 0, project_id, 1, 0,
|
|
366
|
+
'test.recorded', 'project', project_id, '{"original":true}', '2026-07-12T00:00:02.000Z'
|
|
367
|
+
FROM project_authority WHERE singleton = 1;
|
|
368
|
+
`);
|
|
369
|
+
|
|
370
|
+
assert.throws(
|
|
371
|
+
() => db.exec(`UPDATE workflow_domain_events SET payload_json = '{}' WHERE event_id = 'event-immutable'`),
|
|
372
|
+
/workflow domain events are immutable/i,
|
|
373
|
+
);
|
|
374
|
+
assert.throws(
|
|
375
|
+
() => db.exec(`DELETE FROM workflow_domain_events WHERE event_id = 'event-immutable'`),
|
|
376
|
+
/workflow domain events are immutable/i,
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("v30 upgrade preserves legacy rows and creates an independently healthy backup", () => {
|
|
381
|
+
const dbPath = createDatabasePath();
|
|
382
|
+
rewindToV30(dbPath);
|
|
383
|
+
|
|
384
|
+
assert.equal(openDatabase(dbPath), true);
|
|
385
|
+
closeDatabase();
|
|
386
|
+
|
|
387
|
+
let projectId = "";
|
|
388
|
+
const upgraded = openRawDatabase(dbPath);
|
|
389
|
+
try {
|
|
390
|
+
assert.equal(maxSchemaVersion(upgraded), 31);
|
|
391
|
+
assert.equal(tableExists(upgraded, "project_authority"), true);
|
|
392
|
+
assert.equal(upgraded.prepare("SELECT title FROM milestones WHERE id = 'M-LEGACY'").get()?.title, "Preserved legacy milestone");
|
|
393
|
+
assert.equal(upgraded.prepare("SELECT payload_json FROM audit_events WHERE event_id = 'audit-legacy'").get()?.payload_json, '{"kept":true}');
|
|
394
|
+
assert.equal(upgraded.prepare("PRAGMA quick_check").get()?.quick_check, "ok");
|
|
395
|
+
projectId = String(upgraded.prepare("SELECT project_id FROM project_authority").get()?.project_id);
|
|
396
|
+
} finally {
|
|
397
|
+
upgraded.close();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const backup = openRawDatabase(`${dbPath}.backup-v30`);
|
|
401
|
+
try {
|
|
402
|
+
assert.equal(maxSchemaVersion(backup), 30);
|
|
403
|
+
assert.equal(tableExists(backup, "project_authority"), false);
|
|
404
|
+
assert.equal(backup.prepare("SELECT title FROM milestones WHERE id = 'M-LEGACY'").get()?.title, "Preserved legacy milestone");
|
|
405
|
+
assert.equal(backup.prepare("PRAGMA quick_check").get()?.quick_check, "ok");
|
|
406
|
+
} finally {
|
|
407
|
+
backup.close();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const reopened = inspectFromFreshProcess(dbPath);
|
|
411
|
+
assert.deepEqual(reopened, {
|
|
412
|
+
version: 31,
|
|
413
|
+
authority: { project_id: projectId, revision: 0, authority_epoch: 0 },
|
|
414
|
+
legacy: { title: "Preserved legacy milestone" },
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const restoredPath = join(dirname(dbPath), "restored.db");
|
|
418
|
+
copyFileSync(`${dbPath}.backup-v30`, restoredPath);
|
|
419
|
+
const restored = inspectFromFreshProcess(restoredPath);
|
|
420
|
+
assert.equal(restored.version, 31);
|
|
421
|
+
assert.deepEqual(restored.legacy, { title: "Preserved legacy milestone" });
|
|
422
|
+
assert.deepEqual(
|
|
423
|
+
Object.fromEntries(
|
|
424
|
+
Object.entries(restored.authority as Record<string, unknown>).filter(([key]) => key !== "project_id"),
|
|
425
|
+
),
|
|
426
|
+
{ revision: 0, authority_epoch: 0 },
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("faulted v30 migration leaves no v31 state and retries cleanly", () => {
|
|
431
|
+
const dbPath = createDatabasePath();
|
|
432
|
+
rewindToV30(dbPath);
|
|
433
|
+
|
|
434
|
+
_setMigrationFaultForTest(true);
|
|
435
|
+
assert.throws(() => openDatabase(dbPath), /migration fault injected/);
|
|
436
|
+
_setMigrationFaultForTest(false);
|
|
437
|
+
|
|
438
|
+
const rolledBack = openRawDatabase(dbPath);
|
|
439
|
+
try {
|
|
440
|
+
assert.equal(maxSchemaVersion(rolledBack), 30);
|
|
441
|
+
assert.equal(tableExists(rolledBack, "project_authority"), false);
|
|
442
|
+
assert.equal(tableExists(rolledBack, "workflow_operations"), false);
|
|
443
|
+
assert.equal(tableExists(rolledBack, "workflow_domain_events"), false);
|
|
444
|
+
assert.equal(tableExists(rolledBack, "workflow_outbox"), false);
|
|
445
|
+
} finally {
|
|
446
|
+
rolledBack.close();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
assert.equal(openDatabase(dbPath), true);
|
|
450
|
+
closeDatabase();
|
|
451
|
+
const retried = openRawDatabase(dbPath);
|
|
452
|
+
try {
|
|
453
|
+
assert.equal(maxSchemaVersion(retried), 31);
|
|
454
|
+
assert.equal(retried.prepare("SELECT COUNT(*) AS count FROM project_authority").get()?.count, 1);
|
|
455
|
+
assert.equal(retried.prepare("SELECT COUNT(*) AS count FROM milestones WHERE id = 'M-LEGACY'").get()?.count, 1);
|
|
456
|
+
} finally {
|
|
457
|
+
retried.close();
|
|
458
|
+
}
|
|
459
|
+
});
|
|
@@ -101,9 +101,9 @@ describe('db-engine migrate guards', () => {
|
|
|
101
101
|
assert.ok(openDatabase(dbPath), 'fresh open should succeed');
|
|
102
102
|
closeDatabase();
|
|
103
103
|
|
|
104
|
-
// Rewind
|
|
105
|
-
//
|
|
106
|
-
const startVersion =
|
|
104
|
+
// Rewind to v28 and remove a v29 column so the multi-step migration has
|
|
105
|
+
// real work to redo even as later schema versions are added.
|
|
106
|
+
const startVersion = 28;
|
|
107
107
|
const raw = openRawSqliteForTest(dbPath);
|
|
108
108
|
raw.exec('DELETE FROM schema_version');
|
|
109
109
|
raw.exec(`INSERT INTO schema_version (version, applied_at) VALUES (${startVersion}, '2026-01-01T00:00:00Z')`);
|
|
@@ -206,6 +206,38 @@ test("checkEngineHealth ignores stale suffixed flat-phase duplicate when bare mi
|
|
|
206
206
|
);
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
+
test("checkEngineHealth reads task checkboxes from the canonical milestone worktree", async (t) => {
|
|
210
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-checkbox-worktree-"));
|
|
211
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
212
|
+
|
|
213
|
+
const gsdDir = join(base, ".gsd");
|
|
214
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
215
|
+
|
|
216
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
217
|
+
insertMilestone({ id: "M003", title: "Milestone", status: "active" });
|
|
218
|
+
insertSlice({ id: "S01", milestoneId: "M003", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
|
|
219
|
+
insertTask({ id: "T03", milestoneId: "M003", sliceId: "S01", title: "Task", status: "complete", sequence: 1 });
|
|
220
|
+
|
|
221
|
+
const plan = await renderPlanFromDb(base, "M003", "S01");
|
|
222
|
+
const checkedPlan = readFileSync(plan.planPath, "utf-8");
|
|
223
|
+
writeFileSync(plan.planPath, checkedPlan.replace("- [x] **T03**:", "- [ ] **T03**:"), "utf-8");
|
|
224
|
+
|
|
225
|
+
const worktree = join(base, ".gsd-worktrees", "M003");
|
|
226
|
+
const worktreePlan = join(worktree, ".gsd", "phases", "03-milestone", "03-01-PLAN.md");
|
|
227
|
+
mkdirSync(join(worktree, ".gsd", "phases", "03-milestone"), { recursive: true });
|
|
228
|
+
writeFileSync(join(worktree, ".git"), `gitdir: ${join(base, ".git", "worktrees", "M003")}\n`);
|
|
229
|
+
writeFileSync(worktreePlan, checkedPlan, "utf-8");
|
|
230
|
+
|
|
231
|
+
const issues: any[] = [];
|
|
232
|
+
await checkEngineHealth(base, issues, []);
|
|
233
|
+
|
|
234
|
+
assert.deepEqual(
|
|
235
|
+
issues.filter((issue) => issue.code === "checkbox_db_status_divergence").map((issue) => issue.unitId),
|
|
236
|
+
[],
|
|
237
|
+
"the stale project-root PLAN must not compete with the live milestone worktree projection",
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
209
241
|
test("checkEngineHealth reads task checkboxes from the <tasks> block, not a stray line above it", async (t) => {
|
|
210
242
|
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-task-section-"));
|
|
211
243
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
@@ -375,6 +407,38 @@ test("checkEngineHealth reports artifact rows whose files are missing on disk",
|
|
|
375
407
|
assert.equal(missing.fixable, false);
|
|
376
408
|
});
|
|
377
409
|
|
|
410
|
+
test("checkEngineHealth resolves artifact rows through the canonical milestone worktree", async (t) => {
|
|
411
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-artifact-worktree-"));
|
|
412
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
413
|
+
|
|
414
|
+
const gsdDir = join(base, ".gsd");
|
|
415
|
+
const artifactPath = "phases/03-milestone/03-01-PLAN.md";
|
|
416
|
+
const worktree = join(base, ".gsd-worktrees", "M003");
|
|
417
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
418
|
+
mkdirSync(join(worktree, ".gsd", "phases", "03-milestone"), { recursive: true });
|
|
419
|
+
writeFileSync(join(worktree, ".git"), `gitdir: ${join(base, ".git", "worktrees", "M003")}\n`);
|
|
420
|
+
writeFileSync(join(worktree, ".gsd", artifactPath), "# Plan\n", "utf-8");
|
|
421
|
+
|
|
422
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
423
|
+
insertArtifact({
|
|
424
|
+
path: artifactPath,
|
|
425
|
+
artifact_type: "PLAN",
|
|
426
|
+
milestone_id: "M003",
|
|
427
|
+
slice_id: "S01",
|
|
428
|
+
task_id: null,
|
|
429
|
+
full_content: "# Plan\n",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const issues: any[] = [];
|
|
433
|
+
await checkEngineHealth(base, issues, []);
|
|
434
|
+
|
|
435
|
+
assert.equal(
|
|
436
|
+
issues.some((issue) => issue.code === "artifact_file_missing" && issue.file === artifactPath),
|
|
437
|
+
false,
|
|
438
|
+
"worktree-local artifact rows must not be reported missing from the project root",
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
378
442
|
test("checkEngineHealth resolves escaped .gsd artifact rows against the project .gsd directory", async (t) => {
|
|
379
443
|
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-escaped-artifact-"));
|
|
380
444
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
@@ -74,7 +74,8 @@ test("#2885: cost table includes openai-codex provider models", () => {
|
|
|
74
74
|
const ids = BUNDLED_COST_TABLE.map(e => e.id);
|
|
75
75
|
const codexModels = [
|
|
76
76
|
"gpt-5.1", "gpt-5.1-codex-max", "gpt-5.1-codex-mini",
|
|
77
|
-
"gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5",
|
|
77
|
+
"gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5",
|
|
78
|
+
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
|
|
78
79
|
];
|
|
79
80
|
for (const model of codexModels) {
|
|
80
81
|
assert.ok(ids.includes(model), `cost table should include openai-codex model "${model}"`);
|
|
@@ -110,10 +111,24 @@ test("gpt-5.5 uses official OpenAI list pricing", () => {
|
|
|
110
111
|
assert.equal(entry.updatedAt, "2026-04-23");
|
|
111
112
|
});
|
|
112
113
|
|
|
113
|
-
test("gpt-5.6
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
test("gpt-5.6 bare alias is not treated as a real cost-table model", () => {
|
|
115
|
+
assert.equal(lookupModelCost("gpt-5.6"), undefined);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("gpt-5.6 variants use published pricing", () => {
|
|
119
|
+
const sol = lookupModelCost("openai-codex/gpt-5.6-sol");
|
|
120
|
+
assert.ok(sol, "lookupModelCost should find gpt-5.6-sol");
|
|
121
|
+
assert.equal(sol.inputPer1k, 0.005);
|
|
122
|
+
assert.equal(sol.outputPer1k, 0.03);
|
|
123
|
+
assert.deepEqual(sol.tiers?.[0], { inputTokensAbove: 272000, inputPer1k: 0.01, outputPer1k: 0.045 });
|
|
124
|
+
|
|
125
|
+
const terra = lookupModelCost("gpt-5.6-terra");
|
|
126
|
+
assert.ok(terra, "lookupModelCost should find gpt-5.6-terra");
|
|
127
|
+
assert.equal(terra.inputPer1k, 0.0025);
|
|
128
|
+
assert.equal(terra.outputPer1k, 0.015);
|
|
129
|
+
|
|
130
|
+
const luna = lookupModelCost("gpt-5.6-luna");
|
|
131
|
+
assert.ok(luna, "lookupModelCost should find gpt-5.6-luna");
|
|
132
|
+
assert.equal(luna.inputPer1k, 0.001);
|
|
133
|
+
assert.equal(luna.outputPer1k, 0.006);
|
|
119
134
|
});
|
|
@@ -10,6 +10,7 @@ import assert from "node:assert/strict";
|
|
|
10
10
|
import { mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
|
+
import { isCustomProvider } from "../preferences-models.ts";
|
|
13
14
|
|
|
14
15
|
// ─── Test helpers ─────────────────────────────────────────────────────────────
|
|
15
16
|
|
|
@@ -303,3 +304,57 @@ describe("custom provider session model overrides PREFERENCES.md (#4122)", () =>
|
|
|
303
304
|
});
|
|
304
305
|
});
|
|
305
306
|
|
|
307
|
+
describe("custom provider detection (#801)", () => {
|
|
308
|
+
let originalGsdHome: string | undefined;
|
|
309
|
+
let tmpGsdHome: string;
|
|
310
|
+
|
|
311
|
+
beforeEach(() => {
|
|
312
|
+
originalGsdHome = process.env.GSD_HOME;
|
|
313
|
+
tmpGsdHome = makeTmpDir("gsd-home");
|
|
314
|
+
process.env.GSD_HOME = tmpGsdHome;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
afterEach(() => {
|
|
318
|
+
if (originalGsdHome === undefined) {
|
|
319
|
+
delete process.env.GSD_HOME;
|
|
320
|
+
} else {
|
|
321
|
+
process.env.GSD_HOME = originalGsdHome;
|
|
322
|
+
}
|
|
323
|
+
try { rmSync(tmpGsdHome, { recursive: true, force: true }); } catch {}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("detects extension-registered providers that are not declared in models.json", () => {
|
|
327
|
+
const registry = {
|
|
328
|
+
getAllWithDiscovered: () => [
|
|
329
|
+
{ provider: "pi-llama-cpp" },
|
|
330
|
+
],
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
assert.equal(isCustomProvider("pi-llama-cpp", registry), true,
|
|
334
|
+
"runtime providers outside the built-in catalog should count as custom");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("does not treat built-in providers as custom just because they are in the registry", () => {
|
|
338
|
+
const registry = {
|
|
339
|
+
getAll: () => [
|
|
340
|
+
{ provider: "claude-code" },
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
assert.equal(isCustomProvider("claude-code", registry), false,
|
|
345
|
+
"built-in providers must still defer to PREFERENCES.md");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("does not treat bundled external-CLI providers as custom", () => {
|
|
349
|
+
// claude-code/cursor-agent/google-gemini-cli/google-antigravity register at
|
|
350
|
+
// runtime (so they are absent from the generated @gsd/pi-ai catalog), but
|
|
351
|
+
// they are first-party built-ins that PREFERENCES.md can route to. They must
|
|
352
|
+
// never be classified as custom, otherwise auto-mode silently ignores
|
|
353
|
+
// PREFERENCES.md for the default provider.
|
|
354
|
+
for (const provider of ["cursor-agent", "google-gemini-cli", "google-antigravity"]) {
|
|
355
|
+
const registry = { getAll: () => [{ provider }] };
|
|
356
|
+
assert.equal(isCustomProvider(provider, registry), false,
|
|
357
|
+
`${provider} is a built-in CLI provider and must defer to PREFERENCES.md`);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -900,7 +900,7 @@ test("#2885: openai-codex standard-tier models are recognized", () => {
|
|
|
900
900
|
|
|
901
901
|
test("#2885: openai-codex heavy-tier models are recognized", () => {
|
|
902
902
|
const config = { ...defaultRoutingConfig(), enabled: true };
|
|
903
|
-
const heavyModels = ["gpt-5", "gpt-5-pro", "gpt-5.1", "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.4", "gpt-5.5", "gpt-5.6", "o4-mini", "o4-mini-deep-research"];
|
|
903
|
+
const heavyModels = ["gpt-5", "gpt-5-pro", "gpt-5.1", "gpt-5.2", "gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.4", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "o4-mini", "o4-mini-deep-research"];
|
|
904
904
|
for (const model of heavyModels) {
|
|
905
905
|
const result = resolveModelForComplexity(
|
|
906
906
|
makeClassification("heavy"),
|