From 399f8f3f36c25487ef869b3e0fd896f9678710cb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 00:11:51 +0000 Subject: [PATCH 1/2] test: Identifier ID generation and Protected.isSensitiveWrite edge cases Add first-ever tests for Identifier (monotonic ID generation, prefix validation, sort ordering, timestamp extraction) and expand Protected.isSensitiveWrite coverage for private key extensions, remaining sensitive dirs/files, and Windows paths. Co-Authored-By: Claude Opus 4.6 (1M context) https://claude.ai/code/session_011oA4CKCyHc7Bv7MizQ5bh8 --- .../opencode/test/file/path-traversal.test.ts | 48 +++++++++++++ packages/opencode/test/id/id.test.ts | 67 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 packages/opencode/test/id/id.test.ts diff --git a/packages/opencode/test/file/path-traversal.test.ts b/packages/opencode/test/file/path-traversal.test.ts index 90ce4fbc23..093a418833 100644 --- a/packages/opencode/test/file/path-traversal.test.ts +++ b/packages/opencode/test/file/path-traversal.test.ts @@ -132,6 +132,54 @@ describe("Protected.isSensitiveWrite", () => { expect(Protected.isSensitiveWrite("package.json")).toBeUndefined() expect(Protected.isSensitiveWrite("models/schema.sql")).toBeUndefined() }) + + test("detects private key and certificate extensions", () => { + expect(Protected.isSensitiveWrite("certs/server.pem")).toBe("server.pem") + expect(Protected.isSensitiveWrite("ssl/private.key")).toBe("private.key") + expect(Protected.isSensitiveWrite("auth/cert.p12")).toBe("cert.p12") + expect(Protected.isSensitiveWrite("deploy/signing.pfx")).toBe("signing.pfx") + }) + + test("detects remaining sensitive directories", () => { + expect(Protected.isSensitiveWrite(".gnupg/pubring.kbx")).toBe(".gnupg") + expect(Protected.isSensitiveWrite(".gcloud/credentials.db")).toBe(".gcloud") + expect(Protected.isSensitiveWrite(".kube/config")).toBe(".kube") + expect(Protected.isSensitiveWrite(".docker/config.json")).toBe(".docker") + }) + + test("detects remaining sensitive files", () => { + expect(Protected.isSensitiveWrite(".npmrc")).toBe(".npmrc") + expect(Protected.isSensitiveWrite(".pypirc")).toBe(".pypirc") + expect(Protected.isSensitiveWrite(".netrc")).toBe(".netrc") + expect(Protected.isSensitiveWrite(".htpasswd")).toBe(".htpasswd") + expect(Protected.isSensitiveWrite(".pgpass")).toBe(".pgpass") + expect(Protected.isSensitiveWrite("id_rsa")).toBe("id_rsa") + expect(Protected.isSensitiveWrite("id_ed25519")).toBe("id_ed25519") + }) + + test("detects sensitive files in subdirectories", () => { + expect(Protected.isSensitiveWrite("config/subdir/.npmrc")).toBe(".npmrc") + expect(Protected.isSensitiveWrite("deploy/certs/server.key")).toBe("server.key") + }) + + test("handles Windows backslash paths", () => { + expect(Protected.isSensitiveWrite(".git\\config")).toBe(".git") + expect(Protected.isSensitiveWrite("config\\.env")).toBe(".env") + expect(Protected.isSensitiveWrite(".ssh\\id_rsa")).toBe(".ssh") + }) + + test("detects .env variant files via startsWith check", () => { + expect(Protected.isSensitiveWrite(".env.production.local")).toBe(".env.production.local") + expect(Protected.isSensitiveWrite(".env.test")).toBe(".env.test") + expect(Protected.isSensitiveWrite(".env.custom")).toBe(".env.custom") + }) + + test("does not flag files that merely contain sensitive substrings", () => { + expect(Protected.isSensitiveWrite("src/git-helper.ts")).toBeUndefined() + expect(Protected.isSensitiveWrite("docs/env-setup.md")).toBeUndefined() + expect(Protected.isSensitiveWrite("lib/ssh-utils.js")).toBeUndefined() + expect(Protected.isSensitiveWrite("my-key-manager.ts")).toBeUndefined() + }) }) /* diff --git a/packages/opencode/test/id/id.test.ts b/packages/opencode/test/id/id.test.ts new file mode 100644 index 0000000000..74bc6d46ed --- /dev/null +++ b/packages/opencode/test/id/id.test.ts @@ -0,0 +1,67 @@ +import { describe, test, expect } from "bun:test" +import { Identifier } from "../../src/id/id" + +describe("Identifier: monotonic ID generation and timestamp extraction", () => { + test("ascending IDs have correct prefix", () => { + const id = Identifier.ascending("session") + expect(id.startsWith("ses_")).toBe(true) + + const toolId = Identifier.ascending("tool") + expect(toolId.startsWith("tool_")).toBe(true) + + const msgId = Identifier.ascending("message") + expect(msgId.startsWith("msg_")).toBe(true) + }) + + test("descending IDs have correct prefix", () => { + const id = Identifier.descending("session") + expect(id.startsWith("ses_")).toBe(true) + + const id2 = Identifier.descending("message") + expect(id2.startsWith("msg_")).toBe(true) + }) + + test("ascending IDs sort chronologically", () => { + const id1 = Identifier.create("tool", false, Date.now() - 10000) + const id2 = Identifier.create("tool", false, Date.now()) + expect(id1 < id2).toBe(true) + }) + + test("descending IDs sort reverse-chronologically", () => { + const id1 = Identifier.create("tool", true, Date.now() - 10000) + const id2 = Identifier.create("tool", true, Date.now()) + expect(id1 > id2).toBe(true) + }) + + test("timestamp() preserves ordering for ascending IDs", () => { + // timestamp() extracts a value from the lower 48 bits of the encoded ID. + // It doesn't round-trip to the exact millisecond, but it preserves ordering, + // which is what Truncate.cleanup() relies on for age comparisons. + const t1 = Date.now() - 60000 + const t2 = Date.now() + const id1 = Identifier.create("session", false, t1) + const id2 = Identifier.create("session", false, t2) + expect(Identifier.timestamp(id1)).toBeLessThan(Identifier.timestamp(id2)) + }) + + test("given ID is returned if prefix matches", () => { + const existing = "ses_abc123" + const result = Identifier.ascending("session", existing) + expect(result).toBe(existing) + }) + + test("given ID throws if prefix mismatches", () => { + expect(() => Identifier.ascending("session", "msg_abc123")).toThrow( + "does not start with ses", + ) + }) + + test("monotonic counter differentiates same-millisecond IDs", () => { + const ts = Date.now() + const id1 = Identifier.create("tool", false, ts) + const id2 = Identifier.create("tool", false, ts) + expect(id1).not.toBe(id2) + // Both ascending, counter increments, so id1 < id2 + expect(id1 < id2).toBe(true) + }) +}) From c7c399a26d235ae30afa0cc2379345fbe03ce757 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 00:14:34 +0000 Subject: [PATCH 2/2] fix: resolve pre-existing typecheck errors in training-import.test.ts The mock for TrainingStore.count was missing `context` and `rule` keys, and the fs.readFile mock had an overload mismatch. These errors blocked the pre-push typecheck hook. https://claude.ai/code/session_011oA4CKCyHc7Bv7MizQ5bh8 --- packages/opencode/test/altimate/training-import.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/opencode/test/altimate/training-import.test.ts b/packages/opencode/test/altimate/training-import.test.ts index 847c323335..fee9dee188 100644 --- a/packages/opencode/test/altimate/training-import.test.ts +++ b/packages/opencode/test/altimate/training-import.test.ts @@ -48,12 +48,13 @@ function setupMocks(opts: { saveSpy?.mockRestore() budgetSpy?.mockRestore() - readFileSpy = spyOn(fs, "readFile").mockImplementation(async () => opts.fileContent) + readFileSpy = spyOn(fs, "readFile").mockImplementation((() => Promise.resolve(opts.fileContent)) as any) countSpy = spyOn(TrainingStore, "count").mockImplementation(async () => ({ standard: opts.currentCount ?? 0, glossary: opts.currentCount ?? 0, playbook: opts.currentCount ?? 0, - naming: opts.currentCount ?? 0, + context: opts.currentCount ?? 0, + rule: opts.currentCount ?? 0, pattern: opts.currentCount ?? 0, })) saveSpy = spyOn(TrainingStore, "save").mockImplementation(async () => {