fix(handlers): truncate manifest.json at last '}' before JSON parsing (#1480) #1485

Closed
fullstack-engineer wants to merge 1 commits from fix/issue-1480-manifest-json5 into staging
2 changed files with 55 additions and 0 deletions
@@ -25,6 +25,7 @@ package handlers
// to the pre-refactor hardcoded list so nothing regresses.
import (
"bytes"
"encoding/json"
"log"
"os"
@@ -101,6 +102,13 @@ func loadRuntimesFromManifest(path string) (map[string]struct{}, error) {
if err != nil {
return nil, err
}
// Truncate at the last '}' to drop trailing JSON5-style // comments
// (e.g. "// Triggered by Integration Tester at 2026-05-10T08:52Z")
// that would otherwise cause Go's json.Unmarshal to fail with
// "invalid character '/' after top-level value".
if last := bytes.LastIndex(data, []byte("}")); last != -1 {
data = data[:last+1]
}
var m manifestFile
if err := json.Unmarshal(data, &m); err != nil {
return nil, err
@@ -83,6 +83,53 @@ func TestLoadRuntimesFromManifest_MalformedJSON(t *testing.T) {
}
}
func TestLoadRuntimesFromManifest_StripsJSON5TrailingComment(t *testing.T) {
// Regression: manifest.json has a trailing JSON5-style // comment appended
// by the Integration Tester (// Triggered by ...). Go's encoding/json
// throws "invalid character '/' after top-level value" on the '/'.
// loadRuntimesFromManifest must truncate at the last '}' before parsing
// so the comment is ignored and the valid JSON body is processed.
dir := t.TempDir()
path := filepath.Join(dir, "manifest.json")
content := `{
"workspace_templates": [
{"name": "claude-code-default", "repo": "org/t-cc"},
{"name": "langgraph", "repo": "org/t-lg"}
]
}
// Triggered by Integration Tester at 2026-05-10T08:52Z`
_ = os.WriteFile(path, []byte(content), 0600)
got, err := loadRuntimesFromManifest(path)
if err != nil {
t.Fatalf("expected successful parse despite trailing comment, got: %v", err)
}
for _, must := range []string{"claude-code", "langgraph", "external", "kimi", "kimi-cli"} {
if _, ok := got[must]; !ok {
t.Errorf("expected runtime %q in set after JSON5 comment strip: %v", must, keys(got))
}
}
}
func TestLoadRuntimesFromManifest_ExtraDataAfterClosingBrace(t *testing.T) {
// A file with trailing garbage after the closing '}' (not a // comment)
// should also be tolerated — the parser truncates at last '}';
// anything after is discarded.
dir := t.TempDir()
path := filepath.Join(dir, "manifest.json")
content := `{"workspace_templates":[{"name":"hermes","repo":"org/t"}]}
extra data that should be ignored`
_ = os.WriteFile(path, []byte(content), 0600)
got, err := loadRuntimesFromManifest(path)
if err != nil {
t.Fatalf("expected parse with trailing data: %v", err)
}
if _, ok := got["hermes"]; !ok {
t.Errorf("expected hermes runtime after truncation: %v", keys(got))
}
}
// TestRealManifestParses — sanity check against the actual
// monorepo manifest.json so a future schema change to that file
// (e.g. workspace_templates → workspace_runtime_templates) surfaces